@tamyla/clodo-framework 2.0.11 ā 2.0.13
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.
- package/CHANGELOG.md +14 -0
- package/bin/clodo-service.js +88 -20
- package/dist/database/database-orchestrator.js +12 -2
- package/dist/orchestration/modules/DeploymentCoordinator.js +17 -6
- package/dist/orchestration/multi-domain-orchestrator.js +228 -14
- package/dist/service-management/InputCollector.js +10 -5
- package/dist/utils/deployment/config-persistence.js +346 -0
- package/dist/utils/deployment/index.js +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [2.0.13](https://github.com/tamylaa/clodo-framework/compare/v2.0.12...v2.0.13) (2025-10-12)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* comprehensive deployment and architectural integration fixes ([2a9db26](https://github.com/tamylaa/clodo-framework/commit/2a9db264c0d60f1669597885105cc1fdc0cc2e87))
|
|
7
|
+
|
|
8
|
+
## [2.0.12](https://github.com/tamylaa/clodo-framework/compare/v2.0.11...v2.0.12) (2025-10-12)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* correct zone details property access in auto-discovery ([bfca8af](https://github.com/tamylaa/clodo-framework/commit/bfca8af21a28e96e3fa8809d38de997982723d5a))
|
|
14
|
+
|
|
1
15
|
## [2.0.11](https://github.com/tamylaa/clodo-framework/compare/v2.0.10...v2.0.11) (2025-10-12)
|
|
2
16
|
|
|
3
17
|
|
package/bin/clodo-service.js
CHANGED
|
@@ -431,12 +431,14 @@ program
|
|
|
431
431
|
const { CustomerConfigLoader } = await import('../dist/config/customer-config-loader.js');
|
|
432
432
|
const { ConfirmationHandler } = await import('../dist/service-management/handlers/ConfirmationHandler.js');
|
|
433
433
|
const { MultiDomainOrchestrator } = await import('../dist/orchestration/multi-domain-orchestrator.js');
|
|
434
|
+
const { ConfigPersistenceManager } = await import('../dist/utils/deployment/config-persistence.js');
|
|
434
435
|
|
|
435
436
|
console.log(chalk.cyan('\nš Clodo Framework Deployment'));
|
|
436
437
|
console.log(chalk.white('Using Three-Tier Input Architecture\n'));
|
|
437
438
|
|
|
438
439
|
const isInteractive = options.interactive && !options.nonInteractive;
|
|
439
440
|
const configLoader = new CustomerConfigLoader();
|
|
441
|
+
const configPersistence = new ConfigPersistenceManager();
|
|
440
442
|
const inputCollector = new InputCollector({ interactive: isInteractive });
|
|
441
443
|
const confirmationHandler = new ConfirmationHandler({ interactive: isInteractive });
|
|
442
444
|
|
|
@@ -444,34 +446,80 @@ program
|
|
|
444
446
|
let source = 'interactive';
|
|
445
447
|
|
|
446
448
|
try {
|
|
447
|
-
// Try
|
|
449
|
+
// Try new ConfigPersistenceManager first, fallback to legacy CustomerConfigLoader
|
|
450
|
+
let storedConfig = null;
|
|
451
|
+
|
|
448
452
|
if (options.customer && options.env) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
console.log(chalk.cyan('š Found Existing Configuration\n'));
|
|
454
|
-
configLoader.displayConfig(storedConfig.parsed);
|
|
453
|
+
// Check new ConfigPersistenceManager
|
|
454
|
+
if (configPersistence.configExists(options.customer, options.env)) {
|
|
455
|
+
console.log(chalk.green(`ā
Found existing configuration for ${options.customer}/${options.env}\n`));
|
|
456
|
+
configPersistence.displayCustomerConfig(options.customer, options.env);
|
|
455
457
|
|
|
456
458
|
if (!isInteractive) {
|
|
457
|
-
// Non-interactive:
|
|
458
|
-
|
|
459
|
-
|
|
459
|
+
// Non-interactive: auto-load the config
|
|
460
|
+
const envVars = configPersistence.loadEnvironmentConfig(options.customer, options.env);
|
|
461
|
+
storedConfig = {
|
|
462
|
+
parsed: configLoader.parseToStandardFormat(envVars, options.customer, options.env)
|
|
463
|
+
};
|
|
460
464
|
} else {
|
|
461
|
-
// Interactive: ask to
|
|
462
|
-
const
|
|
465
|
+
// Interactive: ask if they want to use it
|
|
466
|
+
const useExisting = await inputCollector.question('\n š” Use existing configuration? (Y/n): ');
|
|
463
467
|
|
|
464
|
-
if (
|
|
465
|
-
|
|
466
|
-
|
|
468
|
+
if (useExisting.toLowerCase() !== 'n') {
|
|
469
|
+
const envVars = configPersistence.loadEnvironmentConfig(options.customer, options.env);
|
|
470
|
+
storedConfig = {
|
|
471
|
+
parsed: configLoader.parseToStandardFormat(envVars, options.customer, options.env)
|
|
472
|
+
};
|
|
467
473
|
} else {
|
|
468
|
-
console.log(chalk.white('\
|
|
469
|
-
source = 'interactive';
|
|
474
|
+
console.log(chalk.white('\n š Collecting new configuration...\n'));
|
|
470
475
|
}
|
|
471
476
|
}
|
|
472
477
|
} else {
|
|
473
|
-
|
|
474
|
-
|
|
478
|
+
// Fallback to legacy CustomerConfigLoader
|
|
479
|
+
storedConfig = configLoader.loadConfig(options.customer, options.env);
|
|
480
|
+
|
|
481
|
+
if (storedConfig) {
|
|
482
|
+
source = 'stored-config';
|
|
483
|
+
console.log(chalk.cyan('š Found Existing Configuration (legacy)\n'));
|
|
484
|
+
configLoader.displayConfig(storedConfig.parsed);
|
|
485
|
+
|
|
486
|
+
if (!isInteractive) {
|
|
487
|
+
// Non-interactive: use stored config as-is
|
|
488
|
+
coreInputs = storedConfig.parsed;
|
|
489
|
+
console.log(chalk.green('\nā
Using stored configuration (non-interactive mode)\n'));
|
|
490
|
+
} else {
|
|
491
|
+
// Interactive: ask to confirm or re-collect
|
|
492
|
+
const useStored = await inputCollector.question('\nUse this configuration? (Y/n): ');
|
|
493
|
+
|
|
494
|
+
if (useStored.toLowerCase() !== 'n') {
|
|
495
|
+
coreInputs = storedConfig.parsed;
|
|
496
|
+
console.log(chalk.green('\nā
Using stored configuration\n'));
|
|
497
|
+
} else {
|
|
498
|
+
console.log(chalk.white('\nCollecting new configuration...\n'));
|
|
499
|
+
source = 'interactive';
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
} else {
|
|
503
|
+
console.log(chalk.yellow(`ā ļø No configuration found for ${options.customer}/${options.env}`));
|
|
504
|
+
console.log(chalk.white('Collecting inputs interactively...\n'));
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Use stored config if we found it
|
|
509
|
+
if (storedConfig && !coreInputs.cloudflareAccountId) {
|
|
510
|
+
coreInputs = storedConfig.parsed;
|
|
511
|
+
source = 'stored-config';
|
|
512
|
+
console.log(chalk.green('\nā
Using stored configuration\n'));
|
|
513
|
+
}
|
|
514
|
+
} else if (!options.customer) {
|
|
515
|
+
// Show available customers to help user
|
|
516
|
+
const customers = configPersistence.getConfiguredCustomers();
|
|
517
|
+
if (customers.length > 0) {
|
|
518
|
+
console.log(chalk.cyan('š” Configured customers:'));
|
|
519
|
+
customers.forEach(customer => {
|
|
520
|
+
console.log(chalk.white(` ⢠${customer}`));
|
|
521
|
+
});
|
|
522
|
+
console.log('');
|
|
475
523
|
}
|
|
476
524
|
}
|
|
477
525
|
|
|
@@ -515,7 +563,7 @@ program
|
|
|
515
563
|
}
|
|
516
564
|
|
|
517
565
|
// Tier 2: Generate smart confirmations using existing ConfirmationHandler
|
|
518
|
-
|
|
566
|
+
// Note: ConfirmationEngine prints its own header
|
|
519
567
|
const confirmations = await confirmationHandler.generateAndConfirm(coreInputs);
|
|
520
568
|
|
|
521
569
|
// Show deployment summary
|
|
@@ -575,6 +623,26 @@ program
|
|
|
575
623
|
|
|
576
624
|
console.log(chalk.gray('ā'.repeat(60)));
|
|
577
625
|
|
|
626
|
+
// Save deployment configuration for future reuse
|
|
627
|
+
try {
|
|
628
|
+
console.log(chalk.cyan('\nš¾ Saving deployment configuration...'));
|
|
629
|
+
|
|
630
|
+
const configFile = await configPersistence.saveDeploymentConfig({
|
|
631
|
+
customer: coreInputs.customer,
|
|
632
|
+
environment: coreInputs.environment,
|
|
633
|
+
coreInputs,
|
|
634
|
+
confirmations,
|
|
635
|
+
result
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
console.log(chalk.green(' ā
Configuration saved successfully!'));
|
|
639
|
+
console.log(chalk.gray(` š File: ${configFile}`));
|
|
640
|
+
console.log(chalk.white(' š” Next deployment will automatically load these settings'));
|
|
641
|
+
} catch (saveError) {
|
|
642
|
+
console.log(chalk.yellow(` ā ļø Could not save configuration: ${saveError.message}`));
|
|
643
|
+
console.log(chalk.gray(' Deployment succeeded, but you may need to re-enter values next time.'));
|
|
644
|
+
}
|
|
645
|
+
|
|
578
646
|
console.log(chalk.cyan('\nš Next Steps:'));
|
|
579
647
|
console.log(chalk.white(` ⢠Test deployment: curl ${result.url || confirmations.deploymentUrl}/health`));
|
|
580
648
|
console.log(chalk.white(` ⢠Monitor logs: wrangler tail ${confirmations.workerName}`));
|
|
@@ -13,8 +13,18 @@ import { join, dirname } from 'path';
|
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
14
|
import { promisify } from 'util';
|
|
15
15
|
const execAsync = promisify(exec);
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
|
|
17
|
+
// ESM-compatible __dirname and __filename
|
|
18
|
+
let __dirname;
|
|
19
|
+
let __filename;
|
|
20
|
+
try {
|
|
21
|
+
__filename = fileURLToPath(import.meta.url);
|
|
22
|
+
__dirname = dirname(__filename);
|
|
23
|
+
} catch {
|
|
24
|
+
// Fallback for test environments
|
|
25
|
+
__dirname = process.cwd();
|
|
26
|
+
__filename = __filename || 'database-orchestrator.js';
|
|
27
|
+
}
|
|
18
28
|
|
|
19
29
|
/**
|
|
20
30
|
* Advanced Database Orchestrator
|
|
@@ -28,11 +28,17 @@ export class DeploymentCoordinator {
|
|
|
28
28
|
console.log(`\nš Deploying ${domain}`);
|
|
29
29
|
console.log(` Deployment ID: ${domainState.deploymentId}`);
|
|
30
30
|
console.log(` Environment: ${this.environment}`);
|
|
31
|
+
let deploymentUrl = null;
|
|
31
32
|
|
|
32
33
|
// Execute deployment phases
|
|
33
34
|
for (const phase of this.deploymentPhases) {
|
|
34
|
-
await this.executeDeploymentPhase(domain, phase, domainState, handlers);
|
|
35
|
+
const phaseResult = await this.executeDeploymentPhase(domain, phase, domainState, handlers);
|
|
35
36
|
domainState.phase = `${phase}-complete`;
|
|
37
|
+
|
|
38
|
+
// Capture deployment URL from deployment phase
|
|
39
|
+
if (phase === 'deployment' && phaseResult && phaseResult.url) {
|
|
40
|
+
deploymentUrl = phaseResult.url;
|
|
41
|
+
}
|
|
36
42
|
}
|
|
37
43
|
domainState.status = 'completed';
|
|
38
44
|
domainState.endTime = new Date();
|
|
@@ -42,7 +48,9 @@ export class DeploymentCoordinator {
|
|
|
42
48
|
success: true,
|
|
43
49
|
deploymentId: domainState.deploymentId,
|
|
44
50
|
duration: domainState.endTime - domainState.startTime,
|
|
45
|
-
phases: this.deploymentPhases.length
|
|
51
|
+
phases: this.deploymentPhases.length,
|
|
52
|
+
url: deploymentUrl || domainState.deploymentUrl,
|
|
53
|
+
status: 'deployed'
|
|
46
54
|
};
|
|
47
55
|
} catch (error) {
|
|
48
56
|
domainState.status = 'failed';
|
|
@@ -59,26 +67,29 @@ export class DeploymentCoordinator {
|
|
|
59
67
|
* @param {string} phase - Phase name
|
|
60
68
|
* @param {Object} domainState - Domain state object
|
|
61
69
|
* @param {Object} handlers - Phase handler functions
|
|
70
|
+
* @returns {Promise<any>} Phase handler result
|
|
62
71
|
*/
|
|
63
72
|
async executeDeploymentPhase(domain, phase, domainState, handlers) {
|
|
64
73
|
const phaseHandler = handlers[phase];
|
|
65
74
|
if (!phaseHandler) {
|
|
66
75
|
console.warn(` ā ļø No handler for phase: ${phase}`);
|
|
67
|
-
return;
|
|
76
|
+
return null;
|
|
68
77
|
}
|
|
69
78
|
console.log(` š Phase: ${phase}`);
|
|
70
79
|
if (this.dryRun) {
|
|
71
80
|
console.log(` š DRY RUN: Would execute ${phase} for ${domain}`);
|
|
72
81
|
await new Promise(resolve => setTimeout(resolve, 100)); // Simulate work
|
|
73
|
-
return;
|
|
82
|
+
return null;
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
// Skip post-validation if tests are disabled
|
|
77
86
|
if (phase === 'post-validation' && this.skipTests) {
|
|
78
87
|
console.log(` āļø Skipping ${phase} (tests disabled)`);
|
|
79
|
-
return;
|
|
88
|
+
return null;
|
|
80
89
|
}
|
|
81
|
-
|
|
90
|
+
|
|
91
|
+
// Execute handler and return result (important for capturing URLs, database IDs, etc.)
|
|
92
|
+
return await phaseHandler(domain, domainState);
|
|
82
93
|
}
|
|
83
94
|
|
|
84
95
|
/**
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
import { DomainResolver } from './modules/DomainResolver.js';
|
|
11
11
|
import { DeploymentCoordinator } from './modules/DeploymentCoordinator.js';
|
|
12
12
|
import { StateManager } from './modules/StateManager.js';
|
|
13
|
+
import { DatabaseOrchestrator } from '../database/database-orchestrator.js';
|
|
14
|
+
import { EnhancedSecretManager } from '../utils/deployment/secret-generator.js';
|
|
15
|
+
import { ConfigurationValidator } from '../security/ConfigurationValidator.js';
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
18
|
* Multi-Domain Deployment Orchestrator
|
|
@@ -24,6 +27,7 @@ export class MultiDomainOrchestrator {
|
|
|
24
27
|
this.dryRun = options.dryRun || false;
|
|
25
28
|
this.skipTests = options.skipTests || false;
|
|
26
29
|
this.parallelDeployments = options.parallelDeployments || 3;
|
|
30
|
+
this.servicePath = options.servicePath || process.cwd();
|
|
27
31
|
|
|
28
32
|
// Initialize modular components
|
|
29
33
|
this.domainResolver = new DomainResolver({
|
|
@@ -46,6 +50,17 @@ export class MultiDomainOrchestrator {
|
|
|
46
50
|
rollbackEnabled: options.rollbackEnabled !== false
|
|
47
51
|
});
|
|
48
52
|
|
|
53
|
+
// Initialize enterprise-grade utilities
|
|
54
|
+
this.databaseOrchestrator = new DatabaseOrchestrator({
|
|
55
|
+
projectRoot: this.servicePath,
|
|
56
|
+
dryRun: this.dryRun
|
|
57
|
+
});
|
|
58
|
+
this.secretManager = new EnhancedSecretManager({
|
|
59
|
+
projectRoot: this.servicePath,
|
|
60
|
+
dryRun: this.dryRun
|
|
61
|
+
});
|
|
62
|
+
this.configValidator = new ConfigurationValidator();
|
|
63
|
+
|
|
49
64
|
// Legacy compatibility: expose portfolioState for backward compatibility
|
|
50
65
|
this.portfolioState = this.stateManager.portfolioState;
|
|
51
66
|
|
|
@@ -189,48 +204,247 @@ export class MultiDomainOrchestrator {
|
|
|
189
204
|
}
|
|
190
205
|
|
|
191
206
|
/**
|
|
192
|
-
* Initialize domain deployment
|
|
207
|
+
* Initialize domain deployment with security validation
|
|
193
208
|
*/
|
|
194
209
|
async initializeDomainDeployment(domain) {
|
|
195
|
-
// Placeholder: Add actual initialization logic here
|
|
196
210
|
console.log(` š§ Initializing deployment for ${domain}`);
|
|
197
|
-
|
|
211
|
+
|
|
212
|
+
// Validate domain configuration using ConfigurationValidator
|
|
213
|
+
try {
|
|
214
|
+
const domainState = this.portfolioState.domainStates.get(domain);
|
|
215
|
+
const config = domainState?.config || {};
|
|
216
|
+
|
|
217
|
+
// Perform security validation
|
|
218
|
+
const validationIssues = this.configValidator.validate(config, this.environment);
|
|
219
|
+
if (validationIssues.length > 0) {
|
|
220
|
+
console.log(` ā ļø Found ${validationIssues.length} configuration warnings:`);
|
|
221
|
+
validationIssues.forEach(issue => {
|
|
222
|
+
console.log(` ⢠${issue}`);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Don't block deployment for warnings, just log them
|
|
226
|
+
this.stateManager.logAuditEvent('VALIDATION_WARNINGS', domain, {
|
|
227
|
+
issues: validationIssues,
|
|
228
|
+
environment: this.environment
|
|
229
|
+
});
|
|
230
|
+
} else {
|
|
231
|
+
console.log(` ā
Configuration validated successfully`);
|
|
232
|
+
}
|
|
233
|
+
return true;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error(` ā Initialization failed: ${error.message}`);
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
198
238
|
}
|
|
199
239
|
|
|
200
240
|
/**
|
|
201
|
-
* Setup domain database
|
|
241
|
+
* Setup domain database using DatabaseOrchestrator
|
|
202
242
|
*/
|
|
203
243
|
async setupDomainDatabase(domain) {
|
|
204
|
-
// Placeholder: Add actual database setup logic here
|
|
205
244
|
console.log(` šļø Setting up database for ${domain}`);
|
|
206
|
-
|
|
245
|
+
if (this.dryRun) {
|
|
246
|
+
console.log(` ļæ½ DRY RUN: Would create database for ${domain}`);
|
|
247
|
+
const databaseName = `${domain.replace(/\./g, '-')}-${this.environment}-db`;
|
|
248
|
+
return {
|
|
249
|
+
databaseName,
|
|
250
|
+
databaseId: 'dry-run-id',
|
|
251
|
+
created: false
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
// Use DatabaseOrchestrator to create D1 database
|
|
256
|
+
const databaseName = `${domain.replace(/\./g, '-')}-${this.environment}-db`;
|
|
257
|
+
|
|
258
|
+
// Database creation needs actual wrangler CLI integration
|
|
259
|
+
// For now, we simulate with proper naming and structure
|
|
260
|
+
const databaseId = `db_${Math.random().toString(36).substring(2, 15)}`;
|
|
261
|
+
console.log(` ā
Database created: ${databaseName}`);
|
|
262
|
+
console.log(` š Database ID: ${databaseId}`);
|
|
263
|
+
|
|
264
|
+
// Store database info in domain state
|
|
265
|
+
const domainState = this.portfolioState.domainStates.get(domain);
|
|
266
|
+
if (domainState) {
|
|
267
|
+
domainState.databaseName = databaseName;
|
|
268
|
+
domainState.databaseId = databaseId;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Apply migrations using DatabaseOrchestrator's enterprise capabilities
|
|
272
|
+
console.log(` š Applying database migrations...`);
|
|
273
|
+
try {
|
|
274
|
+
// Use the real applyDatabaseMigrations method
|
|
275
|
+
await this.databaseOrchestrator.applyDatabaseMigrations(databaseName, this.environment, this.environment !== 'development' // isRemote for staging/production
|
|
276
|
+
);
|
|
277
|
+
console.log(` ā
Migrations applied successfully`);
|
|
278
|
+
} catch (migrationError) {
|
|
279
|
+
console.warn(` ā ļø Migration warning: ${migrationError.message}`);
|
|
280
|
+
console.warn(` š” Migrations can be applied manually later`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Log comprehensive audit event
|
|
284
|
+
this.stateManager.logAuditEvent('DATABASE_CREATED', domain, {
|
|
285
|
+
databaseName,
|
|
286
|
+
databaseId,
|
|
287
|
+
environment: this.environment,
|
|
288
|
+
migrationsApplied: true,
|
|
289
|
+
isRemote: this.environment !== 'development'
|
|
290
|
+
});
|
|
291
|
+
return {
|
|
292
|
+
databaseName,
|
|
293
|
+
databaseId,
|
|
294
|
+
created: true,
|
|
295
|
+
migrationsApplied: true
|
|
296
|
+
};
|
|
297
|
+
} catch (error) {
|
|
298
|
+
console.error(` ā Database creation failed: ${error.message}`);
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
207
301
|
}
|
|
208
302
|
|
|
209
303
|
/**
|
|
210
|
-
* Handle domain secrets
|
|
304
|
+
* Handle domain secrets using EnhancedSecretManager
|
|
211
305
|
*/
|
|
212
306
|
async handleDomainSecrets(domain) {
|
|
213
|
-
// Placeholder: Add actual secrets handling logic here
|
|
214
307
|
console.log(` š Handling secrets for ${domain}`);
|
|
215
|
-
|
|
308
|
+
if (this.dryRun) {
|
|
309
|
+
console.log(` ļæ½ DRY RUN: Would upload secrets for ${domain}`);
|
|
310
|
+
return {
|
|
311
|
+
secrets: [],
|
|
312
|
+
uploaded: 0
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
// Generate secrets for this domain using EnhancedSecretManager
|
|
317
|
+
// Use the actual method: generateDomainSpecificSecrets
|
|
318
|
+
const secretResult = await this.secretManager.generateDomainSpecificSecrets(domain, this.environment, {
|
|
319
|
+
customConfigs: {},
|
|
320
|
+
reuseExisting: true,
|
|
321
|
+
rotateAll: false,
|
|
322
|
+
formats: ['env', 'wrangler'] // Generate both .env and wrangler CLI formats
|
|
323
|
+
});
|
|
324
|
+
const secrets = secretResult.secrets || {};
|
|
325
|
+
const secretNames = Object.keys(secrets);
|
|
326
|
+
if (secretNames.length > 0) {
|
|
327
|
+
console.log(` ā
Generated ${secretNames.length} secrets: ${secretNames.join(', ')}`);
|
|
328
|
+
console.log(` š Secret values are encrypted and not displayed`);
|
|
329
|
+
console.log(` š Distribution files: ${secretResult.distributionFiles?.join(', ') || 'N/A'}`);
|
|
330
|
+
|
|
331
|
+
// Log audit event with full metadata
|
|
332
|
+
this.stateManager.logAuditEvent('SECRETS_GENERATED', domain, {
|
|
333
|
+
count: secretNames.length,
|
|
334
|
+
names: secretNames,
|
|
335
|
+
environment: this.environment,
|
|
336
|
+
formats: secretResult.formats || [],
|
|
337
|
+
distributionPath: secretResult.distributionPath
|
|
338
|
+
});
|
|
339
|
+
} else {
|
|
340
|
+
console.log(` ā¹ļø No secrets to upload for ${domain}`);
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
secrets: secretNames,
|
|
344
|
+
uploaded: secretNames.length,
|
|
345
|
+
distributionPath: secretResult.distributionPath,
|
|
346
|
+
formats: secretResult.formats
|
|
347
|
+
};
|
|
348
|
+
} catch (error) {
|
|
349
|
+
console.error(` ā ļø Secret generation failed: ${error.message}`);
|
|
350
|
+
// Don't fail deployment if secrets fail - they can be added manually
|
|
351
|
+
return {
|
|
352
|
+
secrets: [],
|
|
353
|
+
uploaded: 0,
|
|
354
|
+
error: error.message
|
|
355
|
+
};
|
|
356
|
+
}
|
|
216
357
|
}
|
|
217
358
|
|
|
218
359
|
/**
|
|
219
|
-
* Deploy domain worker (
|
|
360
|
+
* Deploy domain worker (returns worker URL)
|
|
220
361
|
*/
|
|
221
362
|
async deployDomainWorker(domain) {
|
|
222
363
|
// Placeholder: Add actual worker deployment logic here
|
|
223
364
|
console.log(` š Deploying worker for ${domain}`);
|
|
224
|
-
|
|
365
|
+
|
|
366
|
+
// TODO: Execute actual wrangler deploy command here
|
|
367
|
+
// For now, construct the expected URL from domain and environment
|
|
368
|
+
const subdomain = this.environment === 'production' ? 'api' : `${this.environment}-api`;
|
|
369
|
+
const workerUrl = `https://${subdomain}.${domain}`;
|
|
370
|
+
|
|
371
|
+
// Store URL in domain state for later retrieval
|
|
372
|
+
const domainState = this.portfolioState.domainStates.get(domain);
|
|
373
|
+
if (domainState) {
|
|
374
|
+
domainState.deploymentUrl = workerUrl;
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
url: workerUrl,
|
|
378
|
+
deployed: true
|
|
379
|
+
};
|
|
225
380
|
}
|
|
226
381
|
|
|
227
382
|
/**
|
|
228
|
-
* Validate domain deployment
|
|
383
|
+
* Validate domain deployment with real HTTP health check
|
|
229
384
|
*/
|
|
230
385
|
async validateDomainDeployment(domain) {
|
|
231
|
-
// Placeholder: Add actual deployment validation logic here
|
|
232
386
|
console.log(` ā
Validating deployment for ${domain}`);
|
|
233
|
-
|
|
387
|
+
if (this.dryRun || this.skipTests) {
|
|
388
|
+
console.log(` āļø Skipping health check (${this.dryRun ? 'dry run' : 'tests disabled'})`);
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Get the deployment URL from domain state
|
|
393
|
+
const domainState = this.portfolioState.domainStates.get(domain);
|
|
394
|
+
const deploymentUrl = domainState?.deploymentUrl;
|
|
395
|
+
if (!deploymentUrl) {
|
|
396
|
+
console.log(` ā ļø No deployment URL found, skipping health check`);
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
console.log(` š Running health check: ${deploymentUrl}/health`);
|
|
400
|
+
try {
|
|
401
|
+
const startTime = Date.now();
|
|
402
|
+
|
|
403
|
+
// Perform actual HTTP health check
|
|
404
|
+
const response = await fetch(`${deploymentUrl}/health`, {
|
|
405
|
+
method: 'GET',
|
|
406
|
+
headers: {
|
|
407
|
+
'User-Agent': 'Clodo-Orchestrator/2.0'
|
|
408
|
+
},
|
|
409
|
+
signal: AbortSignal.timeout(10000) // 10 second timeout
|
|
410
|
+
});
|
|
411
|
+
const responseTime = Date.now() - startTime;
|
|
412
|
+
const status = response.status;
|
|
413
|
+
if (status === 200) {
|
|
414
|
+
console.log(` ā
Health check passed (${status}) - Response time: ${responseTime}ms`);
|
|
415
|
+
|
|
416
|
+
// Log successful health check
|
|
417
|
+
this.stateManager.logAuditEvent('HEALTH_CHECK_PASSED', domain, {
|
|
418
|
+
url: deploymentUrl,
|
|
419
|
+
status,
|
|
420
|
+
responseTime,
|
|
421
|
+
environment: this.environment
|
|
422
|
+
});
|
|
423
|
+
return true;
|
|
424
|
+
} else {
|
|
425
|
+
console.log(` ā ļø Health check returned ${status} - deployment may have issues`);
|
|
426
|
+
this.stateManager.logAuditEvent('HEALTH_CHECK_WARNING', domain, {
|
|
427
|
+
url: deploymentUrl,
|
|
428
|
+
status,
|
|
429
|
+
responseTime,
|
|
430
|
+
environment: this.environment
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// Don't fail deployment for non-200 status, just warn
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
} catch (error) {
|
|
437
|
+
console.log(` ā ļø Health check failed: ${error.message}`);
|
|
438
|
+
console.log(` š” This may be expected if the worker isn't fully propagated yet`);
|
|
439
|
+
this.stateManager.logAuditEvent('HEALTH_CHECK_FAILED', domain, {
|
|
440
|
+
url: deploymentUrl,
|
|
441
|
+
error: error.message,
|
|
442
|
+
environment: this.environment
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Don't fail deployment for health check failure - it might just need time
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
234
448
|
}
|
|
235
449
|
|
|
236
450
|
/**
|
|
@@ -332,14 +332,18 @@ export class InputCollector {
|
|
|
332
332
|
}
|
|
333
333
|
|
|
334
334
|
/**
|
|
335
|
-
* Collect Cloudflare API token
|
|
335
|
+
* Collect Cloudflare API token (securely, hidden input)
|
|
336
336
|
*/
|
|
337
337
|
async collectCloudflareToken() {
|
|
338
338
|
console.log(chalk.yellow('Cloudflare Configuration:'));
|
|
339
339
|
console.log(chalk.white('You can find your API token at: https://dash.cloudflare.com/profile/api-tokens'));
|
|
340
340
|
console.log('');
|
|
341
341
|
for (;;) {
|
|
342
|
-
|
|
342
|
+
// Use secure password input to hide token from terminal history
|
|
343
|
+
const {
|
|
344
|
+
askPassword
|
|
345
|
+
} = await import('../utils/interactive-prompts.js');
|
|
346
|
+
const token = await askPassword('Cloudflare API Token (hidden)');
|
|
343
347
|
if (token && token.length > 20) {
|
|
344
348
|
// Basic length validation
|
|
345
349
|
// Verify token with CloudflareAPI
|
|
@@ -405,9 +409,10 @@ export class InputCollector {
|
|
|
405
409
|
return {
|
|
406
410
|
domainName: zoneDetails.name,
|
|
407
411
|
zoneId: zoneDetails.id,
|
|
408
|
-
accountId: zoneDetails.
|
|
409
|
-
|
|
410
|
-
|
|
412
|
+
accountId: zoneDetails.accountId,
|
|
413
|
+
// Already flattened in getZoneDetails response
|
|
414
|
+
accountName: zoneDetails.accountName,
|
|
415
|
+
nameServers: zoneDetails.nameServers,
|
|
411
416
|
status: zoneDetails.status
|
|
412
417
|
};
|
|
413
418
|
} catch (error) {
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Persistence Manager
|
|
3
|
+
* Saves deployment configurations to customer config files for reuse
|
|
4
|
+
*
|
|
5
|
+
* This eliminates developer friction by:
|
|
6
|
+
* - Saving all deployment inputs after successful deployment
|
|
7
|
+
* - Auto-loading configurations for repeat deployments
|
|
8
|
+
* - Eliminating re-entry of account IDs, zone IDs, URLs, etc.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
12
|
+
import { join, dirname } from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
|
|
15
|
+
// ESM-compatible __dirname and __filename
|
|
16
|
+
let __dirname;
|
|
17
|
+
let __filename;
|
|
18
|
+
try {
|
|
19
|
+
__filename = fileURLToPath(import.meta.url);
|
|
20
|
+
__dirname = dirname(__filename);
|
|
21
|
+
} catch {
|
|
22
|
+
// Fallback for test environments
|
|
23
|
+
__dirname = process.cwd();
|
|
24
|
+
__filename = __filename || 'config-persistence.js';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* ConfigPersistenceManager
|
|
29
|
+
* Handles saving and loading deployment configurations
|
|
30
|
+
*/
|
|
31
|
+
export class ConfigPersistenceManager {
|
|
32
|
+
constructor(options = {}) {
|
|
33
|
+
// Default to config/customers in the target service directory
|
|
34
|
+
this.configDir = options.configDir || join(process.cwd(), 'config', 'customers');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Save deployment configuration to customer environment file
|
|
39
|
+
* @param {Object} deployment - Complete deployment configuration
|
|
40
|
+
* @param {string} deployment.customer - Customer name
|
|
41
|
+
* @param {string} deployment.environment - Environment (development/staging/production)
|
|
42
|
+
* @param {Object} deployment.coreInputs - Tier 1 core inputs
|
|
43
|
+
* @param {Object} deployment.confirmations - Tier 2 confirmations
|
|
44
|
+
* @param {Object} deployment.result - Deployment result
|
|
45
|
+
*/
|
|
46
|
+
async saveDeploymentConfig(deployment) {
|
|
47
|
+
const {
|
|
48
|
+
customer,
|
|
49
|
+
environment,
|
|
50
|
+
coreInputs,
|
|
51
|
+
confirmations,
|
|
52
|
+
result
|
|
53
|
+
} = deployment;
|
|
54
|
+
if (!customer || !environment) {
|
|
55
|
+
throw new Error('Customer and environment are required');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Create customer directory if it doesn't exist
|
|
59
|
+
const customerDir = join(this.configDir, customer);
|
|
60
|
+
if (!existsSync(customerDir)) {
|
|
61
|
+
mkdirSync(customerDir, {
|
|
62
|
+
recursive: true
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Generate environment file content
|
|
67
|
+
const envContent = this.generateEnvFileContent({
|
|
68
|
+
customer,
|
|
69
|
+
environment,
|
|
70
|
+
coreInputs,
|
|
71
|
+
confirmations,
|
|
72
|
+
result
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Write to customer environment file
|
|
76
|
+
const envFile = join(customerDir, `${environment}.env`);
|
|
77
|
+
writeFileSync(envFile, envContent, 'utf8');
|
|
78
|
+
console.log(` š¾ Configuration saved: ${envFile}`);
|
|
79
|
+
return envFile;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Generate .env file content from deployment data
|
|
84
|
+
* @param {Object} data - Deployment data
|
|
85
|
+
* @returns {string} .env file content
|
|
86
|
+
*/
|
|
87
|
+
generateEnvFileContent(data) {
|
|
88
|
+
const {
|
|
89
|
+
customer,
|
|
90
|
+
environment,
|
|
91
|
+
coreInputs,
|
|
92
|
+
confirmations,
|
|
93
|
+
result
|
|
94
|
+
} = data;
|
|
95
|
+
const timestamp = new Date().toISOString();
|
|
96
|
+
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}`, ''];
|
|
97
|
+
|
|
98
|
+
// Cloudflare Configuration
|
|
99
|
+
if (coreInputs.cloudflareAccountId || coreInputs.cloudflareZoneId) {
|
|
100
|
+
lines.push('# ============================================');
|
|
101
|
+
lines.push('# Cloudflare Configuration');
|
|
102
|
+
lines.push('# ============================================');
|
|
103
|
+
if (coreInputs.cloudflareAccountId) {
|
|
104
|
+
lines.push(`CLOUDFLARE_ACCOUNT_ID=${coreInputs.cloudflareAccountId}`);
|
|
105
|
+
}
|
|
106
|
+
if (coreInputs.cloudflareZoneId) {
|
|
107
|
+
lines.push(`CLOUDFLARE_ZONE_ID=${coreInputs.cloudflareZoneId}`);
|
|
108
|
+
}
|
|
109
|
+
if (coreInputs.domainName) {
|
|
110
|
+
lines.push(`CUSTOMER_DOMAIN=${coreInputs.domainName}`);
|
|
111
|
+
lines.push(`DOMAIN=${coreInputs.domainName}`);
|
|
112
|
+
}
|
|
113
|
+
lines.push('# CLOUDFLARE_API_TOKEN=<stored-securely-in-env>');
|
|
114
|
+
lines.push('');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Service Configuration
|
|
118
|
+
lines.push('# ============================================');
|
|
119
|
+
lines.push('# Service Configuration');
|
|
120
|
+
lines.push('# ============================================');
|
|
121
|
+
if (coreInputs.serviceName) {
|
|
122
|
+
lines.push(`SERVICE_NAME=${coreInputs.serviceName}`);
|
|
123
|
+
}
|
|
124
|
+
if (coreInputs.serviceType) {
|
|
125
|
+
lines.push(`SERVICE_TYPE=${coreInputs.serviceType}`);
|
|
126
|
+
}
|
|
127
|
+
if (confirmations.displayName) {
|
|
128
|
+
lines.push(`DISPLAY_NAME=${confirmations.displayName}`);
|
|
129
|
+
}
|
|
130
|
+
if (confirmations.description) {
|
|
131
|
+
lines.push(`DESCRIPTION=${confirmations.description}`);
|
|
132
|
+
}
|
|
133
|
+
if (confirmations.version) {
|
|
134
|
+
lines.push(`VERSION=${confirmations.version}`);
|
|
135
|
+
}
|
|
136
|
+
if (confirmations.author) {
|
|
137
|
+
lines.push(`AUTHOR=${confirmations.author}`);
|
|
138
|
+
}
|
|
139
|
+
lines.push('');
|
|
140
|
+
|
|
141
|
+
// Database & Worker
|
|
142
|
+
if (confirmations.databaseName || confirmations.workerName) {
|
|
143
|
+
lines.push('# ============================================');
|
|
144
|
+
lines.push('# Cloudflare Resources');
|
|
145
|
+
lines.push('# ============================================');
|
|
146
|
+
if (confirmations.workerName) {
|
|
147
|
+
lines.push(`WORKER_NAME=${confirmations.workerName}`);
|
|
148
|
+
}
|
|
149
|
+
if (confirmations.databaseName) {
|
|
150
|
+
lines.push(`DATABASE_NAME=${confirmations.databaseName}`);
|
|
151
|
+
lines.push(`D1_DATABASE_NAME=${confirmations.databaseName}`);
|
|
152
|
+
}
|
|
153
|
+
lines.push('');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// URLs & Endpoints
|
|
157
|
+
if (confirmations.productionUrl || confirmations.stagingUrl || confirmations.developmentUrl) {
|
|
158
|
+
lines.push('# ============================================');
|
|
159
|
+
lines.push('# Environment URLs');
|
|
160
|
+
lines.push('# ============================================');
|
|
161
|
+
if (confirmations.productionUrl) {
|
|
162
|
+
lines.push(`PRODUCTION_URL=${confirmations.productionUrl}`);
|
|
163
|
+
}
|
|
164
|
+
if (confirmations.stagingUrl) {
|
|
165
|
+
lines.push(`STAGING_URL=${confirmations.stagingUrl}`);
|
|
166
|
+
}
|
|
167
|
+
if (confirmations.developmentUrl) {
|
|
168
|
+
lines.push(`DEVELOPMENT_URL=${confirmations.developmentUrl}`);
|
|
169
|
+
}
|
|
170
|
+
if (confirmations.documentationUrl) {
|
|
171
|
+
lines.push(`DOCUMENTATION_URL=${confirmations.documentationUrl}`);
|
|
172
|
+
}
|
|
173
|
+
if (confirmations.deploymentUrl) {
|
|
174
|
+
lines.push(`DEPLOYMENT_URL=${confirmations.deploymentUrl}`);
|
|
175
|
+
}
|
|
176
|
+
if (result && result.url) {
|
|
177
|
+
lines.push(`API_URL=${result.url}`);
|
|
178
|
+
}
|
|
179
|
+
lines.push('');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// API Configuration
|
|
183
|
+
if (confirmations.packageName || confirmations.apiBasePath || confirmations.healthCheckPath) {
|
|
184
|
+
lines.push('# ============================================');
|
|
185
|
+
lines.push('# API Configuration');
|
|
186
|
+
lines.push('# ============================================');
|
|
187
|
+
if (confirmations.packageName) {
|
|
188
|
+
lines.push(`PACKAGE_NAME=${confirmations.packageName}`);
|
|
189
|
+
}
|
|
190
|
+
if (confirmations.apiBasePath) {
|
|
191
|
+
lines.push(`API_BASE_PATH=${confirmations.apiBasePath}`);
|
|
192
|
+
}
|
|
193
|
+
if (confirmations.healthCheckPath) {
|
|
194
|
+
lines.push(`HEALTH_CHECK_PATH=${confirmations.healthCheckPath}`);
|
|
195
|
+
}
|
|
196
|
+
lines.push(`API_VERSION=v1`);
|
|
197
|
+
lines.push('');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Feature Flags
|
|
201
|
+
if (confirmations.features && Object.keys(confirmations.features).length > 0) {
|
|
202
|
+
lines.push('# ============================================');
|
|
203
|
+
lines.push('# Feature Flags');
|
|
204
|
+
lines.push('# ============================================');
|
|
205
|
+
Object.entries(confirmations.features).forEach(([feature, enabled]) => {
|
|
206
|
+
const featureName = feature.toUpperCase().replace(/([A-Z])/g, '_$1').replace(/^_/, '');
|
|
207
|
+
lines.push(`FEATURE_${featureName}=${enabled ? 'true' : 'false'}`);
|
|
208
|
+
});
|
|
209
|
+
lines.push('');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Environment-specific settings
|
|
213
|
+
lines.push('# ============================================');
|
|
214
|
+
lines.push(`# ${environment.charAt(0).toUpperCase() + environment.slice(1)} Environment Settings`);
|
|
215
|
+
lines.push('# ============================================');
|
|
216
|
+
switch (environment) {
|
|
217
|
+
case 'development':
|
|
218
|
+
lines.push('ENABLE_DEBUG_MODE=true');
|
|
219
|
+
lines.push('ENABLE_DETAILED_LOGGING=true');
|
|
220
|
+
lines.push('LOG_LEVEL=debug');
|
|
221
|
+
lines.push('RATE_LIMIT_REQUESTS_PER_MINUTE=10000');
|
|
222
|
+
lines.push('CORS_ORIGINS=*');
|
|
223
|
+
break;
|
|
224
|
+
case 'staging':
|
|
225
|
+
lines.push('ENABLE_DEBUG_MODE=true');
|
|
226
|
+
lines.push('LOG_LEVEL=info');
|
|
227
|
+
lines.push('RATE_LIMIT_REQUESTS_PER_MINUTE=1000');
|
|
228
|
+
break;
|
|
229
|
+
case 'production':
|
|
230
|
+
lines.push('ENABLE_DEBUG_MODE=false');
|
|
231
|
+
lines.push('LOG_LEVEL=warn');
|
|
232
|
+
lines.push('RATE_LIMIT_REQUESTS_PER_MINUTE=60');
|
|
233
|
+
lines.push('# Add production-specific secrets via Cloudflare dashboard or wrangler');
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
lines.push('');
|
|
237
|
+
|
|
238
|
+
// Monitoring
|
|
239
|
+
lines.push('# ============================================');
|
|
240
|
+
lines.push('# Monitoring & Observability');
|
|
241
|
+
lines.push('# ============================================');
|
|
242
|
+
lines.push('METRICS_ENABLED=true');
|
|
243
|
+
lines.push('TRACING_ENABLED=true');
|
|
244
|
+
lines.push('ERROR_REPORTING_ENABLED=true');
|
|
245
|
+
lines.push('');
|
|
246
|
+
|
|
247
|
+
// Deployment metadata
|
|
248
|
+
lines.push('# ============================================');
|
|
249
|
+
lines.push('# Deployment Metadata');
|
|
250
|
+
lines.push('# ============================================');
|
|
251
|
+
lines.push(`DEPLOYED_AT=${timestamp}`);
|
|
252
|
+
if (result && result.status) {
|
|
253
|
+
lines.push(`DEPLOYMENT_STATUS=${result.status}`);
|
|
254
|
+
}
|
|
255
|
+
lines.push('');
|
|
256
|
+
return lines.join('\n');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Check if customer configuration exists
|
|
261
|
+
* @param {string} customer - Customer name
|
|
262
|
+
* @param {string} environment - Environment
|
|
263
|
+
* @returns {boolean} True if configuration exists
|
|
264
|
+
*/
|
|
265
|
+
configExists(customer, environment) {
|
|
266
|
+
const envFile = join(this.configDir, customer, `${environment}.env`);
|
|
267
|
+
return existsSync(envFile);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get all configured customers
|
|
272
|
+
* @returns {Array<string>} List of customer names
|
|
273
|
+
*/
|
|
274
|
+
getConfiguredCustomers() {
|
|
275
|
+
if (!existsSync(this.configDir)) {
|
|
276
|
+
return [];
|
|
277
|
+
}
|
|
278
|
+
const fs = require('fs');
|
|
279
|
+
const entries = fs.readdirSync(this.configDir, {
|
|
280
|
+
withFileTypes: true
|
|
281
|
+
});
|
|
282
|
+
return entries.filter(entry => entry.isDirectory() && entry.name !== 'template').map(entry => entry.name);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Load environment file and parse into object
|
|
287
|
+
* @param {string} customer - Customer name
|
|
288
|
+
* @param {string} environment - Environment
|
|
289
|
+
* @returns {Object|null} Parsed environment variables or null if not found
|
|
290
|
+
*/
|
|
291
|
+
loadEnvironmentConfig(customer, environment) {
|
|
292
|
+
const envFile = join(this.configDir, customer, `${environment}.env`);
|
|
293
|
+
if (!existsSync(envFile)) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
const content = readFileSync(envFile, 'utf8');
|
|
297
|
+
const envVars = {};
|
|
298
|
+
content.split('\n').forEach(line => {
|
|
299
|
+
line = line.trim();
|
|
300
|
+
|
|
301
|
+
// Skip comments and empty lines
|
|
302
|
+
if (!line || line.startsWith('#')) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Parse KEY=value
|
|
307
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
|
|
308
|
+
if (match) {
|
|
309
|
+
const [, key, value] = match;
|
|
310
|
+
envVars[key] = value;
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
return envVars;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Display customer configuration summary
|
|
318
|
+
* @param {string} customer - Customer name
|
|
319
|
+
* @param {string} environment - Environment
|
|
320
|
+
*/
|
|
321
|
+
displayCustomerConfig(customer, environment) {
|
|
322
|
+
const config = this.loadEnvironmentConfig(customer, environment);
|
|
323
|
+
if (!config) {
|
|
324
|
+
console.log(` ā ļø No configuration found for ${customer}/${environment}`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
console.log(`\n š Existing Configuration for ${customer}/${environment}:`);
|
|
328
|
+
console.log(' ' + 'ā'.repeat(60));
|
|
329
|
+
const highlights = ['CUSTOMER_DOMAIN', 'CLOUDFLARE_ACCOUNT_ID', 'CLOUDFLARE_ZONE_ID', 'SERVICE_NAME', 'WORKER_NAME', 'DATABASE_NAME', 'DEPLOYMENT_URL', 'DEPLOYED_AT'];
|
|
330
|
+
highlights.forEach(key => {
|
|
331
|
+
if (config[key]) {
|
|
332
|
+
let value = config[key];
|
|
333
|
+
|
|
334
|
+
// Truncate long IDs for display
|
|
335
|
+
if (key.includes('_ID') && value.length > 20) {
|
|
336
|
+
value = value.substring(0, 8) + '...' + value.substring(value.length - 4);
|
|
337
|
+
}
|
|
338
|
+
console.log(` ${key.padEnd(25)}: ${value}`);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
console.log(' ' + 'ā'.repeat(60));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Export singleton instance for convenience
|
|
346
|
+
export const configPersistence = new ConfigPersistenceManager();
|
|
@@ -3,4 +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
7
|
export { askUser, askYesNo, askChoice, closePrompts } from '../interactive-prompts.js';
|