@tamyla/clodo-framework 2.0.18 → 2.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,448 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Unified Configuration Manager
5
+ * Single source of truth for customer .env configuration operations
6
+ *
7
+ * Consolidates functionality from:
8
+ * - CustomerConfigLoader (loading configs)
9
+ * - ConfigPersistenceManager (saving configs)
10
+ *
11
+ * Eliminates duplicate .env parsing and provides clean, unified API
12
+ *
13
+ * @module UnifiedConfigManager
14
+ */
15
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs';
16
+ import { resolve, join } from 'path';
17
+ import { getDirname } from '../esm-helper.js';
18
+ import { createLogger } from '../index.js';
19
+ const __dirname = getDirname(import.meta.url, 'src/utils/config');
20
+ const logger = createLogger('UnifiedConfigManager');
21
+
22
+ /**
23
+ * UnifiedConfigManager
24
+ * Manages customer configuration loading, saving, and operations
25
+ */
26
+ export class UnifiedConfigManager {
27
+ constructor(options = {}) {
28
+ this.configDir = options.configDir || resolve(__dirname, '..', '..', '..', 'config', 'customers');
29
+ this.verbose = options.verbose || false;
30
+ }
31
+
32
+ /**
33
+ * Load customer configuration from .env file
34
+ * Returns null if not found or is a template
35
+ *
36
+ * @param {string} customer - Customer name
37
+ * @param {string} environment - Environment (development, staging, production)
38
+ * @returns {Object|null} - Parsed config or null
39
+ */
40
+ loadCustomerConfig(customer, environment) {
41
+ const configPath = resolve(this.configDir, customer, `${environment}.env`);
42
+ if (!existsSync(configPath)) {
43
+ if (this.verbose) {
44
+ logger.info(`Config not found: ${configPath}`);
45
+ }
46
+ return null;
47
+ }
48
+ try {
49
+ const envVars = this._parseEnvFile(configPath);
50
+
51
+ // Check if this is a template (not real data)
52
+ if (this.isTemplateConfig(envVars)) {
53
+ if (this.verbose) {
54
+ logger.info(`Skipping template config for ${customer}/${environment}`);
55
+ }
56
+ return null;
57
+ }
58
+
59
+ // Convert to standard format
60
+ return this.parseToStandardFormat(envVars, customer, environment);
61
+ } catch (error) {
62
+ logger.error(`Failed to load config for ${customer}/${environment}:`, error.message);
63
+ return null;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Load customer config safely - never throws, always returns object
69
+ *
70
+ * @param {string} customer - Customer name
71
+ * @param {string} environment - Environment
72
+ * @returns {Object} - Parsed config or defaults
73
+ */
74
+ loadCustomerConfigSafe(customer, environment) {
75
+ const config = this.loadCustomerConfig(customer, environment);
76
+ if (!config) {
77
+ // Return minimal defaults
78
+ return {
79
+ customer: customer,
80
+ environment: environment,
81
+ serviceName: customer,
82
+ serviceType: 'generic',
83
+ domainName: null,
84
+ cloudflareToken: process.env.CLOUDFLARE_API_TOKEN || null,
85
+ cloudflareAccountId: null,
86
+ cloudflareZoneId: null,
87
+ envVars: {}
88
+ };
89
+ }
90
+ return config;
91
+ }
92
+
93
+ /**
94
+ * Parse env vars into standard format matching InputCollector output
95
+ * Ensures compatibility between stored configs and collected inputs
96
+ *
97
+ * @param {Object} envVars - Environment variables from .env file
98
+ * @param {string} customer - Customer name
99
+ * @param {string} environment - Environment
100
+ * @returns {Object} - Standardized configuration object
101
+ */
102
+ parseToStandardFormat(envVars, customer, environment) {
103
+ return {
104
+ customer: customer,
105
+ serviceName: envVars.SERVICE_NAME || customer,
106
+ serviceType: envVars.SERVICE_TYPE || 'generic',
107
+ domainName: envVars.DOMAIN || envVars.CUSTOMER_DOMAIN,
108
+ cloudflareToken: envVars.CLOUDFLARE_API_TOKEN || process.env.CLOUDFLARE_API_TOKEN,
109
+ cloudflareAccountId: envVars.CLOUDFLARE_ACCOUNT_ID,
110
+ cloudflareZoneId: envVars.CLOUDFLARE_ZONE_ID,
111
+ environment: environment,
112
+ // Additional fields
113
+ displayName: envVars.DISPLAY_NAME || customer,
114
+ description: envVars.DESCRIPTION,
115
+ workerName: envVars.WORKER_NAME,
116
+ databaseName: envVars.DATABASE_NAME || envVars.D1_DATABASE_NAME,
117
+ deploymentUrl: envVars.DEPLOYMENT_URL || envVars.API_DOMAIN,
118
+ healthCheckPath: envVars.HEALTH_CHECK_PATH || '/health',
119
+ apiBasePath: envVars.API_BASE_PATH || '/api/v1',
120
+ logLevel: envVars.LOG_LEVEL || 'info',
121
+ nodeCompatibility: envVars.NODE_COMPATIBILITY || 'v18',
122
+ // Keep raw env vars for access
123
+ envVars: envVars
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Save deployment configuration to customer .env file
129
+ *
130
+ * @param {string} customer - Customer name
131
+ * @param {string} environment - Environment
132
+ * @param {Object} deploymentData - Complete deployment data
133
+ * @param {Object} deploymentData.coreInputs - Tier 1 core inputs
134
+ * @param {Object} deploymentData.confirmations - Tier 2 confirmations
135
+ * @param {Object} deploymentData.result - Deployment result (optional)
136
+ * @returns {string} - Path to saved file
137
+ */
138
+ async saveCustomerConfig(customer, environment, deploymentData) {
139
+ if (!customer || !environment) {
140
+ throw new Error('Customer and environment are required');
141
+ }
142
+
143
+ // Create customer directory if it doesn't exist
144
+ const customerDir = join(this.configDir, customer);
145
+ if (!existsSync(customerDir)) {
146
+ mkdirSync(customerDir, {
147
+ recursive: true
148
+ });
149
+ logger.info(`Created customer directory: ${customerDir}`);
150
+ }
151
+
152
+ // Generate .env file content
153
+ const envContent = this._generateEnvContent({
154
+ customer,
155
+ environment,
156
+ ...deploymentData
157
+ });
158
+
159
+ // Write to customer environment file
160
+ const envFile = join(customerDir, `${environment}.env`);
161
+ writeFileSync(envFile, envContent, 'utf8');
162
+ logger.info(`Configuration saved: ${envFile}`);
163
+ return envFile;
164
+ }
165
+
166
+ /**
167
+ * List all configured customers
168
+ *
169
+ * @returns {Array<string>} - List of customer names
170
+ */
171
+ listCustomers() {
172
+ if (!existsSync(this.configDir)) {
173
+ return [];
174
+ }
175
+ const entries = readdirSync(this.configDir);
176
+
177
+ // Filter to only directories
178
+ const customers = entries.filter(entry => {
179
+ const fullPath = join(this.configDir, entry);
180
+ return statSync(fullPath).isDirectory();
181
+ });
182
+ return customers.sort();
183
+ }
184
+
185
+ /**
186
+ * Display customer configuration for review
187
+ *
188
+ * @param {string} customer - Customer name
189
+ * @param {string} environment - Environment
190
+ */
191
+ displayCustomerConfig(customer, environment) {
192
+ const config = this.loadCustomerConfig(customer, environment);
193
+ if (!config) {
194
+ console.log(`\n⚠️ No configuration found for ${customer}/${environment}`);
195
+ return;
196
+ }
197
+ console.log(`\n📋 Configuration for ${customer} (${environment})`);
198
+ console.log('='.repeat(60));
199
+ console.log('\n🏢 Customer Identity:');
200
+ console.log(` Customer: ${config.customer}`);
201
+ console.log(` Service Name: ${config.serviceName}`);
202
+ console.log(` Service Type: ${config.serviceType}`);
203
+ if (config.domainName) {
204
+ console.log('\n🌐 Domain Configuration:');
205
+ console.log(` Domain: ${config.domainName}`);
206
+ console.log(` Deployment URL: ${config.deploymentUrl || 'Not set'}`);
207
+ }
208
+ if (config.cloudflareAccountId) {
209
+ console.log('\n☁️ Cloudflare Configuration:');
210
+ console.log(` Account ID: ${config.cloudflareAccountId}`);
211
+ console.log(` Zone ID: ${config.cloudflareZoneId || 'Not set'}`);
212
+ }
213
+ if (config.databaseName) {
214
+ console.log('\n🗄️ Database Configuration:');
215
+ console.log(` Database: ${config.databaseName}`);
216
+ }
217
+ console.log('\n' + '='.repeat(60));
218
+ }
219
+
220
+ /**
221
+ * Check if configuration is a template (not real data)
222
+ *
223
+ * @param {Object} envVars - Environment variables
224
+ * @returns {boolean} - True if template
225
+ */
226
+ isTemplateConfig(envVars) {
227
+ return envVars.CUSTOMER_NAME?.includes('{{') || envVars.CLOUDFLARE_ACCOUNT_ID === '00000000000000000000000000000000' || !envVars.CLOUDFLARE_ACCOUNT_ID || !envVars.DOMAIN;
228
+ }
229
+
230
+ /**
231
+ * Check if customer configuration exists
232
+ *
233
+ * @param {string} customer - Customer name
234
+ * @param {string} environment - Environment
235
+ * @returns {boolean} - True if exists
236
+ */
237
+ configExists(customer, environment) {
238
+ const configPath = resolve(this.configDir, customer, `${environment}.env`);
239
+ return existsSync(configPath);
240
+ }
241
+
242
+ /**
243
+ * Get missing fields from configuration
244
+ *
245
+ * @param {Object} config - Configuration object
246
+ * @param {Array<string>} requiredFields - Required field names
247
+ * @returns {Array<string>} - Missing field names
248
+ */
249
+ getMissingFields(config, requiredFields = []) {
250
+ const missing = [];
251
+ for (const field of requiredFields) {
252
+ if (!config[field] || config[field] === null || config[field] === '') {
253
+ missing.push(field);
254
+ }
255
+ }
256
+ return missing;
257
+ }
258
+
259
+ /**
260
+ * Merge stored config with collected inputs
261
+ * Collected inputs take precedence over stored config
262
+ *
263
+ * @param {Object} storedConfig - Config from .env file
264
+ * @param {Object} collectedInputs - Inputs from InputCollector
265
+ * @returns {Object} - Merged configuration
266
+ */
267
+ mergeConfigs(storedConfig, collectedInputs) {
268
+ return {
269
+ ...storedConfig,
270
+ ...collectedInputs,
271
+ // Merge envVars separately
272
+ envVars: {
273
+ ...(storedConfig.envVars || {}),
274
+ ...(collectedInputs.envVars || {})
275
+ }
276
+ };
277
+ }
278
+
279
+ /**
280
+ * Parse .env file into key-value pairs
281
+ * PRIVATE - Consolidated implementation from both old managers
282
+ *
283
+ * @param {string} filePath - Path to .env file
284
+ * @returns {Object} - Parsed environment variables
285
+ */
286
+ _parseEnvFile(filePath) {
287
+ const content = readFileSync(filePath, 'utf-8');
288
+ const result = {};
289
+ content.split('\n').forEach(line => {
290
+ line = line.trim();
291
+
292
+ // Skip empty lines and comments
293
+ if (!line || line.startsWith('#')) {
294
+ return;
295
+ }
296
+
297
+ // Parse KEY=VALUE
298
+ const match = line.match(/^([^=]+)=(.*)$/);
299
+ if (match) {
300
+ const key = match[1].trim();
301
+ let value = match[2].trim();
302
+
303
+ // Remove quotes if present
304
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
305
+ value = value.slice(1, -1);
306
+ }
307
+ result[key] = value;
308
+ }
309
+ });
310
+ return result;
311
+ }
312
+
313
+ /**
314
+ * Generate .env file content from deployment data
315
+ * PRIVATE - From ConfigPersistenceManager
316
+ *
317
+ * @param {Object} data - Deployment data
318
+ * @returns {string} - .env file content
319
+ */
320
+ _generateEnvContent(data) {
321
+ const {
322
+ customer,
323
+ environment,
324
+ coreInputs = {},
325
+ confirmations = {},
326
+ result = {}
327
+ } = data;
328
+ const timestamp = new Date().toISOString();
329
+ const lines = [`# Deployment Configuration - ${customer} (${environment})`, `# Last Updated: ${timestamp}`, `# Auto-generated by Clodo Framework deployment`, '', '# ============================================', '# Core Customer Identity', '# ============================================', `CUSTOMER_ID=${customer}`, `CUSTOMER_NAME=${customer}`, `ENVIRONMENT=${environment}`, ''];
330
+
331
+ // Cloudflare Configuration
332
+ if (coreInputs.cloudflareAccountId || coreInputs.cloudflareZoneId) {
333
+ lines.push('# ============================================');
334
+ lines.push('# Cloudflare Configuration');
335
+ lines.push('# ============================================');
336
+ if (coreInputs.cloudflareAccountId) {
337
+ lines.push(`CLOUDFLARE_ACCOUNT_ID=${coreInputs.cloudflareAccountId}`);
338
+ }
339
+ if (coreInputs.cloudflareZoneId) {
340
+ lines.push(`CLOUDFLARE_ZONE_ID=${coreInputs.cloudflareZoneId}`);
341
+ }
342
+ if (coreInputs.cloudflareToken) {
343
+ lines.push(`CLOUDFLARE_API_TOKEN=${coreInputs.cloudflareToken}`);
344
+ }
345
+ lines.push('');
346
+ }
347
+
348
+ // Service Configuration
349
+ lines.push('# ============================================');
350
+ lines.push('# Service Configuration');
351
+ lines.push('# ============================================');
352
+ if (coreInputs.serviceName) {
353
+ lines.push(`SERVICE_NAME=${coreInputs.serviceName}`);
354
+ }
355
+ if (coreInputs.serviceType) {
356
+ lines.push(`SERVICE_TYPE=${coreInputs.serviceType}`);
357
+ }
358
+ if (confirmations.displayName) {
359
+ lines.push(`DISPLAY_NAME=${confirmations.displayName}`);
360
+ }
361
+ if (confirmations.description) {
362
+ lines.push(`DESCRIPTION=${confirmations.description}`);
363
+ }
364
+ lines.push('');
365
+
366
+ // Domain Configuration
367
+ if (coreInputs.domainName || confirmations.deploymentUrl) {
368
+ lines.push('# ============================================');
369
+ lines.push('# Domain Configuration');
370
+ lines.push('# ============================================');
371
+ if (coreInputs.domainName) {
372
+ lines.push(`DOMAIN=${coreInputs.domainName}`);
373
+ lines.push(`CUSTOMER_DOMAIN=${coreInputs.domainName}`);
374
+ }
375
+ if (confirmations.deploymentUrl) {
376
+ lines.push(`DEPLOYMENT_URL=${confirmations.deploymentUrl}`);
377
+ lines.push(`API_DOMAIN=${confirmations.deploymentUrl}`);
378
+ }
379
+ lines.push('');
380
+ }
381
+
382
+ // Database Configuration
383
+ if (result.databaseName || confirmations.databaseName) {
384
+ lines.push('# ============================================');
385
+ lines.push('# Database Configuration');
386
+ lines.push('# ============================================');
387
+ const dbName = result.databaseName || confirmations.databaseName;
388
+ lines.push(`DATABASE_NAME=${dbName}`);
389
+ lines.push(`D1_DATABASE_NAME=${dbName}`);
390
+ if (result.databaseId) {
391
+ lines.push(`D1_DATABASE_ID=${result.databaseId}`);
392
+ }
393
+ lines.push('');
394
+ }
395
+
396
+ // Worker Configuration
397
+ if (confirmations.workerName || result.workerUrl) {
398
+ lines.push('# ============================================');
399
+ lines.push('# Worker Configuration');
400
+ lines.push('# ============================================');
401
+ if (confirmations.workerName) {
402
+ lines.push(`WORKER_NAME=${confirmations.workerName}`);
403
+ }
404
+ if (result.workerUrl) {
405
+ lines.push(`WORKER_URL=${result.workerUrl}`);
406
+ }
407
+ lines.push('');
408
+ }
409
+
410
+ // Deployment Result
411
+ if (result.url || result.deploymentId) {
412
+ lines.push('# ============================================');
413
+ lines.push('# Deployment Information');
414
+ lines.push('# ============================================');
415
+ if (result.url) {
416
+ lines.push(`DEPLOYMENT_URL=${result.url}`);
417
+ }
418
+ if (result.deploymentId) {
419
+ lines.push(`DEPLOYMENT_ID=${result.deploymentId}`);
420
+ }
421
+ if (result.timestamp) {
422
+ lines.push(`DEPLOYED_AT=${result.timestamp}`);
423
+ }
424
+ lines.push('');
425
+ }
426
+
427
+ // Additional Configuration
428
+ lines.push('# ============================================');
429
+ lines.push('# Additional Configuration');
430
+ lines.push('# ============================================');
431
+ lines.push(`HEALTH_CHECK_PATH=${confirmations.healthCheckPath || '/health'}`);
432
+ lines.push(`API_BASE_PATH=${confirmations.apiBasePath || '/api/v1'}`);
433
+ lines.push(`LOG_LEVEL=${confirmations.logLevel || 'info'}`);
434
+ lines.push(`NODE_COMPATIBILITY=${confirmations.nodeCompatibility || 'v18'}`);
435
+ lines.push('');
436
+ return lines.join('\n');
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Factory function for convenience
442
+ */
443
+ export function createUnifiedConfigManager(options = {}) {
444
+ return new UnifiedConfigManager(options);
445
+ }
446
+
447
+ // Export singleton instance for convenience
448
+ export const unifiedConfigManager = new UnifiedConfigManager();
@@ -3,5 +3,5 @@
3
3
 
4
4
  export { ConfigurationCacheManager } from './config-cache.js';
5
5
  export { EnhancedSecretManager } from './secret-generator.js';
6
- export { ConfigPersistenceManager } from './config-persistence.js';
6
+ export { UnifiedConfigManager, unifiedConfigManager } from '../config/unified-config-manager.js';
7
7
  export { askUser, askYesNo, askChoice, closePrompts } from '../interactive-prompts.js';