@tamyla/clodo-framework 3.0.12 → 3.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 CHANGED
@@ -1,3 +1,10 @@
1
+ ## [3.0.13](https://github.com/tamylaa/clodo-framework/compare/v3.0.12...v3.0.13) (2025-10-16)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * resolve circular dependency in unified-config-manager ([dff0841](https://github.com/tamylaa/clodo-framework/commit/dff0841c3bfc4ce0200b057c99ab76ea09641c68))
7
+
1
8
  ## [3.0.12](https://github.com/tamylaa/clodo-framework/compare/v3.0.11...v3.0.12) (2025-10-14)
2
9
 
3
10
 
@@ -18,6 +18,8 @@ import chalk from 'chalk';
18
18
  import { join } from 'path';
19
19
  import { ServiceOrchestrator } from '../dist/service-management/ServiceOrchestrator.js';
20
20
  import { InputCollector } from '../dist/service-management/InputCollector.js';
21
+ import { readFileSync, existsSync } from 'fs';
22
+ import { resolve } from 'path';
21
23
 
22
24
  const program = new Command();
23
25
 
@@ -26,6 +28,117 @@ program
26
28
  .description('Unified conversational CLI for Clodo Framework service lifecycle management')
27
29
  .version('1.0.0');
28
30
 
31
+ // Helper function to load JSON configuration file
32
+ function loadJsonConfig(configPath) {
33
+ try {
34
+ const fullPath = resolve(configPath);
35
+ if (!existsSync(fullPath)) {
36
+ throw new Error(`Configuration file not found: ${fullPath}`);
37
+ }
38
+
39
+ const content = readFileSync(fullPath, 'utf8');
40
+ const config = JSON.parse(content);
41
+
42
+ // Validate required fields
43
+ const required = ['customer', 'environment', 'domainName', 'cloudflareToken'];
44
+ const missing = required.filter(field => !config[field]);
45
+
46
+ if (missing.length > 0) {
47
+ throw new Error(`Missing required configuration fields: ${missing.join(', ')}`);
48
+ }
49
+
50
+ console.log(chalk.green(`✅ Loaded configuration from: ${fullPath}`));
51
+ return config;
52
+ } catch (error) {
53
+ if (error instanceof SyntaxError) {
54
+ throw new Error(`Invalid JSON in configuration file: ${error.message}`);
55
+ }
56
+ throw error;
57
+ }
58
+ }
59
+
60
+ // Simple progress indicator for deployment steps
61
+ function showProgress(message, duration = 2000) {
62
+ return new Promise(resolve => {
63
+ process.stdout.write(chalk.cyan(`⏳ ${message}...`));
64
+
65
+ const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
66
+ let i = 0;
67
+
68
+ const interval = setInterval(() => {
69
+ process.stdout.write(`\r${chalk.cyan(spinner[i])} ${message}...`);
70
+ i = (i + 1) % spinner.length;
71
+ }, 100);
72
+
73
+ setTimeout(() => {
74
+ clearInterval(interval);
75
+ process.stdout.write(`\r${chalk.green('✅')} ${message}... Done!\n`);
76
+ resolve();
77
+ }, duration);
78
+ });
79
+ }
80
+
81
+ // Early validation function to check prerequisites before deployment
82
+ async function validateDeploymentPrerequisites(coreInputs, options) {
83
+ const issues = [];
84
+
85
+ console.log(chalk.cyan('\n🔍 Pre-deployment Validation'));
86
+ console.log(chalk.gray('─'.repeat(40)));
87
+
88
+ // Check required fields
89
+ if (!coreInputs.customer) {
90
+ issues.push('Customer name is required');
91
+ }
92
+ if (!coreInputs.environment) {
93
+ issues.push('Environment is required');
94
+ }
95
+ if (!coreInputs.domainName) {
96
+ issues.push('Domain name is required');
97
+ }
98
+ if (!coreInputs.cloudflareToken) {
99
+ issues.push('Cloudflare API token is required');
100
+ }
101
+
102
+ // Check Cloudflare token format (basic validation)
103
+ if (coreInputs.cloudflareToken && !coreInputs.cloudflareToken.startsWith('CLOUDFLARE_API_TOKEN=')) {
104
+ if (coreInputs.cloudflareToken.length < 40) {
105
+ issues.push('Cloudflare API token appears to be invalid (too short)');
106
+ }
107
+ }
108
+
109
+ // Check if service path exists
110
+ if (options.servicePath && options.servicePath !== '.') {
111
+ const { existsSync } = await import('fs');
112
+ if (!existsSync(options.servicePath)) {
113
+ issues.push(`Service path does not exist: ${options.servicePath}`);
114
+ }
115
+ }
116
+
117
+ // Check for wrangler.toml if not dry run
118
+ if (!options.dryRun) {
119
+ const { existsSync } = await import('fs');
120
+ const { join } = await import('path');
121
+ const wranglerPath = join(options.servicePath || '.', 'wrangler.toml');
122
+ if (!existsSync(wranglerPath)) {
123
+ issues.push('wrangler.toml not found in service directory');
124
+ }
125
+ }
126
+
127
+ // Report issues
128
+ if (issues.length > 0) {
129
+ console.log(chalk.red('\n❌ Validation Failed:'));
130
+ issues.forEach(issue => {
131
+ console.log(chalk.red(` • ${issue}`));
132
+ });
133
+ console.log(chalk.gray('\n─'.repeat(40)));
134
+ return false;
135
+ }
136
+
137
+ console.log(chalk.green('✅ All prerequisites validated'));
138
+ console.log(chalk.gray('─'.repeat(40)));
139
+ return true;
140
+ }
141
+
29
142
  // Main interactive command
