@tamyla/clodo-framework 3.0.12 → 3.0.14

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,20 @@
1
+ ## [3.0.14](https://github.com/tamylaa/clodo-framework/compare/v3.0.13...v3.0.14) (2025-10-20)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * add test environment compatibility fixes for ES modules ([71a033d](https://github.com/tamylaa/clodo-framework/commit/71a033d5d959480879c1a4771005553116e8671f))
7
+ * ignore deployment artifacts in deployments/ directory ([768f453](https://github.com/tamylaa/clodo-framework/commit/768f453cbabb984509613a382d86875c1309bcd2))
8
+ * resolve circular dependency in FeatureFlagManager ([d04956f](https://github.com/tamylaa/clodo-framework/commit/d04956f83527d3236fce2fe9750f71cdf0ba8d8e))
9
+ * resolve ESLint errors and update test expectations ([b1188be](https://github.com/tamylaa/clodo-framework/commit/b1188be66d3723bfff62431f6e0eabec685b0111))
10
+ * revert Jest config to working configuration that passes tests ([35a2846](https://github.com/tamylaa/clodo-framework/commit/35a28466c8a48c78011c128f89695399e8500341))
11
+ * update Jest config to properly handle ES modules ([1a951c7](https://github.com/tamylaa/clodo-framework/commit/1a951c7ae147ddb8bef0ec1508c858ac044423b7))
12
+ * update package-lock.json to sync with package.json dependencies ([a3ef884](https://github.com/tamylaa/clodo-framework/commit/a3ef8844cba2b5657a3493b8339c98049a57dd72))
13
+ * update package.json semantic release config to support main branch ([a06b2e8](https://github.com/tamylaa/clodo-framework/commit/a06b2e899ceb7699b67292157991d64c88dedae3))
14
+ * update semantic release config to support main branch ([4102e6c](https://github.com/tamylaa/clodo-framework/commit/4102e6c4cb4f52d065750239c2a0c5a89b733ac0))
15
+ * update version to 3.0.13 to sync with release history ([f9bb4d4](https://github.com/tamylaa/clodo-framework/commit/f9bb4d463a3884b82e972962e5fcd21a272c3acb))
16
+ * use os.tmpdir() for test paths to fix CI permission errors ([022c771](https://github.com/tamylaa/clodo-framework/commit/022c771994635002410c27d8eb7ec8f0614acf6f))
17
+
1
18
  ## [3.0.12](https://github.com/tamylaa/clodo-framework/compare/v3.0.11...v3.0.12) (2025-10-14)
2
19
 
3
20
 
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * Clodo Framework - Unified Three-Tier Service Management CLI
@@ -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')
@@ -414,6 +527,86 @@ program
414
527
  }
415
528
  });
416
529
 
530
+ // Professional Edition: assess command (from clodo-orchestration)
531
+ program
532
+ .command('assess [service-path]')
533
+ .description('Run intelligent capability assessment (requires @tamyla/clodo-orchestration)')
534
+ .option('--export <file>', 'Export assessment results to JSON file')
535
+ .option('--domain <domain>', 'Domain name for assessment')
536
+ .option('--service-type <type>', 'Service type for assessment')
537
+ .option('--token <token>', 'Cloudflare API token')
538
+ .action(async (servicePath, options) => {
539
+ try {
540
+ // Try to load professional orchestration package
541
+ let orchestrationModule;
542
+ try {
543
+ orchestrationModule = await import('@tamyla/clodo-orchestration');
544
+ } catch (err) {
545
+ console.error(chalk.red('❌ clodo-orchestration package not found'));
546
+ console.error(chalk.yellow('💡 Install with: npm install @tamyla/clodo-orchestration'));
547
+ process.exit(1);
548
+ }
549
+
550
+ const {
551
+ CapabilityAssessmentEngine,
552
+ ServiceAutoDiscovery,
553
+ runAssessmentWorkflow
554
+ } = orchestrationModule;
555
+
556
+ const targetPath = servicePath || process.cwd();
557
+ console.log(chalk.cyan('\n🧠 Professional Capability Assessment'));
558
+ console.log(chalk.gray('─'.repeat(60)));
559
+ console.log(chalk.white(`Service Path: ${targetPath}`));
560
+
561
+ if (options.domain) {
562
+ console.log(chalk.white(`Domain: ${options.domain}`));
563
+ }
564
+ if (options.serviceType) {
565
+ console.log(chalk.white(`Service Type: ${options.serviceType}`));
566
+ }
567
+ console.log(chalk.gray('─'.repeat(60)));
568
+
569
+ // Use the assessment workflow
570
+ const assessment = await runAssessmentWorkflow({
571
+ servicePath: targetPath,
572
+ domain: options.domain,
573
+ serviceType: options.serviceType,
574
+ token: options.token || process.env.CLOUDFLARE_API_TOKEN
575
+ });
576
+
577
+ // Display results
578
+ console.log(chalk.cyan('\n✅ Assessment Results'));
579
+ console.log(chalk.gray('─'.repeat(60)));
580
+ console.log(chalk.white(`Service Type: ${assessment.mergedInputs?.serviceType || assessment.serviceType || 'Not determined'}`));
581
+ console.log(chalk.white(`Confidence: ${assessment.confidence}%`));
582
+
583
+ if (assessment.gapAnalysis?.missing) {
584
+ console.log(chalk.white(`Missing Capabilities: ${assessment.gapAnalysis.missing.length}`));
585
+ if (assessment.gapAnalysis.missing.length > 0) {
586
+ console.log(chalk.yellow('\n⚠️ Missing:'));
587
+ assessment.gapAnalysis.missing.forEach(gap => {
588
+ console.log(chalk.yellow(` • ${gap.capability}: ${gap.reason || 'Not available'}`));
589
+ });
590
+ }
591
+ }
592
+
593
+ console.log(chalk.gray('─'.repeat(60)));
594
+
595
+ // Export results if requested
596
+ if (options.export) {
597
+ require('fs').writeFileSync(options.export, JSON.stringify(assessment, null, 2));
598
+ console.log(chalk.green(`\n📄 Results exported to: ${options.export}`));
599
+ }
600
+
601
+ } catch (error) {
602
+ console.error(chalk.red(`Assessment failed: ${error.message}`));
603
+ if (process.env.DEBUG) {
604
+ console.error(chalk.gray(error.stack));
605
+ }
606
+ process.exit(1);
607
+ }
608
+ });
609
+
417
610
  // Deploy command - using existing InputCollector + CustomerConfigLoader (reusable pattern)
418
611
  program
419
612
  .command('deploy')
@@ -422,6 +615,10 @@ program
422
615
  .option('-e, --env <environment>', 'Target environment (development, staging, production)')
423
616
  .option('-i, --interactive', 'Interactive mode (review confirmations)', true)
424
617
  .option('--non-interactive', 'Non-interactive mode (use stored config)')
618
+ .option('--config-file <file>', 'Load configuration from JSON file')
619
+ .option('--defaults', 'Use default values where possible (non-interactive)')
620
+ .option('--quiet', 'Quiet mode - minimal output for CI/CD')
621
+ .option('--json-output', 'Output results as JSON for scripting')
425
622
  .option('--dry-run', 'Simulate deployment without making changes')
426
623
  .option('--domain <domain>', 'Domain name (overrides stored config)')
427
624
  .option('--service-path <path>', 'Path to service directory', '.')
@@ -436,7 +633,9 @@ program
436
633
  console.log(chalk.cyan('\n🚀 Clodo Framework Deployment'));
437
634
  console.log(chalk.white('Using Three-Tier Input Architecture\n'));
438
635
 
439
- const isInteractive = options.interactive && !options.nonInteractive;
636
+ const isInteractive = options.interactive && !options.nonInteractive && !options.configFile && !options.defaults;
637
+ const isQuiet = options.quiet;
638
+ const jsonOutput = options.jsonOutput;
440
639
  const configManager = new UnifiedConfigManager({
441
640
  configDir: join(process.cwd(), 'config', 'customers')
442
641
  });
@@ -446,216 +645,285 @@ program
446
645
  let coreInputs = {};
447
646
  let source = 'interactive';
448
647
 
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) {
648
+ // Interactive mode: run input collector
649
+ if (isInteractive) {
650
+ await inputCollector.collect();
651
+ } else {
652
+ // Non-interactive mode: load from config file or stored config
653
+
654
+ // Load from JSON config file if specified
655
+ if (options.configFile) {
656
+ coreInputs = loadJsonConfig(options.configFile);
657
+ source = 'config-file';
658
+ } else if (options.customer && options.env) {
659
+ // Check UnifiedConfigManager for existing config
660
+ if (configManager.configExists(options.customer, options.env)) {
661
+ console.log(chalk.green(`✅ Found existing configuration for ${options.customer}/${options.env}\n`));
662
+ configManager.displayCustomerConfig(options.customer, options.env);
663
+
460
664
  // Non-interactive: auto-load the config
461
- storedConfig = configManager.loadCustomerConfig(options.customer, options.env);
665
+ const storedConfig = configManager.loadCustomerConfig(options.customer, options.env);
462
666
  if (storedConfig) {
463
667
  coreInputs = storedConfig;
464
668
  source = 'stored-config';
465
669
  }
466
670
  } 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';
671
+ console.log(chalk.yellow(`⚠️ No configuration found for ${options.customer}/${options.env}`));
672
+ console.log(chalk.white('Collecting inputs interactively...\n'));
673
+ }
674
+ } else {
675
+ console.log(chalk.yellow('⚠️ Customer and environment not specified'));
676
+ console.log(chalk.white('Use --customer and --env options, or run in interactive mode'));
677
+ process.exit(1);
678
+ }
679
+
680
+ // Collect inputs if we don't have them from stored config
681
+ if (!coreInputs.cloudflareAccountId) {
682
+ console.log(chalk.cyan('📊 Tier 1: Core Input Collection\n'));
683
+
684
+ // Collect basic info with smart customer selection
685
+ let customer = options.customer;
686
+ if (!customer) {
687
+ const customers = configManager.listCustomers();
688
+ if (customers.length > 0) {
689
+ const selection = await inputCollector.question('Select customer (enter number or name): ');
690
+
691
+ // Try to parse as number first
692
+ const num = parseInt(selection);
693
+ if (!isNaN(num) && num >= 1 && num <= customers.length) {
694
+ customer = customers[num - 1];
695
+ console.log(chalk.green(`✓ Selected: ${customer}\n`));
696
+ } else if (customers.includes(selection)) {
697
+ customer = selection;
698
+ console.log(chalk.green(`✓ Selected: ${customer}\n`));
699
+ } else {
700
+ // New customer name
701
+ customer = selection;
702
+ console.log(chalk.yellow(`⚠️ Creating new customer: ${customer}\n`));
475
703
  }
476
704
  } else {
477
- console.log(chalk.white('\n 📝 Collecting new configuration...\n'));
705
+ customer = await inputCollector.question('Customer name: ');
478
706
  }
479
707
  }
480
- } else {
481
- console.log(chalk.yellow(`⚠️ No configuration found for ${options.customer}/${options.env}`));
482
- console.log(chalk.white('Collecting inputs interactively...\n'));
708
+ const environment = options.env || await inputCollector.collectEnvironment();
709
+ const serviceName = await inputCollector.collectServiceName();
710
+ const serviceType = await inputCollector.collectServiceType();
711
+
712
+ // Collect Cloudflare token
713
+ const cloudflareToken = process.env.CLOUDFLARE_API_TOKEN || await inputCollector.collectCloudflareToken();
714
+
715
+ // Use CloudflareAPI for automatic domain discovery
716
+ console.log(chalk.cyan('⏳ Fetching Cloudflare configuration...'));
717
+ const cloudflareConfig = await inputCollector.collectCloudflareConfigWithDiscovery(
718
+ cloudflareToken,
719
+ options.domain
720
+ );
721
+
722
+ // Combine all inputs
723
+ coreInputs = {
724
+ customer,
725
+ environment,
726
+ serviceName,
727
+ serviceType,
728
+ domainName: cloudflareConfig.domainName,
729
+ cloudflareToken,
730
+ cloudflareAccountId: cloudflareConfig.accountId,
731
+ cloudflareZoneId: cloudflareConfig.zoneId
732
+ };
733
+
734
+ source = 'interactive';
735
+ }
736
+
737
+ // Allow domain override
738
+ if (options.domain) {
739
+ coreInputs.domainName = options.domain;
740
+ }
741
+
742
+ // Early validation before proceeding
743
+ const isValid = await validateDeploymentPrerequisites(coreInputs, options);
744
+ if (!isValid) {
745
+ console.log(chalk.red('\n❌ Deployment cancelled due to validation errors.'));
746
+ console.log(chalk.cyan('💡 Fix the issues above and try again.'));
747
+ process.exit(1);
748
+ }
749
+
750
+ // Tier 2: Generate smart confirmations using existing ConfirmationHandler
751
+ // Note: ConfirmationEngine prints its own header
752
+ const confirmations = await confirmationHandler.generateAndConfirm(coreInputs);
753
+
754
+ // Show deployment summary
755
+ console.log(chalk.cyan('\n📊 Deployment Summary'));
756
+ console.log(chalk.gray('─'.repeat(60)));
757
+
758
+ console.log(chalk.white(`Source: ${source}`));
759
+ console.log(chalk.white(`Customer: ${coreInputs.customer}`));
760
+ console.log(chalk.white(`Environment: ${coreInputs.environment}`));
761
+ console.log(chalk.white(`Domain: ${coreInputs.domainName}`));
762
+ console.log(chalk.white(`Account ID: ${coreInputs.cloudflareAccountId ? coreInputs.cloudflareAccountId.substring(0, 8) + '...' : 'N/A'}`));
763
+ console.log(chalk.white(`Zone ID: ${coreInputs.cloudflareZoneId ? coreInputs.cloudflareZoneId.substring(0, 8) + '...' : 'N/A'}`));
764
+
765
+ if (confirmations.workerName) {
766
+ console.log(chalk.white(`Worker: ${confirmations.workerName}`));
767
+ }
768
+ if (confirmations.databaseName) {
769
+ console.log(chalk.white(`Database: ${confirmations.databaseName}`));
483
770
  }
484
771
 
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'));
772
+ console.log(chalk.gray('─'.repeat(60)));
773
+
774
+ if (options.dryRun) {
775
+ console.log(chalk.yellow('\n🔍 DRY RUN MODE - No changes will be made\n'));
490
776
  }
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}`));
777
+
778
+ // Tier 3: Execute deployment
779
+ console.log(chalk.cyan('\n⚙️ Tier 3: Automated Deployment\n'));
780
+
781
+ // OPTIONAL: Try to load professional orchestration package if available
782
+ let professionalOrchestration = null;
783
+ try {
784
+ const { runAssessmentWorkflow } = await import('@tamyla/clodo-orchestration');
785
+ professionalOrchestration = { runAssessmentWorkflow };
786
+ console.log(chalk.cyan('📊 Professional Edition (clodo-orchestration) detected'));
787
+ } catch (err) {
788
+ // clodo-orchestration not installed, continue with standard assessment
789
+ if (process.env.DEBUG) {
790
+ console.log(chalk.gray(` ℹ️ clodo-orchestration not available: ${err.message}`));
791
+ }
792
+ }
793
+
794
+ // INTEGRATION: Add intelligent service discovery and assessment
795
+ console.log(chalk.cyan('🧠 Performing Intelligent Service Assessment...'));
796
+ const { CapabilityAssessmentEngine } = await import('../dist/service-management/CapabilityAssessmentEngine.js');
797
+ const assessor = new CapabilityAssessmentEngine(options.servicePath || process.cwd());
798
+
799
+ try {
800
+ const assessment = await assessor.assessCapabilities({
801
+ serviceName: coreInputs.serviceName,
802
+ serviceType: coreInputs.serviceType,
803
+ environment: coreInputs.environment,
804
+ domainName: coreInputs.domainName
498
805
  });
806
+
807
+ // Display assessment results
808
+ console.log(chalk.green('✅ Assessment Complete'));
809
+ console.log(chalk.white(` Service Type: ${assessment.mergedInputs.serviceType || 'Not determined'}`));
810
+ console.log(chalk.white(` Confidence: ${assessment.confidence}%`));
811
+ console.log(chalk.white(` Missing Capabilities: ${assessment.gapAnalysis.missing.length}`));
812
+
813
+ // Show any blocking issues
814
+ const blockingIssues = assessment.gapAnalysis.missing.filter(gap => gap.priority === 'blocked');
815
+ if (blockingIssues.length > 0) {
816
+ console.log(chalk.yellow('\n⚠️ Permission-limited capabilities detected:'));
817
+ blockingIssues.forEach(issue => {
818
+ console.log(chalk.yellow(` - ${issue.capability}: ${issue.reason}`));
819
+ });
820
+ }
821
+
822
+ console.log('');
823
+
824
+ } catch (assessmentError) {
825
+ console.log(chalk.yellow('⚠️ Assessment failed, proceeding with deployment:'), assessmentError.message);
499
826
  console.log('');
500
827
  }
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
828
 
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: ');
829
+ const orchestrator = new MultiDomainOrchestrator({
830
+ domains: [coreInputs.domainName],
831
+ environment: coreInputs.environment,
832
+ dryRun: options.dryRun,
833
+ servicePath: options.servicePath,
834
+ cloudflareToken: coreInputs.cloudflareToken,
835
+ cloudflareAccountId: coreInputs.cloudflareAccountId
836
+ });
837
+
838
+ // Show progress for initialization
839
+ await showProgress('Initializing deployment orchestrator', 1500);
840
+ await orchestrator.initialize();
841
+
842
+ console.log(chalk.cyan('🚀 Starting deployment...\n'));
843
+
844
+ // Show progress during deployment
845
+ const deployPromise = orchestrator.deploySingleDomain(coreInputs.domainName, {
846
+ ...coreInputs,
847
+ ...confirmations,
848
+ servicePath: options.servicePath
849
+ });
850
+
851
+ // Show deployment progress
852
+ const progressMessages = [
853
+ 'Preparing deployment configuration',
854
+ 'Setting up Cloudflare resources',
855
+ 'Deploying worker script',
856
+ 'Configuring database connections',
857
+ 'Setting up domain routing',
858
+ 'Running final health checks'
859
+ ];
860
+
861
+ let progressIndex = 0;
862
+ const progressInterval = setInterval(() => {
863
+ if (progressIndex < progressMessages.length) {
864
+ console.log(chalk.gray(` ${progressMessages[progressIndex]}`));
865
+ progressIndex++;
529
866
  }
867
+ }, 2000);
868
+
869
+ const result = await deployPromise;
870
+ clearInterval(progressInterval);
871
+
872
+ // Fill in remaining progress messages quickly
873
+ while (progressIndex < progressMessages.length) {
874
+ console.log(chalk.gray(` ${progressMessages[progressIndex]}`));
875
+ progressIndex++;
530
876
  }
531
- const environment = options.env || await inputCollector.collectEnvironment();
532
- const serviceName = await inputCollector.collectServiceName();
533
- const serviceType = await inputCollector.collectServiceType();
534
877
 
535
- // Collect Cloudflare token
536
- const cloudflareToken = process.env.CLOUDFLARE_API_TOKEN || await inputCollector.collectCloudflareToken();
878
+ // Display results with cleaner formatting
879
+ console.log(chalk.green('\n✅ Deployment Completed Successfully!'));
880
+ console.log(chalk.gray('─'.repeat(60)));
537
881
 
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
- );
882
+ // Show key information prominently
883
+ if (result.url || confirmations.deploymentUrl) {
884
+ console.log(chalk.white(`🌐 Service URL: ${chalk.bold(result.url || confirmations.deploymentUrl)}`));
885
+ }
544
886
 
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
- };
887
+ console.log(chalk.white(`👤 Customer: ${coreInputs.customer}`));
888
+ console.log(chalk.white(`🏭 Environment: ${coreInputs.environment}`));
889
+ console.log(chalk.white(`🔧 Worker: ${confirmations.workerName || 'N/A'}`));
556
890
 
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...'));
891
+ if (result.health) {
892
+ const healthStatus = result.health.toLowerCase().includes('ok') || result.health.toLowerCase().includes('healthy')
893
+ ? chalk.green('✅ Healthy')
894
+ : chalk.yellow('⚠️ Check required');
895
+ console.log(chalk.white(`💚 Health: ${healthStatus}`));
896
+ }
631
897
 
632
- const configFile = await configManager.saveCustomerConfig(
633
- coreInputs.customer,
634
- coreInputs.environment,
635
- {
636
- coreInputs,
637
- confirmations,
638
- result
639
- }
640
- );
898
+ console.log(chalk.gray('─'.repeat(60)));
899
+
900
+ // Save deployment configuration for future reuse
901
+ try {
902
+ console.log(chalk.cyan('\n💾 Saving deployment configuration...'));
903
+
904
+ const configFile = await configManager.saveCustomerConfig(
905
+ coreInputs.customer,
906
+ coreInputs.environment,
907
+ {
908
+ coreInputs,
909
+ confirmations,
910
+ result
911
+ }
912
+ );
913
+
914
+ console.log(chalk.green(' ✅ Configuration saved for future deployments'));
915
+ console.log(chalk.gray(` 📄 File: ${configFile}`));
916
+ } catch (saveError) {
917
+ console.log(chalk.yellow(` ⚠️ Could not save configuration: ${saveError.message}`));
918
+ console.log(chalk.gray(' Deployment succeeded, but you may need to re-enter values next time.'));
919
+ }
641
920
 
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.'));
921
+ console.log(chalk.cyan('\n📋 Next Steps:'));
922
+ console.log(chalk.white(` Test deployment: curl ${result.url || confirmations.deploymentUrl}/health`));
923
+ console.log(chalk.white(` Monitor logs: wrangler tail ${confirmations.workerName}`));
924
+ console.log(chalk.white(` • View dashboard: https://dash.cloudflare.com`));
648
925
  }
649
926
 
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
927
  } catch (error) {
660
928
  console.error(chalk.red(`\n❌ Deployment failed: ${error.message}`));
661
929
 
@@ -687,4 +955,30 @@ program
687
955
  }
688
956
  });
689
957
 
958
+ // Utility function to redact sensitive information from logs
959
+ function redactSensitiveInfo(text) {
960
+ if (typeof text !== 'string') return text;
961
+
962
+ // Patterns to redact
963
+ const patterns = [
964
+ // Cloudflare API tokens
965
+ [/(CLOUDFLARE_API_TOKEN=?)(\w{20,})/gi, '$1[REDACTED]'],
966
+ // Generic API tokens/keys
967
+ [/(api[_-]?token|api[_-]?key|auth[_-]?token)["']?[:=]\s*["']?([a-zA-Z0-9_-]{20,})["']?/gi, '$1: [REDACTED]'],
968
+ // Passwords
969
+ [/(password|passwd|pwd)["']?[:=]\s*["']?([^"'\s]{3,})["']?/gi, '$1: [REDACTED]'],
970
+ // Secrets
971
+ [/(secret|key)["']?[:=]\s*["']?([a-zA-Z0-9_-]{10,})["']?/gi, '$1: [REDACTED]'],
972
+ // Account IDs (partial redaction)
973
+ [/(account[_-]?id|zone[_-]?id)["']?[:=]\s*["']?([a-zA-Z0-9]{8})([a-zA-Z0-9]{24,})["']?/gi, '$1: $2[REDACTED]']
974
+ ];
975
+
976
+ let redacted = text;
977
+ patterns.forEach(([pattern, replacement]) => {
978
+ redacted = redacted.replace(pattern, replacement);
979
+ });
980
+
981
+ return redacted;
982
+ }
983
+
690
984
  program.parse();
@@ -17,8 +17,13 @@ import { execSync } from 'child_process';
17
17
  import { fileURLToPath } from 'url';
18
18
 
19
19
  const __dirname = (() => {
20
- const filename = fileURLToPath(import.meta.url);
21
- return dirname(filename);
20
+ try {
21
+ const filename = fileURLToPath(import.meta.url);
22
+ return dirname(filename);
23
+ } catch (error) {
24
+ // Fallback for test environments - use current working directory
25
+ return process.cwd();
26
+ }
22
27
  })();
23
28
 
24
29
  const SECRET_CONFIGS = {
@@ -3,10 +3,16 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSy
3
3
  import { resolve, join } from 'path';
4
4
  import toml from '@iarna/toml';
5
5
  import { createDomainConfigSchema, validateDomainConfig, createDomainRegistry } from './domains.js';
6
- import { createLogger } from '../utils/index.js';
7
6
  import { getDirname } from '../utils/esm-helper.js';
8
7
  const __dirname = getDirname(import.meta.url, 'src/config');
9
- const logger = createLogger('CustomerConfig');
8
+
9
+ // Simple inline logger to avoid circular dependency with index.js
10
+ const logger = {
11
+ info: (message, ...args) => console.log(`[CustomerConfig] ${message}`, ...args),
12
+ error: (message, ...args) => console.error(`[CustomerConfig] ${message}`, ...args),
13
+ warn: (message, ...args) => console.warn(`[CustomerConfig] ${message}`, ...args),
14
+ debug: (message, ...args) => console.debug(`[CustomerConfig] ${message}`, ...args)
15
+ };
10
16
 
11
17
  /**
12
18
  * Customer Configuration Manager
@@ -1,5 +1,11 @@
1
- import { deepMerge, validateRequired, createLogger } from '../utils/index.js';
2
- const logger = createLogger('DomainConfig');
1
+ // Simple inline logger to avoid circular dependency with index.js
2
+ import { validateRequired, deepMerge } from '../utils/index.js';
3
+ const logger = {
4
+ info: (message, ...args) => console.log(`[DomainConfig] ${message}`, ...args),
5
+ error: (message, ...args) => console.error(`[DomainConfig] ${message}`, ...args),
6
+ warn: (message, ...args) => console.warn(`[DomainConfig] ${message}`, ...args),
7
+ debug: (message, ...args) => console.debug(`[DomainConfig] ${message}`, ...args)
8
+ };
3
9
 
4
10
  /**
5
11
  * Creates a base domain configuration schema
@@ -1,5 +1,10 @@
1
- import { createLogger } from '../utils/index.js';
2
- const logger = createLogger('FeatureFlags');
1
+ // Simple inline logger to avoid circular dependency with index.js
2
+ const logger = {
3
+ info: (message, ...args) => console.log(`[FeatureFlagManager] ${message}`, ...args),
4
+ error: (message, ...args) => console.error(`[FeatureFlagManager] ${message}`, ...args),
5
+ warn: (message, ...args) => console.warn(`[FeatureFlagManager] ${message}`, ...args),
6
+ debug: (message, ...args) => console.debug(`[FeatureFlagManager] ${message}`, ...args)
7
+ };
3
8
 
4
9
  /**
5
10
  * Feature Flag Manager Class
package/dist/index.js CHANGED
@@ -24,6 +24,14 @@ export { WranglerDeployer } from './deployment/wrangler-deployer.js';
24
24
  // Security components
25
25
  export * from './security/index.js';
26
26
 
27
+ // Service management components
28
+ export { ServiceCreator, createService } from './service-management/ServiceCreator.js';
29
+ export { ServiceOrchestrator } from './service-management/ServiceOrchestrator.js';
30
+ export { InputHandler } from './service-management/handlers/InputHandler.js';
31
+ export { ConfirmationHandler } from './service-management/handlers/ConfirmationHandler.js';
32
+ export { GenerationHandler } from './service-management/handlers/GenerationHandler.js';
33
+ export { ValidationHandler } from './service-management/handlers/ValidationHandler.js';
34
+
27
35
  // Framework version info
28
36
  export const FRAMEWORK_VERSION = '1.0.0';
29
37
  export const FRAMEWORK_NAME = 'Clodo Framework';
@@ -597,7 +597,7 @@ export class MultiDomainOrchestrator {
597
597
  }
598
598
 
599
599
  /**
600
- * Validate domain deployment with real HTTP health check
600
+ * Validate domain deployment with real HTTP health check (with retries)
601
601
  */
602
602
  async validateDomainDeployment(domain) {
603
603
  console.log(` ✅ Validating deployment for ${domain}`);
@@ -614,54 +614,75 @@ export class MultiDomainOrchestrator {
614
614
  return true;
615
615
  }
616
616
  console.log(` 🔍 Running health check: ${deploymentUrl}/health`);
617
- try {
618
- const startTime = Date.now();
619
-
620
- // Perform actual HTTP health check
621
- const response = await fetch(`${deploymentUrl}/health`, {
622
- method: 'GET',
623
- headers: {
624
- 'User-Agent': 'Clodo-Orchestrator/2.0'
625
- },
626
- signal: AbortSignal.timeout(10000) // 10 second timeout
627
- });
628
- const responseTime = Date.now() - startTime;
629
- const status = response.status;
630
- if (status === 200) {
631
- console.log(` ✅ Health check passed (${status}) - Response time: ${responseTime}ms`);
632
-
633
- // Log successful health check
634
- this.stateManager.logAuditEvent('HEALTH_CHECK_PASSED', domain, {
635
- url: deploymentUrl,
636
- status,
637
- responseTime,
638
- environment: this.environment
639
- });
640
- return true;
641
- } else {
642
- console.log(` ⚠️ Health check returned ${status} - deployment may have issues`);
643
- this.stateManager.logAuditEvent('HEALTH_CHECK_WARNING', domain, {
644
- url: deploymentUrl,
645
- status,
646
- responseTime,
647
- environment: this.environment
617
+
618
+ // Retry logic for health checks
619
+ const maxRetries = 3;
620
+ const retryDelay = 5000; // 5 seconds between retries
621
+
622
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
623
+ try {
624
+ const startTime = Date.now();
625
+ console.log(` Attempt ${attempt}/${maxRetries}...`);
626
+
627
+ // Perform actual HTTP health check
628
+ const response = await fetch(`${deploymentUrl}/health`, {
629
+ method: 'GET',
630
+ headers: {
631
+ 'User-Agent': 'Clodo-Orchestrator/2.0'
632
+ },
633
+ signal: AbortSignal.timeout(15000) // 15 second timeout
648
634
  });
635
+ const responseTime = Date.now() - startTime;
636
+ const status = response.status;
637
+ if (status === 200) {
638
+ console.log(` ✅ Health check passed (${status}) - Response time: ${responseTime}ms`);
639
+
640
+ // Log successful health check
641
+ this.stateManager.logAuditEvent('HEALTH_CHECK_PASSED', domain, {
642
+ url: deploymentUrl,
643
+ status,
644
+ responseTime,
645
+ attempt,
646
+ environment: this.environment
647
+ });
648
+ return true;
649
+ } else {
650
+ const errorMsg = `Health check returned ${status} - deployment may have issues`;
651
+ console.log(` ⚠️ ${errorMsg}`);
652
+ this.stateManager.logAuditEvent('HEALTH_CHECK_WARNING', domain, {
653
+ url: deploymentUrl,
654
+ status,
655
+ responseTime,
656
+ attempt,
657
+ environment: this.environment
658
+ });
649
659
 
650
- // Don't fail deployment for non-200 status, just warn
651
- return true;
652
- }
653
- } catch (error) {
654
- console.log(` ⚠️ Health check failed: ${error.message}`);
655
- console.log(` 💡 This may be expected if the worker isn't fully propagated yet`);
656
- this.stateManager.logAuditEvent('HEALTH_CHECK_FAILED', domain, {
657
- url: deploymentUrl,
658
- error: error.message,
659
- environment: this.environment
660
- });
660
+ // Don't fail deployment for non-200 status, just warn
661
+ return true;
662
+ }
663
+ } catch (error) {
664
+ const isLastAttempt = attempt === maxRetries;
665
+ const errorMsg = `Health check failed: ${error.message}`;
666
+ if (isLastAttempt) {
667
+ console.log(` ❌ ${errorMsg} (final attempt)`);
668
+ console.log(` 💡 The service may still be deploying. Check manually: curl ${deploymentUrl}/health`);
669
+ this.stateManager.logAuditEvent('HEALTH_CHECK_FAILED', domain, {
670
+ url: deploymentUrl,
671
+ error: error.message,
672
+ attempts: maxRetries,
673
+ environment: this.environment
674
+ });
661
675
 
662
- // Don't fail deployment for health check failure - it might just need time
663
- return true;
676
+ // Don't fail deployment for health check failure - it might just need time
677
+ return true;
678
+ } else {
679
+ console.log(` ⚠️ ${errorMsg} (attempt ${attempt}/${maxRetries})`);
680
+ console.log(` ⏳ Retrying in ${retryDelay / 1000} seconds...`);
681
+ await new Promise(resolve => setTimeout(resolve, retryDelay));
682
+ }
683
+ }
664
684
  }
685
+ return true;
665
686
  }
666
687
 
667
688
  /**
@@ -81,10 +81,10 @@ export class ConfirmationEngine {
81
81
  version: '1.0.0',
82
82
  // 4. Author - Default framework author
83
83
  author: 'Clodo Framework',
84
- // 5-7. URLs - Derived from domain
85
- productionUrl: `https://api.${domainName}`,
86
- stagingUrl: `https://staging-api.${domainName}`,
87
- developmentUrl: `https://dev-api.${domainName}`,
84
+ // 5-7. URLs - Derived from domain and service name
85
+ productionUrl: `https://${serviceName}.${domainName}`,
86
+ stagingUrl: `https://${serviceName}-staging.${domainName}`,
87
+ developmentUrl: `https://${serviceName}-dev.${domainName}`,
88
88
  // 8. Features - Based on service type
89
89
  features: this.generateFeaturesForType(serviceType),
90
90
  // 9. Database Name - Cloudflare D1 naming
@@ -36,8 +36,13 @@ import { fileURLToPath } from 'url';
36
36
  import { dirname, join, relative } from 'path';
37
37
  import { existsSync, mkdirSync, writeFileSync } from 'fs';
38
38
  const __dirname = (() => {
39
- const filename = fileURLToPath(import.meta.url);
40
- return dirname(filename);
39
+ try {
40
+ const filename = fileURLToPath(import.meta.url);
41
+ return dirname(filename);
42
+ } catch (error) {
43
+ // Fallback for test environments - use current working directory
44
+ return process.cwd();
45
+ }
41
46
  })();
42
47
  export class GenerationEngine {
43
48
  constructor(options = {}) {
@@ -148,12 +153,14 @@ export class GenerationEngine {
148
153
  const wranglerConfig = serviceInitializer.generateWranglerConfig(coreInputs.serviceName, {
149
154
  type: coreInputs.serviceType,
150
155
  env: coreInputs.environment
151
- }, [{
152
- domain: coreInputs.domainName,
153
- accountId: coreInputs.cloudflareAccountId,
154
- zoneId: coreInputs.cloudflareZoneId,
155
- name: `${coreInputs.serviceName}-${coreInputs.environment}`
156
- }]);
156
+ }, {
157
+ domains: [{
158
+ domain: coreInputs.domainName,
159
+ accountId: coreInputs.cloudflareAccountId,
160
+ zoneId: coreInputs.cloudflareZoneId,
161
+ name: `${coreInputs.serviceName}-${coreInputs.environment}`
162
+ }]
163
+ });
157
164
 
158
165
  // Write wrangler.toml
159
166
  const wranglerPath = join(servicePath, 'wrangler.toml');
@@ -177,12 +184,14 @@ export class GenerationEngine {
177
184
  const domainsContent = serviceInitializer.generateDomainsConfig(coreInputs.serviceName, {
178
185
  type: coreInputs.serviceType,
179
186
  env: coreInputs.environment
180
- }, [{
181
- domain: coreInputs.domainName,
182
- accountId: coreInputs.cloudflareAccountId,
183
- zoneId: coreInputs.cloudflareZoneId,
184
- name: `${coreInputs.serviceName}-${coreInputs.environment}`
185
- }]);
187
+ }, {
188
+ domains: [{
189
+ domain: coreInputs.domainName,
190
+ accountId: coreInputs.cloudflareAccountId,
191
+ zoneId: coreInputs.cloudflareZoneId,
192
+ name: `${coreInputs.serviceName}-${coreInputs.environment}`
193
+ }]
194
+ });
186
195
  const domainsPath = join(servicePath, 'src', 'config', 'domains.js');
187
196
  writeFileSync(domainsPath, domainsContent, 'utf8');
188
197
  files.push(domainsPath);
@@ -15,6 +15,9 @@ import { createInterface } from 'readline';
15
15
  import chalk from 'chalk';
16
16
  import { validateServiceName, validateDomainName } from '../utils/validation.js';
17
17
  import { uiStructuresLoader } from '../utils/ui-structures-loader.js';
18
+
19
+ // Assessment capabilities moved to @tamyla/clodo-orchestration (professional edition)
20
+
18
21
  export class InputCollector {
19
22
  constructor(options = {}) {
20
23
  this.interactive = options.interactive !== false;
@@ -80,6 +83,8 @@ export class InputCollector {
80
83
  source: 'user-provided',
81
84
  required: true
82
85
  };
86
+
87
+ // Assessment moved to professional edition (@tamyla/clodo-orchestration)
83
88
  }
84
89
  }
85
90
 
@@ -158,9 +163,9 @@ export class InputCollector {
158
163
  case 'production-url':
159
164
  return domainName ? `https://api.${domainName}` : '';
160
165
  case 'staging-url':
161
- return domainName ? `https://staging-api.${domainName}` : '';
166
+ return domainName && serviceName ? `https://${serviceName}-staging.${domainName}` : '';
162
167
  case 'development-url':
163
- return domainName ? `https://dev-api.${domainName}` : '';
168
+ return domainName && serviceName ? `https://${serviceName}-dev.${domainName}` : '';
164
169
  case 'service-directory':
165
170
  return serviceName ? `./services/${serviceName}` : '';
166
171
  case 'database-name':
@@ -6,13 +6,18 @@
6
6
  import { readFileSync, writeFileSync, mkdirSync, existsSync, cpSync, readdirSync, statSync } from 'fs';
7
7
  import { join, dirname, resolve } from 'path';
8
8
  import { fileURLToPath } from 'url';
9
- const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = dirname(__filename);
11
- const TEMPLATES_DIR = join(__dirname, '..', '..', 'templates');
12
9
  const SERVICE_TYPES = ['data-service', 'auth-service', 'content-service', 'api-gateway', 'generic'];
13
10
  export class ServiceCreator {
14
11
  constructor(options = {}) {
15
- this.templatesDir = options.templatesDir || TEMPLATES_DIR;
12
+ const templatesDir = (() => {
13
+ try {
14
+ return join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'templates');
15
+ } catch (error) {
16
+ // Fallback for test environments - use current working directory
17
+ return join(process.cwd(), 'templates');
18
+ }
19
+ })();
20
+ this.templatesDir = options.templatesDir || templatesDir;
16
21
  this.serviceTypes = options.serviceTypes || SERVICE_TYPES;
17
22
  }
18
23
 
@@ -6,11 +6,20 @@
6
6
  import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, readdirSync } from 'fs';
7
7
  import { join, dirname, resolve } from 'path';
8
8
  import { fileURLToPath } from 'url';
9
- const FRAMEWORK_ROOT = (() => {
10
- const filename = fileURLToPath(import.meta.url);
11
- const dirname_ = dirname(filename);
12
- return resolve(dirname_, '..', '..');
13
- })();
9
+
10
+ // Get framework root - handle both ES module and CommonJS environments
11
+ const getFrameworkRoot = () => {
12
+ try {
13
+ // Try ES module approach
14
+ const filename = fileURLToPath(import.meta.url);
15
+ const dirname_ = dirname(filename);
16
+ return resolve(dirname_, '..', '..');
17
+ } catch (error) {
18
+ // Fallback for test environments - use current working directory
19
+ return process.cwd();
20
+ }
21
+ };
22
+ const FRAMEWORK_ROOT = getFrameworkRoot();
14
23
  const TEMPLATES_DIR = join(FRAMEWORK_ROOT, 'templates');
15
24
  const SERVICE_TYPES = ['generic', 'data-service', 'auth-service', 'content-service', 'api-gateway'];
16
25
  export class ServiceInitializer {
@@ -4,4 +4,11 @@
4
4
  */
5
5
 
6
6
  export { ServiceCreator, createService } from './ServiceCreator.js';
7
- export { ServiceInitializer, initializeService } from './ServiceInitializer.js';
7
+ export { ServiceInitializer, initializeService } from './ServiceInitializer.js';
8
+
9
+ // Assessment capabilities moved to @tamyla/clodo-orchestration
10
+ // - AssessmentCache
11
+ // - CapabilityAssessmentEngine
12
+ // - ServiceAutoDiscovery
13
+ // - Data-bridge components
14
+ // These are only available in the professional edition (clodo-orchestration)
@@ -16,8 +16,13 @@ import { join, dirname } from 'path';
16
16
  import { execSync } from 'child_process';
17
17
  import { fileURLToPath } from 'url';
18
18
  const __dirname = (() => {
19
- const filename = fileURLToPath(import.meta.url);
20
- return dirname(filename);
19
+ try {
20
+ const filename = fileURLToPath(import.meta.url);
21
+ return dirname(filename);
22
+ } catch (error) {
23
+ // Fallback for test environments - use current working directory
24
+ return process.cwd();
25
+ }
21
26
  })();
22
27
  const SECRET_CONFIGS = {
23
28
  'AUTH_JWT_SECRET': {
@@ -15,9 +15,13 @@
15
15
  import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs';
16
16
  import { resolve, join } from 'path';
17
17
  import { getDirname } from '../esm-helper.js';
18
- import { createLogger } from '../index.js';
19
18
  const __dirname = getDirname(import.meta.url, 'src/utils/config');
20
- const logger = createLogger('UnifiedConfigManager');
19
+
20
+ // Simple inline logger to avoid circular dependency with index.js
21
+ const logger = {
22
+ info: (message, ...args) => console.log(`[UnifiedConfigManager] ${message}`, ...args),
23
+ error: (message, ...args) => console.error(`[UnifiedConfigManager] ${message}`, ...args)
24
+ };
21
25
 
22
26
  /**
23
27
  * UnifiedConfigManager
@@ -1,7 +1,13 @@
1
1
  import { featureManager, COMMON_FEATURES } from '../config/features.js';
2
2
  import { getDomainFromEnv, createEnvironmentConfig } from '../config/domains.js';
3
- import { createLogger } from '../utils/index.js';
4
- const logger = createLogger('WorkerIntegration');
3
+
4
+ // Simple inline logger to avoid circular dependency with index.js
5
+ const logger = {
6
+ info: (message, ...args) => console.log(`[WorkerIntegration] ${message}`, ...args),
7
+ error: (message, ...args) => console.error(`[WorkerIntegration] ${message}`, ...args),
8
+ warn: (message, ...args) => console.warn(`[WorkerIntegration] ${message}`, ...args),
9
+ debug: (message, ...args) => console.debug(`[WorkerIntegration] ${message}`, ...args)
10
+ };
5
11
 
6
12
  /**
7
13
  * Initializes a service with domain and feature context
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamyla/clodo-framework",
3
- "version": "3.0.12",
3
+ "version": "3.0.14",
4
4
  "description": "Reusable framework for Clodo-style software architecture on Cloudflare Workers + D1",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -74,7 +74,7 @@
74
74
  "clean": "rimraf dist",
75
75
  "clean:generated": "rimraf generated",
76
76
  "clean:all": "npm run clean && npm run clean:generated",
77
- "test": "jest --passWithNoTests",
77
+ "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --passWithNoTests",
78
78
  "test:watch": "jest --watch",
79
79
  "test:coverage": "jest --coverage",
80
80
  "lint": "eslint --config ./eslint.config.js src",
@@ -112,7 +112,7 @@
112
112
  "modular"
113
113
  ],
114
114
  "author": "Tamyla",
115
- "license": "GPL-3.0-or-later",
115
+ "license": "MIT",
116
116
  "dependencies": {
117
117
  "@iarna/toml": "^2.2.5",
118
118
  "chalk": "^5.3.0",
@@ -129,6 +129,7 @@
129
129
  "@semantic-release/git": "^10.0.1",
130
130
  "@types/node": "^20.19.19",
131
131
  "babel-plugin-transform-import-meta": "^2.3.3",
132
+ "cross-env": "^10.1.0",
132
133
  "eslint": "^8.54.0",
133
134
  "jest": "^29.7.0",
134
135
  "rimraf": "^5.0.10",
@@ -148,6 +149,7 @@
148
149
  "homepage": "https://clodo-framework.tamyla.com",
149
150
  "release": {
150
151
  "branches": [
152
+ "main",
151
153
  "master"
152
154
  ],
153
155
  "plugins": [
@@ -18,9 +18,9 @@ export const domains = {
18
18
  accountId: process.env.CLOUDFLARE_ACCOUNT_ID || '', // Configure in setup
19
19
  zoneId: process.env.CLOUDFLARE_ZONE_ID || '', // Configure in setup
20
20
  domains: {
21
- production: process.env.PRODUCTION_DOMAIN || 'api.{{SERVICE_NAME}}.com',
22
- staging: process.env.STAGING_DOMAIN || 'staging-api.{{SERVICE_NAME}}.com',
23
- development: process.env.DEVELOPMENT_DOMAIN || 'dev-api.{{SERVICE_NAME}}.com'
21
+ production: process.env.PRODUCTION_DOMAIN || '{{SERVICE_NAME}}.{{DOMAIN_NAME}}',
22
+ staging: process.env.STAGING_DOMAIN || '{{SERVICE_NAME}}-staging.{{DOMAIN_NAME}}',
23
+ development: process.env.DEVELOPMENT_DOMAIN || '{{SERVICE_NAME}}-dev.{{DOMAIN_NAME}}'
24
24
  },
25
25
  services: [
26
26
  '{{SERVICE_NAME}}'