refactor(installer): merge ConfigCollector into OfficialModules

Move all config collection state and methods from config-collector.js
into OfficialModules. Move interactive config prompting from install()
into ui.js so install() never prompts. Delete config-collector.js and
eliminate the moduleConfigs parameter chain from _installAndConfigure,
_installOfficialModules, and _installCustomModules.
This commit is contained in:
Alex Verkhovsky 2026-03-21 23:58:37 -06:00
parent e0e59e66bf
commit 232fba8cc8
4 changed files with 1353 additions and 1384 deletions

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,6 @@ const { CustomModules } = require('../modules/custom-modules');
const { IdeManager } = require('../ide/manager'); const { IdeManager } = require('../ide/manager');
const { FileOps } = require('../../../lib/file-ops'); const { FileOps } = require('../../../lib/file-ops');
const { Config } = require('../../../lib/config'); const { Config } = require('../../../lib/config');
const { ConfigCollector } = require('./config-collector');
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
const { ManifestGenerator } = require('./manifest-generator'); const { ManifestGenerator } = require('./manifest-generator');
const { IdeConfigManager } = require('./ide-config-manager'); const { IdeConfigManager } = require('./ide-config-manager');
@ -27,7 +26,6 @@ class Installer {
this.ideManager = new IdeManager(); this.ideManager = new IdeManager();
this.fileOps = new FileOps(); this.fileOps = new FileOps();
this.config = new Config(); this.config = new Config();
this.configCollector = new ConfigCollector();
this.ideConfigManager = new IdeConfigManager(); this.ideConfigManager = new IdeConfigManager();
this.installedFiles = new Set(); // Track all installed files this.installedFiles = new Set(); // Track all installed files
this.bmadFolderName = BMAD_FOLDER_NAME; this.bmadFolderName = BMAD_FOLDER_NAME;
@ -49,7 +47,7 @@ class Installer {
const paths = await InstallPaths.create(config); const paths = await InstallPaths.create(config);
// Collect configurations for official modules // Collect configurations for official modules
const moduleConfigs = await this._collectConfigs(config, paths); await this.officialModules.collectConfigs(config, paths);
await this.customModules.discoverPaths(config, paths); await this.customModules.discoverPaths(config, paths);
@ -77,7 +75,7 @@ class Installer {
await this._cacheCustomModules(paths, addResult); await this._cacheCustomModules(paths, addResult);
const { officialModules, allModules } = await this._buildModuleLists(config, customConfig, paths); const { officialModules, allModules } = await this._buildModuleLists(config, customConfig, paths);
await this._installAndConfigure(config, customConfig, paths, moduleConfigs, officialModules, allModules, addResult); await this._installAndConfigure(config, customConfig, paths, officialModules, allModules, addResult);
await this._setupIdes(config, ideConfigurations, allModules, paths, addResult); await this._setupIdes(config, ideConfigurations, allModules, paths, addResult);
@ -291,9 +289,10 @@ class Installer {
/** /**
* Install modules, create directories, generate configs and manifests. * Install modules, create directories, generate configs and manifests.
*/ */
async _installAndConfigure(config, customConfig, paths, moduleConfigs, officialModules, allModules, addResult) { async _installAndConfigure(config, customConfig, paths, officialModules, allModules, addResult) {
const isQuickUpdate = config.isQuickUpdate(); const isQuickUpdate = config.isQuickUpdate();
const finalCustomContent = customConfig.customContent; const finalCustomContent = customConfig.customContent;
const moduleConfigs = this.officialModules.moduleConfigs;
const dirResults = { createdDirs: [], movedDirs: [], createdWdsFolders: [] }; const dirResults = { createdDirs: [], movedDirs: [], createdWdsFolders: [] };
@ -305,12 +304,12 @@ class Installer {
task: async (message) => { task: async (message) => {
const installedModuleNames = new Set(); const installedModuleNames = new Set();
await this._installOfficialModules(config, paths, moduleConfigs, officialModules, addResult, isQuickUpdate, { await this._installOfficialModules(config, paths, officialModules, addResult, isQuickUpdate, {
message, message,
installedModuleNames, installedModuleNames,
}); });
await this._installCustomModules(customConfig, paths, moduleConfigs, finalCustomContent, addResult, isQuickUpdate, { await this._installCustomModules(customConfig, paths, finalCustomContent, addResult, isQuickUpdate, {
message, message,
installedModuleNames, installedModuleNames,
}); });
@ -336,7 +335,7 @@ class Installer {
const result = await this.officialModules.createModuleDirectories(moduleName, paths.bmadDir, { const result = await this.officialModules.createModuleDirectories(moduleName, paths.bmadDir, {
installedIDEs: config.ides || [], installedIDEs: config.ides || [],
moduleConfig: moduleConfigs[moduleName] || {}, moduleConfig: moduleConfigs[moduleName] || {},
existingModuleConfig: this.configCollector.existingConfig?.[moduleName] || {}, existingModuleConfig: this.officialModules.existingConfig?.[moduleName] || {},
coreConfig: moduleConfigs.core || {}, coreConfig: moduleConfigs.core || {},
logger: moduleLogger, logger: moduleLogger,
silent: true, silent: true,
@ -542,6 +541,7 @@ class Installer {
force: originalConfig.force || false, force: originalConfig.force || false,
actionType: originalConfig.actionType, actionType: originalConfig.actionType,
coreConfig: originalConfig.coreConfig || {}, coreConfig: originalConfig.coreConfig || {},
moduleConfigs: originalConfig.moduleConfigs || null,
hasCoreConfig() { hasCoreConfig() {
return this.coreConfig && Object.keys(this.coreConfig).length > 0; return this.coreConfig && Object.keys(this.coreConfig).length > 0;
}, },
@ -551,33 +551,6 @@ class Installer {
}; };
} }
/**
* Collect configurations for official modules (core + selected).
* Custom module configs are handled separately in CustomModules.discoverPaths.
*/
async _collectConfigs(config, paths) {
// Seed core config if pre-collected from interactive UI
if (config.hasCoreConfig()) {
this.configCollector.collectedConfig.core = config.coreConfig;
this.configCollector.allAnswers = {};
for (const [key, value] of Object.entries(config.coreConfig)) {
this.configCollector.allAnswers[`core_${key}`] = value;
}
}
// Quick update already collected everything
if (config.isQuickUpdate()) {
return this.configCollector.collectedConfig;
}
// Modules to collect — skip core if its config was pre-collected
const toCollect = config.hasCoreConfig() ? config.modules.filter((m) => m !== 'core') : [...config.modules];
return await this.configCollector.collectAllConfigurations(toCollect, paths.projectRoot, {
skipPrompts: config.skipPrompts,
});
}
/** /**
* Scan the custom module cache directory and register any cached custom modules * Scan the custom module cache directory and register any cached custom modules
* that aren't already known from the manifest or external module list. * that aren't already known from the manifest or external module list.
@ -649,7 +622,7 @@ class Installer {
config.coreConfig = existingCoreConfig; config.coreConfig = existingCoreConfig;
customConfig.coreConfig = existingCoreConfig; customConfig.coreConfig = existingCoreConfig;
this.configCollector.collectedConfig.core = existingCoreConfig; this.officialModules.moduleConfigs.core = existingCoreConfig;
} catch (error) { } catch (error) {
await prompts.log.warn(`Warning: Could not read existing core config: ${error.message}`); await prompts.log.warn(`Warning: Could not read existing core config: ${error.message}`);
} }
@ -705,13 +678,12 @@ class Installer {
* Install official (non-custom) modules. * Install official (non-custom) modules.
* @param {Object} config - Installation configuration * @param {Object} config - Installation configuration
* @param {Object} paths - InstallPaths instance * @param {Object} paths - InstallPaths instance
* @param {Object} moduleConfigs - Collected module configurations
* @param {string[]} officialModules - Official module IDs to install * @param {string[]} officialModules - Official module IDs to install
* @param {Function} addResult - Callback to record installation results * @param {Function} addResult - Callback to record installation results
* @param {boolean} isQuickUpdate - Whether this is a quick update * @param {boolean} isQuickUpdate - Whether this is a quick update
* @param {Object} ctx - Shared context: { message, installedModuleNames } * @param {Object} ctx - Shared context: { message, installedModuleNames }
*/ */
async _installOfficialModules(config, paths, moduleConfigs, officialModules, addResult, isQuickUpdate, ctx) { async _installOfficialModules(config, paths, officialModules, addResult, isQuickUpdate, ctx) {
const { message, installedModuleNames } = ctx; const { message, installedModuleNames } = ctx;
for (const moduleName of officialModules) { for (const moduleName of officialModules) {
@ -720,7 +692,7 @@ class Installer {
message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`); message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
const moduleConfig = this.configCollector.collectedConfig[moduleName] || {}; const moduleConfig = this.officialModules.moduleConfigs[moduleName] || {};
await this.officialModules.install( await this.officialModules.install(
moduleName, moduleName,
paths.bmadDir, paths.bmadDir,
@ -743,13 +715,12 @@ class Installer {
* Install custom modules from all custom module sources. * Install custom modules from all custom module sources.
* @param {Object} config - Installation configuration * @param {Object} config - Installation configuration
* @param {Object} paths - InstallPaths instance * @param {Object} paths - InstallPaths instance
* @param {Object} moduleConfigs - Collected module configurations
* @param {Object|undefined} finalCustomContent - Custom content from config * @param {Object|undefined} finalCustomContent - Custom content from config
* @param {Function} addResult - Callback to record installation results * @param {Function} addResult - Callback to record installation results
* @param {boolean} isQuickUpdate - Whether this is a quick update * @param {boolean} isQuickUpdate - Whether this is a quick update
* @param {Object} ctx - Shared context: { message, installedModuleNames } * @param {Object} ctx - Shared context: { message, installedModuleNames }
*/ */
async _installCustomModules(customConfig, paths, moduleConfigs, finalCustomContent, addResult, isQuickUpdate, ctx) { async _installCustomModules(customConfig, paths, finalCustomContent, addResult, isQuickUpdate, ctx) {
const { message, installedModuleNames } = ctx; const { message, installedModuleNames } = ctx;
// Collect all custom module IDs with their info from all sources // Collect all custom module IDs with their info from all sources
@ -805,7 +776,7 @@ class Installer {
this.customModules.paths.set(moduleName, customInfo.path); this.customModules.paths.set(moduleName, customInfo.path);
} }
const collectedModuleConfig = moduleConfigs[moduleName] || {}; const collectedModuleConfig = this.officialModules.moduleConfigs[moduleName] || {};
await this.officialModules.install( await this.officialModules.install(
moduleName, moduleName,
paths.bmadDir, paths.bmadDir,
@ -1641,19 +1612,19 @@ class Installer {
// Load existing configs and collect new fields (if any) // Load existing configs and collect new fields (if any)
await prompts.log.info('Checking for new configuration options...'); await prompts.log.info('Checking for new configuration options...');
await this.configCollector.loadExistingConfig(projectDir); await this.officialModules.loadExistingConfig(projectDir);
let promptedForNewFields = false; let promptedForNewFields = false;
// Check core config for new fields // Check core config for new fields
const corePrompted = await this.configCollector.collectModuleConfigQuick('core', projectDir, true); const corePrompted = await this.officialModules.collectModuleConfigQuick('core', projectDir, true);
if (corePrompted) { if (corePrompted) {
promptedForNewFields = true; promptedForNewFields = true;
} }
// Check each module we're updating for new fields (NOT skipped modules) // Check each module we're updating for new fields (NOT skipped modules)
for (const moduleName of modulesToUpdate) { for (const moduleName of modulesToUpdate) {
const modulePrompted = await this.configCollector.collectModuleConfigQuick(moduleName, projectDir, true); const modulePrompted = await this.officialModules.collectModuleConfigQuick(moduleName, projectDir, true);
if (modulePrompted) { if (modulePrompted) {
promptedForNewFields = true; promptedForNewFields = true;
} }
@ -1664,7 +1635,7 @@ class Installer {
} }
// Add metadata // Add metadata
this.configCollector.collectedConfig._meta = { this.officialModules.collectedConfig._meta = {
version: require(path.join(getProjectRoot(), 'package.json')).version, version: require(path.join(getProjectRoot(), 'package.json')).version,
installDate: new Date().toISOString(), installDate: new Date().toISOString(),
lastModified: new Date().toISOString(), lastModified: new Date().toISOString(),
@ -1675,7 +1646,7 @@ class Installer {
directory: projectDir, directory: projectDir,
modules: modulesToUpdate, // Only update modules we have source for (includes core) modules: modulesToUpdate, // Only update modules we have source for (includes core)
ides: configuredIdes, ides: configuredIdes,
coreConfig: this.configCollector.collectedConfig.core, coreConfig: this.officialModules.collectedConfig.core,
actionType: 'install', // Use regular install flow actionType: 'install', // Use regular install flow
_quickUpdate: true, // Flag to skip certain prompts _quickUpdate: true, // Flag to skip certain prompts
_preserveModules: skippedModules, // Preserve these in manifest even though we didn't update them _preserveModules: skippedModules, // Preserve these in manifest even though we didn't update them

File diff suppressed because it is too large Load Diff

View File

@ -431,7 +431,7 @@ class UI {
// Get tool selection // Get tool selection
const toolSelection = await this.promptToolSelection(confirmedDirectory, options); const toolSelection = await this.promptToolSelection(confirmedDirectory, options);
const coreConfig = await this.collectCoreConfig(confirmedDirectory, options); const moduleConfigs = await this.collectModuleConfigs(confirmedDirectory, selectedModules, options);
return { return {
actionType: 'update', actionType: 'update',
@ -439,7 +439,8 @@ class UI {
modules: selectedModules, modules: selectedModules,
ides: toolSelection.ides, ides: toolSelection.ides,
skipIde: toolSelection.skipIde, skipIde: toolSelection.skipIde,
coreConfig: coreConfig, coreConfig: moduleConfigs.core || {},
moduleConfigs: moduleConfigs,
customContent: customModuleResult.customContentConfig, customContent: customModuleResult.customContentConfig,
skipPrompts: options.yes || false, skipPrompts: options.yes || false,
}; };
@ -549,7 +550,7 @@ class UI {
selectedModules.unshift('core'); selectedModules.unshift('core');
} }
let toolSelection = await this.promptToolSelection(confirmedDirectory, options); let toolSelection = await this.promptToolSelection(confirmedDirectory, options);
const coreConfig = await this.collectCoreConfig(confirmedDirectory, options); const moduleConfigs = await this.collectModuleConfigs(confirmedDirectory, selectedModules, options);
return { return {
actionType: 'install', actionType: 'install',
@ -557,7 +558,8 @@ class UI {
modules: selectedModules, modules: selectedModules,
ides: toolSelection.ides, ides: toolSelection.ides,
skipIde: toolSelection.skipIde, skipIde: toolSelection.skipIde,
coreConfig: coreConfig, coreConfig: moduleConfigs.core || {},
moduleConfigs: moduleConfigs,
customContent: customContentConfig, customContent: customContentConfig,
skipPrompts: options.yes || false, skipPrompts: options.yes || false,
}; };
@ -827,16 +829,18 @@ class UI {
} }
/** /**
* Collect core configuration * Collect all module configurations (core + selected modules).
* All interactive prompting happens here in the UI layer.
* @param {string} directory - Installation directory * @param {string} directory - Installation directory
* @param {string[]} modules - Modules to configure (including 'core')
* @param {Object} options - Command-line options * @param {Object} options - Command-line options
* @returns {Object} Core configuration * @returns {Object} Collected module configurations keyed by module name
*/ */
async collectCoreConfig(directory, options = {}) { async collectModuleConfigs(directory, modules, options = {}) {
const { ConfigCollector } = require('../installers/lib/core/config-collector'); const { OfficialModules } = require('../installers/lib/modules/official-modules');
const configCollector = new ConfigCollector(); const configCollector = new OfficialModules();
// If options are provided, set them directly // Seed core config from CLI options if provided
if (options.userName || options.communicationLanguage || options.documentOutputLanguage || options.outputFolder) { if (options.userName || options.communicationLanguage || options.documentOutputLanguage || options.outputFolder) {
const coreConfig = {}; const coreConfig = {};
if (options.userName) { if (options.userName) {
@ -858,8 +862,6 @@ class UI {
// Load existing config to merge with provided options // Load existing config to merge with provided options
await configCollector.loadExistingConfig(directory); await configCollector.loadExistingConfig(directory);
// Merge provided options with existing config (or defaults)
const existingConfig = configCollector.collectedConfig.core || {}; const existingConfig = configCollector.collectedConfig.core || {};
configCollector.collectedConfig.core = { ...existingConfig, ...coreConfig }; configCollector.collectedConfig.core = { ...existingConfig, ...coreConfig };
@ -875,7 +877,6 @@ class UI {
await configCollector.loadExistingConfig(directory); await configCollector.loadExistingConfig(directory);
const existingConfig = configCollector.collectedConfig.core || {}; const existingConfig = configCollector.collectedConfig.core || {};
// If no existing config, use defaults
if (Object.keys(existingConfig).length === 0) { if (Object.keys(existingConfig).length === 0) {
let safeUsername; let safeUsername;
try { try {
@ -892,16 +893,14 @@ class UI {
}; };
await prompts.log.info('Using default configuration (--yes flag)'); await prompts.log.info('Using default configuration (--yes flag)');
} }
} else {
// Load existing configs first if they exist
await configCollector.loadExistingConfig(directory);
// Now collect with existing values as defaults (false = don't skip loading, true = skip completion message)
await configCollector.collectModuleConfig('core', directory, false, true);
} }
const coreConfig = configCollector.collectedConfig.core; // Collect all module configs — core is skipped if already seeded above
// Ensure we always have a core config object, even if empty await configCollector.collectAllConfigurations(modules, directory, {
return coreConfig || {}; skipPrompts: options.yes || false,
});
return configCollector.collectedConfig;
} }
/** /**
@ -1388,37 +1387,6 @@ class UI {
return path.resolve(expanded); return path.resolve(expanded);
} }
/**
* Load existing configurations to use as defaults
* @param {string} directory - Installation directory
* @returns {Object} Existing configurations
*/
async loadExistingConfigurations(directory) {
const configs = {
hasCustomContent: false,
coreConfig: {},
ideConfig: { ides: [], skipIde: false },
};
try {
// Load core config
configs.coreConfig = await this.collectCoreConfig(directory);
// Load IDE configuration
const configuredIdes = await this.getConfiguredIdes(directory);
if (configuredIdes.length > 0) {
configs.ideConfig.ides = configuredIdes;
configs.ideConfig.skipIde = false;
}
return configs;
} catch {
// If loading fails, return empty configs
await prompts.log.warn('Could not load existing configurations');
return configs;
}
}
/** /**
* Get configured IDEs from existing installation * Get configured IDEs from existing installation
* @param {string} directory - Installation directory * @param {string} directory - Installation directory