From 68f723d427ba428e02735a888653b53c88808c2f Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Sat, 21 Mar 2026 05:23:02 -0600 Subject: [PATCH] refactor(installer): normalize config gate and flatten core into module list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split install() input into config (clean official fields) and customConfig (full originalConfig copy for custom module concerns). Core is no longer special-cased — it's just another module in config.modules that goes through OfficialModules.install(). Deletes installCore(), copyCoreFiles(), copyFile(), getFileList() dead code. Updates ui.js and quickUpdate() callers to keep core in the modules list instead of setting installCore: true. --- tools/cli/installers/lib/core/installer.js | 339 +++++++----------- .../lib/modules/official-modules.js | 12 +- tools/cli/lib/ui.js | 15 +- 3 files changed, 139 insertions(+), 227 deletions(-) diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index 9cce32b70..2351efc0f 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -38,14 +38,30 @@ class Installer { * Main installation method * @param {Object} config - Installation configuration * @param {string} config.directory - Target directory - * @param {boolean} config.installCore - Whether to install core - * @param {string[]} config.modules - Modules to install + * @param {string[]} config.modules - Modules to install (including 'core') * @param {string[]} config.ides - IDEs to configure - * @param {boolean} config.skipIde - Skip IDE configuration */ async install(originalConfig) { - // Clone config to avoid mutating the caller's object - const config = { ...originalConfig }; + // Build a normalized config with explicit fields — no opaque spread + // Clean config for the official module install path + const modules = [...(originalConfig.modules || [])]; + if (originalConfig.installCore && !modules.includes('core')) { + modules.unshift('core'); + } + + const config = { + directory: originalConfig.directory, + modules, + ides: originalConfig.skipIde ? [] : [...(originalConfig.ides || [])], + skipPrompts: originalConfig.skipPrompts || false, + verbose: originalConfig.verbose || false, + force: originalConfig.force || false, + actionType: originalConfig.actionType, + coreConfig: originalConfig.coreConfig || {}, + }; + + // Everything — custom modules, quick-update state, the whole mess + const customConfig = { ...originalConfig }; // if core config isn't collected, we haven't run the UI -> display logo/version const hasCoreConfig = config.coreConfig && Object.keys(config.coreConfig).length > 0; @@ -56,7 +72,7 @@ class Installer { const paths = await InstallPaths.create(config); // Collect configurations for official modules - const moduleConfigs = await this._collectConfigs(config, paths); + const moduleConfigs = await this._collectConfigs(customConfig, paths); await this.customModules.discoverPaths(config, paths); this.ideManager.setBmadFolderName(BMAD_FOLDER_NAME); @@ -71,7 +87,7 @@ class Installer { spinner.message('Checking for existing installation...'); const existingInstall = await this.detector.detect(paths.bmadDir); - if (existingInstall.installed && !config.force && !config._quickUpdate) { + if (existingInstall.installed && !config.force && !customConfig._quickUpdate) { spinner.stop('Existing installation detected'); // Check if user already decided what to do (from early menu in ui.js) @@ -93,8 +109,8 @@ class Installer { if (action === 'update') { // Store that we're updating for later processing - config._isUpdate = true; - config._existingInstall = existingInstall; + customConfig._isUpdate = true; + customConfig._existingInstall = existingInstall; // Detect modules that were previously installed but are NOT in the new selection (to be removed) const previouslyInstalledModules = new Set(existingInstall.modules.map((m) => m.id)); @@ -162,8 +178,8 @@ class Installer { const existingFilesManifest = await this.readFilesManifest(paths.bmadDir); const { customFiles, modifiedFiles } = await this.detectCustomFiles(paths.bmadDir, existingFilesManifest); - config._customFiles = customFiles; - config._modifiedFiles = modifiedFiles; + customConfig._customFiles = customFiles; + customConfig._modifiedFiles = modifiedFiles; // Preserve existing core configuration during updates // Read the current core config.yaml to maintain user's settings @@ -174,10 +190,9 @@ class Installer { const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8'); const existingCoreConfig = yaml.parse(coreConfigContent); - // Store in config.coreConfig so it's preserved through the installation + // Preserve through the installation config.coreConfig = existingCoreConfig; - - // Also store in configCollector for use during config collection + customConfig.coreConfig = existingCoreConfig; this.configCollector.collectedConfig.core = existingCoreConfig; } catch (error) { await prompts.log.warn(`Warning: Could not read existing core config: ${error.message}`); @@ -234,7 +249,7 @@ class Installer { } spinner.stop(`Backed up ${customFiles.length} custom files`); - config._tempBackupDir = tempBackupDir; + customConfig._tempBackupDir = tempBackupDir; } // For modified files, back them up to temp directory (will be restored as .bak files after install) @@ -251,21 +266,21 @@ class Installer { } spinner.stop(`Backed up ${modifiedFiles.length} modified files`); - config._tempModifiedBackupDir = tempModifiedBackupDir; + customConfig._tempModifiedBackupDir = tempModifiedBackupDir; } } - } else if (existingInstall.installed && config._quickUpdate) { + } else if (existingInstall.installed && customConfig._quickUpdate) { // Quick update mode - automatically treat as update without prompting spinner.message('Preparing quick update...'); - config._isUpdate = true; - config._existingInstall = existingInstall; + customConfig._isUpdate = true; + customConfig._existingInstall = existingInstall; // Detect custom and modified files BEFORE updating const existingFilesManifest = await this.readFilesManifest(paths.bmadDir); const { customFiles, modifiedFiles } = await this.detectCustomFiles(paths.bmadDir, existingFilesManifest); - config._customFiles = customFiles; - config._modifiedFiles = modifiedFiles; + customConfig._customFiles = customFiles; + customConfig._modifiedFiles = modifiedFiles; // Also check cache directory for custom modules (like quick update does) const cacheDir = paths.customCacheDir; @@ -314,7 +329,7 @@ class Installer { await fs.copy(customFile, backupPath); } spinner.stop(`Backed up ${customFiles.length} custom files`); - config._tempBackupDir = tempBackupDir; + customConfig._tempBackupDir = tempBackupDir; } // Back up modified files @@ -330,7 +345,7 @@ class Installer { await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true }); } spinner.stop(`Backed up ${modifiedFiles.length} modified files`); - config._tempModifiedBackupDir = tempModifiedBackupDir; + customConfig._tempModifiedBackupDir = tempModifiedBackupDir; } } @@ -338,10 +353,10 @@ class Installer { // Skip for quick update since we already have the IDE list spinner.stop('Pre-checks complete'); let toolSelection; - if (config._quickUpdate) { + if (customConfig._quickUpdate) { // Quick update already has IDEs configured, use saved configurations const preConfiguredIdes = {}; - const savedIdeConfigs = config._savedIdeConfigs || {}; + const savedIdeConfigs = customConfig._savedIdeConfigs || {}; for (const ide of config.ides || []) { // Use saved config if available, otherwise mark as already configured (legacy) @@ -364,8 +379,8 @@ class Installer { toolSelection = await this.collectToolConfigurations( paths.projectRoot, config.modules, - config._isFullReinstall || false, - config._previouslyConfiguredIdes || [], + customConfig._isFullReinstall || false, + customConfig._previouslyConfiguredIdes || [], preSelectedIdes, config.skipPrompts || false, ); @@ -397,8 +412,8 @@ class Installer { } // Detect IDEs that were previously installed but are NOT in the new selection (to be removed) - if (config._isUpdate && config._existingInstall) { - const previouslyInstalledIdes = new Set(config._existingInstall.ides || []); + if (customConfig._isUpdate && customConfig._existingInstall) { + const previouslyInstalledIdes = new Set(customConfig._existingInstall.ides || []); const newlySelectedIdes = new Set(config.ides || []); const idesToRemove = [...previouslyInstalledIdes].filter((ide) => !newlySelectedIdes.has(ide)); @@ -491,15 +506,15 @@ class Installer { } // Custom content is already handled in UI before module selection - const finalCustomContent = config.customContent; + const finalCustomContent = customConfig.customContent; // Build custom module ID set first (needed to filter official list) const customModuleIds = new Set(); for (const id of this.customModules.paths.keys()) { customModuleIds.add(id); } - if (config._customModuleSources) { - for (const [moduleId, customInfo] of config._customModuleSources) { + if (customConfig._customModuleSources) { + for (const [moduleId, customInfo] of customConfig._customModuleSources) { if (!customModuleIds.has(moduleId) && (await fs.pathExists(customInfo.sourcePath))) { customModuleIds.add(moduleId); } @@ -520,7 +535,7 @@ 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)); + const officialModules = (config.modules || []).filter((m) => !customModuleIds.has(m)); // Combined list for manifest generation and IDE setup const allModules = [...officialModules]; @@ -536,7 +551,7 @@ class Installer { // ───────────────────────────────────────────────────────────────────────── // FIRST TASKS BLOCK: Core installation through manifests (non-interactive) // ───────────────────────────────────────────────────────────────────────── - const isQuickUpdate = config._quickUpdate || false; + const isQuickUpdate = customConfig._quickUpdate || false; // Collect directory creation results for output after tasks() completes const dirResults = { createdDirs: [], movedDirs: [], createdWdsFolders: [] }; @@ -544,20 +559,7 @@ class Installer { // Build task list conditionally const installTasks = []; - // Core installation task - if (config.installCore) { - installTasks.push({ - title: isQuickUpdate ? 'Updating BMAD core' : 'Installing BMAD core', - task: async (message) => { - await this.installCore(paths.bmadDir); - addResult('Core', 'ok', isQuickUpdate ? 'updated' : 'installed'); - await this.generateModuleConfigs(paths.bmadDir, { core: config.coreConfig || {} }); - return isQuickUpdate ? 'Core updated' : 'Core installed'; - }, - }); - } - - // Module installation task + // Module installation task (core is just another module in the list) if (allModules.length > 0) { installTasks.push({ title: isQuickUpdate ? `Updating ${allModules.length} module(s)` : `Installing ${allModules.length} module(s)`, @@ -569,7 +571,7 @@ class Installer { installedModuleNames, }); - await this._installCustomModules(config, paths, moduleConfigs, finalCustomContent, addResult, isQuickUpdate, { + await this._installCustomModules(customConfig, paths, moduleConfigs, finalCustomContent, addResult, isQuickUpdate, { message, installedModuleNames, }); @@ -590,24 +592,7 @@ class Installer { warn: async (msg) => await prompts.log.warn(msg), }; - // Core module directories - if (config.installCore) { - const result = await this.officialModules.createModuleDirectories('core', paths.bmadDir, { - installedIDEs: config.ides || [], - moduleConfig: moduleConfigs.core || {}, - existingModuleConfig: this.configCollector.existingConfig?.core || {}, - coreConfig: moduleConfigs.core || {}, - logger: moduleLogger, - silent: true, - }); - if (result) { - dirResults.createdDirs.push(...result.createdDirs); - dirResults.movedDirs.push(...(result.movedDirs || [])); - dirResults.createdWdsFolders.push(...result.createdWdsFolders); - } - } - - // User-selected module directories + // Module directories (core is in config.modules like any other module) if (config.modules && config.modules.length > 0) { for (const moduleName of config.modules) { message(`Setting up ${moduleName}...`); @@ -649,17 +634,17 @@ class Installer { message('Generating manifests...'); const manifestGen = new ManifestGenerator(); - const allModulesForManifest = config._quickUpdate - ? config._existingModules || allModules || [] - : config._preserveModules - ? [...allModules, ...config._preserveModules] + const allModulesForManifest = customConfig._quickUpdate + ? customConfig._existingModules || allModules || [] + : customConfig._preserveModules + ? [...allModules, ...customConfig._preserveModules] : allModules || []; let modulesForCsvPreserve; - if (config._quickUpdate) { - modulesForCsvPreserve = config._existingModules || allModules || []; + if (customConfig._quickUpdate) { + modulesForCsvPreserve = customConfig._existingModules || allModules || []; } else { - modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules; + modulesForCsvPreserve = customConfig._preserveModules ? [...allModules, ...customConfig._preserveModules] : allModules; } const manifestStats = await manifestGen.generateManifests(paths.bmadDir, allModulesForManifest, [...this.installedFiles], { @@ -769,8 +754,9 @@ class Installer { // File restoration task (only for updates) if ( - config._isUpdate && - ((config._customFiles && config._customFiles.length > 0) || (config._modifiedFiles && config._modifiedFiles.length > 0)) + customConfig._isUpdate && + ((customConfig._customFiles && customConfig._customFiles.length > 0) || + (customConfig._modifiedFiles && customConfig._modifiedFiles.length > 0)) ) { postIdeTasks.push({ title: 'Finalizing installation', @@ -778,12 +764,12 @@ class Installer { let customFiles = []; let modifiedFiles = []; - if (config._customFiles && config._customFiles.length > 0) { - message(`Restoring ${config._customFiles.length} custom files...`); + if (customConfig._customFiles && customConfig._customFiles.length > 0) { + message(`Restoring ${customConfig._customFiles.length} custom files...`); - for (const originalPath of config._customFiles) { + for (const originalPath of customConfig._customFiles) { const relativePath = path.relative(paths.bmadDir, originalPath); - const backupPath = path.join(config._tempBackupDir, relativePath); + const backupPath = path.join(customConfig._tempBackupDir, relativePath); if (await fs.pathExists(backupPath)) { await fs.ensureDir(path.dirname(originalPath)); @@ -791,22 +777,22 @@ class Installer { } } - if (config._tempBackupDir && (await fs.pathExists(config._tempBackupDir))) { - await fs.remove(config._tempBackupDir); + if (customConfig._tempBackupDir && (await fs.pathExists(customConfig._tempBackupDir))) { + await fs.remove(customConfig._tempBackupDir); } - customFiles = config._customFiles; + customFiles = customConfig._customFiles; } - if (config._modifiedFiles && config._modifiedFiles.length > 0) { - modifiedFiles = config._modifiedFiles; + if (customConfig._modifiedFiles && customConfig._modifiedFiles.length > 0) { + modifiedFiles = customConfig._modifiedFiles; - if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) { + if (customConfig._tempModifiedBackupDir && (await fs.pathExists(customConfig._tempModifiedBackupDir))) { message(`Restoring ${modifiedFiles.length} modified files as .bak...`); for (const modifiedFile of modifiedFiles) { const relativePath = path.relative(paths.bmadDir, modifiedFile.path); - const tempBackupPath = path.join(config._tempModifiedBackupDir, relativePath); + const tempBackupPath = path.join(customConfig._tempModifiedBackupDir, relativePath); const bakPath = modifiedFile.path + '.bak'; if (await fs.pathExists(tempBackupPath)) { @@ -815,13 +801,13 @@ class Installer { } } - await fs.remove(config._tempModifiedBackupDir); + await fs.remove(customConfig._tempModifiedBackupDir); } } // Store for summary access - config._restoredCustomFiles = customFiles; - config._restoredModifiedFiles = modifiedFiles; + customConfig._restoredCustomFiles = customFiles; + customConfig._restoredModifiedFiles = modifiedFiles; return 'Installation finalized'; }, @@ -831,8 +817,8 @@ class Installer { await prompts.tasks(postIdeTasks); // Retrieve restored file info for summary - const customFiles = config._restoredCustomFiles || []; - const modifiedFiles = config._restoredModifiedFiles || []; + const customFiles = customConfig._restoredCustomFiles || []; + const modifiedFiles = customConfig._restoredModifiedFiles || []; // Render consolidated summary await this.renderInstallSummary(results, { @@ -863,11 +849,11 @@ class Installer { // Clean up any temp backup directories that were created before the failure try { - if (config._tempBackupDir && (await fs.pathExists(config._tempBackupDir))) { - await fs.remove(config._tempBackupDir); + if (customConfig._tempBackupDir && (await fs.pathExists(customConfig._tempBackupDir))) { + await fs.remove(customConfig._tempBackupDir); } - if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) { - await fs.remove(config._tempModifiedBackupDir); + if (customConfig._tempModifiedBackupDir && (await fs.pathExists(customConfig._tempModifiedBackupDir))) { + await fs.remove(customConfig._tempModifiedBackupDir); } } catch { // Best-effort cleanup — don't mask the original error @@ -881,27 +867,27 @@ class Installer { * Collect configurations for official modules (core + selected). * Custom module configs are handled separately in CustomModules.discoverPaths. */ - async _collectConfigs(config, paths) { + async _collectConfigs(customConfig, paths) { // Seed core config if pre-collected from interactive UI - if (config.coreConfig && Object.keys(config.coreConfig).length > 0) { - this.configCollector.collectedConfig.core = config.coreConfig; + if (customConfig.coreConfig && Object.keys(customConfig.coreConfig).length > 0) { + this.configCollector.collectedConfig.core = customConfig.coreConfig; this.configCollector.allAnswers = {}; - for (const [key, value] of Object.entries(config.coreConfig)) { + for (const [key, value] of Object.entries(customConfig.coreConfig)) { this.configCollector.allAnswers[`core_${key}`] = value; } } // Quick update already collected everything - if (config._quickUpdate) { + if (customConfig._quickUpdate) { return this.configCollector.collectedConfig; } - // Official modules: core + selected (excluding core if already collected) - const officialModules = (config.modules || []).filter((m) => m !== 'core'); - const toCollect = config.coreConfig && Object.keys(config.coreConfig).length > 0 ? officialModules : ['core', ...officialModules]; + // Modules to collect configs for — skip core if its config was pre-collected from UI + const hasCoreConfig = customConfig.coreConfig && Object.keys(customConfig.coreConfig).length > 0; + const toCollect = hasCoreConfig ? (customConfig.modules || []).filter((m) => m !== 'core') : [...(customConfig.modules || [])]; return await this.configCollector.collectAllConfigurations(toCollect, paths.projectRoot, { - skipPrompts: config.skipPrompts, + skipPrompts: customConfig.skipPrompts, }); } @@ -924,24 +910,20 @@ class Installer { message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`); - if (moduleName === 'core') { - await this.installCore(paths.bmadDir); - } else { - const moduleConfig = this.configCollector.collectedConfig[moduleName] || {}; - await this.officialModules.install( - moduleName, - paths.bmadDir, - (filePath) => { - this.installedFiles.add(filePath); - }, - { - skipModuleInstaller: true, - moduleConfig: moduleConfig, - installer: this, - silent: true, - }, - ); - } + const moduleConfig = this.configCollector.collectedConfig[moduleName] || {}; + await this.officialModules.install( + moduleName, + paths.bmadDir, + (filePath) => { + this.installedFiles.add(filePath); + }, + { + skipModuleInstaller: true, + moduleConfig: moduleConfig, + installer: this, + silent: true, + }, + ); addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed'); } @@ -957,7 +939,7 @@ class Installer { * @param {boolean} isQuickUpdate - Whether this is a quick update * @param {Object} ctx - Shared context: { message, installedModuleNames } */ - async _installCustomModules(config, paths, moduleConfigs, finalCustomContent, addResult, isQuickUpdate, ctx) { + async _installCustomModules(customConfig, paths, moduleConfigs, finalCustomContent, addResult, isQuickUpdate, ctx) { const { message, installedModuleNames } = ctx; // Collect all custom module IDs with their info from all sources @@ -973,8 +955,8 @@ class Installer { } // Second: custom module sources from manifest (for quick update) - if (config._customModuleSources) { - for (const [moduleId, customInfo] of config._customModuleSources) { + if (customConfig._customModuleSources) { + for (const [moduleId, customInfo] of customConfig._customModuleSources) { if (!customModules.has(moduleId)) { const info = { ...customInfo }; if (info.sourcePath && !info.path) { @@ -1030,7 +1012,7 @@ class Installer { }, ); await this.generateModuleConfigs(paths.bmadDir, { - [moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig }, + [moduleName]: { ...customConfig.coreConfig, ...customInfo.config, ...collectedModuleConfig }, }); addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed'); @@ -1837,12 +1819,9 @@ class Installer { const allAvailableModules = [...availableModules, ...customModulesFromManifest]; const availableModuleIds = new Set(allAvailableModules.map((m) => m.id)); - // Core module is special - never include it in update flow - const nonCoreInstalledModules = installedModules.filter((id) => id !== 'core'); - // Only update modules that are BOTH installed AND available (we have source for) - const modulesToUpdate = nonCoreInstalledModules.filter((id) => availableModuleIds.has(id)); - const skippedModules = nonCoreInstalledModules.filter((id) => !availableModuleIds.has(id)); + const modulesToUpdate = installedModules.filter((id) => availableModuleIds.has(id)); + const skippedModules = installedModules.filter((id) => !availableModuleIds.has(id)); // Add custom modules that were kept without sources to the skipped modules // This ensures their agents are preserved in the manifest @@ -1892,10 +1871,8 @@ class Installer { // Build the config object for the installer const installConfig = { directory: projectDir, - installCore: true, - modules: modulesToUpdate, // Only update modules we have source for + modules: modulesToUpdate, // Only update modules we have source for (includes core) ides: configuredIdes, - skipIde: configuredIdes.length === 0, coreConfig: this.configCollector.collectedConfig.core, actionType: 'install', // Use regular install flow _quickUpdate: true, // Flag to skip certain prompts @@ -1917,9 +1894,9 @@ class Installer { return { success: true, - moduleCount: modulesToUpdate.length + 1, // +1 for core + moduleCount: modulesToUpdate.length, hadNewFields: promptedForNewFields, - modules: ['core', ...modulesToUpdate], + modules: modulesToUpdate, skippedModules: skippedModules, ides: configuredIdes, }; @@ -2062,14 +2039,15 @@ class Installer { * Private: Update core */ async updateCore(bmadDir, force = false) { - const sourcePath = getModulePath('core'); - const targetPath = path.join(bmadDir, 'core'); - if (force) { - await fs.remove(targetPath); - await this.installCore(bmadDir); + await this.officialModules.install('core', bmadDir, (filePath) => this.installedFiles.add(filePath), { + skipModuleInstaller: true, + silent: true, + }); } else { // Selective update - preserve user modifications + const sourcePath = getModulePath('core'); + const targetPath = path.join(bmadDir, 'core'); await this.fileOps.syncDirectory(sourcePath, targetPath); } } @@ -2554,83 +2532,6 @@ class Installer { return '_bmad-output'; } - /** - * Private: Install core - * @param {string} bmadDir - BMAD installation directory - */ - async installCore(bmadDir) { - const sourcePath = getModulePath('core'); - const targetPath = path.join(bmadDir, 'core'); - - // Copy core files - await this.copyCoreFiles(sourcePath, targetPath); - } - - /** - * Copy core files (similar to copyModuleWithFiltering but for core) - * @param {string} sourcePath - Source path - * @param {string} targetPath - Target path - */ - async copyCoreFiles(sourcePath, targetPath) { - // Get all files in source - const files = await this.getFileList(sourcePath); - - for (const file of files) { - // Skip sub-modules directory - these are IDE-specific and handled separately - if (file.startsWith('sub-modules/')) { - continue; - } - - // Skip module.yaml at root - it's only needed at install time - if (file === 'module.yaml') { - continue; - } - - // Skip config.yaml templates - we'll generate clean ones with actual values - if (file === 'config.yaml' || file.endsWith('/config.yaml') || file === 'custom.yaml' || file.endsWith('/custom.yaml')) { - continue; - } - - const sourceFile = path.join(sourcePath, file); - const targetFile = path.join(targetPath, file); - - // Copy the file with placeholder replacement - await fs.ensureDir(path.dirname(targetFile)); - await this.copyFile(sourceFile, targetFile); - - // Track the installed file - this.installedFiles.add(targetFile); - } - } - - async copyFile(sourcePath, targetPath) { - await fs.copy(sourcePath, targetPath, { overwrite: true }); - } - - /** - * Get list of all files in a directory recursively - * @param {string} dir - Directory path - * @param {string} baseDir - Base directory for relative paths - * @returns {Array} List of relative file paths - */ - async getFileList(dir, baseDir = dir) { - const files = []; - const entries = await fs.readdir(dir, { withFileTypes: true }); - - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - - if (entry.isDirectory()) { - const subFiles = await this.getFileList(fullPath, baseDir); - files.push(...subFiles); - } else { - files.push(path.relative(baseDir, fullPath)); - } - } - - return files; - } - /** * Parse a CSV line, handling quoted fields * @param {string} line - CSV line to parse diff --git a/tools/cli/installers/lib/modules/official-modules.js b/tools/cli/installers/lib/modules/official-modules.js index a2c91d3df..441bea517 100644 --- a/tools/cli/installers/lib/modules/official-modules.js +++ b/tools/cli/installers/lib/modules/official-modules.js @@ -43,8 +43,7 @@ class OfficialModules { } /** - * List all available modules (excluding core which is always installed) - * bmm is the only built-in module, directly under src/bmm-skills + * List all available built-in modules (core and bmm). * All other modules come from external-official-modules.yaml * @returns {Object} Object with modules array and customModules array */ @@ -52,6 +51,15 @@ class OfficialModules { const modules = []; const customModules = []; + // Add built-in core module (directly under src/core-skills) + const corePath = getSourcePath('core-skills'); + if (await fs.pathExists(corePath)) { + const coreInfo = await this.getModuleInfo(corePath, 'core', 'src/core-skills'); + if (coreInfo) { + modules.push(coreInfo); + } + } + // Add built-in bmm module (directly under src/bmm-skills) const bmmPath = getSourcePath('bmm-skills'); if (await fs.pathExists(bmmPath)) { diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index 311280976..7280137d6 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -423,8 +423,10 @@ class UI { selectedModules.push(...customModuleResult.selectedCustomModules); } - // Filter out core - it's always installed via installCore flag - selectedModules = selectedModules.filter((m) => m !== 'core'); + // Ensure core is in the modules list + if (!selectedModules.includes('core')) { + selectedModules.unshift('core'); + } // Get tool selection const toolSelection = await this.promptToolSelection(confirmedDirectory, options); @@ -434,7 +436,6 @@ class UI { return { actionType: 'update', directory: confirmedDirectory, - installCore: true, modules: selectedModules, ides: toolSelection.ides, skipIde: toolSelection.skipIde, @@ -543,14 +544,16 @@ class UI { selectedModules.push(...customContentConfig.selectedModuleIds); } - selectedModules = selectedModules.filter((m) => m !== 'core'); + // Ensure core is in the modules list + if (!selectedModules.includes('core')) { + selectedModules.unshift('core'); + } let toolSelection = await this.promptToolSelection(confirmedDirectory, options); const coreConfig = await this.collectCoreConfig(confirmedDirectory, options); return { actionType: 'install', directory: confirmedDirectory, - installCore: true, modules: selectedModules, ides: toolSelection.ides, skipIde: toolSelection.skipIde, @@ -1069,7 +1072,7 @@ class UI { maxItems: allOptions.length, }); - const result = selected ? selected.filter((m) => m !== 'core') : []; + const result = selected ? [...selected] : []; // Display selected modules as bulleted list if (result.length > 0) {