30
143
  program
31
144
  .command('create')
@@ -422,6 +535,10 @@ program
422
535
  .option('-e, --env <environment>', 'Target environment (development, staging, production)')
423
536
  .option('-i, --interactive', 'Interactive mode (review confirmations)', true)
424
537
  .option('--non-interactive', 'Non-interactive mode (use stored config)')
538
+ .option('--config-file <file>', 'Load configuration from JSON file')
539
+ .option('--defaults', 'Use default values where possible (non-interactive)')
540
+ .option('--quiet', 'Quiet mode - minimal output for CI/CD')
541
+ .option('--json-output', 'Output results as JSON for scripting')
425
542
  .option('--dry-run', 'Simulate deployment without making changes')
426
543
  .option('--domain <domain>', 'Domain name (overrides stored config)')
427
544
  .option('--service-path <path>', 'Path to service directory', '.')
@@ -436,7 +553,9 @@ program
436
553
  console.log(chalk.cyan('\n🚀 Clodo Framework Deployment'));
437
554
  console.log(chalk.white('Using Three-Tier Input Architecture\n'));
438
555
 
439
- const isInteractive = options.interactive && !options.nonInteractive;
556
+ const isInteractive = options.interactive && !options.nonInteractive && !options.configFile && !options.defaults;
557
+ const isQuiet = options.quiet;
558
+ const jsonOutput = options.jsonOutput;
440
559
  const configManager = new UnifiedConfigManager({
441
560
  configDir: join(process.cwd(), 'config', 'customers')
442
561
  });
@@ -446,216 +565,272 @@ program
446
565
  let coreInputs = {};
447
566
  let source = 'interactive';
448
567
 
