From 48795d46dec260bf733e2261bb5e522ec225e8ad Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Mon, 15 Dec 2025 19:16:03 +0800 Subject: [PATCH] core and custom modules all install through the same flow now --- tools/cli/commands/list.js | 14 +- tools/cli/installers/lib/core/installer.js | 173 ++---------------- .../installers/lib/core/manifest-generator.js | 33 +--- tools/cli/installers/lib/core/manifest.js | 7 +- tools/cli/installers/lib/modules/manager.js | 91 ++++----- 5 files changed, 76 insertions(+), 242 deletions(-) diff --git a/tools/cli/commands/list.js b/tools/cli/commands/list.js index 601ff709..de2bd465 100644 --- a/tools/cli/commands/list.js +++ b/tools/cli/commands/list.js @@ -9,7 +9,9 @@ module.exports = { options: [], action: async () => { try { - const modules = await installer.getAvailableModules(); + const result = await installer.getAvailableModules(); + const { modules, customModules } = result; + console.log(chalk.cyan('\n📦 Available BMAD Modules:\n')); for (const module of modules) { @@ -19,6 +21,16 @@ module.exports = { console.log(); } + if (customModules && customModules.length > 0) { + console.log(chalk.cyan('\n🔧 Custom Modules:\n')); + for (const module of customModules) { + console.log(chalk.bold(` ${module.id}`)); + console.log(chalk.dim(` ${module.description}`)); + console.log(chalk.dim(` Version: ${module.version}`)); + console.log(); + } + } + process.exit(0); } catch (error) { console.error(chalk.red('Error:'), error.message); diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index 5d126ad7..9b37a523 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -900,103 +900,35 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice: } if (isCustomModule && customInfo) { - // Install custom module using CustomHandler but as a proper module - const customHandler = new CustomHandler(); - - // Install to module directory instead of custom directory - const moduleTargetPath = path.join(bmadDir, moduleName); - await fs.ensureDir(moduleTargetPath); + // Custom modules are now installed via ModuleManager just like standard modules + // The custom module path should already be in customModulePaths from earlier setup + if (!customModulePaths.has(moduleName) && customInfo.path) { + customModulePaths.set(moduleName, customInfo.path); + this.moduleManager.setCustomModulePaths(customModulePaths); + } // Get collected config for this custom module (from module.yaml prompts) const collectedModuleConfig = moduleConfigs[moduleName] || {}; - const result = await customHandler.install( - customInfo.path, - path.join(bmadDir, 'temp-custom'), - { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig, _bmadDir: bmadDir }, + // Use ModuleManager to install the custom module + await this.moduleManager.install( + moduleName, + 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); + this.installedFiles.push(filePath); + }, + { + isCustom: true, + moduleConfig: collectedModuleConfig, }, ); - // 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); - const movedItems = []; - try { - 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); - movedItems.push({ src: srcPath, dest: destPath }); - } - } catch (moveError) { - // Rollback: restore any successfully moved items - for (const moved of movedItems) { - try { - await fs.move(moved.dest, moved.src); - } catch { - // Best-effort rollback - log if it fails - console.error(`Failed to rollback ${moved.dest} during cleanup`); - } - } - throw new Error(`Failed to move custom module files: ${moveError.message}`); - } - } - try { - await fs.remove(tempCustomPath); - } catch (cleanupError) { - // Non-fatal: temp directory cleanup failed but files were moved successfully - console.warn(`Warning: Could not clean up temp directory: ${cleanupError.message}`); - } - } + // ModuleManager installs directly to the target directory, no need to move files // Create module config (include collected config from module.yaml prompts) await this.generateModuleConfigs(bmadDir, { [moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig }, }); - - // Store custom module info for later manifest update - if (!config._customModulesToTrack) { - config._customModulesToTrack = []; - } - - // For cached modules, use appropriate path handling - let sourcePath; - if (useCache) { - // Check if we have cached modules info (from initial install) - if (finalCustomContent && finalCustomContent.cachedModules) { - sourcePath = finalCustomContent.cachedModules.find((m) => m.id === moduleName)?.relativePath; - } else { - // During update, the sourcePath is already cache-relative if it starts with _config - sourcePath = - customInfo.sourcePath && customInfo.sourcePath.startsWith('_config') - ? customInfo.sourcePath - : path.relative(bmadDir, customInfo.path || customInfo.sourcePath); - } - } else { - sourcePath = path.resolve(customInfo.path || customInfo.sourcePath); - } - - config._customModulesToTrack.push({ - id: customInfo.id, - name: customInfo.name, - sourcePath: useCache ? `_config/custom/${customInfo.id}` : sourcePath, - installDate: new Date().toISOString(), - }); } else { // Regular module installation // Special case for core module @@ -1029,69 +961,7 @@ 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 && - config.customContent.customPath && - config.customContent.selected && - config.customContent.selectedFiles - ) { - // Filter out custom modules that were already installed - const customHandler = new CustomHandler(); - for (const customFile of config.customContent.selectedFiles) { - 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 = new CustomHandler(); - - // Use the remaining files - const customFiles = remainingCustomContent; - - if (customFiles.length > 0) { - console.log(chalk.cyan(`\n Found ${customFiles.length} custom content file(s):`)); - for (const customFile of customFiles) { - const customInfo = await customHandler.getCustomInfo(customFile, projectDir); - if (customInfo) { - console.log(chalk.dim(` • ${customInfo.name} (${customInfo.relativePath})`)); - - // Install the custom content - const result = await customHandler.install( - customInfo.path, - bmadDir, - { ...config.coreConfig, ...customInfo.config }, - (filePath) => { - // Track installed files - this.installedFiles.push(filePath); - }, - ); - - if (result.errors.length > 0) { - console.log(chalk.yellow(` ⚠️ ${result.errors.length} error(s) occurred`)); - for (const error of result.errors) { - console.log(chalk.dim(` - ${error}`)); - } - } else { - console.log(chalk.green(` ✓ Installed ${result.agentsInstalled} agents, ${result.workflowsInstalled} workflows`)); - } - } - } - } - spinner.succeed('Custom content installed'); - } + // All content is now installed as modules - no separate custom content handling needed // Generate clean config.yaml files for each installed module spinner.start('Generating module configurations...'); @@ -1136,16 +1006,9 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice: const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, this.installedFiles, { ides: config.ides || [], preservedModules: modulesForCsvPreserve, // Scan these from installed bmad/ dir - customModules: config._customModulesToTrack || [], // Custom modules to exclude from regular modules list }); - // Add custom modules to manifest (now that it exists) - if (config._customModulesToTrack && config._customModulesToTrack.length > 0) { - spinner.text = 'Storing custom module sources...'; - for (const customModule of config._customModulesToTrack) { - await this.manifest.addCustomModule(bmadDir, customModule); - } - } + // Custom modules are now included in the main modules list - no separate tracking needed spinner.succeed( `Manifests generated: ${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools, ${manifestStats.files} files`, diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index a1308d3a..2de9c2cf 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -34,24 +34,21 @@ class ManifestGenerator { // Store modules list (all modules including preserved ones) const preservedModules = options.preservedModules || []; - const customModules = options.customModules || []; // Scan the bmad directory to find all actually installed modules const installedModules = await this.scanInstalledModules(bmadDir); - // Filter out custom modules from the regular modules list - const customModuleIds = new Set(customModules.map((cm) => cm.id)); - const regularModules = [...new Set(['core', ...selectedModules, ...preservedModules, ...installedModules])].filter( - (module) => !customModuleIds.has(module), - ); + // Since custom modules are now installed the same way as regular modules, + // we don't need to exclude them from manifest generation + const allModules = [...new Set(['core', ...selectedModules, ...preservedModules, ...installedModules])]; - this.modules = regularModules; - this.updatedModules = [...new Set(['core', ...selectedModules, ...installedModules])].filter((module) => !customModuleIds.has(module)); // Also exclude custom modules from rescanning + this.modules = allModules; + this.updatedModules = allModules; // Include ALL modules (including custom) for scanning // For CSV manifests, we need to include ALL modules that are installed // preservedModules controls which modules stay as-is in the CSV (don't get rescanned) // But all modules should be included in the final manifest - this.preservedModules = [...new Set([...preservedModules, ...selectedModules, ...installedModules])]; // Include all installed modules + this.preservedModules = allModules; // Include ALL modules (including custom) this.bmadDir = bmadDir; this.bmadFolderName = path.basename(bmadDir); // Get the actual folder name (e.g., '_bmad' or 'bmad') this.allInstalledFiles = installedFiles; @@ -460,29 +457,13 @@ class ManifestGenerator { async writeMainManifest(cfgDir) { const manifestPath = path.join(cfgDir, 'manifest.yaml'); - // Read existing manifest to preserve custom modules - let existingCustomModules = []; - if (await fs.pathExists(manifestPath)) { - try { - const existingContent = await fs.readFile(manifestPath, 'utf8'); - const existingManifest = yaml.parse(existingContent); - if (existingManifest && existingManifest.customModules) { - existingCustomModules = existingManifest.customModules; - } - } catch { - // If we can't read the existing manifest, continue without preserving custom modules - console.warn('Warning: Could not read existing manifest to preserve custom modules'); - } - } - const manifest = { installation: { version: packageJson.version, installDate: new Date().toISOString(), lastUpdated: new Date().toISOString(), }, - modules: this.modules, - customModules: existingCustomModules, // Preserve custom modules + modules: this.modules, // Include ALL modules (standard and custom) ides: this.selectedIdes, }; diff --git a/tools/cli/installers/lib/core/manifest.js b/tools/cli/installers/lib/core/manifest.js index 24490694..643a945c 100644 --- a/tools/cli/installers/lib/core/manifest.js +++ b/tools/cli/installers/lib/core/manifest.js @@ -62,8 +62,8 @@ class Manifest { version: manifestData.installation?.version, installDate: manifestData.installation?.installDate, lastUpdated: manifestData.installation?.lastUpdated, - modules: manifestData.modules || [], - customModules: manifestData.customModules || [], + modules: manifestData.modules || [], // All modules (standard and custom) + customModules: manifestData.customModules || [], // Keep for backward compatibility ides: manifestData.ides || [], }; } catch (error) { @@ -95,8 +95,7 @@ class Manifest { installDate: manifest.installDate, lastUpdated: manifest.lastUpdated, }, - modules: manifest.modules || [], - customModules: manifest.customModules || [], + modules: manifest.modules || [], // All modules (standard and custom) ides: manifest.ides || [], }; diff --git a/tools/cli/installers/lib/modules/manager.js b/tools/cli/installers/lib/modules/manager.js index 5adf7f86..ce01e538 100644 --- a/tools/cli/installers/lib/modules/manager.js +++ b/tools/cli/installers/lib/modules/manager.js @@ -343,71 +343,50 @@ class ModuleManager { /** * Find the source path for a module by searching all possible locations - * @param {string} moduleName - Name of the module to find + * @param {string} moduleCode - Code of the module to find (from module.yaml) * @returns {string|null} Path to the module source or null if not found */ - async findModuleSource(moduleName) { + async findModuleSource(moduleCode) { const projectRoot = getProjectRoot(); // First check custom module paths if they exist - if (this.customModulePaths && this.customModulePaths.has(moduleName)) { - return this.customModulePaths.get(moduleName); + if (this.customModulePaths && this.customModulePaths.has(moduleCode)) { + return this.customModulePaths.get(moduleCode); } - // First, check src/modules - const srcModulePath = path.join(this.modulesSourcePath, moduleName); - if (await fs.pathExists(srcModulePath)) { - // 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'); + // Search in src/modules by READING module.yaml files to match by code + if (await fs.pathExists(this.modulesSourcePath)) { + const entries = await fs.readdir(this.modulesSourcePath, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory()) { + const modulePath = path.join(this.modulesSourcePath, entry.name); - if ((await fs.pathExists(moduleConfigPath)) || (await fs.pathExists(installerConfigPath))) { - return srcModulePath; - } + // Read module.yaml to get the code + 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'); - // Also check for custom.yaml in src/modules/_module-installer - const customConfigPath = path.join(srcModulePath, '_module-installer', 'custom.yaml'); - if (await fs.pathExists(customConfigPath)) { - return srcModulePath; - } - } - - // If not found in src/modules, search the entire project - const allModulePaths = await this.findModulesInProject(); - for (const modulePath of allModulePaths) { - if (path.basename(modulePath) === moduleName) { - return modulePath; - } - } - - // Also check by module ID (not just folder name) - // Need to read configs to match by ID - for (const modulePath of allModulePaths) { - 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(moduleConfigPath)) { - configPath = moduleConfigPath; - } else if (await fs.pathExists(installerConfigPath)) { - configPath = installerConfigPath; - } else if (await fs.pathExists(customConfigPath)) { - configPath = customConfigPath; - } else if (await fs.pathExists(rootCustomConfigPath)) { - configPath = rootCustomConfigPath; - } - - if (configPath) { - try { - const configContent = await fs.readFile(configPath, 'utf8'); - const config = yaml.parse(configContent); - if (config.code === moduleName) { - return modulePath; + let configPath = null; + if (await fs.pathExists(moduleConfigPath)) { + configPath = moduleConfigPath; + } else if (await fs.pathExists(installerConfigPath)) { + configPath = installerConfigPath; + } else if (await fs.pathExists(customConfigPath)) { + configPath = customConfigPath; + } + + if (configPath) { + try { + const configContent = await fs.readFile(configPath, 'utf8'); + const config = yaml.parse(configContent); + if (config.code === moduleCode) { + return modulePath; + } + } catch (error) { + // Continue to next module if parse fails + console.warn(`Warning: Failed to parse module config at ${configPath}: ${error.message}`); + } } - } catch (error) { - throw new Error(`Failed to parse module.yaml at ${configPath}: ${error.message}`); } } } @@ -417,7 +396,7 @@ class ModuleManager { /** * Install a module - * @param {string} moduleName - Name of the module to install + * @param {string} moduleName - Code of the module to install (from module.yaml) * @param {string} bmadDir - Target bmad directory * @param {Function} fileTrackingCallback - Optional callback to track installed files * @param {Object} options - Additional installation options