From 60238d28540ae3a39729b2b77b8d67c3ea8be0ed Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Mon, 15 Dec 2025 12:55:57 +0800 Subject: [PATCH] default accepted for installer quesitons --- src/modules/cis/module.yaml | 1 - .../installers/lib/core/config-collector.js | 81 ++++++++++++++++--- .../lib/core/custom-module-cache.js | 5 +- .../installers/lib/core/ide-config-manager.js | 5 +- tools/cli/installers/lib/core/installer.js | 15 +++- .../installers/lib/core/manifest-generator.js | 5 +- tools/cli/installers/lib/core/manifest.js | 10 ++- tools/cli/installers/lib/modules/manager.js | 5 +- tools/cli/lib/ui.js | 4 +- 9 files changed, 110 insertions(+), 21 deletions(-) diff --git a/src/modules/cis/module.yaml b/src/modules/cis/module.yaml index 715ab7da..48ac552a 100644 --- a/src/modules/cis/module.yaml +++ b/src/modules/cis/module.yaml @@ -13,5 +13,4 @@ default_selected: false # This module will not be selected by default for new in creativity_self_assessment: prompt: "On a scale of 1 to 10, how would you rate your current level of creativity?" - default: 7 result: "{value}" diff --git a/tools/cli/installers/lib/core/config-collector.js b/tools/cli/installers/lib/core/config-collector.js index ac3e4da2..bdcea5d3 100644 --- a/tools/cli/installers/lib/core/config-collector.js +++ b/tools/cli/installers/lib/core/config-collector.js @@ -303,7 +303,7 @@ class ConfigCollector { } } // Show "no config" message for modules with no new questions - CLIUtils.displayModuleNoConfig(moduleName, moduleConfig.header, moduleConfig.subheader); + console.log(chalk.dim(` ✓ ${moduleName.toUpperCase()} module already up to date`)); return false; // No new fields } @@ -339,7 +339,7 @@ class ConfigCollector { Object.assign(allAnswers, promptedAnswers); } else if (newStaticKeys.length > 0) { // Only static fields, no questions - show no config message - CLIUtils.displayModuleNoConfig(moduleName, moduleConfig.header, moduleConfig.subheader); + console.log(chalk.dim(` ✓ ${moduleName.toUpperCase()} module configuration updated`)); } // Store all answers for cross-referencing @@ -558,21 +558,57 @@ class ConfigCollector { // Collect all answers (static + prompted) let allAnswers = { ...staticAnswers }; - // Display appropriate header based on whether there are questions + // If there are questions to ask, prompt for accepting defaults vs customizing if (questions.length > 0) { - CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader); - console.log(); // Line break before questions - const promptedAnswers = await inquirer.prompt(questions); + // Get friendly module name from config or use uppercase module name + const moduleDisplayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`; - // Merge prompted answers with static answers - Object.assign(allAnswers, promptedAnswers); + // Display the module name in color first + console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName)); + + // Ask user if they want to accept defaults or customize on the next line + const { customize } = await inquirer.prompt([ + { + type: 'confirm', + name: 'customize', + message: 'Accept Defaults (no to customize)?', + default: true, + }, + ]); + + if (customize) { + // Accept defaults - only ask questions that have NO default value + const questionsWithoutDefaults = questions.filter((q) => q.default === undefined || q.default === null || q.default === ''); + + if (questionsWithoutDefaults.length > 0) { + console.log(chalk.dim(`\n Asking required questions for ${moduleName.toUpperCase()}...`)); + const promptedAnswers = await inquirer.prompt(questionsWithoutDefaults); + Object.assign(allAnswers, promptedAnswers); + } + + // For questions with defaults that weren't asked, we need to process them with their default values + const questionsWithDefaults = questions.filter((q) => q.default !== undefined && q.default !== null && q.default !== ''); + for (const question of questionsWithDefaults) { + // Skip function defaults - these are dynamic and will be evaluated later + if (typeof question.default === 'function') { + continue; + } + allAnswers[question.name] = question.default; + } + } else { + // Customize - ask all questions + console.log(chalk.dim(`\n Configuring ${moduleName.toUpperCase()}...`)); + const promptedAnswers = await inquirer.prompt(questions); + Object.assign(allAnswers, promptedAnswers); + } } // Store all answers for cross-referencing Object.assign(this.allAnswers, allAnswers); // Process all answers (both static and prompted) - if (Object.keys(allAnswers).length > 0) { + // Always process if we have any answers or static answers + if (Object.keys(allAnswers).length > 0 || Object.keys(staticAnswers).length > 0) { const answers = allAnswers; // Process answers and build result values @@ -672,7 +708,32 @@ class ConfigCollector { // No longer display completion boxes - keep output clean } else { // No questions for this module - show completion message - CLIUtils.displayModuleNoConfig(moduleName, moduleConfig.header, moduleConfig.subheader); + console.log(chalk.dim(` ✓ ${moduleName.toUpperCase()} module configured`)); + } + + // If we have no collected config for this module, but we have a module schema, + // ensure we have at least an empty object + if (!this.collectedConfig[moduleName]) { + this.collectedConfig[moduleName] = {}; + + // If we accepted defaults and have no answers, we still need to check + // if there are any static values in the schema that should be applied + if (moduleConfig) { + for (const key of Object.keys(moduleConfig)) { + if (key !== 'prompt' && moduleConfig[key] && typeof moduleConfig[key] === 'object') { + const item = moduleConfig[key]; + // For static items (no prompt, just result), apply the result + if (!item.prompt && item.result) { + // Apply any placeholder replacements to the result + let result = item.result; + if (typeof result === 'string') { + result = this.replacePlaceholders(result, moduleName, moduleConfig); + } + this.collectedConfig[moduleName][key] = result; + } + } + } + } } } diff --git a/tools/cli/installers/lib/core/custom-module-cache.js b/tools/cli/installers/lib/core/custom-module-cache.js index 378f94ca..4486e5fe 100644 --- a/tools/cli/installers/lib/core/custom-module-cache.js +++ b/tools/cli/installers/lib/core/custom-module-cache.js @@ -40,7 +40,10 @@ class CustomModuleCache { */ async updateCacheManifest(manifest) { const yaml = require('yaml'); - const content = yaml.stringify(manifest, { + // Clean the manifest to remove any non-serializable values + const cleanManifest = structuredClone(manifest); + + const content = yaml.stringify(cleanManifest, { indent: 2, lineWidth: 0, sortKeys: false, diff --git a/tools/cli/installers/lib/core/ide-config-manager.js b/tools/cli/installers/lib/core/ide-config-manager.js index 2e83c6de..f871e4b5 100644 --- a/tools/cli/installers/lib/core/ide-config-manager.js +++ b/tools/cli/installers/lib/core/ide-config-manager.js @@ -61,7 +61,10 @@ class IdeConfigManager { configuration: configuration || {}, }; - const yamlContent = yaml.stringify(configData, { + // Clean the config to remove any non-serializable values (like functions) + const cleanConfig = structuredClone(configData); + + const yamlContent = yaml.stringify(cleanConfig, { indent: 2, lineWidth: 0, sortKeys: false, diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index ecb2aa09..39ca0431 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -394,8 +394,14 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice: // Clone config to avoid mutating the caller's object const config = { ...originalConfig }; + // Check if core config was already collected in UI + const hasCoreConfig = config.coreConfig && Object.keys(config.coreConfig).length > 0; + // Only display logo if core config wasn't already collected (meaning we're not continuing from UI) - if (!config.coreConfig) { + if (hasCoreConfig) { + // Core config was already collected in UI, show smooth continuation + // Don't clear screen, just continue flow + } else { // Display BMAD logo CLIUtils.displayLogo(); @@ -409,7 +415,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice: const projectDir = path.resolve(config.directory); // If core config was pre-collected (from interactive mode), use it - if (config.coreConfig) { + if (config.coreConfig && Object.keys(config.coreConfig).length > 0) { this.configCollector.collectedConfig.core = config.coreConfig; // Also store in allAnswers for cross-referencing this.configCollector.allAnswers = {}; @@ -1583,8 +1589,11 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice: coreSection = '\n# Core Configuration Values\n'; } + // Clean the config to remove any non-serializable values (like functions) + const cleanConfig = structuredClone(finalConfig); + // Convert config to YAML - let yamlContent = yaml.stringify(finalConfig, { + let yamlContent = yaml.stringify(cleanConfig, { indent: 2, lineWidth: 0, minContentWidth: 0, diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index e36194bd..a1308d3a 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -486,7 +486,10 @@ class ManifestGenerator { ides: this.selectedIdes, }; - const yamlStr = yaml.stringify(manifest, { + // Clean the manifest to remove any non-serializable values + const cleanManifest = structuredClone(manifest); + + const yamlStr = yaml.stringify(cleanManifest, { indent: 2, lineWidth: 0, sortKeys: false, diff --git a/tools/cli/installers/lib/core/manifest.js b/tools/cli/installers/lib/core/manifest.js index a677ed92..24490694 100644 --- a/tools/cli/installers/lib/core/manifest.js +++ b/tools/cli/installers/lib/core/manifest.js @@ -28,7 +28,10 @@ class Manifest { }; // Write YAML manifest - const yamlContent = yaml.stringify(manifestData, { + // Clean the manifest data to remove any non-serializable values + const cleanManifestData = structuredClone(manifestData); + + const yamlContent = yaml.stringify(cleanManifestData, { indent: 2, lineWidth: 0, sortKeys: false, @@ -100,7 +103,10 @@ class Manifest { const manifestPath = path.join(bmadDir, '_config', 'manifest.yaml'); await fs.ensureDir(path.dirname(manifestPath)); - const yamlContent = yaml.stringify(manifestData, { + // Clean the manifest data to remove any non-serializable values + const cleanManifestData = structuredClone(manifestData); + + const yamlContent = yaml.stringify(cleanManifestData, { indent: 2, lineWidth: 0, sortKeys: false, diff --git a/tools/cli/installers/lib/modules/manager.js b/tools/cli/installers/lib/modules/manager.js index 1388546c..12ac96b1 100644 --- a/tools/cli/installers/lib/modules/manager.js +++ b/tools/cli/installers/lib/modules/manager.js @@ -859,7 +859,10 @@ class ModuleManager { // Write back to manifest const yaml = require('yaml'); - const updatedContent = yaml.stringify(manifestData, { + // Clean the manifest data to remove any non-serializable values + const cleanManifestData = structuredClone(manifestData); + + const updatedContent = yaml.stringify(cleanManifestData, { indent: 2, lineWidth: 0, }); diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index c50b5f55..d57c5dd8 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -703,7 +703,9 @@ class UI { // Now collect with existing values as defaults (false = don't skip loading, true = skip completion message) await configCollector.collectModuleConfig('core', directory, false, true); - return configCollector.collectedConfig.core; + const coreConfig = configCollector.collectedConfig.core; + // Ensure we always have a core config object, even if empty + return coreConfig || {}; } /**