refactor(installer): split module install loop into official and custom passes

Extract _installOfficialModules and _installCustomModules from the
interleaved module installation loop. Each method works from its own
source list, eliminating the allModules merge-then-re-split pattern.
Remove unused destructuring of paths into local variables.
This commit is contained in:
Alex Verkhovsky 2026-03-21 03:59:23 -06:00
parent eade619d17
commit fba77e3e89
1 changed files with 202 additions and 146 deletions

View File

@ -50,7 +50,6 @@ class Installer {
} }
const paths = await InstallPaths.create(config); const paths = await InstallPaths.create(config);
const { projectRoot, bmadDir, srcDir } = paths;
// Collect configurations for official modules // Collect configurations for official modules
const moduleConfigs = await this._collectConfigs(config, paths); const moduleConfigs = await this._collectConfigs(config, paths);
@ -72,7 +71,7 @@ class Installer {
try { try {
// Check existing installation // Check existing installation
spinner.message('Checking for existing installation...'); spinner.message('Checking for existing installation...');
const existingInstall = await this.detector.detect(bmadDir); const existingInstall = await this.detector.detect(paths.bmadDir);
if (existingInstall.installed && !config.force && !config._quickUpdate) { if (existingInstall.installed && !config.force && !config._quickUpdate) {
spinner.stop('Existing installation detected'); spinner.stop('Existing installation detected');
@ -87,7 +86,7 @@ class Installer {
} else { } else {
// Fallback: Ask the user (backwards compatibility for other code paths) // Fallback: Ask the user (backwards compatibility for other code paths)
await prompts.log.warn('Existing BMAD installation detected'); await prompts.log.warn('Existing BMAD installation detected');
await prompts.log.message(` Location: ${bmadDir}`); await prompts.log.message(` Location: ${paths.bmadDir}`);
await prompts.log.message(` Version: ${existingInstall.version}`); await prompts.log.message(` Version: ${existingInstall.version}`);
const promptResult = await this.promptUpdateAction(); const promptResult = await this.promptUpdateAction();
@ -162,8 +161,8 @@ class Installer {
} }
// Detect custom and modified files BEFORE updating (compare current files vs files-manifest.csv) // Detect custom and modified files BEFORE updating (compare current files vs files-manifest.csv)
const existingFilesManifest = await this.readFilesManifest(bmadDir); const existingFilesManifest = await this.readFilesManifest(paths.bmadDir);
const { customFiles, modifiedFiles } = await this.detectCustomFiles(bmadDir, existingFilesManifest); const { customFiles, modifiedFiles } = await this.detectCustomFiles(paths.bmadDir, existingFilesManifest);
config._customFiles = customFiles; config._customFiles = customFiles;
config._modifiedFiles = modifiedFiles; config._modifiedFiles = modifiedFiles;
@ -226,12 +225,12 @@ class Installer {
// If there are custom files, back them up temporarily // If there are custom files, back them up temporarily
if (customFiles.length > 0) { if (customFiles.length > 0) {
const tempBackupDir = path.join(projectRoot, '_bmad-custom-backup-temp'); const tempBackupDir = path.join(paths.projectRoot, '_bmad-custom-backup-temp');
await fs.ensureDir(tempBackupDir); await fs.ensureDir(tempBackupDir);
spinner.start(`Backing up ${customFiles.length} custom files...`); spinner.start(`Backing up ${customFiles.length} custom files...`);
for (const customFile of customFiles) { for (const customFile of customFiles) {
const relativePath = path.relative(bmadDir, customFile); const relativePath = path.relative(paths.bmadDir, customFile);
const backupPath = path.join(tempBackupDir, relativePath); const backupPath = path.join(tempBackupDir, relativePath);
await fs.ensureDir(path.dirname(backupPath)); await fs.ensureDir(path.dirname(backupPath));
await fs.copy(customFile, backupPath); await fs.copy(customFile, backupPath);
@ -243,12 +242,12 @@ class Installer {
// For modified files, back them up to temp directory (will be restored as .bak files after install) // For modified files, back them up to temp directory (will be restored as .bak files after install)
if (modifiedFiles.length > 0) { if (modifiedFiles.length > 0) {
const tempModifiedBackupDir = path.join(projectRoot, '_bmad-modified-backup-temp'); const tempModifiedBackupDir = path.join(paths.projectRoot, '_bmad-modified-backup-temp');
await fs.ensureDir(tempModifiedBackupDir); await fs.ensureDir(tempModifiedBackupDir);
spinner.start(`Backing up ${modifiedFiles.length} modified files...`); spinner.start(`Backing up ${modifiedFiles.length} modified files...`);
for (const modifiedFile of modifiedFiles) { for (const modifiedFile of modifiedFiles) {
const relativePath = path.relative(bmadDir, modifiedFile.path); const relativePath = path.relative(paths.bmadDir, modifiedFile.path);
const tempBackupPath = path.join(tempModifiedBackupDir, relativePath); const tempBackupPath = path.join(tempModifiedBackupDir, relativePath);
await fs.ensureDir(path.dirname(tempBackupPath)); await fs.ensureDir(path.dirname(tempBackupPath));
await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true }); await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true });
@ -265,8 +264,8 @@ class Installer {
config._existingInstall = existingInstall; config._existingInstall = existingInstall;
// Detect custom and modified files BEFORE updating // Detect custom and modified files BEFORE updating
const existingFilesManifest = await this.readFilesManifest(bmadDir); const existingFilesManifest = await this.readFilesManifest(paths.bmadDir);
const { customFiles, modifiedFiles } = await this.detectCustomFiles(bmadDir, existingFilesManifest); const { customFiles, modifiedFiles } = await this.detectCustomFiles(paths.bmadDir, existingFilesManifest);
config._customFiles = customFiles; config._customFiles = customFiles;
config._modifiedFiles = modifiedFiles; config._modifiedFiles = modifiedFiles;
@ -310,12 +309,12 @@ class Installer {
// Back up custom files // Back up custom files
if (customFiles.length > 0) { if (customFiles.length > 0) {
const tempBackupDir = path.join(projectRoot, '_bmad-custom-backup-temp'); const tempBackupDir = path.join(paths.projectRoot, '_bmad-custom-backup-temp');
await fs.ensureDir(tempBackupDir); await fs.ensureDir(tempBackupDir);
spinner.start(`Backing up ${customFiles.length} custom files...`); spinner.start(`Backing up ${customFiles.length} custom files...`);
for (const customFile of customFiles) { for (const customFile of customFiles) {
const relativePath = path.relative(bmadDir, customFile); const relativePath = path.relative(paths.bmadDir, customFile);
const backupPath = path.join(tempBackupDir, relativePath); const backupPath = path.join(tempBackupDir, relativePath);
await fs.ensureDir(path.dirname(backupPath)); await fs.ensureDir(path.dirname(backupPath));
await fs.copy(customFile, backupPath); await fs.copy(customFile, backupPath);
@ -326,12 +325,12 @@ class Installer {
// Back up modified files // Back up modified files
if (modifiedFiles.length > 0) { if (modifiedFiles.length > 0) {
const tempModifiedBackupDir = path.join(projectRoot, '_bmad-modified-backup-temp'); const tempModifiedBackupDir = path.join(paths.projectRoot, '_bmad-modified-backup-temp');
await fs.ensureDir(tempModifiedBackupDir); await fs.ensureDir(tempModifiedBackupDir);
spinner.start(`Backing up ${modifiedFiles.length} modified files...`); spinner.start(`Backing up ${modifiedFiles.length} modified files...`);
for (const modifiedFile of modifiedFiles) { for (const modifiedFile of modifiedFiles) {
const relativePath = path.relative(bmadDir, modifiedFile.path); const relativePath = path.relative(paths.bmadDir, modifiedFile.path);
const tempBackupPath = path.join(tempModifiedBackupDir, relativePath); const tempBackupPath = path.join(tempModifiedBackupDir, relativePath);
await fs.ensureDir(path.dirname(tempBackupPath)); await fs.ensureDir(path.dirname(tempBackupPath));
await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true }); await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true });
@ -369,7 +368,7 @@ class Installer {
// Use config.ides if it's an array (even if empty), null means prompt // Use config.ides if it's an array (even if empty), null means prompt
const preSelectedIdes = Array.isArray(config.ides) ? config.ides : null; const preSelectedIdes = Array.isArray(config.ides) ? config.ides : null;
toolSelection = await this.collectToolConfigurations( toolSelection = await this.collectToolConfigurations(
projectRoot, paths.projectRoot,
config.modules, config.modules,
config._isFullReinstall || false, config._isFullReinstall || false,
config._previouslyConfiguredIdes || [], config._previouslyConfiguredIdes || [],
@ -414,7 +413,7 @@ class Installer {
if (config.skipPrompts) { if (config.skipPrompts) {
// Non-interactive mode: silently preserve existing IDE configs // Non-interactive mode: silently preserve existing IDE configs
if (!config.ides) config.ides = []; if (!config.ides) config.ides = [];
const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir); const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(paths.bmadDir);
for (const ide of idesToRemove) { for (const ide of idesToRemove) {
config.ides.push(ide); config.ides.push(ide);
if (savedIdeConfigs[ide] && !ideConfigurations[ide]) { if (savedIdeConfigs[ide] && !ideConfigurations[ide]) {
@ -442,9 +441,9 @@ class Installer {
try { try {
const handler = this.ideManager.handlers.get(ide); const handler = this.ideManager.handlers.get(ide);
if (handler) { if (handler) {
await handler.cleanup(projectRoot); await handler.cleanup(paths.projectRoot);
} }
await this.ideConfigManager.deleteIdeConfig(bmadDir, ide); await this.ideConfigManager.deleteIdeConfig(paths.bmadDir, ide);
await prompts.log.message(` Removed: ${ide}`); await prompts.log.message(` Removed: ${ide}`);
} catch (error) { } catch (error) {
await prompts.log.warn(` Warning: Failed to remove ${ide}: ${error.message}`); await prompts.log.warn(` Warning: Failed to remove ${ide}: ${error.message}`);
@ -455,7 +454,7 @@ class Installer {
await prompts.log.message(' IDE removal cancelled'); await prompts.log.message(' IDE removal cancelled');
// Add IDEs back to selection and restore their saved configurations // Add IDEs back to selection and restore their saved configurations
if (!config.ides) config.ides = []; if (!config.ides) config.ides = [];
const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir); const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(paths.bmadDir);
for (const ide of idesToRemove) { for (const ide of idesToRemove) {
config.ides.push(ide); config.ides.push(ide);
if (savedIdeConfigs[ide] && !ideConfigurations[ide]) { if (savedIdeConfigs[ide] && !ideConfigurations[ide]) {
@ -483,7 +482,7 @@ class Installer {
if (customModulePaths && customModulePaths.size > 0) { if (customModulePaths && customModulePaths.size > 0) {
spinner.message('Caching custom modules...'); spinner.message('Caching custom modules...');
const { CustomModuleCache } = require('./custom-module-cache'); const { CustomModuleCache } = require('./custom-module-cache');
const customCache = new CustomModuleCache(bmadDir); const customCache = new CustomModuleCache(paths.bmadDir);
for (const [moduleId, sourcePath] of customModulePaths) { for (const [moduleId, sourcePath] of customModulePaths) {
const cachedInfo = await customCache.cacheModule(moduleId, sourcePath, { const cachedInfo = await customCache.cacheModule(moduleId, sourcePath, {
@ -502,43 +501,40 @@ class Installer {
// Custom content is already handled in UI before module selection // Custom content is already handled in UI before module selection
const finalCustomContent = config.customContent; const finalCustomContent = config.customContent;
// Prepare modules list including cached custom modules // Official modules to install (filter out core — handled separately by installCore)
let allModules = [...(config.modules || [])]; const officialModules = config.installCore ? (config.modules || []).filter((m) => m !== 'core') : [...(config.modules || [])];
// During quick update, we might have custom module sources from the manifest // Build combined list for manifest generation and IDE setup
const allModules = [...officialModules];
const customModuleIds = new Set();
for (const id of customModulePaths.keys()) {
customModuleIds.add(id);
}
if (config._customModuleSources) { if (config._customModuleSources) {
// Add custom modules from stored sources
for (const [moduleId, customInfo] of config._customModuleSources) { for (const [moduleId, customInfo] of config._customModuleSources) {
if (!allModules.includes(moduleId) && (await fs.pathExists(customInfo.sourcePath))) { if (!customModuleIds.has(moduleId) && (await fs.pathExists(customInfo.sourcePath))) {
allModules.push(moduleId); customModuleIds.add(moduleId);
} }
} }
} }
// Add cached custom modules
if (finalCustomContent && finalCustomContent.cachedModules) { if (finalCustomContent && finalCustomContent.cachedModules) {
for (const cachedModule of finalCustomContent.cachedModules) { for (const cachedModule of finalCustomContent.cachedModules) {
if (!allModules.includes(cachedModule.id)) { customModuleIds.add(cachedModule.id);
allModules.push(cachedModule.id);
} }
} }
}
// Regular custom content from user input (non-cached)
if (finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) { if (finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
// Add custom modules to the installation list
const customHandler = new CustomHandler(); const customHandler = new CustomHandler();
for (const customFile of finalCustomContent.selectedFiles) { for (const customFile of finalCustomContent.selectedFiles) {
const customInfo = await customHandler.getCustomInfo(customFile, projectRoot); const customInfo = await customHandler.getCustomInfo(customFile, paths.projectRoot);
if (customInfo && customInfo.id) { if (customInfo && customInfo.id) {
allModules.push(customInfo.id); customModuleIds.add(customInfo.id);
} }
} }
} }
for (const id of customModuleIds) {
// Don't include core again if already installed if (!allModules.includes(id)) {
if (config.installCore) { allModules.push(id);
allModules = allModules.filter((m) => m !== 'core'); }
} }
// Stop spinner before tasks() takes over progress display // Stop spinner before tasks() takes over progress display
@ -560,112 +556,39 @@ class Installer {
installTasks.push({ installTasks.push({
title: isQuickUpdate ? 'Updating BMAD core' : 'Installing BMAD core', title: isQuickUpdate ? 'Updating BMAD core' : 'Installing BMAD core',
task: async (message) => { task: async (message) => {
await this.installCore(bmadDir); await this.installCore(paths.bmadDir);
addResult('Core', 'ok', isQuickUpdate ? 'updated' : 'installed'); addResult('Core', 'ok', isQuickUpdate ? 'updated' : 'installed');
await this.generateModuleConfigs(bmadDir, { core: config.coreConfig || {} }); await this.generateModuleConfigs(paths.bmadDir, { core: config.coreConfig || {} });
return isQuickUpdate ? 'Core updated' : 'Core installed'; return isQuickUpdate ? 'Core updated' : 'Core installed';
}, },
}); });
} }
// Module installation task // Module installation task
if (allModules && allModules.length > 0) { if (allModules.length > 0) {
installTasks.push({ installTasks.push({
title: isQuickUpdate ? `Updating ${allModules.length} module(s)` : `Installing ${allModules.length} module(s)`, title: isQuickUpdate ? `Updating ${allModules.length} module(s)` : `Installing ${allModules.length} module(s)`,
task: async (message) => { task: async (message) => {
const installedModuleNames = new Set(); const installedModuleNames = new Set();
for (const moduleName of allModules) { await this._installOfficialModules(config, paths, moduleConfigs, officialModules, addResult, isQuickUpdate, {
if (installedModuleNames.has(moduleName)) continue; message,
installedModuleNames.add(moduleName); installedModuleNames,
message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
// Check if this is a custom module
let isCustomModule = false;
let customInfo = null;
// First check if we have a cached version
if (finalCustomContent && finalCustomContent.cachedModules) {
const cachedModule = finalCustomContent.cachedModules.find((m) => m.id === moduleName);
if (cachedModule) {
isCustomModule = true;
customInfo = { id: moduleName, path: cachedModule.cachePath, config: {} };
}
}
// Then check custom module sources from manifest (for quick update)
if (!isCustomModule && config._customModuleSources && config._customModuleSources.has(moduleName)) {
customInfo = config._customModuleSources.get(moduleName);
isCustomModule = true;
if (customInfo.sourcePath && !customInfo.path) {
customInfo.path = path.isAbsolute(customInfo.sourcePath)
? customInfo.sourcePath
: path.join(bmadDir, customInfo.sourcePath);
}
}
// Finally check regular custom content
if (!isCustomModule && finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
const customHandler = new CustomHandler();
for (const customFile of finalCustomContent.selectedFiles) {
const info = await customHandler.getCustomInfo(customFile, projectRoot);
if (info && info.id === moduleName) {
isCustomModule = true;
customInfo = info;
break;
}
}
}
if (isCustomModule && customInfo) {
if (!customModulePaths.has(moduleName) && customInfo.path) {
customModulePaths.set(moduleName, customInfo.path);
this.moduleManager.setCustomModulePaths(customModulePaths);
}
const collectedModuleConfig = moduleConfigs[moduleName] || {};
await this.moduleManager.install(
moduleName,
bmadDir,
(filePath) => {
this.installedFiles.add(filePath);
},
{
isCustom: true,
moduleConfig: collectedModuleConfig,
isQuickUpdate: isQuickUpdate,
installer: this,
silent: true,
},
);
await this.generateModuleConfigs(bmadDir, {
[moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig },
}); });
} else {
// Official module — copy entire module directory await this._installCustomModules(
if (moduleName === 'core') { config,
await this.installCore(bmadDir); paths,
} else { moduleConfigs,
const moduleConfig = this.configCollector.collectedConfig[moduleName] || {}; customModulePaths,
await this.moduleManager.install( finalCustomContent,
moduleName, addResult,
bmadDir, isQuickUpdate,
(filePath) => {
this.installedFiles.add(filePath);
},
{ {
skipModuleInstaller: true, message,
moduleConfig: moduleConfig, installedModuleNames,
installer: this,
silent: true,
}, },
); );
}
}
addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
}
return `${allModules.length} module(s) ${isQuickUpdate ? 'updated' : 'installed'}`; return `${allModules.length} module(s) ${isQuickUpdate ? 'updated' : 'installed'}`;
}, },
@ -685,7 +608,7 @@ class Installer {
// Core module directories // Core module directories
if (config.installCore) { if (config.installCore) {
const result = await this.moduleManager.createModuleDirectories('core', bmadDir, { const result = await this.moduleManager.createModuleDirectories('core', paths.bmadDir, {
installedIDEs: config.ides || [], installedIDEs: config.ides || [],
moduleConfig: moduleConfigs.core || {}, moduleConfig: moduleConfigs.core || {},
existingModuleConfig: this.configCollector.existingConfig?.core || {}, existingModuleConfig: this.configCollector.existingConfig?.core || {},
@ -704,7 +627,7 @@ class Installer {
if (config.modules && config.modules.length > 0) { if (config.modules && config.modules.length > 0) {
for (const moduleName of config.modules) { for (const moduleName of config.modules) {
message(`Setting up ${moduleName}...`); message(`Setting up ${moduleName}...`);
const result = await this.moduleManager.createModuleDirectories(moduleName, bmadDir, { const result = await this.moduleManager.createModuleDirectories(moduleName, paths.bmadDir, {
installedIDEs: config.ides || [], installedIDEs: config.ides || [],
moduleConfig: moduleConfigs[moduleName] || {}, moduleConfig: moduleConfigs[moduleName] || {},
existingModuleConfig: this.configCollector.existingConfig?.[moduleName] || {}, existingModuleConfig: this.configCollector.existingConfig?.[moduleName] || {},
@ -730,7 +653,7 @@ class Installer {
title: 'Generating configurations', title: 'Generating configurations',
task: async (message) => { task: async (message) => {
// Generate clean config.yaml files for each installed module // Generate clean config.yaml files for each installed module
await this.generateModuleConfigs(bmadDir, moduleConfigs); await this.generateModuleConfigs(paths.bmadDir, moduleConfigs);
addResult('Configurations', 'ok', 'generated'); addResult('Configurations', 'ok', 'generated');
// Pre-register manifest files // Pre-register manifest files
@ -755,14 +678,14 @@ class Installer {
modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules; modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules;
} }
const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, [...this.installedFiles], { const manifestStats = await manifestGen.generateManifests(paths.bmadDir, allModulesForManifest, [...this.installedFiles], {
ides: config.ides || [], ides: config.ides || [],
preservedModules: modulesForCsvPreserve, preservedModules: modulesForCsvPreserve,
}); });
// Merge help catalogs // Merge help catalogs
message('Generating help catalog...'); message('Generating help catalog...');
await this.mergeModuleHelpCatalogs(bmadDir); await this.mergeModuleHelpCatalogs(paths.bmadDir);
addResult('Help catalog', 'ok'); addResult('Help catalog', 'ok');
return 'Configurations generated'; return 'Configurations generated';
@ -823,7 +746,7 @@ class Installer {
console.log = () => {}; console.log = () => {};
} }
try { try {
const setupResult = await this.ideManager.setup(ide, projectRoot, bmadDir, { const setupResult = await this.ideManager.setup(ide, paths.projectRoot, paths.bmadDir, {
selectedModules: allModules || [], selectedModules: allModules || [],
preCollectedConfig: ideConfigurations[ide] || null, preCollectedConfig: ideConfigurations[ide] || null,
verbose: config.verbose, verbose: config.verbose,
@ -831,7 +754,7 @@ class Installer {
}); });
if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) { if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) {
await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]); await this.ideConfigManager.saveIdeConfig(paths.bmadDir, ide, ideConfigurations[ide]);
} }
if (setupResult.success) { if (setupResult.success) {
@ -875,7 +798,7 @@ class Installer {
message(`Restoring ${config._customFiles.length} custom files...`); message(`Restoring ${config._customFiles.length} custom files...`);
for (const originalPath of config._customFiles) { for (const originalPath of config._customFiles) {
const relativePath = path.relative(bmadDir, originalPath); const relativePath = path.relative(paths.bmadDir, originalPath);
const backupPath = path.join(config._tempBackupDir, relativePath); const backupPath = path.join(config._tempBackupDir, relativePath);
if (await fs.pathExists(backupPath)) { if (await fs.pathExists(backupPath)) {
@ -898,7 +821,7 @@ class Installer {
message(`Restoring ${modifiedFiles.length} modified files as .bak...`); message(`Restoring ${modifiedFiles.length} modified files as .bak...`);
for (const modifiedFile of modifiedFiles) { for (const modifiedFile of modifiedFiles) {
const relativePath = path.relative(bmadDir, modifiedFile.path); const relativePath = path.relative(paths.bmadDir, modifiedFile.path);
const tempBackupPath = path.join(config._tempModifiedBackupDir, relativePath); const tempBackupPath = path.join(config._tempModifiedBackupDir, relativePath);
const bakPath = modifiedFile.path + '.bak'; const bakPath = modifiedFile.path + '.bak';
@ -929,7 +852,7 @@ class Installer {
// Render consolidated summary // Render consolidated summary
await this.renderInstallSummary(results, { await this.renderInstallSummary(results, {
bmadDir, bmadDir: paths.bmadDir,
modules: config.modules, modules: config.modules,
ides: config.ides, ides: config.ides,
customFiles: customFiles.length > 0 ? customFiles : undefined, customFiles: customFiles.length > 0 ? customFiles : undefined,
@ -938,10 +861,10 @@ class Installer {
return { return {
success: true, success: true,
path: bmadDir, path: paths.bmadDir,
modules: config.modules, modules: config.modules,
ides: config.ides, ides: config.ides,
projectDir: projectRoot, projectDir: paths.projectRoot,
}; };
} catch (error) { } catch (error) {
try { try {
@ -1066,6 +989,139 @@ class Installer {
return customModulePaths; return customModulePaths;
} }
/**
* Install official (non-custom) modules.
* @param {Object} config - Installation configuration
* @param {Object} paths - InstallPaths instance
* @param {Object} moduleConfigs - Collected module configurations
* @param {string[]} officialModules - Official module IDs to install
* @param {Function} addResult - Callback to record installation results
* @param {boolean} isQuickUpdate - Whether this is a quick update
* @param {Object} ctx - Shared context: { message, installedModuleNames }
*/
async _installOfficialModules(config, paths, moduleConfigs, officialModules, addResult, isQuickUpdate, ctx) {
const { message, installedModuleNames } = ctx;
for (const moduleName of officialModules) {
if (installedModuleNames.has(moduleName)) continue;
installedModuleNames.add(moduleName);
message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
if (moduleName === 'core') {
await this.installCore(paths.bmadDir);
} else {
const moduleConfig = this.configCollector.collectedConfig[moduleName] || {};
await this.moduleManager.install(
moduleName,
paths.bmadDir,
(filePath) => {
this.installedFiles.add(filePath);
},
{
skipModuleInstaller: true,
moduleConfig: moduleConfig,
installer: this,
silent: true,
},
);
}
addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
}
}
/**
* Install custom modules from all custom module sources.
* @param {Object} config - Installation configuration
* @param {Object} paths - InstallPaths instance
* @param {Object} moduleConfigs - Collected module configurations
* @param {Map} customModulePaths - Map of custom module ID to source path
* @param {Object|undefined} finalCustomContent - Custom content from config
* @param {Function} addResult - Callback to record installation results
* @param {boolean} isQuickUpdate - Whether this is a quick update
* @param {Object} ctx - Shared context: { message, installedModuleNames }
*/
async _installCustomModules(config, paths, moduleConfigs, customModulePaths, finalCustomContent, addResult, isQuickUpdate, ctx) {
const { message, installedModuleNames } = ctx;
// Collect all custom module IDs with their info from all sources
const customModules = new Map();
// First: cached modules from finalCustomContent
if (finalCustomContent && finalCustomContent.cachedModules) {
for (const cachedModule of finalCustomContent.cachedModules) {
if (!customModules.has(cachedModule.id)) {
customModules.set(cachedModule.id, { id: cachedModule.id, path: cachedModule.cachePath, config: {} });
}
}
}
// Second: custom module sources from manifest (for quick update)
if (config._customModuleSources) {
for (const [moduleId, customInfo] of config._customModuleSources) {
if (!customModules.has(moduleId)) {
const info = { ...customInfo };
if (info.sourcePath && !info.path) {
info.path = path.isAbsolute(info.sourcePath) ? info.sourcePath : path.join(paths.bmadDir, info.sourcePath);
}
customModules.set(moduleId, info);
}
}
}
// Third: regular custom content from user input (non-cached)
if (finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
const customHandler = new CustomHandler();
for (const customFile of finalCustomContent.selectedFiles) {
const info = await customHandler.getCustomInfo(customFile, paths.projectRoot);
if (info && info.id && !customModules.has(info.id)) {
customModules.set(info.id, info);
}
}
}
// Fourth: any remaining custom modules from customModulePaths not yet covered
for (const [moduleId, modulePath] of customModulePaths) {
if (!customModules.has(moduleId)) {
customModules.set(moduleId, { id: moduleId, path: modulePath, config: {} });
}
}
for (const [moduleName, customInfo] of customModules) {
if (installedModuleNames.has(moduleName)) continue;
installedModuleNames.add(moduleName);
message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
if (!customModulePaths.has(moduleName) && customInfo.path) {
customModulePaths.set(moduleName, customInfo.path);
this.moduleManager.setCustomModulePaths(customModulePaths);
}
const collectedModuleConfig = moduleConfigs[moduleName] || {};
await this.moduleManager.install(
moduleName,
paths.bmadDir,
(filePath) => {
this.installedFiles.add(filePath);
},
{
isCustom: true,
moduleConfig: collectedModuleConfig,
isQuickUpdate: isQuickUpdate,
installer: this,
silent: true,
},
);
await this.generateModuleConfigs(paths.bmadDir, {
[moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig },
});
addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
}
}
/** /**
* Collect Tool/IDE configurations after module configuration * Collect Tool/IDE configurations after module configuration
* @param {string} projectDir - Project directory * @param {string} projectDir - Project directory