diff --git a/tools/cli/installers/lib/core/config-collector.js b/tools/cli/installers/lib/core/config-collector.js index e8569cd0f..de69873fd 100644 --- a/tools/cli/installers/lib/core/config-collector.js +++ b/tools/cli/installers/lib/core/config-collector.js @@ -10,19 +10,19 @@ class ConfigCollector { this.collectedConfig = {}; this.existingConfig = null; this.currentProjectDir = null; - this._moduleManagerInstance = null; + this._officialModulesInstance = null; } /** - * Get or create a cached ModuleManager instance (lazy initialization) - * @returns {Object} ModuleManager instance + * Get or create a cached OfficialModules instance (lazy initialization) + * @returns {Object} OfficialModules instance */ - _getModuleManager() { - if (!this._moduleManagerInstance) { - const { ModuleManager } = require('../modules/manager'); - this._moduleManagerInstance = new ModuleManager(); + _getOfficialModules() { + if (!this._officialModulesInstance) { + const { OfficialModules } = require('../modules/official-modules'); + this._officialModulesInstance = new OfficialModules(); } - return this._moduleManagerInstance; + return this._officialModulesInstance; } /** @@ -153,7 +153,7 @@ class ConfigCollector { const results = []; for (const moduleName of modules) { - // Resolve module.yaml path - custom paths first, then standard location, then ModuleManager search + // Resolve module.yaml path - custom paths first, then standard location, then OfficialModules search let moduleConfigPath = null; const customPath = this.customModulePaths?.get(moduleName); if (customPath) { @@ -163,7 +163,7 @@ class ConfigCollector { if (await fs.pathExists(standardPath)) { moduleConfigPath = standardPath; } else { - const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true }); + const moduleSourcePath = await this._getOfficialModules().findModuleSource(moduleName, { silent: true }); if (moduleSourcePath) { moduleConfigPath = path.join(moduleSourcePath, 'module.yaml'); } @@ -364,7 +364,7 @@ class ConfigCollector { // If not found in src/modules, we need to find it by searching the project if (!(await fs.pathExists(moduleConfigPath))) { - const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true }); + const moduleSourcePath = await this._getOfficialModules().findModuleSource(moduleName, { silent: true }); if (moduleSourcePath) { moduleConfigPath = path.join(moduleSourcePath, 'module.yaml'); @@ -378,7 +378,7 @@ class ConfigCollector { configPath = moduleConfigPath; } else { // Check if this is a custom module with custom.yaml - const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true }); + const moduleSourcePath = await this._getOfficialModules().findModuleSource(moduleName, { silent: true }); if (moduleSourcePath) { const rootCustomConfigPath = path.join(moduleSourcePath, 'custom.yaml'); @@ -674,7 +674,7 @@ class ConfigCollector { // If not found in src/modules or custom paths, search the project if (!(await fs.pathExists(moduleConfigPath))) { - const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true }); + const moduleSourcePath = await this._getOfficialModules().findModuleSource(moduleName, { silent: true }); if (moduleSourcePath) { moduleConfigPath = path.join(moduleSourcePath, 'module.yaml'); diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index ab3f0b3e5..071e7dd33 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -2,7 +2,8 @@ const path = require('node:path'); const fs = require('fs-extra'); const { Detector } = require('./detector'); const { Manifest } = require('./manifest'); -const { ModuleManager } = require('../modules/manager'); +const { OfficialModules } = require('../modules/official-modules'); +const { CustomModules } = require('../modules/custom-modules'); const { IdeManager } = require('../ide/manager'); const { FileOps } = require('../../../lib/file-ops'); const { Config } = require('../../../lib/config'); @@ -22,7 +23,8 @@ class Installer { this.externalModuleManager = new ExternalModuleManager(); this.detector = new Detector(); this.manifest = new Manifest(); - this.moduleManager = new ModuleManager(); + this.officialModules = new OfficialModules(); + this.customModules = new CustomModules(); this.ideManager = new IdeManager(); this.fileOps = new FileOps(); this.config = new Config(); @@ -60,7 +62,7 @@ class Installer { const customModulePaths = await this._discoverCustomModulePaths(config, paths); // Wire configs into managers - this.moduleManager.setCustomModulePaths(customModulePaths); + this.customModules.setPaths(customModulePaths); this.ideManager.setBmadFolderName(BMAD_FOLDER_NAME); // Tool selection will be collected after we determine if it's a reinstall/update/new install @@ -220,7 +222,7 @@ class Installer { } // Update module manager with the new custom module paths from cache - this.moduleManager.setCustomModulePaths(customModulePaths); + this.customModules.setPaths(customModulePaths); } // If there are custom files, back them up temporarily @@ -304,7 +306,7 @@ class Installer { } // Update module manager with the new custom module paths from cache - this.moduleManager.setCustomModulePaths(customModulePaths); + this.customModules.setPaths(customModulePaths); } // Back up custom files @@ -494,18 +496,14 @@ class Installer { } // Update module manager with the cached paths - this.moduleManager.setCustomModulePaths(customModulePaths); + this.customModules.setPaths(customModulePaths); addResult('Custom modules cached', 'ok'); } // Custom content is already handled in UI before module selection const finalCustomContent = config.customContent; - // Official modules to install (filter out core — handled separately by installCore) - const officialModules = config.installCore ? (config.modules || []).filter((m) => m !== 'core') : [...(config.modules || [])]; - - // Build combined list for manifest generation and IDE setup - const allModules = [...officialModules]; + // Build custom module ID set first (needed to filter official list) const customModuleIds = new Set(); for (const id of customModulePaths.keys()) { customModuleIds.add(id); @@ -531,6 +529,11 @@ class Installer { } } } + // Official modules: from config.modules, excluding core (handled separately) and custom modules + const officialModules = (config.modules || []).filter((m) => !(config.installCore && m === 'core') && !customModuleIds.has(m)); + + // Combined list for manifest generation and IDE setup + const allModules = [...officialModules]; for (const id of customModuleIds) { if (!allModules.includes(id)) { allModules.push(id); @@ -608,7 +611,7 @@ class Installer { // Core module directories if (config.installCore) { - const result = await this.moduleManager.createModuleDirectories('core', paths.bmadDir, { + const result = await this.officialModules.createModuleDirectories('core', paths.bmadDir, { installedIDEs: config.ides || [], moduleConfig: moduleConfigs.core || {}, existingModuleConfig: this.configCollector.existingConfig?.core || {}, @@ -627,7 +630,7 @@ class Installer { if (config.modules && config.modules.length > 0) { for (const moduleName of config.modules) { message(`Setting up ${moduleName}...`); - const result = await this.moduleManager.createModuleDirectories(moduleName, paths.bmadDir, { + const result = await this.officialModules.createModuleDirectories(moduleName, paths.bmadDir, { installedIDEs: config.ides || [], moduleConfig: moduleConfigs[moduleName] || {}, existingModuleConfig: this.configCollector.existingConfig?.[moduleName] || {}, @@ -1012,7 +1015,7 @@ class Installer { await this.installCore(paths.bmadDir); } else { const moduleConfig = this.configCollector.collectedConfig[moduleName] || {}; - await this.moduleManager.install( + await this.officialModules.install( moduleName, paths.bmadDir, (filePath) => { @@ -1096,11 +1099,11 @@ class Installer { if (!customModulePaths.has(moduleName) && customInfo.path) { customModulePaths.set(moduleName, customInfo.path); - this.moduleManager.setCustomModulePaths(customModulePaths); + this.customModules.setPaths(customModulePaths); } const collectedModuleConfig = moduleConfigs[moduleName] || {}; - await this.moduleManager.install( + await this.officialModules.install( moduleName, paths.bmadDir, (filePath) => { @@ -1112,6 +1115,7 @@ class Installer { isQuickUpdate: isQuickUpdate, installer: this, silent: true, + sourcePath: customInfo.path, }, ); await this.generateModuleConfigs(paths.bmadDir, { @@ -1860,7 +1864,7 @@ class Installer { const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir); // Get available modules (what we have source for) - const availableModulesData = await this.moduleManager.listAvailable(); + const availableModulesData = await this.officialModules.listAvailable(); const availableModules = [...availableModulesData.modules, ...availableModulesData.customModules]; // Add external official modules to available modules @@ -2125,7 +2129,7 @@ class Installer { for (const module of existingInstall.modules) { spinner.message(`Updating module: ${module.id}...`); - await this.moduleManager.update(module.id, bmadDir, config.force, { installer: this }); + await this.officialModules.update(module.id, bmadDir, config.force, { installer: this }); } // Update manifest @@ -2265,7 +2269,7 @@ class Installer { * Get available modules */ async getAvailableModules() { - return await this.moduleManager.listAvailable(); + return await this.officialModules.listAvailable(); } /** diff --git a/tools/cli/installers/lib/modules/custom-modules.js b/tools/cli/installers/lib/modules/custom-modules.js new file mode 100644 index 000000000..4832c4b69 --- /dev/null +++ b/tools/cli/installers/lib/modules/custom-modules.js @@ -0,0 +1,23 @@ +class CustomModules { + constructor() { + this.paths = new Map(); + } + + setPaths(customModulePaths) { + this.paths = customModulePaths; + } + + has(moduleCode) { + return this.paths.has(moduleCode); + } + + get(moduleCode) { + return this.paths.get(moduleCode); + } + + set(moduleId, sourcePath) { + this.paths.set(moduleId, sourcePath); + } +} + +module.exports = { CustomModules }; diff --git a/tools/cli/installers/lib/modules/manager.js b/tools/cli/installers/lib/modules/official-modules.js similarity index 95% rename from tools/cli/installers/lib/modules/manager.js rename to tools/cli/installers/lib/modules/official-modules.js index e862df93a..a2c91d3df 100644 --- a/tools/cli/installers/lib/modules/manager.js +++ b/tools/cli/installers/lib/modules/official-modules.js @@ -5,18 +5,9 @@ const prompts = require('../../../lib/prompts'); const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); const { ExternalModuleManager } = require('./external-manager'); -class ModuleManager { +class OfficialModules { constructor(options = {}) { this.externalModuleManager = new ExternalModuleManager(); - this.customModulePaths = new Map(); - } - - /** - * Set custom module paths for priority lookup - * @param {Map} customModulePaths - Map of module ID to source path - */ - setCustomModulePaths(customModulePaths) { - this.customModulePaths = customModulePaths; } /** @@ -25,7 +16,7 @@ class ModuleManager { * @param {string} targetPath - Target file path * @param {boolean} overwrite - Whether to overwrite existing files (default: true) */ - async copyFileWithPlaceholderReplacement(sourcePath, targetPath, overwrite = true) { + async copyFile(sourcePath, targetPath, overwrite = true) { await fs.copy(sourcePath, targetPath, { overwrite }); } @@ -35,7 +26,7 @@ class ModuleManager { * @param {string} targetDir - Target directory path * @param {boolean} overwrite - Whether to overwrite existing files (default: true) */ - async copyDirectoryWithPlaceholderReplacement(sourceDir, targetDir, overwrite = true) { + async copyDirectory(sourceDir, targetDir, overwrite = true) { await fs.ensureDir(targetDir); const entries = await fs.readdir(sourceDir, { withFileTypes: true }); @@ -44,9 +35,9 @@ class ModuleManager { const targetPath = path.join(targetDir, entry.name); if (entry.isDirectory()) { - await this.copyDirectoryWithPlaceholderReplacement(sourcePath, targetPath, overwrite); + await this.copyDirectory(sourcePath, targetPath, overwrite); } else { - await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, overwrite); + await this.copyFile(sourcePath, targetPath, overwrite); } } } @@ -143,9 +134,12 @@ class ModuleManager { async findModuleSource(moduleCode, options = {}) { const projectRoot = getProjectRoot(); - // First check custom module paths if they exist - if (this.customModulePaths && this.customModulePaths.has(moduleCode)) { - return this.customModulePaths.get(moduleCode); + // Check for core module (directly under src/core-skills) + if (moduleCode === 'core') { + const corePath = getSourcePath('core-skills'); + if (await fs.pathExists(corePath)) { + return corePath; + } } // Check for built-in bmm module (directly under src/bmm-skills) @@ -176,7 +170,7 @@ class ModuleManager { * @param {Object} options.logger - Logger instance for output */ async install(moduleName, bmadDir, fileTrackingCallback = null, options = {}) { - const sourcePath = await this.findModuleSource(moduleName, { silent: options.silent }); + const sourcePath = options.sourcePath || (await this.findModuleSource(moduleName, { silent: options.silent })); const targetPath = path.join(bmadDir, moduleName); // Check if source module exists @@ -397,7 +391,7 @@ class ModuleManager { } // Copy the file with placeholder replacement - await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile); + await this.copyFile(sourceFile, targetFile); // Track the file if callback provided if (fileTrackingCallback) { @@ -652,7 +646,7 @@ class ModuleManager { } // Copy file with placeholder replacement - await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile); + await this.copyFile(sourceFile, targetFile); } } @@ -681,4 +675,4 @@ class ModuleManager { } } -module.exports = { ModuleManager }; +module.exports = { OfficialModules }; diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index ef64f95b7..311280976 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -935,9 +935,9 @@ class UI { } // Add official modules - const { ModuleManager } = require('../installers/lib/modules/manager'); - const moduleManager = new ModuleManager(); - const { modules: availableModules, customModules: customModulesFromCache } = await moduleManager.listAvailable(); + const { OfficialModules } = require('../installers/lib/modules/official-modules'); + const officialModules = new OfficialModules(); + const { modules: availableModules, customModules: customModulesFromCache } = await officialModules.listAvailable(); // First, add all items to appropriate sections const allCustomModules = []; @@ -992,9 +992,9 @@ class UI { * @returns {Array} Selected module codes (excluding core) */ async selectAllModules(installedModuleIds = new Set()) { - const { ModuleManager } = require('../installers/lib/modules/manager'); - const moduleManager = new ModuleManager(); - const { modules: localModules } = await moduleManager.listAvailable(); + const { OfficialModules } = require('../installers/lib/modules/official-modules'); + const officialModulesSource = new OfficialModules(); + const { modules: localModules } = await officialModulesSource.listAvailable(); // Get external modules const externalManager = new ExternalModuleManager(); @@ -1089,9 +1089,9 @@ class UI { * @returns {Array} Default module codes */ async getDefaultModules(installedModuleIds = new Set()) { - const { ModuleManager } = require('../installers/lib/modules/manager'); - const moduleManager = new ModuleManager(); - const { modules: localModules } = await moduleManager.listAvailable(); + const { OfficialModules } = require('../installers/lib/modules/official-modules'); + const officialModules = new OfficialModules(); + const { modules: localModules } = await officialModules.listAvailable(); const defaultModules = [];