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.
386 lines (385 loc) • 13.9 kB
JavaScript
/**
* Configuration Migrator
* Migrate legacy configuration formats to YAML
*
* @version 1.0.0
* @description Handles migration from JSON, ENV, and bash variable formats
*/ import * as fs from 'fs/promises';
import * as path from 'path';
import * as yaml from 'js-yaml';
import Ajv from 'ajv';
/**
* ConfigMigrator - Convert legacy formats to YAML
*/ export class ConfigMigrator {
schemaPath;
ajv;
constructor(schemaPath){
this.schemaPath = schemaPath;
this.ajv = new Ajv({
allErrors: true,
strict: false
});
}
/**
* Migrate JSON configuration to YAML
*/ async migrateJsonToYaml(jsonPath, yamlPath, options = {}) {
const opts = {
dryRun: false,
createBackup: true,
validate: true,
...options
};
try {
// Read JSON file
const jsonContent = await fs.readFile(jsonPath, 'utf-8');
const config = JSON.parse(jsonContent);
// Validate if schema provided
if (opts.validate && this.schemaPath) {
await this.validateConfig(config);
}
// Convert to YAML
const yamlContent = yaml.dump(config, {
indent: 2,
lineWidth: 80,
noRefs: true,
quotingType: "'"
});
// Dry run - return preview without writing
if (opts.dryRun) {
return yamlContent;
}
// Create backup of source file
if (opts.createBackup) {
await this.createBackup(jsonPath);
}
// Write YAML file
await fs.writeFile(yamlPath, yamlContent, 'utf-8');
return yamlContent;
} catch (error) {
if (error instanceof Error) {
throw new Error(`JSON to YAML migration failed: ${error.message}`);
}
throw error;
}
}
/**
* Migrate .env file to YAML
*/ async migrateEnvToYaml(envPath, yamlPath, options = {}) {
const opts = {
dryRun: false,
createBackup: true,
validate: false,
...options
};
try {
// Read ENV file
const envContent = await fs.readFile(envPath, 'utf-8');
// Parse ENV variables
const config = this.parseEnvFile(envContent);
// Convert flat structure to nested
const nestedConfig = this.unflattenObject(config);
// Validate if schema provided
if (opts.validate && this.schemaPath) {
await this.validateConfig(nestedConfig);
}
// Convert to YAML
const yamlContent = yaml.dump(nestedConfig, {
indent: 2,
lineWidth: 80,
noRefs: true
});
// Dry run - return preview
if (opts.dryRun) {
return yamlContent;
}
// Create backup
if (opts.createBackup) {
await this.createBackup(envPath);
}
// Write YAML file
await fs.writeFile(yamlPath, yamlContent, 'utf-8');
return yamlContent;
} catch (error) {
if (error instanceof Error) {
throw new Error(`ENV to YAML migration failed: ${error.message}`);
}
throw error;
}
}
/**
* Migrate bash variable file to YAML
*/ async migrateBashToYaml(bashPath, yamlPath, options = {}) {
const opts = {
dryRun: false,
createBackup: true,
validate: false,
...options
};
try {
// Read bash file
const bashContent = await fs.readFile(bashPath, 'utf-8');
// Parse bash variables
const config = this.parseBashFile(bashContent);
// Convert flat structure to nested
const nestedConfig = this.unflattenObject(config);
// Validate if schema provided
if (opts.validate && this.schemaPath) {
await this.validateConfig(nestedConfig);
}
// Convert to YAML
const yamlContent = yaml.dump(nestedConfig, {
indent: 2,
lineWidth: 80,
noRefs: true
});
// Dry run - return preview
if (opts.dryRun) {
return yamlContent;
}
// Create backup
if (opts.createBackup) {
await this.createBackup(bashPath);
}
// Write YAML file
await fs.writeFile(yamlPath, yamlContent, 'utf-8');
return yamlContent;
} catch (error) {
if (error instanceof Error) {
throw new Error(`Bash to YAML migration failed: ${error.message}`);
}
throw error;
}
}
/**
* Parse .env file into key-value pairs
*/ parseEnvFile(content) {
const config = {};
const lines = content.split('\n');
for (const line of lines){
const trimmed = line.trim();
// Skip comments and empty lines
if (!trimmed || trimmed.startsWith('#')) {
continue;
}
// Parse KEY=value format
const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
if (match) {
const [, key, value] = match;
config[this.toCamelCase(key)] = this.inferType(value.trim());
}
}
return config;
}
/**
* Parse bash variable file
*/ parseBashFile(content) {
const config = {};
const lines = content.split('\n');
for (const line of lines){
const trimmed = line.trim();
// Skip comments, empty lines, and shebang
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('export ')) {
continue;
}
// Parse variable assignments
const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)=["']?([^"']*)["']?$/);
if (match) {
const [, key, value] = match;
config[this.toCamelCase(key)] = this.inferType(value.trim());
}
}
return config;
}
/**
* Infer type from string value
*/ inferType(value) {
// Remove quotes if present
const unquoted = value.replace(/^["']|["']$/g, '');
// Boolean
if (unquoted.toLowerCase() === 'true') return true;
if (unquoted.toLowerCase() === 'false') return false;
// Null
if (unquoted.toLowerCase() === 'null') return null;
// Number (integer)
if (/^-?\d+$/.test(unquoted)) {
return parseInt(unquoted, 10);
}
// Number (float)
if (/^-?\d+\.\d+$/.test(unquoted)) {
return parseFloat(unquoted);
}
// String
return unquoted;
}
/**
* Convert SCREAMING_SNAKE_CASE to camelCase
*/ toCamelCase(str) {
return str.toLowerCase().replace(/_([a-z0-9])/g, (_, letter)=>letter.toUpperCase());
}
/**
* Convert flat object to nested structure
* Example: { databaseType: 'sqlite', databasePath: './data' }
* Becomes: { database: { type: 'sqlite', path: './data' } }
*/ unflattenObject(flat) {
const nested = {};
// Group by common prefixes
for (const [key, value] of Object.entries(flat)){
// Try to detect nested structure by camelCase boundaries
const parts = this.splitCamelCase(key);
if (parts.length === 1) {
// No nesting detected
nested[key] = value;
} else if (parts.length === 2) {
// Two-level nesting: databaseType → database.type
const [prefix, suffix] = parts;
if (!(prefix in nested)) {
nested[prefix] = {};
}
if (typeof nested[prefix] === 'object') {
nested[prefix][suffix] = value;
}
} else if (parts.length >= 3) {
// Multi-level nesting: databasePoolMin → database.pool.min
let current = nested;
for(let i = 0; i < parts.length - 1; i++){
const part = parts[i];
if (!(part in current)) {
current[part] = {};
}
if (typeof current[part] !== 'object' || Array.isArray(current[part])) {
// Can't nest further, use flat key instead
nested[key] = value;
break;
}
current = current[part];
}
if (typeof current === 'object' && !Array.isArray(current)) {
current[parts[parts.length - 1]] = value;
}
}
}
return nested;
}
/**
* Split camelCase string into parts
*/ splitCamelCase(str) {
// Handle common patterns: databaseType, redisHost, etc.
// Split on uppercase boundaries, keeping the uppercase letter
const parts = [];
let current = '';
for(let i = 0; i < str.length; i++){
const char = str[i];
if (char === char.toUpperCase() && char !== char.toLowerCase()) {
// Uppercase letter - start new part
if (current) {
parts.push(current.toLowerCase());
}
current = char;
} else {
current += char;
}
}
if (current) {
parts.push(current.toLowerCase());
}
return parts.length > 0 ? parts : [
str.toLowerCase()
];
}
/**
* Validate configuration against schema
*/ async validateConfig(config) {
if (!this.schemaPath) {
throw new Error('Schema path not provided for validation');
}
const schemaContent = await fs.readFile(this.schemaPath, 'utf-8');
const schema = JSON.parse(schemaContent);
const validate = this.ajv.compile(schema);
const valid = validate(config);
if (!valid) {
const errors = validate.errors?.map((err)=>`${err.instancePath} ${err.message}`).join(', ');
throw new Error(`Configuration validation failed: ${errors}`);
}
}
/**
* Create backup of file
*/ async createBackup(filePath) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const ext = path.extname(filePath);
const base = path.basename(filePath, ext);
const dir = path.dirname(filePath);
const backupPath = path.join(dir, `${base}.${timestamp}.backup${ext}`);
await fs.copyFile(filePath, backupPath);
return backupPath;
}
/**
* Scan directory for legacy configuration files
*/ async scanForLegacyConfigs(directory) {
const legacyFiles = [];
try {
const files = await fs.readdir(directory, {
withFileTypes: true
});
for (const file of files){
if (file.isFile()) {
const fullPath = path.join(directory, file.name);
// Check for legacy formats
if (file.name.endsWith('.json') || file.name === '.env' || file.name.endsWith('.env') || file.name.endsWith('.sh') && file.name.includes('config')) {
legacyFiles.push(fullPath);
}
}
}
} catch (error) {
// Directory doesn't exist or not accessible
return [];
}
return legacyFiles;
}
/**
* Batch migrate multiple files
*/ async batchMigrate(files, outputDir, options = {}) {
const results = [];
for (const filePath of files){
const ext = path.extname(filePath);
const base = path.basename(filePath, ext);
const yamlPath = path.join(outputDir, `${base}.yml`);
try {
let preview;
if (ext === '.json') {
preview = await this.migrateJsonToYaml(filePath, yamlPath, options);
} else if (filePath.includes('.env')) {
preview = await this.migrateEnvToYaml(filePath, yamlPath, options);
} else if (ext === '.sh') {
preview = await this.migrateBashToYaml(filePath, yamlPath, options);
} else {
results.push({
success: false,
sourcePath: filePath,
targetPath: yamlPath,
errors: [
`Unsupported file format: ${ext}`
]
});
continue;
}
results.push({
success: true,
sourcePath: filePath,
targetPath: yamlPath,
preview: options.dryRun ? preview : undefined
});
} catch (error) {
results.push({
success: false,
sourcePath: filePath,
targetPath: yamlPath,
errors: [
error instanceof Error ? error.message : 'Unknown error'
]
});
}
}
return results;
}
}
//# sourceMappingURL=config-migrator.js.map