fix(cli): extract core-config-defaults module and centralize fallback logic
- Extract getDefaultCoreConfig, applyDefaultCoreConfig, and isMissingOrUnresolvedCoreConfigValue into core-config-defaults.js - Memoize YAML parsing with clearable cache for testing - Use prompts.log.warn instead of console.warn for error logging - Guard process.cwd() with try/catch in --yes path - Centralize handler.js placeholder replacement via shared defaults - Add tests for backfill logic and custom handler shared defaults
This commit is contained in:
parent
1571a3cff4
commit
8927e207e8
|
|
@ -15,6 +15,7 @@ const path = require('node:path');
|
||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { ConfigCollector } = require('../tools/cli/installers/lib/core/config-collector');
|
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 { ManifestGenerator } = require('../tools/cli/installers/lib/core/manifest-generator');
|
||||||
const { IdeManager } = require('../tools/cli/installers/lib/ide/manager');
|
const { IdeManager } = require('../tools/cli/installers/lib/ide/manager');
|
||||||
const { clearCache, loadPlatformCodes } = require('../tools/cli/installers/lib/ide/platform-codes');
|
const { clearCache, loadPlatformCodes } = require('../tools/cli/installers/lib/ide/platform-codes');
|
||||||
|
|
@ -2028,6 +2029,38 @@ async function runTests() {
|
||||||
|
|
||||||
console.log('');
|
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
|
// Summary
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
@ -2,6 +2,7 @@ const path = require('node:path');
|
||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { CLIUtils } = require('./cli-utils');
|
const { CLIUtils } = require('./cli-utils');
|
||||||
|
const { applyDefaultCoreConfig, getDefaultCoreConfig: loadDefaultCoreConfig } = require('./core-config-defaults');
|
||||||
const { CustomHandler } = require('../installers/lib/custom/handler');
|
const { CustomHandler } = require('../installers/lib/custom/handler');
|
||||||
const { ExternalModuleManager } = require('../installers/lib/modules/external-manager');
|
const { ExternalModuleManager } = require('../installers/lib/modules/external-manager');
|
||||||
const prompts = require('./prompts');
|
const prompts = require('./prompts');
|
||||||
|
|
@ -49,7 +50,13 @@ class UI {
|
||||||
await prompts.log.info(`Using directory from command-line: ${confirmedDirectory}`);
|
await prompts.log.info(`Using directory from command-line: ${confirmedDirectory}`);
|
||||||
} else if (options.yes) {
|
} else if (options.yes) {
|
||||||
// Default to current directory when --yes flag is set
|
// Default to current directory when --yes flag is set
|
||||||
const cwd = process.cwd();
|
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);
|
const validation = this.validateDirectorySync(cwd);
|
||||||
if (validation) {
|
if (validation) {
|
||||||
throw new Error(`Invalid current directory: ${validation}`);
|
throw new Error(`Invalid current directory: ${validation}`);
|
||||||
|
|
@ -836,39 +843,8 @@ class UI {
|
||||||
* Get default core config values by reading from src/core/module.yaml
|
* 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
|
* @returns {Object} Default core config with user_name, communication_language, document_output_language, output_folder
|
||||||
*/
|
*/
|
||||||
getDefaultCoreConfig() {
|
async getDefaultCoreConfig() {
|
||||||
const { getModulePath } = require('./project-root');
|
return loadDefaultCoreConfig();
|
||||||
const yaml = require('yaml');
|
|
||||||
|
|
||||||
let safeUsername;
|
|
||||||
try {
|
|
||||||
safeUsername = os.userInfo().username;
|
|
||||||
} catch {
|
|
||||||
safeUsername = process.env.USER || process.env.USERNAME || 'User';
|
|
||||||
}
|
|
||||||
const osUsername = safeUsername.charAt(0).toUpperCase() + safeUsername.slice(1);
|
|
||||||
|
|
||||||
const norm = (value, fallback) => (typeof value === 'string' && value.trim() !== '' ? value.trim() : fallback);
|
|
||||||
|
|
||||||
// Read defaults from core module.yaml (single source of truth)
|
|
||||||
try {
|
|
||||||
const moduleYamlPath = path.join(getModulePath('core'), 'module.yaml');
|
|
||||||
const moduleConfig = yaml.parse(fs.readFileSync(moduleYamlPath, 'utf8'));
|
|
||||||
return {
|
|
||||||
user_name: norm(moduleConfig.user_name?.default, osUsername),
|
|
||||||
communication_language: norm(moduleConfig.communication_language?.default, 'English'),
|
|
||||||
document_output_language: norm(moduleConfig.document_output_language?.default, 'English'),
|
|
||||||
output_folder: norm(moduleConfig.output_folder?.default, '_bmad-output'),
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Failed to load module.yaml, falling back to defaults: ${error.message}`);
|
|
||||||
return {
|
|
||||||
user_name: osUsername,
|
|
||||||
communication_language: 'English',
|
|
||||||
document_output_language: 'English',
|
|
||||||
output_folder: '_bmad-output',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -915,32 +891,20 @@ class UI {
|
||||||
) {
|
) {
|
||||||
await configCollector.collectModuleConfig('core', directory, false, true);
|
await configCollector.collectModuleConfig('core', directory, false, true);
|
||||||
} else if (options.yes) {
|
} else if (options.yes) {
|
||||||
// Fill in defaults for any fields not provided via command-line or existing config
|
const defaults = await this.getDefaultCoreConfig();
|
||||||
const isMissingOrUnresolved = (v) => v == null || (typeof v === 'string' && (v.trim() === '' || /^\{[^}]+\}$/.test(v.trim())));
|
const normalizedConfig = applyDefaultCoreConfig(configCollector.collectedConfig.core, defaults);
|
||||||
|
configCollector.collectedConfig.core = normalizedConfig.coreConfig;
|
||||||
const defaults = this.getDefaultCoreConfig();
|
if (normalizedConfig.appliedDefaults) {
|
||||||
for (const [key, value] of Object.entries(defaults)) {
|
await prompts.log.info('Using default configuration (--yes flag)');
|
||||||
if (isMissingOrUnresolved(configCollector.collectedConfig.core[key])) {
|
|
||||||
configCollector.collectedConfig.core[key] = value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (options.yes) {
|
} else if (options.yes) {
|
||||||
// Use all defaults when --yes flag is set, merging with any existing config
|
|
||||||
await configCollector.loadExistingConfig(directory);
|
await configCollector.loadExistingConfig(directory);
|
||||||
const existingConfig = configCollector.collectedConfig.core || {};
|
const existingConfig = configCollector.collectedConfig.core || {};
|
||||||
const defaults = this.getDefaultCoreConfig();
|
const defaults = await this.getDefaultCoreConfig();
|
||||||
configCollector.collectedConfig.core = { ...defaults, ...existingConfig };
|
const normalizedConfig = applyDefaultCoreConfig(existingConfig, defaults);
|
||||||
|
configCollector.collectedConfig.core = normalizedConfig.coreConfig;
|
||||||
// Clean up any unresolved placeholder tokens from existing config
|
if (normalizedConfig.appliedDefaults) {
|
||||||
const isMissingOrUnresolved = (v) => v == null || (typeof v === 'string' && (v.trim() === '' || /^\{[^}]+\}$/.test(v.trim())));
|
|
||||||
for (const [key, value] of Object.entries(configCollector.collectedConfig.core)) {
|
|
||||||
if (isMissingOrUnresolved(value)) {
|
|
||||||
configCollector.collectedConfig.core[key] = defaults[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(existingConfig).length === 0) {
|
|
||||||
await prompts.log.info('Using default configuration (--yes flag)');
|
await prompts.log.info('Using default configuration (--yes flag)');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue