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:
612
scripts/generate-unified-config.ts
Normal file
612
scripts/generate-unified-config.ts
Normal 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 };
|
||||
596
scripts/migrate-to-unified-config.ts
Normal file
596
scripts/migrate-to-unified-config.ts
Normal 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 };
|
||||
Reference in New Issue
Block a user