@tamyla/clodo-framework 2.0.2 ā 2.0.3
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 +8 -0
- package/bin/clodo-service.js +165 -0
- package/dist/config/customer-config-loader.js +247 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## [2.0.3](https://github.com/tamylaa/clodo-framework/compare/v2.0.2...v2.0.3) (2025-10-11)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* Add reusable deployment command with Three-Tier input architecture ([0e13bfc](https://github.com/tamylaa/clodo-framework/commit/0e13bfcdda56d0a137bcd44cfd8a9ca49af30503))
|
|
7
|
+
* test - Remove placeholder tests that require unimplemented classes ([b009b34](https://github.com/tamylaa/clodo-framework/commit/b009b34cf1f9f7542fbaab2fa2419b2766c72f10))
|
|
8
|
+
|
|
1
9
|
## [2.0.2](https://github.com/tamylaa/clodo-framework/compare/v2.0.1...v2.0.2) (2025-10-11)
|
|
2
10
|
|
|
3
11
|
|
package/bin/clodo-service.js
CHANGED
|
@@ -413,4 +413,169 @@ program
|
|
|
413
413
|
}
|
|
414
414
|
});
|
|
415
415
|
|
|
416
|
+
// Deploy command - using existing InputCollector + CustomerConfigLoader (reusable pattern)
|
|
417
|
+
program
|
|
418
|
+
.command('deploy')
|
|
419
|
+
.description('Deploy service using three-tier input architecture')
|
|
420
|
+
.option('-c, --customer <name>', 'Customer name')
|
|
421
|
+
.option('-e, --env <environment>', 'Target environment (development, staging, production)')
|
|
422
|
+
.option('-i, --interactive', 'Interactive mode (review confirmations)', true)
|
|
423
|
+
.option('--non-interactive', 'Non-interactive mode (use stored config)')
|
|
424
|
+
.option('--dry-run', 'Simulate deployment without making changes')
|
|
425
|
+
.option('--domain <domain>', 'Domain name (overrides stored config)')
|
|
426
|
+
.option('--service-path <path>', 'Path to service directory', '.')
|
|
427
|
+
.action(async (options) => {
|
|
428
|
+
try {
|
|
429
|
+
// Use existing reusable components
|
|
430
|
+
const { InputCollector } = await import('../dist/service-management/InputCollector.js');
|
|
431
|
+
const { CustomerConfigLoader } = await import('../dist/config/customer-config-loader.js');
|
|
432
|
+
const { ConfirmationHandler } = await import('../dist/service-management/handlers/ConfirmationHandler.js');
|
|
433
|
+
const { MultiDomainOrchestrator } = await import('../dist/orchestration/multi-domain-orchestrator.js');
|
|
434
|
+
|
|
435
|
+
console.log(chalk.cyan('\nš Clodo Framework Deployment'));
|
|
436
|
+
console.log(chalk.white('Using Three-Tier Input Architecture\n'));
|
|
437
|
+
|
|
438
|
+
const isInteractive = options.interactive && !options.nonInteractive;
|
|
439
|
+
const configLoader = new CustomerConfigLoader();
|
|
440
|
+
const inputCollector = new InputCollector({ interactive: isInteractive });
|
|
441
|
+
const confirmationHandler = new ConfirmationHandler({ interactive: isInteractive });
|
|
442
|
+
|
|
443
|
+
let coreInputs = {};
|
|
444
|
+
let source = 'interactive';
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
// Try to load existing customer config first
|
|
448
|
+
if (options.customer && options.env) {
|
|
449
|
+
const storedConfig = configLoader.loadConfig(options.customer, options.env);
|
|
450
|
+
|
|
451
|
+
if (storedConfig) {
|
|
452
|
+
source = 'stored-config';
|
|
453
|
+
console.log(chalk.cyan('š Found Existing Configuration\n'));
|
|
454
|
+
configLoader.displayConfig(storedConfig.parsed);
|
|
455
|
+
|
|
456
|
+
if (!isInteractive) {
|
|
457
|
+
// Non-interactive: use stored config as-is
|
|
458
|
+
coreInputs = storedConfig.parsed;
|
|
459
|
+
console.log(chalk.green('\nā
Using stored configuration (non-interactive mode)\n'));
|
|
460
|
+
} else {
|
|
461
|
+
// Interactive: ask to confirm or re-collect
|
|
462
|
+
const useStored = await inputCollector.question('\nUse this configuration? (Y/n): ');
|
|
463
|
+
|
|
464
|
+
if (useStored.toLowerCase() !== 'n') {
|
|
465
|
+
coreInputs = storedConfig.parsed;
|
|
466
|
+
console.log(chalk.green('\nā
Using stored configuration\n'));
|
|
467
|
+
} else {
|
|
468
|
+
console.log(chalk.white('\nCollecting new configuration...\n'));
|
|
469
|
+
source = 'interactive';
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
} else {
|
|
473
|
+
console.log(chalk.yellow(`ā ļø No configuration found for ${options.customer}/${options.env}`));
|
|
474
|
+
console.log(chalk.white('Collecting inputs interactively...\n'));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Collect inputs if we don't have them from stored config
|
|
479
|
+
if (!coreInputs.cloudflareAccountId) {
|
|
480
|
+
console.log(chalk.cyan('š Tier 1: Core Input Collection\n'));
|
|
481
|
+
|
|
482
|
+
// Use InputCollector for what it does best - collecting inputs
|
|
483
|
+
coreInputs = {
|
|
484
|
+
customer: options.customer || await inputCollector.question('Customer name: '),
|
|
485
|
+
environment: options.env || await inputCollector.collectEnvironment(),
|
|
486
|
+
serviceName: await inputCollector.collectServiceName(),
|
|
487
|
+
serviceType: await inputCollector.collectServiceType(),
|
|
488
|
+
domainName: options.domain || await inputCollector.collectDomainName(),
|
|
489
|
+
cloudflareToken: process.env.CLOUDFLARE_API_TOKEN || await inputCollector.collectCloudflareToken(),
|
|
490
|
+
cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID || await inputCollector.collectCloudflareAccountId(),
|
|
491
|
+
cloudflareZoneId: process.env.CLOUDFLARE_ZONE_ID || await inputCollector.collectCloudflareZoneId()
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
source = 'interactive';
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Allow domain override
|
|
498
|
+
if (options.domain) {
|
|
499
|
+
coreInputs.domainName = options.domain;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Tier 2: Generate smart confirmations using existing ConfirmationHandler
|
|
503
|
+
console.log(chalk.cyan('āļø Tier 2: Smart Confirmations\n'));
|
|
504
|
+
const confirmations = await confirmationHandler.generateAndConfirm(coreInputs);
|
|
505
|
+
|
|
506
|
+
// Show deployment summary
|
|
507
|
+
console.log(chalk.cyan('\nš Deployment Summary'));
|
|
508
|
+
console.log(chalk.gray('ā'.repeat(60)));
|
|
509
|
+
|
|
510
|
+
console.log(chalk.white(`Source: ${source}`));
|
|
511
|
+
console.log(chalk.white(`Customer: ${coreInputs.customer}`));
|
|
512
|
+
console.log(chalk.white(`Environment: ${coreInputs.environment}`));
|
|
513
|
+
console.log(chalk.white(`Domain: ${coreInputs.domainName}`));
|
|
514
|
+
console.log(chalk.white(`Account ID: ${coreInputs.cloudflareAccountId?.substring(0, 8)}...`));
|
|
515
|
+
console.log(chalk.white(`Zone ID: ${coreInputs.cloudflareZoneId?.substring(0, 8)}...`));
|
|
516
|
+
|
|
517
|
+
if (confirmations.workerName) {
|
|
518
|
+
console.log(chalk.white(`Worker: ${confirmations.workerName}`));
|
|
519
|
+
}
|
|
520
|
+
if (confirmations.databaseName) {
|
|
521
|
+
console.log(chalk.white(`Database: ${confirmations.databaseName}`));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
console.log(chalk.gray('ā'.repeat(60)));
|
|
525
|
+
|
|
526
|
+
if (options.dryRun) {
|
|
527
|
+
console.log(chalk.yellow('\nš DRY RUN MODE - No changes will be made\n'));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Tier 3: Execute deployment
|
|
531
|
+
console.log(chalk.cyan('\nāļø Tier 3: Automated Deployment\n'));
|
|
532
|
+
|
|
533
|
+
const orchestrator = new MultiDomainOrchestrator({
|
|
534
|
+
domains: [coreInputs.domainName],
|
|
535
|
+
environment: coreInputs.environment,
|
|
536
|
+
dryRun: options.dryRun,
|
|
537
|
+
servicePath: options.servicePath
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
await orchestrator.initialize();
|
|
541
|
+
|
|
542
|
+
console.log(chalk.cyan('š Starting deployment...\n'));
|
|
543
|
+
|
|
544
|
+
const result = await orchestrator.deployDomain(coreInputs.domainName, {
|
|
545
|
+
...coreInputs,
|
|
546
|
+
...confirmations,
|
|
547
|
+
servicePath: options.servicePath
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// Display results
|
|
551
|
+
console.log(chalk.green('\nā
Deployment Completed Successfully!'));
|
|
552
|
+
console.log(chalk.gray('ā'.repeat(60)));
|
|
553
|
+
console.log(chalk.white(` Worker: ${confirmations.workerName || 'N/A'}`));
|
|
554
|
+
console.log(chalk.white(` URL: ${result.url || confirmations.deploymentUrl || 'N/A'}`));
|
|
555
|
+
console.log(chalk.white(` Status: ${result.status || 'deployed'}`));
|
|
556
|
+
|
|
557
|
+
if (result.health) {
|
|
558
|
+
console.log(chalk.white(` Health: ${result.health}`));
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
console.log(chalk.gray('ā'.repeat(60)));
|
|
562
|
+
|
|
563
|
+
console.log(chalk.cyan('\nš Next Steps:'));
|
|
564
|
+
console.log(chalk.white(` ⢠Test deployment: curl ${result.url || confirmations.deploymentUrl}/health`));
|
|
565
|
+
console.log(chalk.white(` ⢠Monitor logs: wrangler tail ${confirmations.workerName}`));
|
|
566
|
+
console.log(chalk.white(` ⢠View dashboard: https://dash.cloudflare.com`));
|
|
567
|
+
|
|
568
|
+
} finally {
|
|
569
|
+
inputCollector.close();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
} catch (error) {
|
|
573
|
+
console.error(chalk.red(`\nā Deployment failed: ${error.message}`));
|
|
574
|
+
if (error.stack && process.env.DEBUG) {
|
|
575
|
+
console.error(chalk.gray(error.stack));
|
|
576
|
+
}
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
|
|
416
581
|
program.parse();
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CustomerConfigLoader - Reusable Utility for Loading Customer Configurations
|
|
3
|
+
*
|
|
4
|
+
* This utility:
|
|
5
|
+
* 1. Loads existing customer configs from config/customers/{customer}/{env}.env
|
|
6
|
+
* 2. Provides defaults that can be merged with InputCollector results
|
|
7
|
+
* 3. Works for both service creation and deployment
|
|
8
|
+
*
|
|
9
|
+
* PATTERN: Separation of Concerns
|
|
10
|
+
* - InputCollector: Collects user inputs (pure, reusable)
|
|
11
|
+
* - CustomerConfigLoader: Loads stored configs (pure, reusable)
|
|
12
|
+
* - ServiceOrchestrator/DeploymentOrchestrator: Uses both to orchestrate workflows
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync } from 'fs';
|
|
16
|
+
import { resolve } from 'path';
|
|
17
|
+
import { getDirname } from '../utils/esm-helper.js';
|
|
18
|
+
import { createLogger } from '../utils/index.js';
|
|
19
|
+
import chalk from 'chalk';
|
|
20
|
+
const __dirname = getDirname(import.meta.url, 'src/config');
|
|
21
|
+
const logger = createLogger('CustomerConfigLoader');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse .env file into key-value pairs
|
|
25
|
+
*/
|
|
26
|
+
function parseEnvFile(filePath) {
|
|
27
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
28
|
+
const result = {};
|
|
29
|
+
content.split('\n').forEach(line => {
|
|
30
|
+
line = line.trim();
|
|
31
|
+
|
|
32
|
+
// Skip empty lines and comments
|
|
33
|
+
if (!line || line.startsWith('#')) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Parse KEY=VALUE
|
|
38
|
+
const match = line.match(/^([^=]+)=(.*)$/);
|
|
39
|
+
if (match) {
|
|
40
|
+
const key = match[1].trim();
|
|
41
|
+
let value = match[2].trim();
|
|
42
|
+
|
|
43
|
+
// Remove quotes if present
|
|
44
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
45
|
+
value = value.slice(1, -1);
|
|
46
|
+
}
|
|
47
|
+
result[key] = value;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if config is a template (not real data)
|
|
55
|
+
*/
|
|
56
|
+
function isTemplateConfig(envVars) {
|
|
57
|
+
return envVars.CUSTOMER_NAME?.includes('{{') || envVars.CLOUDFLARE_ACCOUNT_ID === '00000000000000000000000000000000' || !envVars.CLOUDFLARE_ACCOUNT_ID || !envVars.DOMAIN;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* CustomerConfigLoader - Load and parse customer configurations
|
|
62
|
+
*/
|
|
63
|
+
export class CustomerConfigLoader {
|
|
64
|
+
constructor(options = {}) {
|
|
65
|
+
this.configDir = options.configDir || resolve(__dirname, '..', '..', 'config', 'customers');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Load customer configuration if it exists
|
|
70
|
+
* Returns null if not found or is a template
|
|
71
|
+
*
|
|
72
|
+
* @param {string} customer - Customer name
|
|
73
|
+
* @param {string} environment - Environment (development, staging, production)
|
|
74
|
+
* @returns {Object|null} - Parsed config or null
|
|
75
|
+
*/
|
|
76
|
+
loadConfig(customer, environment) {
|
|
77
|
+
const configPath = resolve(this.configDir, customer, `${environment}.env`);
|
|
78
|
+
if (!existsSync(configPath)) {
|
|
79
|
+
logger.debug('Config file not found', {
|
|
80
|
+
customer,
|
|
81
|
+
environment,
|
|
82
|
+
configPath
|
|
83
|
+
});
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const envVars = parseEnvFile(configPath);
|
|
88
|
+
|
|
89
|
+
// Check if it's a template
|
|
90
|
+
if (isTemplateConfig(envVars)) {
|
|
91
|
+
logger.debug('Config is a template, treating as non-existent', {
|
|
92
|
+
customer,
|
|
93
|
+
environment
|
|
94
|
+
});
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
logger.info('Loaded customer config', {
|
|
98
|
+
customer,
|
|
99
|
+
environment
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
customer,
|
|
103
|
+
environment,
|
|
104
|
+
configPath,
|
|
105
|
+
raw: envVars,
|
|
106
|
+
parsed: this.parseToStandardFormat(envVars, customer, environment)
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
logger.error('Failed to load config', {
|
|
110
|
+
customer,
|
|
111
|
+
environment,
|
|
112
|
+
error: error.message
|
|
113
|
+
});
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Parse env vars into standard format that matches InputCollector output
|
|
120
|
+
* This ensures compatibility between stored configs and collected inputs
|
|
121
|
+
*/
|
|
122
|
+
parseToStandardFormat(envVars, customer, environment) {
|
|
123
|
+
return {
|
|
124
|
+
// Core inputs (Tier 1) - matches InputCollector.collectCoreInputs()
|
|
125
|
+
serviceName: envVars.SERVICE_NAME || customer,
|
|
126
|
+
serviceType: envVars.SERVICE_TYPE || 'generic',
|
|
127
|
+
domainName: envVars.DOMAIN || envVars.CUSTOMER_DOMAIN,
|
|
128
|
+
cloudflareToken: envVars.CLOUDFLARE_API_TOKEN || process.env.CLOUDFLARE_API_TOKEN,
|
|
129
|
+
cloudflareAccountId: envVars.CLOUDFLARE_ACCOUNT_ID,
|
|
130
|
+
cloudflareZoneId: envVars.CLOUDFLARE_ZONE_ID,
|
|
131
|
+
environment: environment,
|
|
132
|
+
// Additional metadata
|
|
133
|
+
customer: customer,
|
|
134
|
+
// Deployment-specific (Tier 2 confirmations)
|
|
135
|
+
workerName: envVars.WORKER_NAME,
|
|
136
|
+
databaseName: envVars.DATABASE_NAME || envVars.D1_DATABASE_NAME,
|
|
137
|
+
deploymentUrl: envVars.DEPLOYMENT_URL || envVars.API_DOMAIN,
|
|
138
|
+
healthCheckPath: envVars.HEALTH_CHECK_PATH || '/health',
|
|
139
|
+
apiBasePath: envVars.API_BASE_PATH || '/api/v1',
|
|
140
|
+
logLevel: envVars.LOG_LEVEL || 'info',
|
|
141
|
+
nodeCompatibility: envVars.NODE_COMPATIBILITY || 'v18',
|
|
142
|
+
// All raw env vars for other use cases
|
|
143
|
+
envVars: envVars
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Load config or return empty defaults
|
|
149
|
+
* Never throws - always returns an object
|
|
150
|
+
*/
|
|
151
|
+
loadConfigSafe(customer, environment) {
|
|
152
|
+
const config = this.loadConfig(customer, environment);
|
|
153
|
+
if (config) {
|
|
154
|
+
return config.parsed;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Return defaults if no config found
|
|
158
|
+
return {
|
|
159
|
+
customer,
|
|
160
|
+
environment,
|
|
161
|
+
serviceName: customer,
|
|
162
|
+
serviceType: 'generic',
|
|
163
|
+
domainName: '',
|
|
164
|
+
cloudflareToken: process.env.CLOUDFLARE_API_TOKEN || '',
|
|
165
|
+
cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID || '',
|
|
166
|
+
cloudflareZoneId: process.env.CLOUDFLARE_ZONE_ID || '',
|
|
167
|
+
envVars: {}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Check if customer config exists (and is not a template)
|
|
173
|
+
*/
|
|
174
|
+
hasConfig(customer, environment) {
|
|
175
|
+
return this.loadConfig(customer, environment) !== null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Display loaded config for user review
|
|
180
|
+
*/
|
|
181
|
+
displayConfig(config, options = {}) {
|
|
182
|
+
const {
|
|
183
|
+
showSecrets = false
|
|
184
|
+
} = options;
|
|
185
|
+
console.log(chalk.cyan('\nš Loaded Customer Configuration'));
|
|
186
|
+
console.log(chalk.gray('ā'.repeat(60)));
|
|
187
|
+
console.log(chalk.white(`Customer: ${config.customer}`));
|
|
188
|
+
console.log(chalk.white(`Environment: ${config.environment}`));
|
|
189
|
+
console.log(chalk.white(`Domain: ${config.domainName}`));
|
|
190
|
+
if (showSecrets) {
|
|
191
|
+
console.log(chalk.white(`Account ID: ${config.cloudflareAccountId}`));
|
|
192
|
+
console.log(chalk.white(`Zone ID: ${config.cloudflareZoneId}`));
|
|
193
|
+
} else {
|
|
194
|
+
console.log(chalk.white(`Account ID: ${config.cloudflareAccountId?.substring(0, 8)}...`));
|
|
195
|
+
console.log(chalk.white(`Zone ID: ${config.cloudflareZoneId?.substring(0, 8)}...`));
|
|
196
|
+
}
|
|
197
|
+
if (config.workerName) {
|
|
198
|
+
console.log(chalk.white(`Worker: ${config.workerName}`));
|
|
199
|
+
}
|
|
200
|
+
if (config.databaseName) {
|
|
201
|
+
console.log(chalk.white(`Database: ${config.databaseName}`));
|
|
202
|
+
}
|
|
203
|
+
if (config.deploymentUrl) {
|
|
204
|
+
console.log(chalk.white(`URL: ${config.deploymentUrl}`));
|
|
205
|
+
}
|
|
206
|
+
console.log(chalk.gray('ā'.repeat(60)));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Merge loaded config with collected inputs
|
|
211
|
+
* Collected inputs take precedence over stored config
|
|
212
|
+
*
|
|
213
|
+
* @param {Object} storedConfig - Config from file
|
|
214
|
+
* @param {Object} collectedInputs - Inputs from InputCollector
|
|
215
|
+
* @returns {Object} - Merged configuration
|
|
216
|
+
*/
|
|
217
|
+
mergeConfigs(storedConfig, collectedInputs) {
|
|
218
|
+
return {
|
|
219
|
+
...storedConfig,
|
|
220
|
+
...collectedInputs,
|
|
221
|
+
// Track source of each value
|
|
222
|
+
_sources: {
|
|
223
|
+
storedConfig: Object.keys(storedConfig),
|
|
224
|
+
collectedInputs: Object.keys(collectedInputs)
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get missing fields that need to be collected
|
|
231
|
+
* Compares stored config against required fields
|
|
232
|
+
*/
|
|
233
|
+
getMissingFields(config, requiredFields = []) {
|
|
234
|
+
if (!requiredFields.length) {
|
|
235
|
+
// Default required fields for deployment
|
|
236
|
+
requiredFields = ['customer', 'environment', 'domainName', 'cloudflareToken', 'cloudflareAccountId', 'cloudflareZoneId'];
|
|
237
|
+
}
|
|
238
|
+
return requiredFields.filter(field => !config[field] || config[field] === '');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Factory function for convenience
|
|
244
|
+
*/
|
|
245
|
+
export function createCustomerConfigLoader(options = {}) {
|
|
246
|
+
return new CustomerConfigLoader(options);
|
|
247
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tamyla/clodo-framework",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"description": "Reusable framework for Clodo-style software architecture on Cloudflare Workers + D1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"./handlers": "./dist/handlers/GenericRouteHandler.js",
|
|
21
21
|
"./config": "./dist/config/index.js",
|
|
22
22
|
"./config/discovery": "./dist/config/discovery/domain-discovery.js",
|
|
23
|
+
"./config/customer-loader": "./dist/config/customer-config-loader.js",
|
|
23
24
|
"./worker": "./dist/worker/index.js",
|
|
24
25
|
"./utils": "./dist/utils/index.js",
|
|
25
26
|
"./utils/deployment": "./dist/utils/deployment/index.js",
|