#!/usr/bin/env bun /** * Migration Script: Old Config System → Unified Config * * This script helps migrate from the existing .env and multiple config files * to the new unified configuration system. * * Usage: * bun scripts/migrate-to-unified-config.ts * bun scripts/migrate-to-unified-config.ts --dry-run * bun scripts/migrate-to-unified-config.ts --backup */ import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, } from 'fs'; import { join } from 'path'; interface MigrationOptions { dryRun?: boolean; backup?: boolean; verbose?: boolean; } interface ParsedEnv { [key: string]: string; } class ConfigMigrator { private options: MigrationOptions; private projectRoot: string; private backupDir: string; constructor(options: MigrationOptions = {}) { this.options = options; this.projectRoot = join(__dirname, '..'); this.backupDir = join(this.projectRoot, '.config-backup'); } /** * Run the migration process */ async migrate(): Promise { console.warn('šŸ”„ Starting migration to unified configuration system...'); console.warn(`šŸ“ Project root: ${this.projectRoot}`); if (this.options.backup) { await this.createBackup(); } // Step 1: Read existing configuration const existingConfig = await this.readExistingConfig(); // Step 2: Generate unified config values const unifiedValues = this.mapToUnifiedConfig(existingConfig); // Step 3: Update .env file with unified structure await this.updateMainEnvFile(unifiedValues); // Step 4: Generate new config files await this.generateUnifiedConfigFiles(); // Step 5: Update package.json scripts if needed await this.updatePackageJsonScripts(); console.warn('āœ… Migration completed successfully!'); console.warn('\nšŸ“‹ Next steps:'); console.warn('1. Review the updated .env file'); console.warn('2. Run: make generate-config'); console.warn('3. Test your application: make dev'); console.warn('4. Deploy when ready: make deploy-prod-quick'); if (this.options.backup) { console.warn(`5. Remove backup when satisfied: rm -rf ${this.backupDir}`); } } /** * Create backup of existing configuration files */ private async createBackup(): Promise { console.warn('\nšŸ’¾ Creating backup of existing configuration...'); if (!existsSync(this.backupDir)) { mkdirSync(this.backupDir, { recursive: true }); } const filesToBackup = [ '.env', '.env.production', '.env.example', 'config/app.config.ts', 'k8s-kustomize/base/config.env', 'vite.config.ts', ]; for (const file of filesToBackup) { const sourcePath = join(this.projectRoot, file); if (existsSync(sourcePath)) { const backupPath = join(this.backupDir, file.replace('/', '_')); copyFileSync(sourcePath, backupPath); console.warn(` šŸ“„ Backed up: ${file}`); } } console.warn(`āœ… Backup created in: ${this.backupDir}`); } /** * Read existing configuration from various sources */ private async readExistingConfig(): Promise { console.warn('\nšŸ” Reading existing configuration...'); const config: ParsedEnv = {}; // Read .env files const envFiles = ['.env', '.env.production', '.env.local']; for (const envFile of envFiles) { const envPath = join(this.projectRoot, envFile); if (existsSync(envPath)) { const envData = this.parseEnvFile(envPath); Object.assign(config, envData); console.warn( ` šŸ“„ Read: ${envFile} (${Object.keys(envData).length} variables)` ); } } // Read Kubernetes config.env const k8sConfigPath = join( this.projectRoot, 'k8s-kustomize/base/config.env' ); if (existsSync(k8sConfigPath)) { const k8sData = this.parseEnvFile(k8sConfigPath); Object.assign(config, k8sData); console.warn( ` 🚢 Read: k8s config.env (${Object.keys(k8sData).length} variables)` ); } console.warn( `āœ… Found ${Object.keys(config).length} configuration variables` ); return config; } /** * Parse environment file */ private parseEnvFile(filePath: string): ParsedEnv { try { const content = readFileSync(filePath, 'utf8'); const env: ParsedEnv = {}; content.split('\n').forEach(line => { line = line.trim(); if (line && !line.startsWith('#')) { const [key, ...valueParts] = line.split('='); if (key && valueParts.length > 0) { env[key.trim()] = valueParts .join('=') .trim() .replace(/^["']|["']$/g, ''); } } }); return env; } catch (error) { console.warn(` āš ļø Could not read ${filePath}: ${error}`); return {}; } } /** * Map existing config to unified config structure */ private mapToUnifiedConfig(existingConfig: ParsedEnv): ParsedEnv { console.warn('\nšŸ”„ Mapping to unified configuration structure...'); const unified: ParsedEnv = {}; // Application unified.APP_NAME = existingConfig.APP_NAME || 'RxMinder'; unified.APP_VERSION = existingConfig.APP_VERSION || '1.0.0'; unified.APP_BASE_URL = existingConfig.APP_BASE_URL || existingConfig.VITE_BASE_URL || 'http://localhost:5173'; unified.NODE_ENV = existingConfig.NODE_ENV || 'development'; // Database unified.VITE_COUCHDB_URL = existingConfig.VITE_COUCHDB_URL || existingConfig.COUCHDB_URL || 'http://localhost:5984'; unified.VITE_COUCHDB_USER = existingConfig.VITE_COUCHDB_USER || existingConfig.COUCHDB_USER || 'admin'; unified.VITE_COUCHDB_PASSWORD = existingConfig.VITE_COUCHDB_PASSWORD || existingConfig.COUCHDB_PASSWORD || 'changeme'; // Container & Registry unified.CONTAINER_REGISTRY = existingConfig.CONTAINER_REGISTRY || 'gitea-http.taildb3494.ts.net'; unified.CONTAINER_REPOSITORY = existingConfig.CONTAINER_REPOSITORY || existingConfig.GITEA_REPOSITORY || 'will/meds'; unified.CONTAINER_TAG = this.getContainerTag(existingConfig); // Kubernetes unified.INGRESS_HOST = existingConfig.INGRESS_HOST || existingConfig.APP_BASE_URL?.replace(/^https?:\/\//, '') || 'rxminder.local'; unified.STORAGE_CLASS = existingConfig.STORAGE_CLASS || 'longhorn'; unified.STORAGE_SIZE = existingConfig.STORAGE_SIZE || '1Gi'; // Email unified.VITE_MAILGUN_API_KEY = existingConfig.VITE_MAILGUN_API_KEY || existingConfig.MAILGUN_API_KEY || ''; unified.VITE_MAILGUN_DOMAIN = existingConfig.VITE_MAILGUN_DOMAIN || existingConfig.MAILGUN_DOMAIN || ''; unified.VITE_MAILGUN_FROM_NAME = existingConfig.VITE_MAILGUN_FROM_NAME || existingConfig.MAILGUN_FROM_NAME || 'RxMinder'; unified.VITE_MAILGUN_FROM_EMAIL = existingConfig.VITE_MAILGUN_FROM_EMAIL || existingConfig.MAILGUN_FROM_EMAIL || ''; // OAuth unified.VITE_GOOGLE_CLIENT_ID = existingConfig.VITE_GOOGLE_CLIENT_ID || existingConfig.GOOGLE_CLIENT_ID || ''; unified.VITE_GITHUB_CLIENT_ID = existingConfig.VITE_GITHUB_CLIENT_ID || existingConfig.GITHUB_CLIENT_ID || ''; // Feature Flags unified.ENABLE_EMAIL_VERIFICATION = this.toBooleanString( existingConfig.ENABLE_EMAIL_VERIFICATION, true ); unified.ENABLE_OAUTH = this.toBooleanString( existingConfig.ENABLE_OAUTH, true ); unified.ENABLE_ADMIN_INTERFACE = this.toBooleanString( existingConfig.ENABLE_ADMIN_INTERFACE, true ); unified.ENABLE_MONITORING = this.toBooleanString( existingConfig.ENABLE_MONITORING, false ); unified.DEBUG_MODE = this.toBooleanString( existingConfig.DEBUG_MODE, unified.NODE_ENV === 'development' ); // Performance unified.LOG_LEVEL = existingConfig.LOG_LEVEL || (unified.NODE_ENV === 'production' ? 'warn' : 'info'); unified.CACHE_TTL = existingConfig.CACHE_TTL || '1800'; // Security unified.JWT_SECRET = existingConfig.JWT_SECRET || 'your-super-secret-jwt-key-change-in-production'; console.warn( `āœ… Mapped ${Object.keys(unified).length} unified configuration variables` ); return unified; } /** * Get container tag from existing config */ private getContainerTag(existingConfig: ParsedEnv): string { if (existingConfig.CONTAINER_TAG) return existingConfig.CONTAINER_TAG; // Extract from DOCKER_IMAGE if present if (existingConfig.DOCKER_IMAGE) { const parts = existingConfig.DOCKER_IMAGE.split(':'); if (parts.length > 1) return parts[parts.length - 1]; } // Default based on environment const env = existingConfig.NODE_ENV || 'development'; return env === 'production' ? 'v1.0.0' : env === 'staging' ? 'staging' : 'latest'; } /** * Convert value to boolean string */ private toBooleanString( value: string | undefined, defaultValue: boolean ): string { if (value === undefined) return defaultValue.toString(); return (value.toLowerCase() === 'true' || value === '1').toString(); } /** * Update main .env file with unified structure */ private async updateMainEnvFile(unifiedValues: ParsedEnv): Promise { console.warn('\nšŸ“ Updating .env file with unified structure...'); const envPath = join(this.projectRoot, '.env'); const timestamp = new Date().toISOString(); const content = `# Unified Application Configuration # Migrated on: ${timestamp} # # This file now serves as the single source of truth for configuration. # All other config files are generated from this unified configuration. # ============================================================================ # APPLICATION CONFIGURATION # ============================================================================ # Application Identity APP_NAME=${unifiedValues.APP_NAME} APP_VERSION=${unifiedValues.APP_VERSION} APP_BASE_URL=${unifiedValues.APP_BASE_URL} NODE_ENV=${unifiedValues.NODE_ENV} # ============================================================================ # DATABASE CONFIGURATION # ============================================================================ # CouchDB Configuration VITE_COUCHDB_URL=${unifiedValues.VITE_COUCHDB_URL} VITE_COUCHDB_USER=${unifiedValues.VITE_COUCHDB_USER} VITE_COUCHDB_PASSWORD=${unifiedValues.VITE_COUCHDB_PASSWORD} # ============================================================================ # CONTAINER & DEPLOYMENT CONFIGURATION # ============================================================================ # Container Registry CONTAINER_REGISTRY=${unifiedValues.CONTAINER_REGISTRY} CONTAINER_REPOSITORY=${unifiedValues.CONTAINER_REPOSITORY} CONTAINER_TAG=${unifiedValues.CONTAINER_TAG} # Kubernetes Configuration INGRESS_HOST=${unifiedValues.INGRESS_HOST} STORAGE_CLASS=${unifiedValues.STORAGE_CLASS} STORAGE_SIZE=${unifiedValues.STORAGE_SIZE} # ============================================================================ # EMAIL CONFIGURATION # ============================================================================ # Mailgun Configuration VITE_MAILGUN_API_KEY=${unifiedValues.VITE_MAILGUN_API_KEY} VITE_MAILGUN_DOMAIN=${unifiedValues.VITE_MAILGUN_DOMAIN} VITE_MAILGUN_FROM_NAME=${unifiedValues.VITE_MAILGUN_FROM_NAME} VITE_MAILGUN_FROM_EMAIL=${unifiedValues.VITE_MAILGUN_FROM_EMAIL} # ============================================================================ # OAUTH CONFIGURATION # ============================================================================ # OAuth Provider Configuration VITE_GOOGLE_CLIENT_ID=${unifiedValues.VITE_GOOGLE_CLIENT_ID} VITE_GITHUB_CLIENT_ID=${unifiedValues.VITE_GITHUB_CLIENT_ID} # ============================================================================ # FEATURE FLAGS # ============================================================================ # Application Features ENABLE_EMAIL_VERIFICATION=${unifiedValues.ENABLE_EMAIL_VERIFICATION} ENABLE_OAUTH=${unifiedValues.ENABLE_OAUTH} ENABLE_ADMIN_INTERFACE=${unifiedValues.ENABLE_ADMIN_INTERFACE} ENABLE_MONITORING=${unifiedValues.ENABLE_MONITORING} DEBUG_MODE=${unifiedValues.DEBUG_MODE} # ============================================================================ # PERFORMANCE & SECURITY # ============================================================================ # Logging and Performance LOG_LEVEL=${unifiedValues.LOG_LEVEL} CACHE_TTL=${unifiedValues.CACHE_TTL} # Security JWT_SECRET=${unifiedValues.JWT_SECRET} # ============================================================================ # ENVIRONMENT-SPECIFIC OVERRIDES # ============================================================================ # Add environment-specific variables below this line # These will override the unified configuration for this environment `; if (this.options.dryRun) { console.warn(' šŸ” [DRY RUN] Would update .env file'); console.warn(` Path: ${envPath}`); console.warn(` Size: ${content.length} bytes`); } else { writeFileSync(envPath, content, 'utf8'); console.warn(' āœ… Updated .env file with unified structure'); } } /** * Generate unified config files */ private async generateUnifiedConfigFiles(): Promise { console.warn('\nšŸ› ļø Generating unified configuration files...'); if (this.options.dryRun) { console.warn(' šŸ” [DRY RUN] Would generate unified config files'); } else { try { // Use the new generator script const { ConfigGenerator } = await import('./generate-unified-config'); const generator = new ConfigGenerator({ generateAll: true }); await generator.generate(); console.warn(' āœ… Generated unified configuration files'); } catch (_error) { console.warn(' āš ļø Could not auto-generate config files'); console.warn(' šŸ’” Run manually: make generate-config'); } } } /** * Update package.json scripts to use unified config */ private async updatePackageJsonScripts(): Promise { console.warn('\nšŸ“¦ Checking package.json scripts...'); const packageJsonPath = join(this.projectRoot, 'package.json'); if (!existsSync(packageJsonPath)) { console.warn(' āš ļø package.json not found, skipping script updates'); return; } try { const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); let updated = false; // Update scripts that might reference old config generation const scriptUpdates = { 'generate:config': 'bun scripts/generate-unified-config.ts --all', 'config:dev': 'bun scripts/generate-unified-config.ts development', 'config:prod': 'bun scripts/generate-unified-config.ts production', 'config:staging': 'bun scripts/generate-unified-config.ts staging', }; for (const [scriptName, command] of Object.entries(scriptUpdates)) { if (!packageJson.scripts[scriptName]) { packageJson.scripts[scriptName] = command; updated = true; } } if (updated) { if (this.options.dryRun) { console.warn( ' šŸ” [DRY RUN] Would add unified config scripts to package.json' ); } else { writeFileSync( packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8' ); console.warn(' āœ… Added unified config scripts to package.json'); } } else { console.warn(' ā„¹ļø No package.json script updates needed'); } } catch (error) { console.warn(` āš ļø Could not update package.json: ${error}`); } } } /** * CLI Interface */ async function main() { const args = process.argv.slice(2); const options: MigrationOptions = {}; // Parse command line arguments for (const arg of args) { switch (arg) { case '--dry-run': options.dryRun = true; break; case '--backup': options.backup = true; break; case '--verbose': case '-v': options.verbose = true; break; case '--help': case '-h': showHelp(); process.exit(0); break; default: console.warn(`Unknown option: ${arg}`); break; } } try { const migrator = new ConfigMigrator(options); await migrator.migrate(); } catch (error) { console.error('āŒ Migration failed:', error); process.exit(1); } } /** * Show help message */ function showHelp() { console.warn(` šŸ”„ Configuration Migration Tool Migrates from the old configuration system (multiple .env files, separate Kubernetes configs) to the new unified configuration system. USAGE: bun scripts/migrate-to-unified-config.ts [options] OPTIONS: --dry-run Show what would be changed without making changes --backup Create backup of existing configuration files --verbose, -v Show detailed output --help, -h Show this help message EXAMPLES: bun scripts/migrate-to-unified-config.ts --backup bun scripts/migrate-to-unified-config.ts --dry-run bun scripts/migrate-to-unified-config.ts --backup --verbose WHAT IT DOES: 1. Reads existing .env files and Kubernetes config 2. Maps old config structure to unified configuration 3. Updates .env file with unified structure 4. Generates new configuration files for all environments 5. Updates package.json scripts (if needed) BACKUP: When --backup is used, existing config files are copied to: .config-backup/ AFTER MIGRATION: 1. Review the updated .env file 2. Run: make generate-config 3. Test: make dev 4. Deploy: make deploy-prod-quick `); } // Run CLI if this file is executed directly if (import.meta.main) { main().catch(console.error); } export { ConfigMigrator, type MigrationOptions };