diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index 659d55193..44f31090d 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -1201,19 +1201,11 @@ class Installer { lines.push(` ${icon} ${r.step}${detail}`); } - // Add context info + // Context and warnings lines.push(''); if (context.bmadDir) { lines.push(` Installed to: ${color.dim(context.bmadDir)}`); } - if (context.modules && context.modules.length > 0) { - lines.push(` Modules: ${color.dim(context.modules.join(', '))}`); - } - if (context.ides && context.ides.length > 0) { - lines.push(` Tools: ${color.dim(context.ides.join(', '))}`); - } - - // Custom/modified file warnings if (context.customFiles && context.customFiles.length > 0) { lines.push(` ${color.cyan(`Custom files preserved: ${context.customFiles.length}`)}`); } diff --git a/tools/cli/installers/lib/modules/manager.js b/tools/cli/installers/lib/modules/manager.js index 0af4312fc..a0b50048c 100644 --- a/tools/cli/installers/lib/modules/manager.js +++ b/tools/cli/installers/lib/modules/manager.js @@ -1277,16 +1277,31 @@ class ModuleManager { } } - const installerPath = path.join(sourcePath, '_module-installer', 'installer.js'); + const installerDir = path.join(sourcePath, '_module-installer'); + // Prefer .cjs (always CommonJS) then fall back to .js + const cjsPath = path.join(installerDir, 'installer.cjs'); + const jsPath = path.join(installerDir, 'installer.js'); + const hasCjs = await fs.pathExists(cjsPath); + const installerPath = hasCjs ? cjsPath : jsPath; // Check if module has a custom installer - if (!(await fs.pathExists(installerPath))) { + if (!hasCjs && !(await fs.pathExists(jsPath))) { return; // No custom installer } try { - // Load the module installer - const moduleInstaller = require(installerPath); + // .cjs files are always CommonJS and safe to require(). + // .js files may be ESM (when the package sets "type":"module"), + // so use dynamic import() which handles both CJS and ESM. + let moduleInstaller; + if (hasCjs) { + moduleInstaller = require(installerPath); + } else { + const { pathToFileURL } = require('node:url'); + const imported = await import(pathToFileURL(installerPath).href); + // CJS module.exports lands on .default; ESM default can be object, function, or class + moduleInstaller = imported.default == null ? imported : imported.default; + } if (typeof moduleInstaller.install === 'function') { // Get project root (parent of bmad directory) @@ -1312,8 +1327,12 @@ class ModuleManager { await prompts.log.warn(`Module installer for ${moduleName} returned false`); } } - } catch (error) { - await prompts.log.error(`Error running module installer for ${moduleName}: ${error.message}`); + } catch { + // Post-install scripts are optional; module files are already installed. + // TODO: Eliminate post-install scripts entirely by adding a `directories` key + // to module.yaml that declares which config keys are paths to auto-create. + // The main installer can then handle directory creation centrally, removing + // the need for per-module installer.js scripts and their CJS/ESM issues. } } diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index 9134b4e28..622f5a1e6 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -274,7 +274,6 @@ class UI { await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`); } else { selectedModules = await this.selectAllModules(installedModuleIds); - selectedModules = selectedModules.filter((m) => m !== 'core'); } // After module selection, ask about custom modules @@ -362,6 +361,9 @@ class UI { selectedModules.push(...customModuleResult.selectedCustomModules); } + // Filter out core - it's always installed via installCore flag + selectedModules = selectedModules.filter((m) => m !== 'core'); + // Get tool selection const toolSelection = await this.promptToolSelection(confirmedDirectory, options); @@ -899,107 +901,10 @@ class UI { } /** - * Prompt for module selection - * @param {Array} moduleChoices - Available module choices - * @returns {Array} Selected module IDs - */ - async selectModules(moduleChoices, defaultSelections = null) { - // If defaultSelections is provided, use it to override checked state - // Otherwise preserve the checked state from moduleChoices (set by getModuleChoices) - const choicesWithDefaults = moduleChoices.map((choice) => ({ - ...choice, - ...(defaultSelections === null ? {} : { checked: defaultSelections.includes(choice.value) }), - })); - - // Add a "None" option at the end for users who changed their mind - const choicesWithSkipOption = [ - ...choicesWithDefaults, - { - value: '__NONE__', - label: '\u26A0 None / I changed my mind - skip module installation', - checked: false, - }, - ]; - - const selected = await prompts.multiselect({ - message: 'Select modules to install (use arrow keys, space to toggle):', - choices: choicesWithSkipOption, - required: true, - }); - - // If user selected both "__NONE__" and other items, honor the "None" choice - if (selected && selected.includes('__NONE__') && selected.length > 1) { - await prompts.log.warn('"None / I changed my mind" was selected, so no modules will be installed.'); - return []; - } - - // Filter out the special '__NONE__' value - return selected ? selected.filter((m) => m !== '__NONE__') : []; - } - - /** - * Get external module choices for selection - * @returns {Array} External module choices for prompt - */ - async getExternalModuleChoices() { - const externalManager = new ExternalModuleManager(); - const modules = await externalManager.listAvailable(); - - return modules.map((mod) => ({ - name: mod.name, - value: mod.code, // Use the code (e.g., 'cis') as the value - checked: mod.defaultSelected || false, - hint: mod.description || undefined, // Show description as hint - module: mod, // Store full module info for later use - })); - } - - /** - * Prompt for external module selection - * @param {Array} externalModuleChoices - Available external module choices - * @param {Array} defaultSelections - Module codes to pre-select - * @returns {Array} Selected external module codes - */ - async selectExternalModules(externalModuleChoices, defaultSelections = []) { - // Build a message showing available modules - const message = 'Select official BMad modules to install (use arrow keys, space to toggle):'; - - // Mark choices as checked based on defaultSelections - const choicesWithDefaults = externalModuleChoices.map((choice) => ({ - ...choice, - checked: defaultSelections.includes(choice.value), - })); - - // Add a "None" option at the end for users who changed their mind - const choicesWithSkipOption = [ - ...choicesWithDefaults, - { - name: '⚠ None / I changed my mind - skip external module installation', - value: '__NONE__', - checked: false, - }, - ]; - - const selected = await prompts.multiselect({ - message, - choices: choicesWithSkipOption, - required: true, - }); - - // If user selected both "__NONE__" and other items, honor the "None" choice - if (selected && selected.includes('__NONE__') && selected.length > 1) { - await prompts.log.warn('"None / I changed my mind" was selected, so no external modules will be installed.'); - return []; - } - - // Filter out the special '__NONE__' value - return selected ? selected.filter((m) => m !== '__NONE__') : []; - } - - /** - * Select all modules (core + official + community) using grouped multiselect + * Select all modules (official + community) using grouped multiselect. + * Core is shown as locked but filtered from the result since it's always installed separately. * @param {Set} installedModuleIds - Currently installed module IDs - * @returns {Array} Selected module codes + * @returns {Array} Selected module codes (excluding core) */ async selectAllModules(installedModuleIds = new Set()) { const { ModuleManager } = require('../installers/lib/modules/manager'); @@ -1068,11 +973,7 @@ class UI { } } } - allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint })), { - // "None" option at the end - label: '\u26A0 None - Skip module installation', - value: '__NONE__', - }); + allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint }))); const selected = await prompts.autocompleteMultiselect({ message: 'Select modules to install:', @@ -1083,14 +984,7 @@ class UI { maxItems: allOptions.length, }); - // If user selected both "__NONE__" and other items, honor the "None" choice - if (selected && selected.includes('__NONE__') && selected.length > 1) { - await prompts.log.warn('"None" was selected, so no modules will be installed.'); - return []; - } - - // Filter out the special '__NONE__' value - const result = selected ? selected.filter((m) => m !== '__NONE__') : []; + const result = selected ? selected.filter((m) => m !== 'core') : []; // Display selected modules as bulleted list if (result.length > 0) {