@tamyla/clodo-framework 2.0.2 → 2.0.4

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 CHANGED
@@ -1,3 +1,28 @@
1
+ ## [2.0.4](https://github.com/tamylaa/clodo-framework/compare/v2.0.3...v2.0.4) (2025-10-11)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * ensure clodo-security deploy --help works correctly ([1f81b5f](https://github.com/tamylaa/clodo-framework/commit/1f81b5f0d0590bd3d82470485bde6c449b95c12e))
7
+
8
+ ## [2.0.3](https://github.com/tamylaa/clodo-framework/compare/v2.0.2...v2.0.3) (2025-10-11)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * Add reusable deployment command with Three-Tier input architecture ([0e13bfc](https://github.com/tamylaa/clodo-framework/commit/0e13bfcdda56d0a137bcd44cfd8a9ca49af30503))
14
+ * clodo-security deploy --help and cross-platform deployment scripts ([d7ebbbe](https://github.com/tamylaa/clodo-framework/commit/d7ebbbe8d41c6e4f297f64d19ea5b98172ddee3b))
15
+ * test - Remove placeholder tests that require unimplemented classes ([b009b34](https://github.com/tamylaa/clodo-framework/commit/b009b34cf1f9f7542fbaab2fa2419b2766c72f10))
16
+
17
+ ## [2.0.3](https://github.com/tamylaa/clodo-framework/compare/v2.0.2...v2.0.3) (2025-10-11)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * Add reusable deployment command with Three-Tier input architecture ([0e13bfc](https://github.com/tamylaa/clodo-framework/commit/0e13bfcdda56d0a137bcd44cfd8a9ca49af30503))
23
+ * clodo-security deploy --help and cross-platform deployment scripts ([d7ebbbe](https://github.com/tamylaa/clodo-framework/commit/d7ebbbe8d41c6e4f297f64d19ea5b98172ddee3b))
24
+ * test - Remove placeholder tests that require unimplemented classes ([b009b34](https://github.com/tamylaa/clodo-framework/commit/b009b34cf1f9f7542fbaab2fa2419b2766c72f10))
25
+
1
26
  ## [2.0.2](https://github.com/tamylaa/clodo-framework/compare/v2.0.1...v2.0.2) (2025-10-11)
2
27
 
3
28
 
@@ -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();
@@ -36,7 +36,28 @@ async function main() {
36
36
  break;
37
37
 
38
38
  case 'deploy':
39
- const [deployCustomer, deployEnvironment] = args;
39
+ // Check for help flag first
40
+ if (args.includes('--help') || args.includes('-h')) {
41
+ console.log('Deploy with security validation');
42
+ console.log('');
43
+ console.log('Usage:');
44
+ console.log(' clodo-security deploy <customer> <environment> [options]');
45
+ console.log('');
46
+ console.log('Arguments:');
47
+ console.log(' customer Customer name (e.g., wetechfounders)');
48
+ console.log(' environment Target environment (development, staging, production)');
49
+ console.log('');
50
+ console.log('Options:');
51
+ console.log(' --dry-run Simulate deployment without making changes');
52
+ console.log(' --help, -h Display this help message');
53
+ console.log('');
54
+ console.log('Examples:');
55
+ console.log(' clodo-security deploy wetechfounders development');
56
+ console.log(' clodo-security deploy greatidude production --dry-run');
57
+ break;
58
+ }
59
+
60
+ const [deployCustomer, deployEnvironment] = args.filter(arg => !arg.startsWith('--'));
40
61
  const dryRun = args.includes('--dry-run');
41
62
  const deployResult = await cli.deployWithSecurity(deployCustomer, deployEnvironment, { dryRun });
42
63
  if (deployResult.success) {
@@ -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
+ }
@@ -268,16 +268,24 @@ export class GenerationEngine {
268
268
  main: "src/worker/index.js",
269
269
  type: "module",
270
270
  scripts: {
271
+ // Development
272
+ dev: "wrangler dev",
273
+ // Testing
271
274
  test: "jest",
272
275
  "test:watch": "jest --watch",
273
276
  "test:coverage": "jest --coverage",
274
- dev: "wrangler dev",
275
- deploy: "powershell .\\scripts\\deploy.ps1",
276
- setup: "powershell .\\scripts\\setup.ps1",
277
- "health-check": "powershell .\\scripts\\health-check.ps1",
277
+ // Deployment (cross-platform via framework)
278
+ deploy: "clodo-service deploy",
279
+ "deploy:dev": "node scripts/deploy.js development",
280
+ "deploy:staging": "node scripts/deploy.js staging",
281
+ "deploy:prod": "node scripts/deploy.js production",
282
+ // Code Quality
278
283
  lint: "eslint src/ test/",
279
284
  "lint:fix": "eslint src/ test/ --fix",
280
285
  format: "prettier --write src/ test/",
286
+ // Utilities
287
+ validate: "clodo-service validate .",
288
+ diagnose: "clodo-service diagnose .",
281
289
  build: "wrangler deploy --dry-run",
282
290
  clean: "rimraf dist/ coverage/"
283
291
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamyla/clodo-framework",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
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",