Files
BMAD-METHOD/tools/cli/lib/config.js
2025-09-28 23:17:07 -05:00

211 lines
5.9 KiB
JavaScript

const fs = require('fs-extra');
const yaml = require('js-yaml');
const path = require('node:path');
/**
* Configuration utility class
*/
class Config {
/**
* Load a YAML configuration file
* @param {string} configPath - Path to config file
* @returns {Object} Parsed configuration
*/
async loadYaml(configPath) {
if (!(await fs.pathExists(configPath))) {
throw new Error(`Configuration file not found: ${configPath}`);
}
const content = await fs.readFile(configPath, 'utf8');
return yaml.load(content);
}
/**
* Save configuration to YAML file
* @param {string} configPath - Path to config file
* @param {Object} config - Configuration object
*/
async saveYaml(configPath, config) {
const yamlContent = yaml.dump(config, {
indent: 2,
lineWidth: 120,
noRefs: true,
});
await fs.ensureDir(path.dirname(configPath));
await fs.writeFile(configPath, yamlContent, 'utf8');
}
/**
* Process configuration file (replace placeholders)
* @param {string} configPath - Path to config file
* @param {Object} replacements - Replacement values
*/
async processConfig(configPath, replacements = {}) {
let content = await fs.readFile(configPath, 'utf8');
// Standard replacements
const standardReplacements = {
'{project-root}': replacements.root || '',
'{module}': replacements.module || '',
'{version}': replacements.version || '5.0.0',
'{date}': new Date().toISOString().split('T')[0],
};
// Apply all replacements
const allReplacements = { ...standardReplacements, ...replacements };
for (const [placeholder, value] of Object.entries(allReplacements)) {
if (typeof placeholder === 'string' && typeof value === 'string') {
const regex = new RegExp(placeholder.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`), 'g');
content = content.replace(regex, value);
}
}
await fs.writeFile(configPath, content, 'utf8');
}
/**
* Merge configurations
* @param {Object} base - Base configuration
* @param {Object} override - Override configuration
* @returns {Object} Merged configuration
*/
mergeConfigs(base, override) {
return this.deepMerge(base, override);
}
/**
* Deep merge two objects
* @param {Object} target - Target object
* @param {Object} source - Source object
* @returns {Object} Merged object
*/
deepMerge(target, source) {
const output = { ...target };
if (this.isObject(target) && this.isObject(source)) {
for (const key of Object.keys(source)) {
if (this.isObject(source[key])) {
if (key in target) {
output[key] = this.deepMerge(target[key], source[key]);
} else {
output[key] = source[key];
}
} else {
output[key] = source[key];
}
}
}
return output;
}
/**
* Check if value is an object
* @param {*} item - Item to check
* @returns {boolean} True if object
*/
isObject(item) {
return item && typeof item === 'object' && !Array.isArray(item);
}
/**
* Validate configuration against schema
* @param {Object} config - Configuration to validate
* @param {Object} schema - Validation schema
* @returns {Object} Validation result
*/
validateConfig(config, schema) {
const errors = [];
const warnings = [];
// Check required fields
if (schema.required) {
for (const field of schema.required) {
if (!(field in config)) {
errors.push(`Missing required field: ${field}`);
}
}
}
// Check field types
if (schema.properties) {
for (const [field, spec] of Object.entries(schema.properties)) {
if (field in config) {
const value = config[field];
const expectedType = spec.type;
if (expectedType === 'array' && !Array.isArray(value)) {
errors.push(`Field '${field}' should be an array`);
} else if (expectedType === 'object' && !this.isObject(value)) {
errors.push(`Field '${field}' should be an object`);
} else if (expectedType === 'string' && typeof value !== 'string') {
errors.push(`Field '${field}' should be a string`);
} else if (expectedType === 'number' && typeof value !== 'number') {
errors.push(`Field '${field}' should be a number`);
} else if (expectedType === 'boolean' && typeof value !== 'boolean') {
errors.push(`Field '${field}' should be a boolean`);
}
// Check enum values
if (spec.enum && !spec.enum.includes(value)) {
errors.push(`Field '${field}' must be one of: ${spec.enum.join(', ')}`);
}
}
}
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Get configuration value with fallback
* @param {Object} config - Configuration object
* @param {string} path - Dot-notation path to value
* @param {*} defaultValue - Default value if not found
* @returns {*} Configuration value
*/
getValue(config, path, defaultValue = null) {
const keys = path.split('.');
let current = config;
for (const key of keys) {
if (current && typeof current === 'object' && key in current) {
current = current[key];
} else {
return defaultValue;
}
}
return current;
}
/**
* Set configuration value
* @param {Object} config - Configuration object
* @param {string} path - Dot-notation path to value
* @param {*} value - Value to set
*/
setValue(config, path, value) {
const keys = path.split('.');
const lastKey = keys.pop();
let current = config;
for (const key of keys) {
if (!(key in current) || typeof current[key] !== 'object') {
current[key] = {};
}
current = current[key];
}
current[lastKey] = value;
}
}
module.exports = { Config };