449
- try {
450
- // Try UnifiedConfigManager to load existing config
451
- let storedConfig = null;
452
-
453
- if (options.customer && options.env) {
454
- // Check UnifiedConfigManager for existing config
455
- if (configManager.configExists(options.customer, options.env)) {
456
- console.log(chalk.green(`✅ Found existing configuration for ${options.customer}/${options.env}\n`));
457
- configManager.displayCustomerConfig(options.customer, options.env);
458
-
459
- if (!isInteractive) {
568
+ // Interactive mode: run input collector
569
+ if (isInteractive) {
570
+ await inputCollector.collect();
571
+ } else {
572
+ // Non-interactive mode: load from config file or stored config
573
+
574
+ // Load from JSON config file if specified
575
+ if (options.configFile) {
576
+ coreInputs = loadJsonConfig(options.configFile);
577
+ source = 'config-file';
578
+ } else if (options.customer && options.env) {
579
+ // Check UnifiedConfigManager for existing config
580
+ if (configManager.configExists(options.customer, options.env)) {
581
+ console.log(chalk.green(`✅ Found existing configuration for ${options.customer}/${options.env}\n`));
582
+ configManager.displayCustomerConfig(options.customer, options.env);
583
+
460
584
  // Non-interactive: auto-load the config
461
- storedConfig = configManager.loadCustomerConfig(options.customer, options.env);
585
+ const storedConfig = configManager.loadCustomerConfig(options.customer, options.env);
462
586
  if (storedConfig) {
463
587
  coreInputs = storedConfig;
464
588
  source = 'stored-config';
465
589
  }
466
590
  } else {
467
- // Interactive: ask if they want to use it
468
- const useExisting = await inputCollector.question('\n 💡 Use existing configuration? (Y/n): ');
469
-
470
- if (useExisting.toLowerCase() !== 'n') {
471
- storedConfig = configManager.loadCustomerConfig(options.customer, options.env);
472
- if (storedConfig) {
473
- coreInputs = storedConfig;
474
- source = 'stored-config';
591
+ console.log(chalk.yellow(`⚠️ No configuration found for ${options.customer}/${options.env}`));
592
+ console.log(chalk.white('Collecting inputs interactively...\n'));
593
+ }
594
+ } else {
595
+ console.log(chalk.yellow('⚠️ Customer and environment not specified'));
596
+ console.log(chalk.white('Use --customer and --env options, or run in interactive mode'));
597
+ process.exit(1);
598
+ }
599
+
600
+ // Collect inputs if we don't have them from stored config
601
+ if (!coreInputs.cloudflareAccountId) {
602
+ console.log(chalk.cyan('📊 Tier 1: Core Input Collection\n'));
603
+
604
+ // Collect basic info with smart customer selection
605
+ let customer = options.customer;
606
+ if (!customer) {
607
+ const customers = configManager.listCustomers();
608
+ if (customers.length > 0) {
609
+ const selection = await inputCollector.question('Select customer (enter number or name): ');
610
+
611
+ // Try to parse as number first
612
+ const num = parseInt(selection);
613
+ if (!isNaN(num) && num >= 1 && num <= customers.length) {
614
+ customer = customers[num - 1];
615
+ console.log(chalk.green(`✓ Selected: ${customer}\n`));
616
+ } else if (customers.includes(selection)) {
617
+ customer = selection;
618
+ console.log(chalk.green(`✓ Selected: ${customer}\n`));
619
+ } else {
620
+ // New customer name
621
+ customer = selection;
622
+ console.log(chalk.yellow(`⚠️ Creating new customer: ${customer}\n`));
475
623
  }
476
624
  } else {
477
- console.log(chalk.white('\n 📝 Collecting new configuration...\n'));
625
+ customer = await inputCollector.question('Customer name: ');
478
626
  }
479
627
  }
480
- } else {
481
- console.log(chalk.yellow(`⚠️ No configuration found for ${options.customer}/${options.env}`));
482
- console.log(chalk.white('Collecting inputs interactively...\n'));
628
+ const environment = options.env || await inputCollector.collectEnvironment();
629
+ const serviceName = await inputCollector.collectServiceName();
630
+ const serviceType = await inputCollector.collectServiceType();
631
+
632
+ // Collect Cloudflare token
633
+ const cloudflareToken = process.env.CLOUDFLARE_API_TOKEN || await inputCollector.collectCloudflareToken();
634
+
635
+ // Use CloudflareAPI for automatic domain discovery
636
+ console.log(chalk.cyan('⏳ Fetching Cloudflare configuration...'));
637
+ const cloudflareConfig = await inputCollector.collectCloudflareConfigWithDiscovery(
638
+ cloudflareToken,
639
+ options.domain
640
+ );
641
+
642
+ // Combine all inputs
643
+ coreInputs = {
644
+ customer,
645
+ environment,
646
+ serviceName,
647
+ serviceType,
648
+ domainName: cloudflareConfig.domainName,
649
+ cloudflareToken,
650
+ cloudflareAccountId: cloudflareConfig.accountId,
651
+ cloudflareZoneId: cloudflareConfig.zoneId
652
+ };
653
+
654
+ source = 'interactive';
483
655
  }
484
656
 
485
- // Use stored config if we found it and haven't collected inputs yet
486
- if (storedConfig && !coreInputs.cloudflareAccountId) {
487
- coreInputs = storedConfig;
488
- source = 'stored-config';
489
- console.log(chalk.green('\n✅ Using stored configuration\n'));
657
+ // Allow domain override
658
+ if (options.domain) {
659
+ coreInputs.domainName = options.domain;
490
660
  }
491
- } else if (!options.customer) {
492
- // Show available customers to help user
493
- const customers = configManager.listCustomers();
494
- if (customers.length > 0) {
495
- console.log(chalk.cyan('💡 Configured customers:'));
496
- customers.forEach((customer, index) => {
497
- console.log(chalk.white(` ${index + 1}. ${customer}`));
661
+
662
+ // Early validation before proceeding
663
+ const isValid = await validateDeploymentPrerequisites(coreInputs, options);
664
+ if (!isValid) {
665
+ console.log(chalk.red('\n❌ Deployment cancelled due to validation errors.'));
666
+ console.log(chalk.cyan('💡 Fix the issues above and try again.'));
667
+ process.exit(1);
668
+ }
669
+
670
+ // Tier 2: Generate smart confirmations using existing ConfirmationHandler
671
+ // Note: ConfirmationEngine prints its own header
672
+ const confirmations = await confirmationHandler.generateAndConfirm(coreInputs);
673
+
674
+ // Show deployment summary
675
+ console.log(chalk.cyan('\n📊 Deployment Summary'));
676
+ console.log(chalk.gray('─'.repeat(60)));
677
+
678
+ console.log(chalk.white(`Source: ${source}`));
679
+ console.log(chalk.white(`Customer: ${coreInputs.customer}`));
680
+ console.log(chalk.white(`Environment: ${coreInputs.environment}`));
681
+ console.log(chalk.white(`Domain: ${coreInputs.domainName}`));
682
+ console.log(chalk.white(`Account ID: ${coreInputs.cloudflareAccountId ? coreInputs.cloudflareAccountId.substring(0, 8) + '...' : 'N/A'}`));
683
+ console.log(chalk.white(`Zone ID: ${coreInputs.cloudflareZoneId ? coreInputs.cloudflareZoneId.substring(0, 8) + '...' : 'N/A'}`));
684
+
685
+ if (confirmations.workerName) {
686
+ console.log(chalk.white(`Worker: ${confirmations.workerName}`));
687
+ }
688
+ if (confirmations.databaseName) {
689
+ console.log(chalk.white(`Database: ${confirmations.databaseName}`));
690
+ }
691
+
692
+ console.log(chalk.gray('─'.repeat(60)));
693
+
694
+ if (options.dryRun) {
695
+ console.log(chalk.yellow('\n🔍 DRY RUN MODE - No changes will be made\n'));
696
+ }
697
+
698
+ // Tier 3: Execute deployment
699
+ console.log(chalk.cyan('\n⚙️ Tier 3: Automated Deployment\n'));
700
+
701
+ // INTEGRATION: Add intelligent service discovery and assessment
702
+ console.log(chalk.cyan('🧠 Performing Intelligent Service Assessment...'));
703
+ const { CapabilityAssessmentEngine } = await import('../dist/service-management/CapabilityAssessmentEngine.js');
704
+ const assessor = new CapabilityAssessmentEngine(options.servicePath || process.cwd());
705
+
706
+ try {
707
+ const assessment = await assessor.assessCapabilities({
708
+ serviceName: coreInputs.serviceName,
709
+ serviceType: coreInputs.serviceType,
710
+ environment: coreInputs.environment,
711
+ domainName: coreInputs.domainName
498
712
  });
713
+
714
+ // Display assessment results
715
+ console.log(chalk.green('✅ Assessment Complete'));
716
+ console.log(chalk.white(` Service Type: ${assessment.mergedInputs.serviceType || 'Not determined'}`));
717
+ console.log(chalk.white(` Confidence: ${assessment.confidence}%`));
718
+ console.log(chalk.white(` Missing Capabilities: ${assessment.gapAnalysis.missing.length}`));
719
+
720
+ // Show any blocking issues
721
+ const blockingIssues = assessment.gapAnalysis.missing.filter(gap => gap.priority === 'blocked');
722
+ if (blockingIssues.length > 0) {
723
+ console.log(chalk.yellow('\n⚠️ Permission-limited capabilities detected:'));
724
+ blockingIssues.forEach(issue => {
725
+ console.log(chalk.yellow(` - ${issue.capability}: ${issue.reason}`));
726
+ });
727
+ }
728
+
729
+ console.log('');
730
+
731
+ } catch (assessmentError) {
732
+ console.log(chalk.yellow('⚠️ Assessment failed, proceeding with deployment:'), assessmentError.message);
499
733
  console.log('');
500
734
  }
501
- }
502
-
503
- // Collect inputs if we don't have them from stored config
504
- if (!coreInputs.cloudflareAccountId) {
505
- console.log(chalk.cyan('📊 Tier 1: Core Input Collection\n'));
506
735
 
507
- // Collect basic info with smart customer selection
508
- let customer = options.customer;
509
- if (!customer) {
510
- const customers = configManager.listCustomers();
511
- if (customers.length > 0) {
512
- const selection = await inputCollector.question('Select customer (enter number or name): ');
513
-
514
- // Try to parse as number first
515
- const num = parseInt(selection);
516
- if (!isNaN(num) && num >= 1 && num <= customers.length) {
517
- customer = customers[num - 1];
518
- console.log(chalk.green(`✓ Selected: ${customer}\n`));
519
- } else if (customers.includes(selection)) {
520
- customer = selection;
521
- console.log(chalk.green(`✓ Selected: ${customer}\n`));
522
- } else {
523
- // New customer name
524
- customer = selection;
525
- console.log(chalk.yellow(`⚠️ Creating new customer: ${customer}\n`));
526
- }
527
- } else {
528
- customer = await inputCollector.question('Customer name: ');
736
+ const orchestrator = new MultiDomainOrchestrator({
737
+ domains: [coreInputs.domainName],
738
+ environment: coreInputs.environment,
739
+ dryRun: options.dryRun,
740
+ servicePath: options.servicePath,
741
+ cloudflareToken: coreInputs.cloudflareToken,
742
+ cloudflareAccountId: coreInputs.cloudflareAccountId
743
+ });
744
+
745
+ // Show progress for initialization
746
+ await showProgress('Initializing deployment orchestrator', 1500);
747
+ await orchestrator.initialize();
748
+
749
+ console.log(chalk.cyan('🚀 Starting deployment...\n'));
750
+
751
+ // Show progress during deployment
752
+ const deployPromise = orchestrator.deploySingleDomain(coreInputs.domainName, {
753
+ ...coreInputs,
754
+ ...confirmations,
755
+ servicePath: options.servicePath
756
+ });
757
+
758
+ // Show deployment progress
759
+ const progressMessages = [
760
+ 'Preparing deployment configuration',
761
+ 'Setting up Cloudflare resources',
762
+ 'Deploying worker script',
763
+ 'Configuring database connections',
764
+ 'Setting up domain routing',
765
+ 'Running final health checks'
766
+ ];
767
+
768
+ let progressIndex = 0;
769
+ const progressInterval = setInterval(() => {
770
+ if (progressIndex < progressMessages.length) {
771
+ console.log(chalk.gray(` ${progressMessages[progressIndex]}`));
772
+ progressIndex++;
529
773
  }
774
+ }, 2000);
775
+
776
+ const result = await deployPromise;
777
+ clearInterval(progressInterval);
778
+
779
+ // Fill in remaining progress messages quickly
780
+ while (progressIndex < progressMessages.length) {
781
+ console.log(chalk.gray(` ${progressMessages[progressIndex]}`));
782
+ progressIndex++;
530
783
  }
531
- const environment = options.env || await inputCollector.collectEnvironment();
532
- const serviceName = await inputCollector.collectServiceName();
533
- const serviceType = await inputCollector.collectServiceType();
534
784
 
535
- // Collect Cloudflare token
536
- const cloudflareToken = process.env.CLOUDFLARE_API_TOKEN || await inputCollector.collectCloudflareToken();
785
+ // Display results with cleaner formatting
786
+ console.log(chalk.green('\n✅ Deployment Completed Successfully!'));
787
+ console.log(chalk.gray('─'.repeat(60)));
537
788
 
538
- // Use CloudflareAPI for automatic domain discovery
539
- console.log(chalk.cyan('⏳ Fetching Cloudflare configuration...'));
540
- const cloudflareConfig = await inputCollector.collectCloudflareConfigWithDiscovery(
541
- cloudflareToken,
542
- options.domain
543
- );
789
+ // Show key information prominently
790
+ if (result.url || confirmations.deploymentUrl) {
791
+ console.log(chalk.white(`🌐 Service URL: ${chalk.bold(result.url || confirmations.deploymentUrl)}`));
792
+ }
544
793
 
545
- // Combine all inputs
546
- coreInputs = {
547
- customer,
548
- environment,
549
- serviceName,
550
- serviceType,
551
- domainName: cloudflareConfig.domainName,
552
- cloudflareToken,
553
- cloudflareAccountId: cloudflareConfig.accountId,
554
- cloudflareZoneId: cloudflareConfig.zoneId
555
- };
794
+ console.log(chalk.white(`👤 Customer: ${coreInputs.customer}`));
795
+ console.log(chalk.white(`🏭 Environment: ${coreInputs.environment}`));
796
+ console.log(chalk.white(`🔧 Worker: ${confirmations.workerName || 'N/A'}`));
556
797
 
557
- source = 'interactive';
558
- }
559
-
560
- // Allow domain override
561
- if (options.domain) {
562
- coreInputs.domainName = options.domain;
563
- }
564
-
565
- // Tier 2: Generate smart confirmations using existing ConfirmationHandler
566
- // Note: ConfirmationEngine prints its own header
567
- const confirmations = await confirmationHandler.generateAndConfirm(coreInputs);
568
-
569
- // Show deployment summary
570
- console.log(chalk.cyan('\n📊 Deployment Summary'));
571
- console.log(chalk.gray('─'.repeat(60)));
572
-
573
- console.log(chalk.white(`Source: ${source}`));
574
- console.log(chalk.white(`Customer: ${coreInputs.customer}`));
575
- console.log(chalk.white(`Environment: ${coreInputs.environment}`));
576
- console.log(chalk.white(`Domain: ${coreInputs.domainName}`));
577
- console.log(chalk.white(`Account ID: ${coreInputs.cloudflareAccountId?.substring(0, 8)}...`));
578
- console.log(chalk.white(`Zone ID: ${coreInputs.cloudflareZoneId?.substring(0, 8)}...`));
579
-
580
- if (confirmations.workerName) {
581
- console.log(chalk.white(`Worker: ${confirmations.workerName}`));
582
- }
583
- if (confirmations.databaseName) {
584
- console.log(chalk.white(`Database: ${confirmations.databaseName}`));
585
- }
586
-
587
- console.log(chalk.gray('─'.repeat(60)));
588
-
589
- if (options.dryRun) {
590
- console.log(chalk.yellow('\n🔍 DRY RUN MODE - No changes will be made\n'));
591
- }
592
-
593
- // Tier 3: Execute deployment
594
- console.log(chalk.cyan('\n⚙️ Tier 3: Automated Deployment\n'));
595
-
596
- const orchestrator = new MultiDomainOrchestrator({
597
- domains: [coreInputs.domainName],
598
- environment: coreInputs.environment,
599
- dryRun: options.dryRun,
600
- servicePath: options.servicePath,
601
- cloudflareToken: coreInputs.cloudflareToken,
602
- cloudflareAccountId: coreInputs.cloudflareAccountId
603
- });
604
-
605
- await orchestrator.initialize();
606
-
607
- console.log(chalk.cyan('🚀 Starting deployment...\n'));
608
-
609
- const result = await orchestrator.deploySingleDomain(coreInputs.domainName, {
610
- ...coreInputs,
611
- ...confirmations,
612
- servicePath: options.servicePath
613
- });
614
-
615
- // Display results
616
- console.log(chalk.green('\n✅ Deployment Completed Successfully!'));
617
- console.log(chalk.gray('─'.repeat(60)));
618
- console.log(chalk.white(` Worker: ${confirmations.workerName || 'N/A'}`));
619
- console.log(chalk.white(` URL: ${result.url || confirmations.deploymentUrl || 'N/A'}`));
620
- console.log(chalk.white(` Status: ${result.status || 'deployed'}`));
621
-
622
- if (result.health) {
623
- console.log(chalk.white(` Health: ${result.health}`));
624
- }
625
-
626
- console.log(chalk.gray('─'.repeat(60)));
627
-
628
- // Save deployment configuration for future reuse
629
- try {
630
- console.log(chalk.cyan('\n💾 Saving deployment configuration...'));
798
+ if (result.health) {
799
+ const healthStatus = result.health.toLowerCase().includes('ok') || result.health.toLowerCase().includes('healthy')
800
+ ? chalk.green('✅ Healthy')
801
+ : chalk.yellow('⚠️ Check required');
802
+ console.log(chalk.white(`💚 Health: ${healthStatus}`));
803
+ }
631
804
 
632
- const configFile = await configManager.saveCustomerConfig(
633
- coreInputs.customer,
634
- coreInputs.environment,
635
- {
636
- coreInputs,
637
- confirmations,
638
- result
639
- }
640
- );
805
+ console.log(chalk.gray('─'.repeat(60)));
806
+
807
+ // Save deployment configuration for future reuse
808
+ try {
809
+ console.log(chalk.cyan('\n💾 Saving deployment configuration...'));
810
+
811
+ const configFile = await configManager.saveCustomerConfig(
812
+ coreInputs.customer,
813
+ coreInputs.environment,
814
+ {
815
+ coreInputs,
816
+ confirmations,
817
+ result
818
+ }
819
+ );
820
+
821
+ console.log(chalk.green(' ✅ Configuration saved for future deployments'));
822
+ console.log(chalk.gray(` 📄 File: ${configFile}`));
823
+ } catch (saveError) {
824
+ console.log(chalk.yellow(` ⚠️ Could not save configuration: ${saveError.message}`));
825
+ console.log(chalk.gray(' Deployment succeeded, but you may need to re-enter values next time.'));
826
+ }
641
827
 
642
- console.log(chalk.green(' Configuration saved successfully!'));
643
- console.log(chalk.gray(` 📄 File: ${configFile}`));
644
- console.log(chalk.white(' 💡 Next deployment will automatically load these settings'));
645
- } catch (saveError) {
646
- console.log(chalk.yellow(` ⚠️ Could not save configuration: ${saveError.message}`));
647
- console.log(chalk.gray(' Deployment succeeded, but you may need to re-enter values next time.'));
828
+ console.log(chalk.cyan('\n📋 Next Steps:'));
829
+ console.log(chalk.white(` Test deployment: curl ${result.url || confirmations.deploymentUrl}/health`));
830
+ console.log(chalk.white(` Monitor logs: wrangler tail ${confirmations.workerName}`));
831
+ console.log(chalk.white(` • View dashboard: https://dash.cloudflare.com`));
648
832
  }
649
833
 
650
- console.log(chalk.cyan('\n📋 Next Steps:'));
651
- console.log(chalk.white(` • Test deployment: curl ${result.url || confirmations.deploymentUrl}/health`));
652
- console.log(chalk.white(` • Monitor logs: wrangler tail ${confirmations.workerName}`));
653
- console.log(chalk.white(` • View dashboard: https://dash.cloudflare.com`));
654
-
655
- } finally {
656
- inputCollector.close();
657
- }
658
-
659
834
  } catch (error) {
660
835
  console.error(chalk.red(`\n❌ Deployment failed: ${error.message}`));
661
836
 
@@ -687,4 +862,30 @@ program
687
862
  }
688
863
  });
689
864
 
865
+ // Utility function to redact sensitive information from logs
866
+ function redactSensitiveInfo(text) {
867
+ if (typeof text !== 'string') return text;
868
+
869
+ // Patterns to redact
870
+ const patterns = [
871
+ // Cloudflare API tokens
872
+ [/(CLOUDFLARE_API_TOKEN=?)(\w{20,})/gi, '$1[REDACTED]'],
873
+ // Generic API tokens/keys
874
+ [/(api[_-]?token|api[_-]?key|auth[_-]?token)["']?[:=]\s*["']?([a-zA-Z0-9_-]{20,})["']?/gi, '$1: [REDACTED]'],
875
+ // Passwords
876
+ [/(password|passwd|pwd)["']?[:=]\s*["']?([^"'\s]{3,})["']?/gi, '$1: [REDACTED]'],
877
+ // Secrets
878
+ [/(secret|key)["']?[:=]\s*["']?([a-zA-Z0-9_-]{10,})["']?/gi, '$1: [REDACTED]'],
879
+ // Account IDs (partial redaction)
880
+ [/(account[_-]?id|zone[_-]?id)["']?[:=]\s*["']?([a-zA-Z0-9]{8})([a-zA-Z0-9]{24,})["']?/gi, '$1: $2[REDACTED]']
881
+ ];
882
+
883
+ let redacted = text;
884
+ patterns.forEach(([pattern, replacement]) => {
885
+ redacted = redacted.replace(pattern, replacement);
886
+ });
887
+
888
+ return redacted;
889
+ }
890
+
690
891
  program.parse();