all modules custom or core use the same installer and have consistent behavior now.

This commit is contained in:
Brian Madison
2025-12-07 17:17:50 -06:00
parent baaa984a90
commit 6430173738
44 changed files with 393 additions and 253 deletions

View File

@@ -98,7 +98,7 @@ The installer is a multi-stage system that handles agent compilation, IDE integr
```
1. Collect User Input
- Target directory, modules, IDEs
- Custom module configuration (via install-config.yaml)
- Custom module configuration (via module.yaml)
2. Pre-Installation
- Validate target, check conflicts, backup existing installations
@@ -183,12 +183,12 @@ The installer supports **15 IDE environments** through a base-derived architectu
### Custom Module Configuration
Modules define interactive configuration menus via `install-config.yaml` files in their `_module-installer/` directories.
Modules define interactive configuration menus via `module.yaml` files in their `_module-installer/` directories.
**Config File Location**:
- Core: `src/core/_module-installer/install-config.yaml`
- Modules: `src/modules/{module}/_module-installer/install-config.yaml`
- Core: `src/core/module.yaml`
- Modules: `src/modules/{module}/module.yaml`
**Configuration Types**:

View File

@@ -183,24 +183,28 @@ class ConfigCollector {
// Load module's install config schema
// First, try the standard src/modules location
let installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'install-config.yaml');
let installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'module.yaml');
let moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
// If not found in src/modules, we need to find it by searching the project
if (!(await fs.pathExists(installerConfigPath))) {
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(moduleConfigPath))) {
// Use the module manager to find the module source
const { ModuleManager } = require('../modules/manager');
const moduleManager = new ModuleManager();
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
if (moduleSourcePath) {
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'install-config.yaml');
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'module.yaml');
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
}
}
let configPath = null;
let isCustomModule = false;
if (await fs.pathExists(installerConfigPath)) {
if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else {
// Check if this is a custom module with custom.yaml
@@ -448,22 +452,26 @@ class ConfigCollector {
}
// Load module's config
// First, try the standard src/modules location
let installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'install-config.yaml');
let installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'module.yaml');
let moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
// If not found in src/modules, we need to find it by searching the project
if (!(await fs.pathExists(installerConfigPath))) {
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(moduleConfigPath))) {
// Use the module manager to find the module source
const { ModuleManager } = require('../modules/manager');
const moduleManager = new ModuleManager();
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
if (moduleSourcePath) {
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'install-config.yaml');
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'module.yaml');
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
}
}
let configPath = null;
if (await fs.pathExists(installerConfigPath)) {
if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else {
// No config for this module

View File

@@ -753,10 +753,39 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Resolve dependencies for selected modules
spinner.text = 'Resolving dependencies...';
const projectRoot = getProjectRoot();
const modulesToInstall = config.installCore ? ['core', ...config.modules] : config.modules;
// Add custom content modules to the modules list for installation
let allModules = [...(config.modules || [])];
if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) {
// Add custom modules to the installation list
for (const customFile of config.customContent.selectedFiles) {
const { CustomHandler } = require('../custom/handler');
const customHandler = new CustomHandler();
const customInfo = await customHandler.getCustomInfo(customFile, projectDir);
if (customInfo && customInfo.id) {
allModules.push(customInfo.id);
}
}
}
const modulesToInstall = config.installCore ? ['core', ...allModules] : allModules;
// For dependency resolution, we need to pass the project root
const resolution = await this.dependencyResolver.resolve(projectRoot, config.modules || [], { verbose: config.verbose });
// Create a temporary module manager that knows about custom content locations
const tempModuleManager = new ModuleManager({
scanProjectForModules: true,
});
// Make sure custom modules are discoverable
if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) {
// The dependency resolver needs to know about these modules
// We'll handle custom modules separately in the installation loop
}
const resolution = await this.dependencyResolver.resolve(projectRoot, allModules, {
verbose: config.verbose,
moduleManager: tempModuleManager,
});
if (config.verbose) {
spinner.succeed('Dependencies resolved');
@@ -772,16 +801,90 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
// Install modules with their dependencies
if (config.modules && config.modules.length > 0) {
for (const moduleName of config.modules) {
if (allModules && allModules.length > 0) {
const installedModuleNames = new Set();
for (const moduleName of allModules) {
// Skip if already installed
if (installedModuleNames.has(moduleName)) {
continue;
}
installedModuleNames.add(moduleName);
spinner.start(`Installing module: ${moduleName}...`);
await this.installModuleWithDependencies(moduleName, bmadDir, resolution.byModule[moduleName]);
// Check if this is a custom module
let isCustomModule = false;
let customInfo = null;
if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) {
const { CustomHandler } = require('../custom/handler');
const customHandler = new CustomHandler();
for (const customFile of config.customContent.selectedFiles) {
const info = await customHandler.getCustomInfo(customFile, projectDir);
if (info && info.id === moduleName) {
isCustomModule = true;
customInfo = info;
break;
}
}
}
if (isCustomModule && customInfo) {
// Install custom module using CustomHandler but as a proper module
const { CustomHandler } = require('../custom/handler');
const customHandler = new CustomHandler();
// Install to module directory instead of custom directory
const moduleTargetPath = path.join(bmadDir, moduleName);
await fs.ensureDir(moduleTargetPath);
const result = await customHandler.install(
customInfo.path,
path.join(bmadDir, 'temp-custom'),
{ ...config.coreConfig, ...customInfo.config, _bmadDir: bmadDir },
(filePath) => {
// Track installed files with correct path
const relativePath = path.relative(path.join(bmadDir, 'temp-custom'), filePath);
const finalPath = path.join(moduleTargetPath, relativePath);
this.installedFiles.push(finalPath);
},
);
// Move from temp-custom to actual module directory
const tempCustomPath = path.join(bmadDir, 'temp-custom');
if (await fs.pathExists(tempCustomPath)) {
const customDir = path.join(tempCustomPath, 'custom');
if (await fs.pathExists(customDir)) {
// Move contents to module directory
const items = await fs.readdir(customDir);
for (const item of items) {
const srcPath = path.join(customDir, item);
const destPath = path.join(moduleTargetPath, item);
// If destination exists, remove it first (or we could merge)
if (await fs.pathExists(destPath)) {
await fs.remove(destPath);
}
await fs.move(srcPath, destPath);
}
}
await fs.remove(tempCustomPath);
}
// Create module config
await this.generateModuleConfigs(bmadDir, { [moduleName]: { ...config.coreConfig, ...customInfo.config } });
} else {
// Regular module installation
await this.installModuleWithDependencies(moduleName, bmadDir, resolution.byModule[moduleName]);
}
spinner.succeed(`Module installed: ${moduleName}`);
}
// Install partial modules (only dependencies)
for (const [module, files] of Object.entries(resolution.byModule)) {
if (!config.modules.includes(module) && module !== 'core') {
if (!allModules.includes(module) && module !== 'core') {
const totalFiles =
files.agents.length +
files.tasks.length +
@@ -799,6 +902,11 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
// Install custom content if provided AND selected
// Process custom content that wasn't installed as modules
// This is now handled in the module installation loop above
// This section is kept for backward compatibility with any custom content
// that doesn't have a module structure
const remainingCustomContent = [];
if (
config.customContent &&
config.customContent.hasCustomContent &&
@@ -806,12 +914,26 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
config.customContent.selected &&
config.customContent.selectedFiles
) {
spinner.start('Installing custom content...');
// Filter out custom modules that were already installed
for (const customFile of config.customContent.selectedFiles) {
const { CustomHandler } = require('../custom/handler');
const customHandler = new CustomHandler();
const customInfo = await customHandler.getCustomInfo(customFile, projectDir);
// Skip if this was installed as a module
if (!customInfo || !customInfo.id || !allModules.includes(customInfo.id)) {
remainingCustomContent.push(customFile);
}
}
}
if (remainingCustomContent.length > 0) {
spinner.start('Installing remaining custom content...');
const { CustomHandler } = require('../custom/handler');
const customHandler = new CustomHandler();
// Use the selected files instead of finding all files
const customFiles = config.customContent.selectedFiles;
// Use the remaining files
const customFiles = remainingCustomContent;
if (customFiles.length > 0) {
console.log(chalk.cyan(`\n Found ${customFiles.length} custom content file(s):`));
@@ -867,10 +989,10 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
spinner.start('Generating workflow and agent manifests...');
const manifestGen = new ManifestGenerator();
// Include preserved modules (from quick update) in the manifest
const allModulesToList = config._preserveModules ? [...(config.modules || []), ...config._preserveModules] : config.modules || [];
// Include preserved modules (from quick update) and custom modules in the manifest
const allModulesToList = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules || [];
const manifestStats = await manifestGen.generateManifests(bmadDir, config.modules || [], this.installedFiles, {
const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesToList, this.installedFiles, {
ides: config.ides || [],
preservedModules: config._preserveModules || [], // Scan these from installed bmad/ dir
});

View File

@@ -3,6 +3,7 @@ const fs = require('fs-extra');
const chalk = require('chalk');
const yaml = require('js-yaml');
const { FileOps } = require('../../../lib/file-ops');
const { XmlHandler } = require('../../../lib/xml-handler');
/**
* Handler for custom content (custom.yaml)
@@ -11,6 +12,7 @@ const { FileOps } = require('../../../lib/file-ops');
class CustomHandler {
constructor() {
this.fileOps = new FileOps();
this.xmlHandler = new XmlHandler();
}
/**
@@ -52,6 +54,12 @@ class CustomHandler {
} else if (entry.name === 'custom.yaml') {
// Found a custom.yaml file
customPaths.push(fullPath);
} else if (
entry.name === 'module.yaml' && // Check if this is a custom module (either in _module-installer or in root directory)
// Skip if it's in src/modules (those are standard modules)
!fullPath.includes(path.join('src', 'modules'))
) {
customPaths.push(fullPath);
}
}
} catch {
@@ -66,40 +74,44 @@ class CustomHandler {
}
/**
* Get custom content info from a custom.yaml file
* @param {string} customYamlPath - Path to custom.yaml file
* Get custom content info from a custom.yaml or module.yaml file
* @param {string} configPath - Path to config file
* @param {string} projectRoot - Project root directory for calculating relative paths
* @returns {Object|null} Custom content info
*/
async getCustomInfo(customYamlPath, projectRoot = null) {
async getCustomInfo(configPath, projectRoot = null) {
try {
const configContent = await fs.readFile(customYamlPath, 'utf8');
const configContent = await fs.readFile(configPath, 'utf8');
// Try to parse YAML with error handling
let config;
try {
config = yaml.load(configContent);
} catch (parseError) {
console.warn(chalk.yellow(`Warning: YAML parse error in ${customYamlPath}:`, parseError.message));
console.warn(chalk.yellow(`Warning: YAML parse error in ${configPath}:`, parseError.message));
return null;
}
const customDir = path.dirname(customYamlPath);
// Check if this is an module.yaml (module) or custom.yaml (custom content)
const isInstallConfig = configPath.endsWith('module.yaml');
const configDir = path.dirname(configPath);
// Use provided projectRoot or fall back to process.cwd()
const basePath = projectRoot || process.cwd();
const relativePath = path.relative(basePath, customDir);
const relativePath = path.relative(basePath, configDir);
return {
id: config.code || path.basename(customDir),
name: config.name || `Custom: ${path.basename(customDir)}`,
description: config.description || 'Custom agents and workflows',
path: customDir,
id: config.code || 'unknown-code',
name: config.name,
description: config.description || '',
path: configDir,
relativePath: relativePath,
defaultSelected: config.default_selected === true,
config: config,
isInstallConfig: isInstallConfig, // Track which type this is
};
} catch (error) {
console.warn(chalk.yellow(`Warning: Failed to read ${customYamlPath}:`, error.message));
console.warn(chalk.yellow(`Warning: Failed to read ${configPath}:`, error.message));
return null;
}
}
@@ -131,10 +143,10 @@ class CustomHandler {
await fs.ensureDir(bmadAgentsDir);
await fs.ensureDir(bmadWorkflowsDir);
// Process agents - copy entire agents directory structure
// Process agents - compile and copy agents
const agentsDir = path.join(customPath, 'agents');
if (await fs.pathExists(agentsDir)) {
await this.copyDirectory(agentsDir, bmadAgentsDir, results, fileTrackingCallback, config);
await this.compileAndCopyAgents(agentsDir, bmadAgentsDir, bmadDir, config, fileTrackingCallback, results);
// Count agent files
const agentFiles = await this.findFilesRecursively(agentsDir, ['.agent.yaml', '.md']);
@@ -271,6 +283,114 @@ class CustomHandler {
}
}
}
/**
* Compile .agent.yaml files to .md format and handle sidecars
* @param {string} sourceAgentsPath - Source agents directory
* @param {string} targetAgentsPath - Target agents directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} config - Configuration for placeholder replacement
* @param {Function} fileTrackingCallback - Optional callback to track installed files
* @param {Object} results - Results object to update
*/
async compileAndCopyAgents(sourceAgentsPath, targetAgentsPath, bmadDir, config, fileTrackingCallback, results) {
// Get all .agent.yaml files recursively
const agentFiles = await this.findFilesRecursively(sourceAgentsPath, ['.agent.yaml']);
for (const agentFile of agentFiles) {
const relativePath = path.relative(sourceAgentsPath, agentFile);
const targetDir = path.join(targetAgentsPath, path.dirname(relativePath));
await fs.ensureDir(targetDir);
const agentName = path.basename(agentFile, '.agent.yaml');
const targetMdPath = path.join(targetDir, `${agentName}.md`);
// Use the actual bmadDir if available (for when installing to temp dir)
const actualBmadDir = config._bmadDir || bmadDir;
const customizePath = path.join(actualBmadDir, '_cfg', 'agents', `custom-${agentName}.customize.yaml`);
// Read and compile the YAML
try {
const yamlContent = await fs.readFile(agentFile, 'utf8');
const { compileAgent } = require('../../../lib/agent/compiler');
// Create customize template if it doesn't exist
if (!(await fs.pathExists(customizePath))) {
const { getSourcePath } = require('../../../lib/project-root');
const genericTemplatePath = getSourcePath('utility', 'templates', 'agent.customize.template.yaml');
if (await fs.pathExists(genericTemplatePath)) {
// Copy with placeholder replacement
let templateContent = await fs.readFile(genericTemplatePath, 'utf8');
templateContent = templateContent.replaceAll('{bmad_folder}', config.bmad_folder || 'bmad');
await fs.writeFile(customizePath, templateContent, 'utf8');
console.log(chalk.dim(` Created customize: custom-${agentName}.customize.yaml`));
}
}
// Compile the agent
const { xml } = compileAgent(yamlContent, {}, agentName, relativePath, { config });
// Replace placeholders in the compiled content
let processedXml = xml;
processedXml = processedXml.replaceAll('{bmad_folder}', config.bmad_folder || 'bmad');
processedXml = processedXml.replaceAll('{user_name}', config.user_name || 'User');
processedXml = processedXml.replaceAll('{communication_language}', config.communication_language || 'English');
processedXml = processedXml.replaceAll('{output_folder}', config.output_folder || 'docs');
// Write the compiled MD file
await fs.writeFile(targetMdPath, processedXml, 'utf8');
// Check if agent has sidecar
let hasSidecar = false;
try {
const yamlLib = require('yaml');
const agentYaml = yamlLib.parse(yamlContent);
hasSidecar = agentYaml?.agent?.metadata?.hasSidecar === true;
} catch {
// Continue without sidecar processing
}
// Copy sidecar files if agent has hasSidecar flag
if (hasSidecar && config.agent_sidecar_folder) {
const { copyAgentSidecarFiles } = require('../../../lib/agent/installer');
// Resolve agent sidecar folder path
const projectDir = path.dirname(bmadDir);
const resolvedSidecarFolder = config.agent_sidecar_folder
.replaceAll('{project-root}', projectDir)
.replaceAll('{bmad_folder}', path.basename(bmadDir));
// Create sidecar directory for this agent
const agentSidecarDir = path.join(resolvedSidecarFolder, agentName);
await fs.ensureDir(agentSidecarDir);
// Copy sidecar files
const sidecarResult = copyAgentSidecarFiles(path.dirname(agentFile), agentSidecarDir, agentFile);
if (sidecarResult.copied.length > 0) {
console.log(chalk.dim(` Copied ${sidecarResult.copied.length} sidecar file(s) to: ${agentSidecarDir}`));
}
if (sidecarResult.preserved.length > 0) {
console.log(chalk.dim(` Preserved ${sidecarResult.preserved.length} existing sidecar file(s)`));
}
}
// Track the file
if (fileTrackingCallback) {
fileTrackingCallback(targetMdPath);
}
console.log(
chalk.dim(
` Compiled agent: ${agentName} -> ${path.relative(targetAgentsPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`,
),
);
} catch (error) {
console.warn(chalk.yellow(` Failed to compile agent ${agentName}:`, error.message));
results.errors.push(`Failed to compile agent ${agentName}: ${error.message}`);
}
}
}
}
module.exports = { CustomHandler };

View File

@@ -107,7 +107,7 @@ class ModuleManager {
}
/**
* Find all modules in the project by searching for install-config.yaml files
* Find all modules in the project by searching for module.yaml files
* @returns {Array} List of module paths
*/
async findModulesInProject() {
@@ -144,12 +144,14 @@ class ModuleManager {
continue;
}
// Check if this directory contains a module (install-config.yaml OR custom.yaml)
const installerConfigPath = path.join(fullPath, '_module-installer', 'install-config.yaml');
// Check if this directory contains a module (module.yaml OR custom.yaml)
const moduleConfigPath = path.join(fullPath, 'module.yaml');
const installerConfigPath = path.join(fullPath, '_module-installer', 'module.yaml');
const customConfigPath = path.join(fullPath, '_module-installer', 'custom.yaml');
const rootCustomConfigPath = path.join(fullPath, 'custom.yaml');
if (
(await fs.pathExists(moduleConfigPath)) ||
(await fs.pathExists(installerConfigPath)) ||
(await fs.pathExists(customConfigPath)) ||
(await fs.pathExists(rootCustomConfigPath))
@@ -189,12 +191,17 @@ class ModuleManager {
for (const entry of entries) {
if (entry.isDirectory()) {
const modulePath = path.join(this.modulesSourcePath, entry.name);
// Check for module structure (install-config.yaml OR custom.yaml)
const installerConfigPath = path.join(modulePath, '_module-installer', 'install-config.yaml');
// Check for module structure (module.yaml OR custom.yaml)
const moduleConfigPath = path.join(modulePath, 'module.yaml');
const installerConfigPath = path.join(modulePath, '_module-installer', 'module.yaml');
const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml');
// Skip if this doesn't look like a module
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(customConfigPath))) {
if (
!(await fs.pathExists(moduleConfigPath)) &&
!(await fs.pathExists(installerConfigPath)) &&
!(await fs.pathExists(customConfigPath))
) {
continue;
}
@@ -246,13 +253,16 @@ class ModuleManager {
* @returns {Object|null} Module info or null if not a valid module
*/
async getModuleInfo(modulePath, defaultName, sourceDescription) {
// Check for module structure (install-config.yaml OR custom.yaml)
const installerConfigPath = path.join(modulePath, '_module-installer', 'install-config.yaml');
// Check for module structure (module.yaml OR custom.yaml)
const moduleConfigPath = path.join(modulePath, 'module.yaml');
const installerConfigPath = path.join(modulePath, '_module-installer', 'module.yaml');
const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml');
const rootCustomConfigPath = path.join(modulePath, 'custom.yaml');
let configPath = null;
if (await fs.pathExists(installerConfigPath)) {
if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else if (await fs.pathExists(customConfigPath)) {
configPath = customConfigPath;
@@ -313,10 +323,11 @@ class ModuleManager {
// First, check src/modules
const srcModulePath = path.join(this.modulesSourcePath, moduleName);
if (await fs.pathExists(srcModulePath)) {
// Check if this looks like a module (has install-config.yaml)
const installerConfigPath = path.join(srcModulePath, '_module-installer', 'install-config.yaml');
// Check if this looks like a module (has module.yaml)
const moduleConfigPath = path.join(srcModulePath, 'module.yaml');
const installerConfigPath = path.join(srcModulePath, '_module-installer', 'module.yaml');
if (await fs.pathExists(installerConfigPath)) {
if ((await fs.pathExists(moduleConfigPath)) || (await fs.pathExists(installerConfigPath))) {
return srcModulePath;
}
@@ -338,12 +349,15 @@ class ModuleManager {
// Also check by module ID (not just folder name)
// Need to read configs to match by ID
for (const modulePath of allModulePaths) {
const installerConfigPath = path.join(modulePath, '_module-installer', 'install-config.yaml');
const moduleConfigPath = path.join(modulePath, 'module.yaml');
const installerConfigPath = path.join(modulePath, '_module-installer', 'module.yaml');
const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml');
const rootCustomConfigPath = path.join(modulePath, 'custom.yaml');
let configPath = null;
if (await fs.pathExists(installerConfigPath)) {
if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else if (await fs.pathExists(customConfigPath)) {
configPath = customConfigPath;
@@ -584,7 +598,7 @@ class ModuleManager {
}
// Skip _module-installer directory - it's only needed at install time
if (file.startsWith('_module-installer/')) {
if (file.startsWith('_module-installer/') || file === 'module.yaml') {
continue;
}

View File

@@ -84,8 +84,8 @@ const CLIUtils = {
/**
* Display module configuration header
* @param {string} moduleName - Module name (fallback if no custom header)
* @param {string} header - Custom header from install-config.yaml
* @param {string} subheader - Custom subheader from install-config.yaml
* @param {string} header - Custom header from module.yaml
* @param {string} subheader - Custom subheader from module.yaml
*/
displayModuleConfigHeader(moduleName, header = null, subheader = null) {
// Simple blue banner with custom header/subheader if provided
@@ -100,8 +100,8 @@ const CLIUtils = {
/**
* Display module with no custom configuration
* @param {string} moduleName - Module name (fallback if no custom header)
* @param {string} header - Custom header from install-config.yaml
* @param {string} subheader - Custom subheader from install-config.yaml
* @param {string} header - Custom header from module.yaml
* @param {string} subheader - Custom subheader from module.yaml
*/
displayModuleNoConfig(moduleName, header = null, subheader = null) {
// Show full banner with header/subheader, just like modules with config

View File

@@ -142,8 +142,19 @@ class UI {
if (selectedCustomContent.length > 0) {
customContentConfig.selected = true;
customContentConfig.selectedFiles = selectedCustomContent.map((mod) => mod.replace('__CUSTOM_CONTENT__', ''));
// Filter out custom content markers since they're not real modules
selectedModules = selectedModules.filter((mod) => !mod.startsWith('__CUSTOM_CONTENT__'));
// Convert custom content to module IDs for installation
const customContentModuleIds = [];
const { CustomHandler } = require('../installers/lib/custom/handler');
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];
} else if (customContentConfig.hasCustomContent) {
// User provided custom content but didn't select any
customContentConfig.selected = false;
@@ -669,7 +680,7 @@ class UI {
*/
async promptCustomContentLocation() {
try {
CLIUtils.displaySection('Custom Content', 'Optional: Add custom agents and workflows');
CLIUtils.displaySection('Custom Content', 'Optional: Add custom agents, workflows, and modules');
const { hasCustomContent } = await inquirer.prompt([
{
@@ -703,7 +714,7 @@ class UI {
{
type: 'input',
name: 'directory',
message: 'Enter the path to your custom content 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() === '') {
@@ -736,7 +747,7 @@ class UI {
const customFiles = await customHandler.findCustomContent(expandedPath);
if (customFiles.length === 0) {
console.log(chalk.yellow(`\nNo custom.yaml files found in ${expandedPath}`));
console.log(chalk.yellow(`\nNo custom content found in ${expandedPath}`));
const { tryAgain } = await inquirer.prompt([
{
@@ -755,7 +766,12 @@ class UI {
}
customPath = expandedPath;
console.log(chalk.green(`\n✓ Found ${customFiles.length} custom content file(s)`));
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 };