feat: add unified configuration generation and migration scripts

- Add generate-unified-config.ts for generating environment configs
- Support multiple output formats: env files, Kubernetes configs, Docker
- Include migration script for transitioning from legacy configuration
- Support dry-run and verbose modes for safe configuration changes
- Generate type-safe environment exports for each environment
- Provide comprehensive error handling and validation
This commit is contained in:
William Valentin
2025-09-08 09:44:50 -07:00
parent 8c8cea9e42
commit f3936f23fe
2 changed files with 1208 additions and 0 deletions

View File

@@ -0,0 +1,612 @@
#!/usr/bin/env bun
/**
* Generate Configuration Files from Unified Config
*
* This script reads the unified configuration and generates all necessary
* config files for different environments and deployment targets.
*
* Usage:
* bun scripts/generate-unified-config.ts [environment]
* bun scripts/generate-unified-config.ts production
* bun scripts/generate-unified-config.ts --all
*/
import { writeFileSync, mkdirSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import {
createUnifiedConfigForEnvironment,
exportAsEnvVars,
type Environment,
type UnifiedConfig,
} from '../config/unified.config';
interface GeneratorOptions {
environment?: Environment;
outputDir?: string;
generateAll?: boolean;
dryRun?: boolean;
verbose?: boolean;
}
class ConfigGenerator {
private options: GeneratorOptions;
private projectRoot: string;
constructor(options: GeneratorOptions = {}) {
this.options = options;
this.projectRoot = join(__dirname, '..');
}
/**
* Generate all configuration files
*/
async generate(): Promise<void> {
const environments: Environment[] = this.options.generateAll
? ['development', 'staging', 'production']
: [this.options.environment || this.getCurrentEnvironment()];
console.warn('🔧 Generating unified configuration files...');
console.warn(`📁 Project root: ${this.projectRoot}`);
console.warn(`🌍 Environments: ${environments.join(', ')}`);
for (const env of environments) {
await this.generateForEnvironment(env);
}
console.warn('✅ Configuration generation completed!');
}
/**
* Generate configuration files for a specific environment
*/
private async generateForEnvironment(
environment: Environment
): Promise<void> {
console.warn(`\n🔄 Generating configuration for: ${environment}`);
// Create config instance for specific environment
const config = createUnifiedConfigForEnvironment(environment);
const envVars = exportAsEnvVars(config);
// Generate different config file formats
await Promise.all([
this.generateDotEnv(environment, envVars),
this.generateKubernetesConfig(environment, config, envVars),
this.generateDockerEnv(environment, envVars),
this.generateViteEnv(environment, envVars),
this.generateTypeScriptConfig(environment, config),
]);
console.warn(`✅ Generated configuration for ${environment}`);
}
/**
* Generate .env file
*/
private async generateDotEnv(
environment: Environment,
envVars: Record<string, string>
): Promise<void> {
const content = this.createEnvFileContent(environment, envVars);
const filePath = join(this.projectRoot, `.env.${environment}`);
this.writeFile(filePath, content, `📄 .env.${environment}`);
}
/**
* Generate Kubernetes config.env file
*/
private async generateKubernetesConfig(
environment: Environment,
config: UnifiedConfig,
envVars: Record<string, string>
): Promise<void> {
const overlayDir = join(
this.projectRoot,
'k8s-kustomize',
'overlays',
environment
);
this.ensureDirectoryExists(overlayDir);
// Generate config.env for kustomize
const configContent = this.createKubernetesConfigContent(
environment,
envVars
);
const configPath = join(overlayDir, 'config.env');
this.writeFile(
configPath,
configContent,
`🚢 Kubernetes config.env (${environment})`
);
// Generate kustomization.yaml updates
await this.updateKustomizationFile(environment, config);
}
/**
* Generate Docker environment file
*/
private async generateDockerEnv(
environment: Environment,
envVars: Record<string, string>
): Promise<void> {
const dockerDir = join(this.projectRoot, 'docker');
this.ensureDirectoryExists(dockerDir);
const content = this.createDockerEnvContent(environment, envVars);
const filePath = join(dockerDir, `.env.${environment}`);
this.writeFile(filePath, content, `🐳 Docker .env.${environment}`);
}
/**
* Generate Vite-specific environment file
*/
private async generateViteEnv(
environment: Environment,
envVars: Record<string, string>
): Promise<void> {
const viteVars = Object.entries(envVars)
.filter(([key]) => key.startsWith('VITE_') || this.isViteRelevant(key))
.reduce(
(acc, [key, value]) => {
acc[key.startsWith('VITE_') ? key : `VITE_${key}`] = value;
return acc;
},
{} as Record<string, string>
);
const content = this.createViteEnvContent(environment, viteVars);
const filePath = join(this.projectRoot, `.env.vite.${environment}`);
this.writeFile(filePath, content, `⚡ Vite .env.vite.${environment}`);
}
/**
* Generate TypeScript config export
*/
private async generateTypeScriptConfig(
environment: Environment,
config: UnifiedConfig
): Promise<void> {
const configDir = join(this.projectRoot, 'config', 'generated');
this.ensureDirectoryExists(configDir);
const content = this.createTypeScriptConfigContent(environment, config);
const filePath = join(configDir, `${environment}.config.ts`);
this.writeFile(filePath, content, `📝 TypeScript ${environment}.config.ts`);
}
/**
* Create .env file content
*/
private createEnvFileContent(
environment: Environment,
envVars: Record<string, string>
): string {
const timestamp = new Date().toISOString();
return `# ${environment.toUpperCase()} Environment Configuration
# Generated automatically from unified configuration
# Generated on: ${timestamp}
#
# This file was auto-generated. Do not edit manually.
# To make changes, update config/unified.config.ts and regenerate.
${Object.entries(envVars)
.map(([key, value]) => `${key}=${value}`)
.join('\n')}
# Additional environment-specific variables can be added below
# (These will not be overwritten during regeneration)
`;
}
/**
* Create Kubernetes config.env content
*/
private createKubernetesConfigContent(
environment: Environment,
envVars: Record<string, string>
): string {
const timestamp = new Date().toISOString();
// Filter to Kubernetes-relevant variables
const k8sVars = Object.entries(envVars)
.filter(
([key]) => !key.startsWith('VITE_') && this.isKubernetesRelevant(key)
)
.reduce(
(acc, [key, value]) => {
acc[key] = value;
return acc;
},
{} as Record<string, string>
);
return `# Kubernetes configuration for ${environment}
# Generated automatically from unified configuration
# Generated on: ${timestamp}
${Object.entries(k8sVars)
.map(([key, value]) => `${key}=${value}`)
.join('\n')}
`;
}
/**
* Create Docker environment content
*/
private createDockerEnvContent(
environment: Environment,
envVars: Record<string, string>
): string {
const timestamp = new Date().toISOString();
return `# Docker environment for ${environment}
# Generated on: ${timestamp}
# Container Configuration
DOCKER_IMAGE=${envVars.DOCKER_IMAGE}
CONTAINER_REGISTRY=${envVars.CONTAINER_REGISTRY}
CONTAINER_REPOSITORY=${envVars.CONTAINER_REPOSITORY}
CONTAINER_TAG=${envVars.CONTAINER_TAG}
# Application Configuration
APP_NAME=${envVars.APP_NAME}
NODE_ENV=${envVars.NODE_ENV}
PORT=${envVars.PORT}
# Database Configuration
COUCHDB_URL=${envVars.COUCHDB_URL}
COUCHDB_USER=${envVars.COUCHDB_USER}
COUCHDB_PASSWORD=${envVars.COUCHDB_PASSWORD}
`;
}
/**
* Create Vite environment content
*/
private createViteEnvContent(
environment: Environment,
viteVars: Record<string, string>
): string {
const timestamp = new Date().toISOString();
return `# Vite environment variables for ${environment}
# Generated on: ${timestamp}
#
# These variables are available in the frontend build process
${Object.entries(viteVars)
.map(([key, value]) => `${key}=${value}`)
.join('\n')}
`;
}
/**
* Create TypeScript config content
*/
private createTypeScriptConfigContent(
environment: Environment,
config: UnifiedConfig
): string {
const timestamp = new Date().toISOString();
return `/**
* Generated configuration for ${environment}
* Generated on: ${timestamp}
*
* This file exports the resolved configuration for the ${environment} environment.
* It can be imported by other TypeScript files for type-safe configuration access.
*/
import type { UnifiedConfig } from '../unified.config';
export const ${environment}Config: UnifiedConfig = ${JSON.stringify(config, null, 2)} as const;
export default ${environment}Config;
`;
}
/**
* Update kustomization.yaml file with current configuration
*/
private async updateKustomizationFile(
environment: Environment,
config: UnifiedConfig
): Promise<void> {
const overlayDir = join(
this.projectRoot,
'k8s-kustomize',
'overlays',
environment
);
const kustomizationPath = join(overlayDir, 'kustomization.yaml');
if (!existsSync(kustomizationPath)) {
// Create new kustomization.yaml
const content = this.createKustomizationContent(environment, config);
this.writeFile(
kustomizationPath,
content,
`🎛️ kustomization.yaml (${environment})`
);
} else {
console.warn(
` Kustomization file exists: ${kustomizationPath} (not overwriting)`
);
}
}
/**
* Create kustomization.yaml content
*/
private createKustomizationContent(
environment: Environment,
config: UnifiedConfig
): string {
return `apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
metadata:
name: ${config.app.name}-${environment}
# Reference the base configuration
resources:
- ../../base
- namespace.yaml
# Override namespace for ${environment}
namespace: ${config.kubernetes.namespace}
# ${environment.charAt(0).toUpperCase() + environment.slice(1)}-specific labels
labels:
- pairs:
environment: ${environment}
tier: ${environment === 'production' ? 'prod' : environment}
# ${environment.charAt(0).toUpperCase() + environment.slice(1)} image tags and configurations
images:
- name: frontend-image
newName: ${config.container.registry}/${config.container.repository}
newTag: ${config.container.tag}
- name: couchdb-image
newName: couchdb
newTag: 3.3.2
# ${environment.charAt(0).toUpperCase() + environment.slice(1)} replicas
replicas:
- name: ${config.app.name}-frontend
count: ${config.kubernetes.replicas.frontend}
- name: ${config.app.name}-couchdb
count: ${config.kubernetes.replicas.database}
# Environment-specific patches
patches:
# Resource limits
- target:
kind: Deployment
name: ${config.app.name}-frontend
patch: |-
- op: replace
path: /spec/template/spec/containers/0/resources
value:
requests:
memory: "${config.kubernetes.resources.frontend.requests.memory}"
cpu: "${config.kubernetes.resources.frontend.requests.cpu}"
limits:
memory: "${config.kubernetes.resources.frontend.limits.memory}"
cpu: "${config.kubernetes.resources.frontend.limits.cpu}"
- target:
kind: StatefulSet
name: ${config.app.name}-couchdb
patch: |-
- op: replace
path: /spec/template/spec/containers/0/resources
value:
requests:
memory: "${config.kubernetes.resources.database.requests.memory}"
cpu: "${config.kubernetes.resources.database.requests.cpu}"
limits:
memory: "${config.kubernetes.resources.database.limits.memory}"
cpu: "${config.kubernetes.resources.database.limits.cpu}"
# ConfigMap generation
configMapGenerator:
- name: ${config.app.name}-config
envs:
- config.env
behavior: create
`;
}
/**
* Check if a variable is relevant for Vite
*/
private isViteRelevant(key: string): boolean {
const viteRelevantKeys = [
'APP_NAME',
'APP_VERSION',
'APP_BASE_URL',
'COUCHDB_URL',
'COUCHDB_USER',
'COUCHDB_PASSWORD',
'MAILGUN_API_KEY',
'MAILGUN_DOMAIN',
'MAILGUN_FROM_NAME',
'MAILGUN_FROM_EMAIL',
'GOOGLE_CLIENT_ID',
'GITHUB_CLIENT_ID',
'ENABLE_EMAIL_VERIFICATION',
'ENABLE_OAUTH',
'ENABLE_ADMIN_INTERFACE',
'DEBUG_MODE',
];
return viteRelevantKeys.includes(key);
}
/**
* Check if a variable is relevant for Kubernetes
*/
private isKubernetesRelevant(key: string): boolean {
const k8sIrrelevantKeys = ['VITE_', 'CONTAINER_', 'DOCKER_'];
return !k8sIrrelevantKeys.some(prefix => key.startsWith(prefix));
}
/**
* Get current environment from NODE_ENV
*/
private getCurrentEnvironment(): Environment {
const env = process.env.NODE_ENV || 'development';
return ['development', 'staging', 'production', 'test'].includes(env)
? (env as Environment)
: 'development';
}
/**
* Ensure directory exists
*/
private ensureDirectoryExists(dirPath: string): void {
if (!existsSync(dirPath)) {
mkdirSync(dirPath, { recursive: true });
if (this.options.verbose) {
console.warn(` 📁 Created directory: ${dirPath}`);
}
}
}
/**
* Write file with logging
*/
private writeFile(
filePath: string,
content: string,
description: string
): void {
if (this.options.dryRun) {
console.warn(` 🔍 [DRY RUN] Would write: ${description}`);
console.warn(` Path: ${filePath}`);
return;
}
// Ensure directory exists
this.ensureDirectoryExists(dirname(filePath));
writeFileSync(filePath, content, 'utf8');
console.warn(` ✅ Generated: ${description}`);
if (this.options.verbose) {
console.warn(` Path: ${filePath}`);
console.warn(` Size: ${content.length} bytes`);
}
}
}
/**
* CLI Interface
*/
async function main() {
const args = process.argv.slice(2);
const options: GeneratorOptions = {};
// Parse command line arguments
for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case '--all':
options.generateAll = true;
break;
case '--dry-run':
options.dryRun = true;
break;
case '--verbose':
case '-v':
options.verbose = true;
break;
case '--help':
case '-h':
showHelp();
process.exit(0);
break;
default:
if (!arg.startsWith('--') && !options.environment) {
options.environment = arg as Environment;
}
break;
}
}
// Validate environment
if (
options.environment &&
!['development', 'staging', 'production', 'test'].includes(
options.environment
)
) {
console.error(`❌ Invalid environment: ${options.environment}`);
console.error(
' Valid environments: development, staging, production, test'
);
process.exit(1);
}
try {
const generator = new ConfigGenerator(options);
await generator.generate();
} catch (error) {
console.error('❌ Configuration generation failed:', error);
process.exit(1);
}
}
/**
* Show help message
*/
function showHelp() {
console.warn(`
🔧 Unified Configuration Generator
USAGE:
bun scripts/generate-unified-config.ts [environment] [options]
ARGUMENTS:
environment Target environment (development, staging, production, test)
OPTIONS:
--all Generate configuration for all environments
--dry-run Show what would be generated without writing files
--verbose, -v Show detailed output
--help, -h Show this help message
EXAMPLES:
bun scripts/generate-unified-config.ts development
bun scripts/generate-unified-config.ts production --verbose
bun scripts/generate-unified-config.ts --all
bun scripts/generate-unified-config.ts --dry-run
GENERATED FILES:
📄 .env.[environment] - Environment variables
🚢 k8s-kustomize/overlays/[env]/config.env - Kubernetes configuration
🐳 docker/.env.[environment] - Docker environment
⚡ .env.vite.[environment] - Vite-specific variables
📝 config/generated/[env].config.ts - TypeScript config export
The generator reads from config/unified.config.ts and creates all necessary
configuration files for the specified environment(s).
`);
}
// Run CLI if this file is executed directly
if (import.meta.main) {
main().catch(console.error);
}
export { ConfigGenerator, type GeneratorOptions };

View File

@@ -0,0 +1,596 @@
#!/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<void> {
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<void> {
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<ParsedEnv> {
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<void> {
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<void> {
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<void> {
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 };