refactor(installer): split ModuleManager into OfficialModules and CustomModules

OfficialModules handles source resolution (core, bmm, external),
module install/update/remove, directory creation, and file copying.
CustomModules holds the custom module paths Map. findModuleSource
now knows core explicitly. install() accepts optional sourcePath
for custom modules that already know their source.
This commit is contained in:
Alex Verkhovsky 2026-03-21 04:35:47 -06:00
parent 2a9df6377e
commit ea8c076e29
5 changed files with 83 additions and 62 deletions

View File

@ -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');

View File

@ -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();
}
/**

View File

@ -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 };

View File

@ -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<string, string>} 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 };

View File

@ -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 = [];