diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 8b6f505de..2d840f713 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -15,6 +15,7 @@ const path = require('node:path'); const os = require('node:os'); const fs = require('fs-extra'); const { ConfigCollector } = require('../tools/cli/installers/lib/core/config-collector'); +const { applyDefaultCoreConfig, clearCoreConfigDefaultsCache, getDefaultCoreConfig } = require('../tools/cli/lib/core-config-defaults'); const { ManifestGenerator } = require('../tools/cli/installers/lib/core/manifest-generator'); const { IdeManager } = require('../tools/cli/installers/lib/ide/manager'); const { clearCache, loadPlatformCodes } = require('../tools/cli/installers/lib/ide/platform-codes'); @@ -2028,6 +2029,38 @@ async function runTests() { console.log(''); + // ============================================================ + // Test Suite 34: Core Config Default Backfill + // ============================================================ + console.log(`${colors.yellow}Test Suite 34: Core Config Default Backfill${colors.reset}\n`); + + try { + clearCoreConfigDefaultsCache(); + const defaults = await getDefaultCoreConfig(); + const normalized = applyDefaultCoreConfig( + { + user_name: '{user_name}', + communication_language: 'Spanish', + document_output_language: '', + output_folder: '{output_folder}', + }, + defaults, + ); + + assert(normalized.appliedDefaults === true, 'Core config backfill reports when defaults were applied'); + assert(normalized.coreConfig.user_name === defaults.user_name, 'Core config backfill replaces unresolved user_name placeholder'); + assert(normalized.coreConfig.communication_language === 'Spanish', 'Core config backfill preserves existing valid values'); + assert( + normalized.coreConfig.document_output_language === defaults.document_output_language, + 'Core config backfill replaces blank document output language', + ); + assert(normalized.coreConfig.output_folder === defaults.output_folder, 'Core config backfill replaces unresolved output_folder'); + } catch (error) { + assert(false, 'Core config default backfill test succeeds', error.message); + } + + console.log(''); + // ============================================================ // Summary // ============================================================ diff --git a/tools/cli/lib/core-config-defaults.js b/tools/cli/lib/core-config-defaults.js new file mode 100644 index 000000000..931722b3f --- /dev/null +++ b/tools/cli/lib/core-config-defaults.js @@ -0,0 +1,89 @@ +const os = require('node:os'); +const fs = require('fs-extra'); +const yaml = require('yaml'); +const prompts = require('./prompts'); +const { getModulePath } = require('./project-root'); + +let cachedCoreConfigDefaults = null; + +function getFallbackUsername() { + let safeUsername; + try { + safeUsername = os.userInfo().username; + } catch { + safeUsername = process.env.USER || process.env.USERNAME || 'User'; + } + + if (typeof safeUsername !== 'string' || safeUsername.trim() === '') { + return 'User'; + } + + const normalizedUsername = safeUsername.trim(); + return normalizedUsername.charAt(0).toUpperCase() + normalizedUsername.slice(1); +} + +function normalizeDefaultString(value, fallback) { + return typeof value === 'string' && value.trim() !== '' ? value.trim() : fallback; +} + +function isMissingOrUnresolvedCoreConfigValue(value) { + return value == null || (typeof value === 'string' && (value.trim() === '' || /^\{[^}]+\}$/.test(value.trim()))); +} + +function applyDefaultCoreConfig(coreConfig = {}, defaults = {}) { + const normalizedConfig = { ...coreConfig }; + let appliedDefaults = false; + + for (const [key, value] of Object.entries(defaults)) { + if (isMissingOrUnresolvedCoreConfigValue(normalizedConfig[key])) { + normalizedConfig[key] = value; + appliedDefaults = true; + } + } + + return { coreConfig: normalizedConfig, appliedDefaults }; +} + +async function getDefaultCoreConfig() { + if (cachedCoreConfigDefaults) { + return { ...cachedCoreConfigDefaults }; + } + + const fallbackDefaults = { + user_name: getFallbackUsername(), + communication_language: 'English', + document_output_language: 'English', + output_folder: '_bmad-output', + }; + + try { + const moduleYamlPath = getModulePath('core', 'module.yaml'); + const moduleConfig = yaml.parse(await fs.readFile(moduleYamlPath, 'utf8')) || {}; + + cachedCoreConfigDefaults = { + user_name: normalizeDefaultString(moduleConfig.user_name?.default, fallbackDefaults.user_name), + communication_language: normalizeDefaultString(moduleConfig.communication_language?.default, fallbackDefaults.communication_language), + document_output_language: normalizeDefaultString( + moduleConfig.document_output_language?.default, + fallbackDefaults.document_output_language, + ), + output_folder: normalizeDefaultString(moduleConfig.output_folder?.default, fallbackDefaults.output_folder), + }; + } catch (error) { + await prompts.log.warn(`Failed to load module.yaml, falling back to defaults: ${error.message}`); + cachedCoreConfigDefaults = fallbackDefaults; + } + + return { ...cachedCoreConfigDefaults }; +} + +function clearCoreConfigDefaultsCache() { + cachedCoreConfigDefaults = null; +} + +module.exports = { + applyDefaultCoreConfig, + clearCoreConfigDefaultsCache, + getDefaultCoreConfig, + isMissingOrUnresolvedCoreConfigValue, +}; diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index 3f25dae03..d4805670a 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -2,6 +2,7 @@ const path = require('node:path'); const os = require('node:os'); const fs = require('fs-extra'); const { CLIUtils } = require('./cli-utils'); +const { applyDefaultCoreConfig, getDefaultCoreConfig: loadDefaultCoreConfig } = require('./core-config-defaults'); const { CustomHandler } = require('../installers/lib/custom/handler'); const { ExternalModuleManager } = require('../installers/lib/modules/external-manager'); const prompts = require('./prompts'); @@ -47,6 +48,21 @@ class UI { } confirmedDirectory = expandedDir; await prompts.log.info(`Using directory from command-line: ${confirmedDirectory}`); + } else if (options.yes) { + // Default to current directory when --yes flag is set + let cwd; + try { + cwd = process.cwd(); + } catch (error) { + await prompts.log.error(`Failed to resolve current directory (--yes flag): ${error.message}`); + throw new Error(`Unable to determine current directory: ${error.message}`); + } + const validation = this.validateDirectorySync(cwd); + if (validation) { + throw new Error(`Invalid current directory: ${validation}`); + } + confirmedDirectory = cwd; + await prompts.log.info(`Using current directory (--yes flag): ${confirmedDirectory}`); } else { confirmedDirectory = await this.getConfirmedDirectory(); } @@ -823,6 +839,14 @@ class UI { return { existingInstall, installedModuleIds, bmadDir }; } + /** + * Get default core config values by reading from src/core/module.yaml + * @returns {Object} Default core config with user_name, communication_language, document_output_language, output_folder + */ + async getDefaultCoreConfig() { + return loadDefaultCoreConfig(); + } + /** * Collect core configuration * @param {string} directory - Installation directory @@ -866,27 +890,21 @@ class UI { (!options.userName || !options.communicationLanguage || !options.documentOutputLanguage || !options.outputFolder) ) { await configCollector.collectModuleConfig('core', directory, false, true); + } else if (options.yes) { + const defaults = await this.getDefaultCoreConfig(); + const normalizedConfig = applyDefaultCoreConfig(configCollector.collectedConfig.core, defaults); + configCollector.collectedConfig.core = normalizedConfig.coreConfig; + if (normalizedConfig.appliedDefaults) { + await prompts.log.info('Using default configuration (--yes flag)'); + } } } else if (options.yes) { - // Use all defaults when --yes flag is set await configCollector.loadExistingConfig(directory); const existingConfig = configCollector.collectedConfig.core || {}; - - // If no existing config, use defaults - if (Object.keys(existingConfig).length === 0) { - let safeUsername; - try { - safeUsername = os.userInfo().username; - } catch { - safeUsername = process.env.USER || process.env.USERNAME || 'User'; - } - const defaultUsername = safeUsername.charAt(0).toUpperCase() + safeUsername.slice(1); - configCollector.collectedConfig.core = { - user_name: defaultUsername, - communication_language: 'English', - document_output_language: 'English', - output_folder: '_bmad-output', - }; + const defaults = await this.getDefaultCoreConfig(); + const normalizedConfig = applyDefaultCoreConfig(existingConfig, defaults); + configCollector.collectedConfig.core = normalizedConfig.coreConfig; + if (normalizedConfig.appliedDefaults) { await prompts.log.info('Using default configuration (--yes flag)'); } } else {