mirror of
https://github.com/bmadcode/BMAD-METHOD.git
synced 2025-12-29 16:14:59 +00:00
agent customzation almost working again
This commit is contained in:
@@ -32,7 +32,7 @@ class CustomModuleCache {
|
||||
|
||||
const content = await fs.readFile(this.manifestPath, 'utf8');
|
||||
const yaml = require('js-yaml');
|
||||
return yaml.load(content) || {};
|
||||
return yaml.parse(content) || {};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -49,7 +49,7 @@ class Detector {
|
||||
if (await fs.pathExists(coreConfigPath)) {
|
||||
try {
|
||||
const configContent = await fs.readFile(coreConfigPath, 'utf8');
|
||||
const config = yaml.load(configContent);
|
||||
const config = yaml.parse(configContent);
|
||||
if (!result.version && config.version) {
|
||||
result.version = config.version;
|
||||
}
|
||||
@@ -77,7 +77,7 @@ class Detector {
|
||||
if (await fs.pathExists(moduleConfigPath)) {
|
||||
try {
|
||||
const configContent = await fs.readFile(moduleConfigPath, 'utf8');
|
||||
const config = yaml.load(configContent);
|
||||
const config = yaml.parse(configContent);
|
||||
moduleInfo.version = config.version || 'unknown';
|
||||
moduleInfo.name = config.name || moduleId;
|
||||
moduleInfo.description = config.description;
|
||||
@@ -106,7 +106,7 @@ class Detector {
|
||||
|
||||
try {
|
||||
const configContent = await fs.readFile(moduleConfigPath, 'utf8');
|
||||
const config = yaml.load(configContent);
|
||||
const config = yaml.parse(configContent);
|
||||
moduleInfo.version = config.version || 'unknown';
|
||||
moduleInfo.name = config.name || entry.name;
|
||||
moduleInfo.description = config.description;
|
||||
@@ -239,7 +239,7 @@ class Detector {
|
||||
try {
|
||||
const yaml = require('js-yaml');
|
||||
const manifestContent = await fs.readFile(manifestPath, 'utf8');
|
||||
const manifest = yaml.load(manifestContent);
|
||||
const manifest = yaml.parse(manifestContent);
|
||||
// V6+ manifest has installation.version
|
||||
return manifest && manifest.installation && manifest.installation.version;
|
||||
} catch {
|
||||
|
||||
@@ -1448,6 +1448,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||
await fs.ensureDir(bmadDir);
|
||||
await fs.ensureDir(path.join(bmadDir, '_cfg'));
|
||||
await fs.ensureDir(path.join(bmadDir, '_cfg', 'agents'));
|
||||
await fs.ensureDir(path.join(bmadDir, '_cfg', 'custom'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1699,25 +1700,55 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||
const sourcePath = getModulePath('core');
|
||||
const targetPath = path.join(bmadDir, 'core');
|
||||
|
||||
// Copy core files with filtering for localskip agents
|
||||
await this.copyDirectoryWithFiltering(sourcePath, targetPath);
|
||||
// Copy core files (skip .agent.yaml files like modules do)
|
||||
await this.copyCoreFiles(sourcePath, targetPath);
|
||||
|
||||
// Compile agents using the same compiler as modules
|
||||
const { ModuleManager } = require('../modules/manager');
|
||||
const moduleManager = new ModuleManager();
|
||||
await moduleManager.compileModuleAgents(sourcePath, targetPath, 'core', bmadDir);
|
||||
|
||||
// Process agent files to inject activation block
|
||||
await this.processAgentFiles(targetPath, 'core');
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy directory with filtering for localskip agents
|
||||
* @param {string} sourcePath - Source directory path
|
||||
* @param {string} targetPath - Target directory path
|
||||
* Copy core files (similar to copyModuleWithFiltering but for core)
|
||||
* @param {string} sourcePath - Source path
|
||||
* @param {string} targetPath - Target path
|
||||
*/
|
||||
async copyDirectoryWithFiltering(sourcePath, targetPath) {
|
||||
// Get all files in source directory
|
||||
async copyCoreFiles(sourcePath, targetPath) {
|
||||
// Get all files in source
|
||||
const files = await this.getFileList(sourcePath);
|
||||
|
||||
for (const file of files) {
|
||||
// Skip sub-modules directory - these are IDE-specific and handled separately
|
||||
if (file.startsWith('sub-modules/')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip sidecar directories - they are handled separately during agent compilation
|
||||
if (
|
||||
path
|
||||
.dirname(file)
|
||||
.split('/')
|
||||
.some((dir) => dir.toLowerCase().includes('sidecar'))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip _module-installer directory - it's only needed at install time
|
||||
if (file.startsWith('_module-installer/') || file === 'module.yaml') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip config.yaml templates - we'll generate clean ones with actual values
|
||||
if (file === 'config.yaml' || file.endsWith('/config.yaml')) {
|
||||
if (file === 'config.yaml' || file.endsWith('/config.yaml') || file === 'custom.yaml' || file.endsWith('/custom.yaml')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip .agent.yaml files - they will be compiled separately
|
||||
if (file.endsWith('.agent.yaml')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1725,7 +1756,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||
const targetFile = path.join(targetPath, file);
|
||||
|
||||
// Check if this is an agent file
|
||||
if (file.includes('agents/') && file.endsWith('.md')) {
|
||||
if (file.startsWith('agents/') && file.endsWith('.md')) {
|
||||
// Read the file to check for localskip
|
||||
const content = await fs.readFile(sourceFile, 'utf8');
|
||||
|
||||
@@ -1737,8 +1768,14 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the file with placeholder replacement
|
||||
await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile, this.bmadFolderName || 'bmad');
|
||||
// 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');
|
||||
}
|
||||
|
||||
// Track the installed file
|
||||
this.installedFiles.push(targetFile);
|
||||
@@ -1798,104 +1835,77 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||
const agentFiles = await fs.readdir(agentsPath);
|
||||
|
||||
for (const agentFile of agentFiles) {
|
||||
// Handle YAML agents - build them to .md
|
||||
// Skip .agent.yaml files - they should already be compiled by compileModuleAgents
|
||||
if (agentFile.endsWith('.agent.yaml')) {
|
||||
const agentName = agentFile.replace('.agent.yaml', '');
|
||||
const yamlPath = path.join(agentsPath, agentFile);
|
||||
const mdPath = path.join(agentsPath, `${agentName}.md`);
|
||||
const customizePath = path.join(cfgAgentsDir, `${moduleName}-${agentName}.customize.yaml`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create customize template if it doesn't exist
|
||||
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');
|
||||
console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`));
|
||||
}
|
||||
// Only process .md files (already compiled from YAML)
|
||||
if (!agentFile.endsWith('.md')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const agentName = agentFile.replace('.md', '');
|
||||
const mdPath = path.join(agentsPath, agentFile);
|
||||
const customizePath = path.join(cfgAgentsDir, `${moduleName}-${agentName}.customize.yaml`);
|
||||
|
||||
// For .md files that are already compiled, we don't need to do much
|
||||
// Just ensure the customize template exists
|
||||
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');
|
||||
console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`));
|
||||
}
|
||||
}
|
||||
|
||||
// Build YAML + customize to .md
|
||||
const customizeExists = await fs.pathExists(customizePath);
|
||||
let xmlContent = await this.xmlHandler.buildFromYaml(yamlPath, customizeExists ? customizePath : null, {
|
||||
includeMetadata: true,
|
||||
});
|
||||
// Read the existing .md file to check for sidecar info
|
||||
let hasSidecar = false;
|
||||
try {
|
||||
const content = await fs.readFile(mdPath, 'utf8');
|
||||
// Look for sidecar metadata in the frontmatter or content
|
||||
hasSidecar = content.includes('hasSidecar') && content.includes('true');
|
||||
} catch {
|
||||
// Continue without sidecar processing
|
||||
}
|
||||
|
||||
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime
|
||||
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
|
||||
// Copy sidecar files if agent has hasSidecar flag
|
||||
if (hasSidecar) {
|
||||
const { copyAgentSidecarFiles } = require('../../../lib/agent/installer');
|
||||
|
||||
// Replace _bmad with actual folder name
|
||||
xmlContent = xmlContent.replaceAll('_bmad', this.bmadFolderName || 'bmad');
|
||||
// Get agent sidecar folder from core config
|
||||
const coreConfigPath = path.join(bmadDir, 'bmb', 'config.yaml');
|
||||
let agentSidecarFolder;
|
||||
|
||||
// Replace {agent_sidecar_folder} if configured
|
||||
const coreConfig = this.configCollector.collectedConfig.core || {};
|
||||
if (coreConfig.agent_sidecar_folder && xmlContent.includes('{agent_sidecar_folder}')) {
|
||||
xmlContent = xmlContent.replaceAll('{agent_sidecar_folder}', coreConfig.agent_sidecar_folder);
|
||||
}
|
||||
|
||||
// Process TTS injection points (pass targetPath for tracking)
|
||||
xmlContent = this.processTTSInjectionPoints(xmlContent, mdPath);
|
||||
|
||||
// Check if agent has sidecar and copy it
|
||||
let agentYamlContent = null;
|
||||
let hasSidecar = false;
|
||||
|
||||
try {
|
||||
agentYamlContent = await fs.readFile(yamlPath, 'utf8');
|
||||
if (await fs.pathExists(coreConfigPath)) {
|
||||
const yamlLib = require('yaml');
|
||||
const agentYaml = yamlLib.parse(agentYamlContent);
|
||||
hasSidecar = agentYaml?.agent?.metadata?.hasSidecar === true;
|
||||
} catch {
|
||||
// Continue without sidecar processing
|
||||
const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
|
||||
const coreConfig = yamlLib.parse(coreConfigContent);
|
||||
agentSidecarFolder = coreConfig.agent_sidecar_folder || agentSidecarFolder;
|
||||
}
|
||||
|
||||
// Write the built .md file to bmad/{module}/agents/ with POSIX-compliant final newline
|
||||
const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n';
|
||||
await fs.writeFile(mdPath, content, 'utf8');
|
||||
this.installedFiles.push(mdPath);
|
||||
// Resolve path variables
|
||||
const resolvedSidecarFolder = agentSidecarFolder
|
||||
.replaceAll('{project-root}', projectDir)
|
||||
.replaceAll('_bmad', this.bmadFolderName || 'bmad');
|
||||
|
||||
// Copy sidecar files if agent has hasSidecar flag
|
||||
if (hasSidecar) {
|
||||
const { copyAgentSidecarFiles } = require('../../../lib/agent/installer');
|
||||
// Create sidecar directory for this agent
|
||||
const agentSidecarDir = path.join(resolvedSidecarFolder, agentName);
|
||||
await fs.ensureDir(agentSidecarDir);
|
||||
|
||||
// Get agent sidecar folder from core config
|
||||
const coreConfigPath = path.join(bmadDir, 'bmb', 'config.yaml');
|
||||
let agentSidecarFolder;
|
||||
// Find and copy sidecar folder from source module
|
||||
const sourceModulePath = moduleName === 'core' ? getModulePath('core') : getSourcePath(`modules/${moduleName}`);
|
||||
const sourceAgentPath = path.join(sourceModulePath, 'agents');
|
||||
|
||||
if (await fs.pathExists(coreConfigPath)) {
|
||||
const yamlLib = require('yaml');
|
||||
const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
|
||||
const coreConfig = yamlLib.parse(coreConfigContent);
|
||||
agentSidecarFolder = coreConfig.agent_sidecar_folder || agentSidecarFolder;
|
||||
}
|
||||
// Copy sidecar files (preserve existing, add new)
|
||||
const sidecarResult = copyAgentSidecarFiles(sourceAgentPath, agentSidecarDir, null);
|
||||
|
||||
// Resolve path variables
|
||||
const resolvedSidecarFolder = agentSidecarFolder
|
||||
.replaceAll('{project-root}', projectDir)
|
||||
.replaceAll('_bmad', this.bmadFolderName || 'bmad');
|
||||
|
||||
// Create sidecar directory for this agent
|
||||
const agentSidecarDir = path.join(resolvedSidecarFolder, agentName);
|
||||
await fs.ensureDir(agentSidecarDir);
|
||||
|
||||
// Find and copy sidecar folder from source module
|
||||
const sourceModulePath = getSourcePath(`modules/${moduleName}`);
|
||||
const sourceAgentPath = path.join(sourceModulePath, 'agents');
|
||||
|
||||
// Copy sidecar files (preserve existing, add new)
|
||||
const sidecarResult = copyAgentSidecarFiles(sourceAgentPath, agentSidecarDir, yamlPath);
|
||||
|
||||
if (sidecarResult.copied.length > 0) {
|
||||
console.log(chalk.dim(` Copied ${sidecarResult.copied.length} new sidecar file(s) to: ${agentSidecarDir}`));
|
||||
}
|
||||
if (sidecarResult.preserved.length > 0) {
|
||||
console.log(chalk.dim(` Preserved ${sidecarResult.preserved.length} existing sidecar file(s)`));
|
||||
}
|
||||
if (sidecarResult.copied.length > 0) {
|
||||
console.log(chalk.dim(` Copied ${sidecarResult.copied.length} new sidecar file(s) to: ${agentSidecarDir}`));
|
||||
}
|
||||
if (sidecarResult.preserved.length > 0) {
|
||||
console.log(chalk.dim(` Preserved ${sidecarResult.preserved.length} existing sidecar file(s)`));
|
||||
}
|
||||
|
||||
// Remove the source YAML file - we can regenerate from installer source if needed
|
||||
await fs.remove(yamlPath);
|
||||
|
||||
console.log(chalk.dim(` Built agent: ${agentName}.md${hasSidecar ? ' (with sidecar)' : ''}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1940,7 +1950,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||
if (customizeExists) {
|
||||
const customizeContent = await fs.readFile(customizePath, 'utf8');
|
||||
const yaml = require('js-yaml');
|
||||
const customizeYaml = yaml.load(customizeContent);
|
||||
const customizeYaml = yaml.parse(customizeContent);
|
||||
|
||||
// Detect what fields are customized (similar to rebuildAgentFiles)
|
||||
if (customizeYaml) {
|
||||
@@ -2064,34 +2074,52 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||
}
|
||||
}
|
||||
|
||||
// Build YAML + customize to .md
|
||||
let xmlContent = await this.xmlHandler.buildFromYaml(sourceYamlPath, customizeExists ? customizePath : null, {
|
||||
includeMetadata: true,
|
||||
// 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
|
||||
const answers = {};
|
||||
if (customizeData.persona) {
|
||||
Object.assign(answers, customizeData.persona);
|
||||
}
|
||||
if (customizeData.agent?.metadata) {
|
||||
Object.assign(answers, { metadata: customizeData.agent.metadata });
|
||||
}
|
||||
if (customizeData.critical_actions) {
|
||||
answers.critical_actions = customizeData.critical_actions;
|
||||
}
|
||||
if (customizeData.memories) {
|
||||
answers.memories = customizeData.memories;
|
||||
}
|
||||
|
||||
// Get core config for agent_sidecar_folder
|
||||
const coreConfigPath = path.join(bmadDir, 'bmb', '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 { xml } = await compileAgent(yamlContent, answers, agentName, path.relative(bmadDir, targetMdPath), {
|
||||
config: coreConfig,
|
||||
});
|
||||
|
||||
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime
|
||||
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
|
||||
|
||||
// Replace {agent_sidecar_folder} if configured
|
||||
const coreConfigPath = path.join(bmadDir, 'bmb', 'config.yaml');
|
||||
let agentSidecarFolder = null;
|
||||
|
||||
if (await fs.pathExists(coreConfigPath)) {
|
||||
const yamlLib = require('yaml');
|
||||
const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
|
||||
const coreConfig = yamlLib.parse(coreConfigContent);
|
||||
agentSidecarFolder = coreConfig.agent_sidecar_folder;
|
||||
}
|
||||
|
||||
if (agentSidecarFolder && xmlContent.includes('{agent_sidecar_folder}')) {
|
||||
xmlContent = xmlContent.replaceAll('{agent_sidecar_folder}', agentSidecarFolder);
|
||||
}
|
||||
|
||||
// Process TTS injection points (pass targetPath for tracking)
|
||||
xmlContent = this.processTTSInjectionPoints(xmlContent, targetMdPath);
|
||||
// Replace _bmad with actual folder name if needed
|
||||
const finalXml = xml.replaceAll('_bmad', path.basename(bmadDir));
|
||||
|
||||
// Write the rebuilt .md file with POSIX-compliant final newline
|
||||
const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n';
|
||||
const content = finalXml.endsWith('\n') ? finalXml : finalXml + '\n';
|
||||
await fs.writeFile(targetMdPath, content, 'utf8');
|
||||
|
||||
// Display result with customizations if any
|
||||
@@ -2119,8 +2147,18 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||
throw new Error(`BMAD not installed at ${bmadDir}`);
|
||||
}
|
||||
|
||||
// Get installed modules from manifest
|
||||
const manifestPath = path.join(bmadDir, '_cfg', 'manifest.yaml');
|
||||
let installedModules = [];
|
||||
let manifest = null;
|
||||
if (await fs.pathExists(manifestPath)) {
|
||||
const manifestContent = await fs.readFile(manifestPath, 'utf8');
|
||||
const yaml = require('js-yaml');
|
||||
manifest = yaml.load(manifestContent);
|
||||
installedModules = manifest.modules || [];
|
||||
}
|
||||
|
||||
// Check for custom modules with missing sources
|
||||
const manifest = await this.manifest.read(bmadDir);
|
||||
if (manifest && manifest.customModules && manifest.customModules.length > 0) {
|
||||
console.log(chalk.yellow('\nChecking custom module sources before compilation...'));
|
||||
|
||||
@@ -2130,7 +2168,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||
}
|
||||
|
||||
const projectRoot = getProjectRoot();
|
||||
const installedModules = manifest.modules || [];
|
||||
await this.handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, 'compile-agents', installedModules);
|
||||
}
|
||||
|
||||
@@ -2177,21 +2214,9 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||
}
|
||||
}
|
||||
|
||||
// Skip full manifest regeneration during compileAgents to preserve custom agents
|
||||
// Custom agents are already added to manifests during individual installation
|
||||
// Only regenerate YAML manifest for IDE updates if needed
|
||||
const existingManifestPath = path.join(bmadDir, '_cfg', 'manifest.yaml');
|
||||
let existingIdes = [];
|
||||
if (await fs.pathExists(existingManifestPath)) {
|
||||
const manifestContent = await fs.readFile(existingManifestPath, 'utf8');
|
||||
const yaml = require('js-yaml');
|
||||
const manifest = yaml.load(manifestContent);
|
||||
existingIdes = manifest.ides || [];
|
||||
}
|
||||
|
||||
// Update IDE configurations using the existing IDE list from manifest
|
||||
if (existingIdes && existingIdes.length > 0) {
|
||||
for (const ide of existingIdes) {
|
||||
if (manifest && manifest.ides && manifest.ides.length > 0) {
|
||||
for (const ide of manifest.ides) {
|
||||
await this.ideManager.setup(ide, projectDir, bmadDir, {
|
||||
selectedModules: installedModules,
|
||||
skipModuleInstall: true, // Skip module installation, just update IDE files
|
||||
@@ -2770,8 +2795,11 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||
const relativePath = path.relative(bmadDir, fullPath);
|
||||
const fileName = path.basename(fullPath);
|
||||
|
||||
// Skip _cfg directory - system files
|
||||
if (relativePath.startsWith('_cfg/') || relativePath.startsWith('_cfg\\')) {
|
||||
// Skip _cfg directory EXCEPT for agent customizations
|
||||
if (
|
||||
(relativePath.startsWith('_cfg/') || relativePath.startsWith('_cfg\\')) && // Allow .customize.yaml files in _cfg/agents/
|
||||
!(relativePath.includes('/agents/') && fileName.endsWith('.customize.yaml'))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -142,14 +142,14 @@ class ManifestGenerator {
|
||||
let workflow;
|
||||
if (entry.name === 'workflow.yaml') {
|
||||
// Parse YAML workflow
|
||||
workflow = yaml.load(content);
|
||||
workflow = yaml.parse(content);
|
||||
} else {
|
||||
// Parse MD workflow with YAML frontmatter
|
||||
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||
if (!frontmatterMatch) {
|
||||
continue; // Skip MD files without frontmatter
|
||||
}
|
||||
workflow = yaml.load(frontmatterMatch[1]);
|
||||
workflow = yaml.parse(frontmatterMatch[1]);
|
||||
}
|
||||
|
||||
// Skip template workflows (those with placeholder values)
|
||||
@@ -459,7 +459,7 @@ class ManifestGenerator {
|
||||
if (await fs.pathExists(manifestPath)) {
|
||||
try {
|
||||
const existingContent = await fs.readFile(manifestPath, 'utf8');
|
||||
const existingManifest = yaml.load(existingContent);
|
||||
const existingManifest = yaml.parse(existingContent);
|
||||
if (existingManifest && existingManifest.customModules) {
|
||||
existingCustomModules = existingManifest.customModules;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ class CustomHandler {
|
||||
// Try to parse YAML with error handling
|
||||
let config;
|
||||
try {
|
||||
config = yaml.load(configContent);
|
||||
config = yaml.parse(configContent);
|
||||
} catch (parseError) {
|
||||
console.warn(chalk.yellow(`Warning: YAML parse error in ${configPath}:`, parseError.message));
|
||||
return null;
|
||||
|
||||
@@ -348,7 +348,7 @@ class BaseIdeSetup {
|
||||
try {
|
||||
const yaml = require('js-yaml');
|
||||
const content = await fs.readFile(fullPath, 'utf8');
|
||||
const workflowData = yaml.load(content);
|
||||
const workflowData = yaml.parse(content);
|
||||
|
||||
if (workflowData && workflowData.name) {
|
||||
workflows.push({
|
||||
@@ -456,7 +456,7 @@ class BaseIdeSetup {
|
||||
if (frontmatterMatch) {
|
||||
const yaml = require('js-yaml');
|
||||
try {
|
||||
const frontmatter = yaml.load(frontmatterMatch[1]);
|
||||
const frontmatter = yaml.parse(frontmatterMatch[1]);
|
||||
standalone = frontmatter.standalone === true;
|
||||
} catch {
|
||||
// Ignore YAML parse errors
|
||||
|
||||
@@ -50,7 +50,7 @@ class AntigravitySetup extends BaseIdeSetup {
|
||||
try {
|
||||
// Load injection configuration
|
||||
const configContent = await fs.readFile(injectionConfigPath, 'utf8');
|
||||
const injectionConfig = yaml.load(configContent);
|
||||
const injectionConfig = yaml.parse(configContent);
|
||||
|
||||
// Ask about subagents if they exist and we haven't asked yet
|
||||
if (injectionConfig.subagents && !config.subagentChoices) {
|
||||
|
||||
@@ -49,7 +49,7 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
||||
try {
|
||||
// Load injection configuration
|
||||
const configContent = await fs.readFile(injectionConfigPath, 'utf8');
|
||||
const injectionConfig = yaml.load(configContent);
|
||||
const injectionConfig = yaml.parse(configContent);
|
||||
|
||||
// Ask about subagents if they exist and we haven't asked yet
|
||||
if (injectionConfig.subagents && !config.subagentChoices) {
|
||||
|
||||
@@ -34,7 +34,7 @@ class GeminiSetup extends BaseIdeSetup {
|
||||
if (await fs.pathExists(coreConfigPath)) {
|
||||
try {
|
||||
const configContent = await fs.readFile(coreConfigPath, 'utf8');
|
||||
const config = yaml.load(configContent);
|
||||
const config = yaml.parse(configContent);
|
||||
|
||||
if (config.user_name) {
|
||||
configValues.user_name = config.user_name;
|
||||
|
||||
@@ -150,7 +150,7 @@ class KiroCliSetup extends BaseIdeSetup {
|
||||
*/
|
||||
async processAgentFile(agentFile, agentsDir, projectDir) {
|
||||
const yamlContent = await fs.readFile(agentFile, 'utf8');
|
||||
const agentData = yaml.load(yamlContent);
|
||||
const agentData = yaml.parse(yamlContent);
|
||||
|
||||
if (!this.validateBmadCompliance(agentData)) {
|
||||
return;
|
||||
|
||||
@@ -152,7 +152,7 @@ class OpenCodeSetup extends BaseIdeSetup {
|
||||
|
||||
let frontmatter = {};
|
||||
try {
|
||||
frontmatter = yaml.load(match[1]) || {};
|
||||
frontmatter = yaml.parse(match[1]) || {};
|
||||
} catch {
|
||||
frontmatter = {};
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ async function loadModuleInjectionConfig(handler, moduleName) {
|
||||
}
|
||||
|
||||
const configContent = await fs.readFile(configPath, 'utf8');
|
||||
const config = yaml.load(configContent) || {};
|
||||
const config = yaml.parse(configContent) || {};
|
||||
|
||||
return {
|
||||
config,
|
||||
|
||||
@@ -822,12 +822,27 @@ class ModuleManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for customizations
|
||||
// Check for customizations and build answers object
|
||||
let customizedFields = [];
|
||||
let answers = {};
|
||||
if (await fs.pathExists(customizePath)) {
|
||||
const customizeContent = await fs.readFile(customizePath, 'utf8');
|
||||
const customizeData = yaml.load(customizeContent);
|
||||
customizedFields = customizeData.customized_fields || [];
|
||||
|
||||
// Build answers object from customizations
|
||||
if (customizeData.persona) {
|
||||
Object.assign(answers, customizeData.persona);
|
||||
}
|
||||
if (customizeData.agent?.metadata) {
|
||||
Object.assign(answers, { metadata: customizeData.agent.metadata });
|
||||
}
|
||||
if (customizeData.critical_actions) {
|
||||
answers.critical_actions = customizeData.critical_actions;
|
||||
}
|
||||
if (customizeData.memories) {
|
||||
answers.memories = customizeData.memories;
|
||||
}
|
||||
}
|
||||
|
||||
// Load core config to get agent_sidecar_folder
|
||||
@@ -835,23 +850,22 @@ class ModuleManager {
|
||||
let coreConfig = {};
|
||||
|
||||
if (await fs.pathExists(coreConfigPath)) {
|
||||
const yamlLib = require('yaml');
|
||||
const yaml = require('yaml');
|
||||
const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
|
||||
coreConfig = yamlLib.parse(coreConfigContent);
|
||||
coreConfig = yaml.load(coreConfigContent);
|
||||
}
|
||||
|
||||
// Check if agent has sidecar
|
||||
let hasSidecar = false;
|
||||
try {
|
||||
const yamlLib = require('yaml');
|
||||
const agentYaml = yamlLib.parse(yamlContent);
|
||||
const agentYaml = yaml.parse(yamlContent);
|
||||
hasSidecar = agentYaml?.agent?.metadata?.hasSidecar === true;
|
||||
} catch {
|
||||
// Continue without sidecar processing
|
||||
}
|
||||
|
||||
// Compile with customizations if any
|
||||
const { xml } = compileAgent(yamlContent, {}, agentName, relativePath, { config: this.coreConfig });
|
||||
const { xml } = await compileAgent(yamlContent, answers, agentName, relativePath, { config: coreConfig });
|
||||
|
||||
// Replace _bmad placeholder if needed
|
||||
if (xml.includes('_bmad') && this.bmadFolderName) {
|
||||
|
||||
@@ -9,6 +9,8 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { processAgentYaml, extractInstallConfig, stripInstallConfig, getDefaultValues } = require('./template-engine');
|
||||
const { escapeXml } = require('../../../lib/xml-utils');
|
||||
const { ActivationBuilder } = require('../activation-builder');
|
||||
const { AgentAnalyzer } = require('../agent-analyzer');
|
||||
|
||||
/**
|
||||
* Build frontmatter for agent
|
||||
@@ -30,137 +32,7 @@ You must fully embody this agent's persona and follow all activation instruction
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build simple activation block for custom agents
|
||||
* @param {Array} criticalActions - Agent-specific critical actions
|
||||
* @param {Array} menuItems - Menu items to determine which handlers to include
|
||||
* @param {string} deploymentType - 'ide' or 'web' - filters commands based on ide-only/web-only flags
|
||||
* @returns {string} Activation XML
|
||||
*/
|
||||
function buildSimpleActivation(criticalActions = [], menuItems = [], deploymentType = 'ide') {
|
||||
let activation = '<activation critical="MANDATORY">\n';
|
||||
|
||||
let stepNum = 1;
|
||||
|
||||
// Standard steps
|
||||
activation += ` <step n="${stepNum++}">Load persona from this current agent file (already in context)</step>\n`;
|
||||
activation += ` <step n="${stepNum++}">Load and read {project-root}/.bmad/core/config.yaml to get {user_name}, {communication_language}, {output_folder}</step>\n`;
|
||||
activation += ` <step n="${stepNum++}">Remember: user's name is {user_name}</step>\n`;
|
||||
|
||||
// Agent-specific steps from critical_actions
|
||||
for (const action of criticalActions) {
|
||||
activation += ` <step n="${stepNum++}">${action}</step>\n`;
|
||||
}
|
||||
|
||||
// Menu and interaction steps
|
||||
activation += ` <step n="${stepNum++}">ALWAYS communicate in {communication_language}</step>\n`;
|
||||
activation += ` <step n="${stepNum++}">Show greeting using {user_name} from config, communicate in {communication_language}, then display numbered list of
|
||||
ALL menu items from menu section</step>\n`;
|
||||
activation += ` <step n="${stepNum++}">STOP and WAIT for user input - do NOT execute menu items automatically - accept number or cmd trigger or fuzzy command
|
||||
match</step>\n`;
|
||||
activation += ` <step n="${stepNum++}">On user input: Number → execute menu item[n] | Text → case-insensitive substring match | Multiple matches → ask user
|
||||
to clarify | No match → show "Not recognized"</step>\n`;
|
||||
|
||||
// Filter menu items based on deployment type
|
||||
const filteredMenuItems = menuItems.filter((item) => {
|
||||
// Skip web-only commands for IDE deployment
|
||||
if (deploymentType === 'ide' && item['web-only'] === true) {
|
||||
return false;
|
||||
}
|
||||
// Skip ide-only commands for web deployment
|
||||
if (deploymentType === 'web' && item['ide-only'] === true) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Detect which handlers are actually used in the filtered menu
|
||||
const usedHandlers = new Set();
|
||||
for (const item of filteredMenuItems) {
|
||||
if (item.action) usedHandlers.add('action');
|
||||
if (item.workflow) usedHandlers.add('workflow');
|
||||
if (item.exec) usedHandlers.add('exec');
|
||||
if (item.tmpl) usedHandlers.add('tmpl');
|
||||
if (item.data) usedHandlers.add('data');
|
||||
if (item['validate-workflow']) usedHandlers.add('validate-workflow');
|
||||
}
|
||||
|
||||
// Only include menu-handlers section if handlers are used
|
||||
if (usedHandlers.size > 0) {
|
||||
activation += ` <step n="${stepNum++}">When executing a menu item: Check menu-handlers section below - extract any attributes from the selected menu item and follow the corresponding handler instructions</step>\n`;
|
||||
|
||||
// Menu handlers - only include what's used
|
||||
activation += `
|
||||
<menu-handlers>
|
||||
<handlers>\n`;
|
||||
|
||||
if (usedHandlers.has('action')) {
|
||||
activation += ` <handler type="action">
|
||||
When menu item has: action="#id" → Find prompt with id="id" in current agent XML, execute its content
|
||||
When menu item has: action="text" → Execute the text directly as an inline instruction
|
||||
</handler>\n`;
|
||||
}
|
||||
|
||||
if (usedHandlers.has('workflow')) {
|
||||
activation += ` <handler type="workflow">
|
||||
When menu item has: workflow="path/to/workflow.yaml"
|
||||
1. CRITICAL: Always LOAD {project-root}/.bmad/core/tasks/workflow.xml
|
||||
2. Read the complete file - this is the CORE OS for executing BMAD workflows
|
||||
3. Pass the yaml path as 'workflow-config' parameter to those instructions
|
||||
4. Execute workflow.xml instructions precisely following all steps
|
||||
5. Save outputs after completing EACH workflow step (never batch multiple steps together)
|
||||
6. If workflow.yaml path is "todo", inform user the workflow hasn't been implemented yet
|
||||
</handler>\n`;
|
||||
}
|
||||
|
||||
if (usedHandlers.has('exec')) {
|
||||
activation += ` <handler type="exec">
|
||||
When menu item has: exec="command" → Execute the command directly
|
||||
</handler>\n`;
|
||||
}
|
||||
|
||||
if (usedHandlers.has('tmpl')) {
|
||||
activation += ` <handler type="tmpl">
|
||||
When menu item has: tmpl="template-path" → Load and apply the template
|
||||
</handler>\n`;
|
||||
}
|
||||
|
||||
if (usedHandlers.has('data')) {
|
||||
activation += ` <handler type="data">
|
||||
When menu item has: data="path/to/x.json|yaml|yml"
|
||||
Load the file, parse as JSON/YAML, make available as {data} to subsequent operations
|
||||
</handler>\n`;
|
||||
}
|
||||
|
||||
if (usedHandlers.has('validate-workflow')) {
|
||||
activation += ` <handler type="validate-workflow">
|
||||
When menu item has: validate-workflow="path/to/workflow.yaml"
|
||||
1. CRITICAL: Always LOAD {project-root}/.bmad/core/tasks/validate-workflow.xml
|
||||
2. Read the complete file - this is the CORE OS for validating BMAD workflows
|
||||
3. Pass the workflow.yaml path as 'workflow' parameter to those instructions
|
||||
4. Pass any checklist.md from the workflow location as 'checklist' parameter if available
|
||||
5. Execute validate-workflow.xml instructions precisely following all steps
|
||||
6. Generate validation report with thorough analysis
|
||||
</handler>\n`;
|
||||
}
|
||||
|
||||
activation += ` </handlers>
|
||||
</menu-handlers>\n`;
|
||||
}
|
||||
|
||||
activation += `
|
||||
<rules>
|
||||
- ALWAYS communicate in {communication_language} UNLESS contradicted by communication_style
|
||||
- Stay in character until exit selected
|
||||
- Menu triggers use asterisk (*) - NOT markdown, display exactly as shown
|
||||
- Number all lists, use letters for sub-options
|
||||
- Load files ONLY when executing menu items or a workflow or command requires it. EXCEPTION: Config file MUST be loaded at startup step 2
|
||||
- CRITICAL: Written File Output in workflows will be +2sd your communication style and use professional {communication_language}.
|
||||
</rules>
|
||||
</activation>\n`;
|
||||
|
||||
return activation;
|
||||
}
|
||||
// buildSimpleActivation function removed - replaced by ActivationBuilder for proper fragment loading from src/utility/agent-components/
|
||||
|
||||
/**
|
||||
* Build persona XML section
|
||||
@@ -370,9 +242,9 @@ function processExecArray(execArray) {
|
||||
* @param {Object} agentYaml - Parsed and processed agent YAML
|
||||
* @param {string} agentName - Final agent name (for ID and frontmatter)
|
||||
* @param {string} targetPath - Target path for agent ID
|
||||
* @returns {string} Compiled XML string with frontmatter
|
||||
* @returns {Promise<string>} Compiled XML string with frontmatter
|
||||
*/
|
||||
function compileToXml(agentYaml, agentName = '', targetPath = '') {
|
||||
async function compileToXml(agentYaml, agentName = '', targetPath = '') {
|
||||
const agent = agentYaml.agent;
|
||||
const meta = agent.metadata;
|
||||
|
||||
@@ -394,8 +266,16 @@ function compileToXml(agentYaml, agentName = '', targetPath = '') {
|
||||
|
||||
xml += `<agent ${agentAttrs.join(' ')}>\n`;
|
||||
|
||||
// Activation block - pass menu items and deployment type to determine which handlers to include
|
||||
xml += buildSimpleActivation(agent.critical_actions || [], agent.menu || [], 'ide');
|
||||
// Activation block - use ActivationBuilder for proper fragment loading
|
||||
const activationBuilder = new ActivationBuilder();
|
||||
const analyzer = new AgentAnalyzer();
|
||||
const profile = analyzer.analyzeAgentObject(agentYaml);
|
||||
xml += await activationBuilder.buildActivation(
|
||||
profile,
|
||||
meta,
|
||||
agent.critical_actions || [],
|
||||
false, // forWebBundle - set to false for IDE deployment
|
||||
);
|
||||
|
||||
// Persona section
|
||||
xml += buildPersonaXml(agent.persona);
|
||||
@@ -424,15 +304,20 @@ function compileToXml(agentYaml, agentName = '', targetPath = '') {
|
||||
* @param {string} agentName - Optional final agent name (user's custom persona name)
|
||||
* @param {string} targetPath - Optional target path for agent ID
|
||||
* @param {Object} options - Additional options including config
|
||||
* @returns {Object} { xml: string, metadata: Object }
|
||||
* @returns {Promise<Object>} { xml: string, metadata: Object }
|
||||
*/
|
||||
function compileAgent(yamlContent, answers = {}, agentName = '', targetPath = '', options = {}) {
|
||||
async function compileAgent(yamlContent, answers = {}, agentName = '', targetPath = '', options = {}) {
|
||||
// Parse YAML
|
||||
const agentYaml = yaml.parse(yamlContent);
|
||||
let agentYaml = yaml.parse(yamlContent);
|
||||
|
||||
// Note: agentName parameter is for UI display only, not for modifying the YAML
|
||||
// The persona name (metadata.name) should always come from the YAML file
|
||||
// We should NEVER modify metadata.name as it's part of the agent's identity
|
||||
// Apply customization merges before template processing
|
||||
// Handle metadata overrides (like name)
|
||||
if (answers.metadata) {
|
||||
agentYaml.agent.metadata = { ...agentYaml.agent.metadata, ...answers.metadata };
|
||||
// Remove from answers so it doesn't get processed as template variables
|
||||
const { metadata, ...templateAnswers } = answers;
|
||||
answers = templateAnswers;
|
||||
}
|
||||
|
||||
// Extract install_config
|
||||
const installConfig = extractInstallConfig(agentYaml);
|
||||
@@ -456,7 +341,7 @@ function compileAgent(yamlContent, answers = {}, agentName = '', targetPath = ''
|
||||
const cleanYaml = stripInstallConfig(processedYaml);
|
||||
|
||||
// Replace {agent_sidecar_folder} in XML content
|
||||
let xml = compileToXml(cleanYaml, agentName, targetPath);
|
||||
let xml = await compileToXml(cleanYaml, agentName, targetPath);
|
||||
if (finalAnswers.agent_sidecar_folder) {
|
||||
xml = xml.replaceAll('{agent_sidecar_folder}', finalAnswers.agent_sidecar_folder);
|
||||
}
|
||||
@@ -543,7 +428,6 @@ module.exports = {
|
||||
compileAgentFile,
|
||||
escapeXml,
|
||||
buildFrontmatter,
|
||||
buildSimpleActivation,
|
||||
buildPersonaXml,
|
||||
buildPromptsXml,
|
||||
buildMenuXml,
|
||||
|
||||
@@ -1,23 +1,3 @@
|
||||
/**
|
||||
* File: tools/cli/lib/ui.js
|
||||
*
|
||||
* BMAD Method - Business Model Agile Development Method
|
||||
* Repository: https://github.com/paulpreibisch/BMAD-METHOD
|
||||
*
|
||||
* Copyright (c) 2025 Paul Preibisch
|
||||
* Licensed under the Apache License, Version 2.0
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* @fileoverview Interactive installation prompts and user input collection for BMAD CLI
|
||||
* @context Guides users through installation configuration including core settings, modules, IDEs, and optional AgentVibes TTS
|
||||
* @architecture Facade pattern - presents unified installation flow, delegates to Detector/ConfigCollector/IdeManager for specifics
|
||||
* @dependencies inquirer (prompts), chalk (formatting), detector.js (existing installation detection)
|
||||
* @entrypoints Called by install.js command via ui.promptInstall(), returns complete configuration object
|
||||
* @patterns Progressive disclosure (prompts in order), early IDE selection (Windows compat), AgentVibes auto-detection
|
||||
* @related installer.js (consumes config), AgentVibes#34 (TTS integration), promptAgentVibes()
|
||||
*/
|
||||
|
||||
const chalk = require('chalk');
|
||||
const inquirer = require('inquirer');
|
||||
const path = require('node:path');
|
||||
@@ -56,7 +36,6 @@ class UI {
|
||||
// Check if there's an existing BMAD installation
|
||||
const fs = require('fs-extra');
|
||||
const path = require('node:path');
|
||||
// Use findBmadDir to detect any custom folder names (V6+)
|
||||
const bmadDir = await installer.findBmadDir(confirmedDirectory);
|
||||
const hasExistingInstall = await fs.pathExists(bmadDir);
|
||||
|
||||
@@ -131,60 +110,9 @@ class UI {
|
||||
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
|
||||
const coreConfig = await this.collectCoreConfig(confirmedDirectory);
|
||||
|
||||
// For new installations, create the directory structure first so we can cache custom content
|
||||
if (!hasExistingInstall && customContentConfig._shouldAsk) {
|
||||
// Create the bmad directory based on core config
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const bmadFolderName = '_bmad';
|
||||
const bmadDir = path.join(confirmedDirectory, bmadFolderName);
|
||||
|
||||
await fs.ensureDir(bmadDir);
|
||||
await fs.ensureDir(path.join(bmadDir, '_cfg'));
|
||||
await fs.ensureDir(path.join(bmadDir, '_cfg', 'custom'));
|
||||
|
||||
// Now prompt for custom content
|
||||
customContentConfig = await this.promptCustomContentLocation();
|
||||
|
||||
// If custom content found, cache it
|
||||
if (customContentConfig.hasCustomContent) {
|
||||
const { CustomModuleCache } = require('../installers/lib/core/custom-module-cache');
|
||||
const cache = new CustomModuleCache(bmadDir);
|
||||
|
||||
const customHandler = new CustomHandler();
|
||||
const customFiles = await customHandler.findCustomContent(customContentConfig.customPath);
|
||||
|
||||
for (const customFile of customFiles) {
|
||||
const customInfo = await customHandler.getCustomInfo(customFile);
|
||||
if (customInfo && customInfo.id) {
|
||||
// Cache the module source
|
||||
await cache.cacheModule(customInfo.id, customInfo.path, {
|
||||
name: customInfo.name,
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
console.log(chalk.dim(` Cached ${customInfo.name} to _cfg/custom/${customInfo.id}`));
|
||||
}
|
||||
}
|
||||
|
||||
// Update config to use cached modules
|
||||
customContentConfig.cachedModules = [];
|
||||
for (const customFile of customFiles) {
|
||||
const customInfo = await customHandler.getCustomInfo(customFile);
|
||||
if (customInfo && customInfo.id) {
|
||||
customContentConfig.cachedModules.push({
|
||||
id: customInfo.id,
|
||||
cachePath: path.join(bmadDir, '_cfg', 'custom', customInfo.id),
|
||||
// Store relative path from cache for the manifest
|
||||
relativePath: path.join('_cfg', 'custom', customInfo.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✓ Cached ${customFiles.length} custom module(s)`));
|
||||
}
|
||||
|
||||
// Clear the flag
|
||||
// Custom content will be handled during installation phase
|
||||
// Store the custom content config for later use
|
||||
if (customContentConfig._shouldAsk) {
|
||||
delete customContentConfig._shouldAsk;
|
||||
}
|
||||
|
||||
@@ -202,44 +130,26 @@ class UI {
|
||||
// Check which custom content items were selected
|
||||
const selectedCustomContent = selectedModules.filter((mod) => mod.startsWith('__CUSTOM_CONTENT__'));
|
||||
|
||||
// For cached modules (new installs), check if any cached modules were selected
|
||||
let selectedCachedModules = [];
|
||||
if (customContentConfig.cachedModules) {
|
||||
selectedCachedModules = selectedModules.filter(
|
||||
(mod) => !mod.startsWith('__CUSTOM_CONTENT__') && customContentConfig.cachedModules.some((cm) => cm.id === mod),
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedCustomContent.length > 0 || selectedCachedModules.length > 0) {
|
||||
if (selectedCustomContent.length > 0) {
|
||||
customContentConfig.selected = true;
|
||||
customContentConfig.selectedFiles = selectedCustomContent.map((mod) => mod.replace('__CUSTOM_CONTENT__', ''));
|
||||
|
||||
// Handle directory-based custom content (existing installs)
|
||||
if (selectedCustomContent.length > 0) {
|
||||
customContentConfig.selectedFiles = selectedCustomContent.map((mod) => mod.replace('__CUSTOM_CONTENT__', ''));
|
||||
// Convert custom content to module IDs for installation
|
||||
const customContentModuleIds = [];
|
||||
const customHandler = new CustomHandler();
|
||||
for (const customFile of customContentConfig.selectedFiles) {
|
||||
// Get the module info to extract the ID
|
||||
const customInfo = await customHandler.getCustomInfo(customFile);
|
||||
if (customInfo) {
|
||||
customContentModuleIds.push(customInfo.id);
|
||||
}
|
||||
// Convert custom content to module IDs for installation
|
||||
const customContentModuleIds = [];
|
||||
const customHandler = new CustomHandler();
|
||||
for (const customFile of customContentConfig.selectedFiles) {
|
||||
// Get the module info to extract the ID
|
||||
const customInfo = await customHandler.getCustomInfo(customFile);
|
||||
if (customInfo) {
|
||||
customContentModuleIds.push(customInfo.id);
|
||||
}
|
||||
// Filter out custom content markers and add module IDs
|
||||
selectedModules = [...selectedModules.filter((mod) => !mod.startsWith('__CUSTOM_CONTENT__')), ...customContentModuleIds];
|
||||
}
|
||||
|
||||
// For cached modules, they're already module IDs, just mark as selected
|
||||
if (selectedCachedModules.length > 0) {
|
||||
customContentConfig.selectedCachedModules = selectedCachedModules;
|
||||
// No need to filter since they're already proper module IDs
|
||||
}
|
||||
// Filter out custom content markers and add module IDs
|
||||
selectedModules = [...selectedModules.filter((mod) => !mod.startsWith('__CUSTOM_CONTENT__')), ...customContentModuleIds];
|
||||
} else if (customContentConfig.hasCustomContent) {
|
||||
// User provided custom content but didn't select any
|
||||
customContentConfig.selected = false;
|
||||
customContentConfig.selectedFiles = [];
|
||||
customContentConfig.selectedCachedModules = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,67 +520,20 @@ class UI {
|
||||
const hasCustomContentItems = false;
|
||||
|
||||
// Add custom content items
|
||||
if (customContentConfig && customContentConfig.hasCustomContent) {
|
||||
if (customContentConfig.cachedModules) {
|
||||
// New installation - show cached modules
|
||||
for (const cachedModule of customContentConfig.cachedModules) {
|
||||
// Get the module info from cache
|
||||
const yaml = require('js-yaml');
|
||||
const fs = require('fs-extra');
|
||||
if (customContentConfig && customContentConfig.hasCustomContent && customContentConfig.customPath) {
|
||||
// Existing installation - show from directory
|
||||
const customHandler = new CustomHandler();
|
||||
const customFiles = await customHandler.findCustomContent(customContentConfig.customPath);
|
||||
|
||||
// Try multiple possible config file locations
|
||||
const possibleConfigPaths = [
|
||||
path.join(cachedModule.cachePath, 'module.yaml'),
|
||||
path.join(cachedModule.cachePath, 'custom.yaml'),
|
||||
path.join(cachedModule.cachePath, '_module-installer', 'module.yaml'),
|
||||
path.join(cachedModule.cachePath, '_module-installer', 'custom.yaml'),
|
||||
];
|
||||
|
||||
let moduleData = null;
|
||||
let foundPath = null;
|
||||
|
||||
for (const configPath of possibleConfigPaths) {
|
||||
if (await fs.pathExists(configPath)) {
|
||||
try {
|
||||
const yamlContent = await fs.readFile(configPath, 'utf8');
|
||||
moduleData = yaml.load(yamlContent);
|
||||
foundPath = configPath;
|
||||
break;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse config at ${configPath}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleData) {
|
||||
// Use the name from the custom info if we have it
|
||||
const moduleName = cachedModule.name || moduleData.name || cachedModule.id;
|
||||
|
||||
customContentItems.push({
|
||||
name: `${chalk.cyan('✓')} ${moduleName} ${chalk.gray('(cached)')}`,
|
||||
value: cachedModule.id, // Use module ID directly
|
||||
checked: true, // Default to selected
|
||||
cached: true,
|
||||
});
|
||||
} else {
|
||||
// Module config not found - skip silently (non-critical)
|
||||
}
|
||||
}
|
||||
} else if (customContentConfig.customPath) {
|
||||
// Existing installation - show from directory
|
||||
const customHandler = new CustomHandler();
|
||||
const customFiles = await customHandler.findCustomContent(customContentConfig.customPath);
|
||||
|
||||
for (const customFile of customFiles) {
|
||||
const customInfo = await customHandler.getCustomInfo(customFile);
|
||||
if (customInfo) {
|
||||
customContentItems.push({
|
||||
name: `${chalk.cyan('✓')} ${customInfo.name} ${chalk.gray(`(${customInfo.relativePath})`)}`,
|
||||
value: `__CUSTOM_CONTENT__${customFile}`, // Unique value for each custom content
|
||||
checked: true, // Default to selected since user chose to provide custom content
|
||||
path: customInfo.path, // Track path to avoid duplicates
|
||||
});
|
||||
}
|
||||
for (const customFile of customFiles) {
|
||||
const customInfo = await customHandler.getCustomInfo(customFile);
|
||||
if (customInfo) {
|
||||
customContentItems.push({
|
||||
name: `${chalk.cyan('✓')} ${customInfo.name} ${chalk.gray(`(${customInfo.relativePath})`)}`,
|
||||
value: `__CUSTOM_CONTENT__${customFile}`, // Unique value for each custom content
|
||||
checked: true, // Default to selected since user chose to provide custom content
|
||||
path: customInfo.path, // Track path to avoid duplicates
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -804,120 +667,6 @@ class UI {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for custom content location
|
||||
* @returns {Object} Custom content configuration
|
||||
*/
|
||||
async promptCustomContentLocation() {
|
||||
try {
|
||||
// Skip custom content installation - always return false
|
||||
return { hasCustomContent: false };
|
||||
|
||||
// TODO: Custom content installation temporarily disabled
|
||||
// CLIUtils.displaySection('Custom Content', 'Optional: Add custom agents, workflows, and modules');
|
||||
|
||||
// const { hasCustomContent } = await inquirer.prompt([
|
||||
// {
|
||||
// type: 'list',
|
||||
// name: 'hasCustomContent',
|
||||
// message: 'Do you have custom content to install?',
|
||||
// choices: [
|
||||
// { name: 'No (skip custom content)', value: 'none' },
|
||||
// { name: 'Enter a directory path', value: 'directory' },
|
||||
// { name: 'Enter a URL', value: 'url' },
|
||||
// ],
|
||||
// default: 'none',
|
||||
// },
|
||||
// ]);
|
||||
|
||||
// if (hasCustomContent === 'none') {
|
||||
// return { hasCustomContent: false };
|
||||
// }
|
||||
|
||||
// TODO: Custom content installation temporarily disabled
|
||||
// if (hasCustomContent === 'url') {
|
||||
// console.log(chalk.yellow('\nURL-based custom content installation is coming soon!'));
|
||||
// console.log(chalk.cyan('For now, please download your custom content and choose "Enter a directory path".\n'));
|
||||
// return { hasCustomContent: false };
|
||||
// }
|
||||
|
||||
// if (hasCustomContent === 'directory') {
|
||||
// let customPath;
|
||||
// while (!customPath) {
|
||||
// let expandedPath;
|
||||
// const { directory } = await inquirer.prompt([
|
||||
// {
|
||||
// type: 'input',
|
||||
// name: 'directory',
|
||||
// message: 'Enter directory to search for custom content (will scan subfolders):',
|
||||
// default: process.cwd(), // Use actual current working directory
|
||||
// validate: async (input) => {
|
||||
// if (!input || input.trim() === '') {
|
||||
// return 'Please enter a directory path';
|
||||
// }
|
||||
|
||||
// try {
|
||||
// expandedPath = this.expandUserPath(input.trim());
|
||||
// } catch (error) {
|
||||
// return error.message;
|
||||
// }
|
||||
|
||||
// // Check if the path exists
|
||||
// const pathExists = await fs.pathExists(expandedPath);
|
||||
// if (!pathExists) {
|
||||
// return 'Directory does not exist';
|
||||
// }
|
||||
|
||||
// return true;
|
||||
// },
|
||||
// },
|
||||
// ]);
|
||||
|
||||
// // Now expand the path for use after the prompt
|
||||
// expandedPath = this.expandUserPath(directory.trim());
|
||||
|
||||
// // Check if directory has custom content
|
||||
// const customHandler = new CustomHandler();
|
||||
// const customFiles = await customHandler.findCustomContent(expandedPath);
|
||||
|
||||
// if (customFiles.length === 0) {
|
||||
// console.log(chalk.yellow(`\nNo custom content found in ${expandedPath}`));
|
||||
|
||||
// const { tryAgain } = await inquirer.prompt([
|
||||
// {
|
||||
// type: 'confirm',
|
||||
// name: 'tryAgain',
|
||||
// message: 'Try a different directory?',
|
||||
// default: true,
|
||||
// },
|
||||
// ]);
|
||||
|
||||
// if (tryAgain) {
|
||||
// continue;
|
||||
// } else {
|
||||
// return { hasCustomContent: false };
|
||||
// }
|
||||
// }
|
||||
|
||||
// customPath = expandedPath;
|
||||
// console.log(chalk.green(`\n✓ Found ${customFiles.length} custom content item(s):`));
|
||||
// for (const file of customFiles) {
|
||||
// const relativePath = path.relative(expandedPath, path.dirname(file));
|
||||
// const folderName = path.dirname(file).split(path.sep).pop();
|
||||
// console.log(chalk.dim(` • ${folderName} ${chalk.gray(`(${relativePath})`)}`));
|
||||
// }
|
||||
// }
|
||||
|
||||
// return { hasCustomContent: true, customPath };
|
||||
// }
|
||||
|
||||
// return { hasCustomContent: false };
|
||||
} catch (error) {
|
||||
console.error(chalk.red('Error in custom content prompt:'), error);
|
||||
return { hasCustomContent: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm directory selection
|
||||
* @param {string} directory - The directory path
|
||||
|
||||
Reference in New Issue
Block a user