claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.
312 lines (311 loc) • 10.2 kB
JavaScript
/**
* Configuration Manager v2
* YAML-based configuration management with hot reload and migration support
*
* @version 2.0.0
* @description Standardized configuration management for CFN
*/ import * as fs from 'fs/promises';
import * as path from 'path';
import * as yaml from 'js-yaml';
import Ajv from 'ajv';
import { EventEmitter } from 'events';
import * as chokidar from 'chokidar';
/**
* ConfigManager - Type-safe YAML configuration management
*/ export class ConfigManager extends EventEmitter {
config = {};
configPath;
schemaPath;
ajv;
watcher = null;
environment = 'default';
options;
constructor(configPath, schemaPath, options = {}){
super();
this.configPath = configPath;
this.schemaPath = schemaPath;
this.ajv = new Ajv({
allErrors: true,
strict: false
});
this.options = {
enableHotReload: false,
validateOnLoad: true,
coerceTypes: true,
...options
};
}
/**
* Load configuration from file
*/ async load(environment = 'default') {
this.environment = environment;
try {
// Load base configuration
const baseConfig = await this.loadConfigFile(this.configPath);
// Load environment-specific overrides if not default
let envConfig = {};
if (environment !== 'default') {
const envPath = this.getEnvironmentConfigPath(environment);
try {
envConfig = await this.loadConfigFile(envPath);
} catch (error) {
// Environment override file doesn't exist - that's okay
this.emit('warning', `Environment config not found: ${envPath}`);
}
}
// Merge configurations (environment overrides base)
this.config = this.deepMerge(baseConfig, envConfig);
// Type coercion if enabled
if (this.options.coerceTypes) {
this.config = this.coerceTypes(this.config);
}
// Validate against schema
if (this.options.validateOnLoad) {
await this.validate();
}
this.emit('loaded', this.config);
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to load configuration: ${error.message}`);
}
throw error;
}
}
/**
* Load configuration from YAML or JSON file
*/ async loadConfigFile(filePath) {
const content = await fs.readFile(filePath, 'utf-8');
const ext = path.extname(filePath).toLowerCase();
try {
if (ext === '.yml' || ext === '.yaml') {
return yaml.load(content);
} else if (ext === '.json') {
this.emit('warning', `JSON format is deprecated. Please migrate to YAML: ${filePath}`);
return JSON.parse(content);
} else {
throw new Error(`Unsupported configuration format: ${ext}`);
}
} catch (error) {
if (error instanceof yaml.YAMLException) {
throw new Error(`YAML parsing error: ${error.message}`);
}
throw error;
}
}
/**
* Get environment-specific config path
*/ getEnvironmentConfigPath(environment) {
const dir = path.dirname(this.configPath);
const ext = path.extname(this.configPath);
return path.join(dir, `${environment}${ext}`);
}
/**
* Validate configuration against JSON schema
*/ async validate() {
const schemaContent = await fs.readFile(this.schemaPath, 'utf-8');
const schema = JSON.parse(schemaContent);
const validate = this.ajv.compile(schema);
const valid = validate(this.config);
if (!valid) {
const errors = validate.errors?.map((err)=>`${err.instancePath} ${err.message}`).join(', ');
throw new Error(`Configuration schema validation failed: ${errors}`);
}
}
/**
* Coerce types from strings to proper types
*/ coerceTypes(obj, parentKey) {
if (obj === null || obj === undefined) {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item)=>this.coerceTypes(item));
}
if (typeof obj === 'object') {
const coerced = {};
for (const [key, value] of Object.entries(obj)){
coerced[key] = this.coerceTypes(value, key);
}
return coerced;
}
if (typeof obj === 'string') {
// Don't coerce 'version' fields - keep as strings
if (parentKey === 'version') {
return obj;
}
// Boolean coercion (case insensitive)
const lower = obj.toLowerCase();
if (lower === 'true') return true;
if (lower === 'false') return false;
// Null coercion
if (lower === 'null') return null;
// Number coercion - only if it looks like a pure number
// Don't coerce version strings like "1.0", "2.5.0"
if (/^-?\d+$/.test(obj)) {
return parseInt(obj, 10);
}
if (/^-?\d+\.\d+$/.test(obj) && !obj.match(/\d+\.\d+\.\d+/)) {
return parseFloat(obj);
}
}
return obj;
}
/**
* Deep merge two objects
*/ deepMerge(target, source) {
if (!source) return target;
if (!target) return source;
const output = {
...target
};
for(const key in source){
if (source.hasOwnProperty(key)) {
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
if (key in output) {
output[key] = this.deepMerge(output[key], source[key]);
} else {
output[key] = source[key];
}
} else {
output[key] = source[key];
}
}
}
return output;
}
/**
* Get configuration value by key path
*/ get(keyPath, defaultValue) {
const keys = keyPath.split('.');
let value = this.config;
for (const key of keys){
if (value && typeof value === 'object' && key in value) {
value = value[key];
} else {
return defaultValue;
}
}
return value;
}
/**
* Get all configuration
*/ getAll() {
return {
...this.config
};
}
/**
* Set configuration value (runtime only, not persisted)
*/ set(keyPath, value) {
const keys = keyPath.split('.');
let current = this.config;
for(let i = 0; i < keys.length - 1; i++){
const key = keys[i];
if (!(key in current) || typeof current[key] !== 'object') {
current[key] = {};
}
current = current[key];
}
current[keys[keys.length - 1]] = value;
this.emit('changed', keyPath, value);
}
/**
* Enable hot reload (file watching)
*/ async enableHotReload() {
if (this.watcher) {
return; // Already watching
}
const watchPaths = [
this.configPath
];
// Add environment-specific config if not default
if (this.environment !== 'default') {
watchPaths.push(this.getEnvironmentConfigPath(this.environment));
}
this.watcher = chokidar.watch(watchPaths, {
persistent: true,
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: 300,
pollInterval: 100
}
});
this.watcher.on('change', async (filePath)=>{
try {
await this.reload();
this.emit('reload', this.config);
} catch (error) {
this.emit('error', error);
}
});
this.emit('hotReloadEnabled');
}
/**
* Disable hot reload
*/ async disableHotReload() {
if (this.watcher) {
await this.watcher.close();
this.watcher = null;
this.emit('hotReloadDisabled');
}
}
/**
* Reload configuration from disk
*/ async reload() {
await this.load(this.environment);
}
/**
* Check if configuration has a key
*/ has(keyPath) {
const keys = keyPath.split('.');
let value = this.config;
for (const key of keys){
if (value && typeof value === 'object' && key in value) {
value = value[key];
} else {
return false;
}
}
return true;
}
/**
* Export configuration to YAML string
*/ toYAML() {
return yaml.dump(this.config, {
indent: 2,
lineWidth: 80,
noRefs: true
});
}
/**
* Export configuration to JSON string
*/ toJSON() {
return JSON.stringify(this.config, null, 2);
}
/**
* Cleanup resources
*/ async destroy() {
await this.disableHotReload();
this.removeAllListeners();
this.config = {};
}
}
/**
* Singleton instance for global configuration
*/ let globalInstance = null;
export function getGlobalConfigManager() {
if (!globalInstance) {
throw new Error('ConfigManager not initialized. Call initializeConfigManager() first.');
}
return globalInstance;
}
export function initializeConfigManager(configPath, schemaPath, options) {
globalInstance = new ConfigManager(configPath, schemaPath, options);
return globalInstance;
}
export function resetGlobalConfigManager() {
if (globalInstance) {
globalInstance.destroy();
globalInstance = null;
}
}
//# sourceMappingURL=config-manager.js.map