@tamyla/clodo-framework 3.0.11 → 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.
@@ -6,13 +6,10 @@
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
+ this.templatesDir = options.templatesDir || join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'templates');
16
13
  this.serviceTypes = options.serviceTypes || SERVICE_TYPES;
17
14
  }
18
15
 
@@ -17,6 +17,7 @@ import { ValidationHandler } from './handlers/ValidationHandler.js';
17
17
  // Legacy imports for backward compatibility
18
18
  import { ServiceCreator } from './ServiceCreator.js';
19
19
  import { ErrorTracker } from './ErrorTracker.js';
20
+ import { CapabilityAssessmentEngine } from './CapabilityAssessmentEngine.js';
20
21
  import chalk from 'chalk';
21
22
  import fs from 'fs/promises';
22
23
  import path from 'path';
@@ -60,11 +61,17 @@ export class ServiceOrchestrator {
60
61
  // Tier 2: Smart confirmations for 15 derived values
61
62
  const confirmedValues = await this.confirmationHandler.generateAndConfirm(coreInputs);
62
63
 
64
+ // Pre-generation assessment for final validation
65
+ console.log(chalk.yellow('šŸ” Pre-Generation Assessment'));
66
+ console.log(chalk.white('Final validation of deployment readiness...\n'));
67
+ const finalAssessment = await this.runFinalAssessment(coreInputs, confirmedValues);
68
+
63
69
  // Tier 3: Automated generation of 67 components
64
70
  console.log(chalk.yellow('āš™ļø Tier 3: Automated Generation'));
65
71
  console.log(chalk.white('Generating 67 configuration files and service components...\n'));
66
72
  const generationResult = await this.generationHandler.generateService(coreInputs, confirmedValues, {
67
- outputPath: this.outputPath
73
+ outputPath: this.outputPath,
74
+ assessment: finalAssessment // Pass assessment for generation optimization
68
75
  });
69
76
 
70
77
  // Display results
@@ -624,10 +631,271 @@ export class ServiceOrchestrator {
624
631
  return [];
625
632
  }
626
633
 
634
+ /**
635
+ * Run final assessment before generation
636
+ */
637
+ async runFinalAssessment(coreInputs, confirmedValues) {
638
+ try {
639
+ // Import assessment engine dynamically to avoid circular dependencies
640
+ const {
641
+ CapabilityAssessmentEngine
642
+ } = await import('./CapabilityAssessmentEngine.js');
643
+
644
+ // Combine core inputs and confirmed values for assessment
645
+ const assessmentInputs = {
646
+ ...coreInputs,
647
+ ...confirmedValues,
648
+ // Ensure we have the right field names
649
+ serviceName: coreInputs.serviceName || coreInputs['service-name'],
650
+ serviceType: coreInputs.serviceType || coreInputs['service-type'] || 'generic',
651
+ domainName: coreInputs.domainName || coreInputs['domain-name'],
652
+ environment: coreInputs.environment || coreInputs['environment'] || 'development',
653
+ cloudflareToken: coreInputs.cloudflareToken || coreInputs['cloudflare-api-token']
654
+ };
655
+
656
+ // Create assessment engine with caching
657
+ const assessmentEngine = new CapabilityAssessmentEngine(this.outputPath, {
658
+ cacheEnabled: true,
659
+ cache: {
660
+ ttl: 5 * 60 * 1000
661
+ } // 5 minutes for final assessment
662
+ });
663
+
664
+ // Run comprehensive assessment
665
+ const assessment = await assessmentEngine.assessCapabilities(assessmentInputs);
666
+
667
+ // Check for deployment blockers
668
+ const blockers = assessment.gapAnalysis.missing.filter(gap => gap.priority === 'blocked');
669
+ if (blockers.length > 0) {
670
+ console.log(chalk.red('\n🚫 Deployment Blockers Detected:'));
671
+ blockers.forEach(blocker => {
672
+ console.log(chalk.red(` • ${blocker.capability}: ${blocker.reason}`));
673
+ });
674
+ if (this.interactive) {
675
+ const proceed = await this.confirmationHandler.promptHandler.confirm('\nContinue with generation despite blockers? (Service may not deploy successfully)', false);
676
+ if (!proceed) {
677
+ throw new Error('Service creation cancelled due to deployment blockers');
678
+ }
679
+ } else {
680
+ console.log(chalk.yellow('āš ļø Proceeding with generation despite blockers (non-interactive mode)'));
681
+ }
682
+ } else {
683
+ console.log(chalk.green('āœ… No deployment blockers detected'));
684
+ }
685
+ return assessment;
686
+ } catch (error) {
687
+ console.log(chalk.yellow(`āš ļø Final assessment failed: ${error.message}`));
688
+ console.log(chalk.gray('Continuing with service generation...'));
689
+ return null;
690
+ }
691
+ }
692
+
627
693
  /**
628
694
  * Escape special regex characters for safe replacement
629
695
  */
630
696
  escapeRegExp(string) {
631
697
  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
632
698
  }
699
+
700
+ /**
701
+ * Run pre-deploy assessment to validate deployment readiness
702
+ */
703
+ async runPreDeployAssessment(deployConfig) {
704
+ console.log(chalk.yellow('šŸ” Running Pre-Deploy Assessment...'));
705
+ try {
706
+ // Create assessment engine
707
+ const assessmentEngine = new CapabilityAssessmentEngine(this.outputPath, {
708
+ cacheEnabled: true,
709
+ cache: {
710
+ ttl: 10 * 60 * 1000
711
+ } // 10 minutes for deploy assessment
712
+ });
713
+
714
+ // Run assessment
715
+ const assessment = await assessmentEngine.assessCapabilities(deployConfig);
716
+
717
+ // Check for critical gaps
718
+ const criticalGaps = assessment.gapAnalysis.missing.filter(gap => gap.priority === 'blocked');
719
+ const warningGaps = assessment.gapAnalysis.missing.filter(gap => gap.priority === 'warning');
720
+ if (criticalGaps.length > 0) {
721
+ console.log(chalk.red('\n🚫 Critical Deployment Blockers:'));
722
+ criticalGaps.forEach(gap => {
723
+ console.log(chalk.red(` • ${gap.capability}: ${gap.reason}`));
724
+ });
725
+ console.log(chalk.red('\nDeployment blocked due to critical gaps.'));
726
+ return {
727
+ ...assessment,
728
+ deployable: false,
729
+ reason: 'Critical deployment blockers detected'
730
+ };
731
+ }
732
+ if (warningGaps.length > 0) {
733
+ console.log(chalk.yellow('\nāš ļø Deployment Warnings:'));
734
+ warningGaps.forEach(gap => {
735
+ console.log(chalk.yellow(` • ${gap.capability}: ${gap.reason}`));
736
+ });
737
+ console.log(chalk.yellow('Deployment possible but may have issues.'));
738
+ return {
739
+ ...assessment,
740
+ deployable: true,
741
+ warnings: warningGaps.length
742
+ };
743
+ }
744
+ console.log(chalk.green('āœ… Deployment assessment passed - no blockers detected'));
745
+ return {
746
+ ...assessment,
747
+ deployable: true
748
+ };
749
+ } catch (error) {
750
+ console.log(chalk.red(`āŒ Pre-deploy assessment failed: ${error.message}`));
751
+ throw error;
752
+ }
753
+ }
754
+
755
+ /**
756
+ * Deploy service with assessment integration
757
+ */
758
+ async deployService(deployOptions) {
759
+ console.log(chalk.cyan('šŸš€ Deploying Service with Assessment Integration...'));
760
+ const {
761
+ skipAssessment = false,
762
+ forceDeployment = false,
763
+ saveAssessment = false
764
+ } = deployOptions;
765
+ try {
766
+ // Run pre-deploy assessment unless skipped
767
+ if (!skipAssessment) {
768
+ console.log(chalk.yellow('šŸ” Running Pre-Deploy Assessment...'));
769
+ const assessmentResult = await this.runPreDeployAssessment(deployOptions);
770
+
771
+ // Check if deployment should be blocked
772
+ if (!assessmentResult.deployable && !forceDeployment) {
773
+ console.log(chalk.red('Deployment blocked by assessment. Use --force to override.'));
774
+ return {
775
+ success: false,
776
+ reason: 'Assessment blocked deployment',
777
+ assessment: assessmentResult
778
+ };
779
+ }
780
+ if (assessmentResult.warnings && !forceDeployment) {
781
+ const proceed = await this.confirmDeploymentWithAssessment(assessmentResult);
782
+ if (!proceed) {
783
+ console.log(chalk.yellow('Deployment cancelled by user.'));
784
+ return {
785
+ success: false,
786
+ reason: 'User cancelled deployment',
787
+ assessment: assessmentResult
788
+ };
789
+ }
790
+ }
791
+
792
+ // Save assessment results if requested
793
+ if (saveAssessment) {
794
+ await this.saveAssessmentResults(assessmentResult, deployOptions.servicePath || this.outputPath);
795
+ }
796
+ deployOptions.assessment = assessmentResult;
797
+ }
798
+
799
+ // Execute deployment
800
+ console.log(chalk.yellow('āš™ļø Executing Deployment...'));
801
+ const deployResult = await this.executeDeployment(deployOptions);
802
+ console.log(chalk.green('āœ… Deployment completed successfully'));
803
+ return {
804
+ success: true,
805
+ deployment: deployResult
806
+ };
807
+ } catch (error) {
808
+ console.log(chalk.red(`āŒ Deployment failed: ${error.message}`));
809
+ throw error;
810
+ }
811
+ }
812
+
813
+ /**
814
+ * Execute the actual deployment process
815
+ */
816
+ async executeDeployment(deployOptions) {
817
+ // This would integrate with actual deployment logic
818
+ // For now, return a mock successful result
819
+ console.log(chalk.gray(' • Validating deployment configuration...'));
820
+ console.log(chalk.gray(' • Uploading worker script...'));
821
+ console.log(chalk.gray(' • Configuring routes...'));
822
+ console.log(chalk.gray(' • Deployment completed'));
823
+ return {
824
+ workerId: 'mock-worker-id',
825
+ deploymentId: 'mock-deployment-id',
826
+ urls: {
827
+ worker: `https://${deployOptions.workerName || 'worker'}.example.com`,
828
+ api: `https://${deployOptions.serviceName || 'service'}.example.com`
829
+ },
830
+ timestamp: new Date().toISOString()
831
+ };
832
+ }
833
+
834
+ /**
835
+ * Confirm deployment with assessment warnings
836
+ */
837
+ async confirmDeploymentWithAssessment(assessmentResult) {
838
+ if (!this.interactive) {
839
+ return true; // Auto-confirm in non-interactive mode
840
+ }
841
+ if (!assessmentResult || !assessmentResult.gapAnalysis) {
842
+ return true; // Auto-confirm if no assessment result
843
+ }
844
+ const warnings = (assessmentResult.gapAnalysis.missing || []).filter(gap => gap.priority === 'warning');
845
+ const recommendations = (assessmentResult.recommendations || []).slice(0, 3);
846
+ console.log(chalk.yellow('\nāš ļø Assessment detected warnings:'));
847
+ warnings.forEach(warning => {
848
+ console.log(chalk.yellow(` • ${warning.capability}: ${warning.reason}`));
849
+ });
850
+ if (recommendations.length > 0) {
851
+ console.log(chalk.blue('\nšŸ’” Recommendations:'));
852
+ recommendations.forEach(rec => {
853
+ console.log(chalk.blue(` • ${rec.action}`));
854
+ });
855
+ }
856
+ const proceed = await this.confirmationHandler.promptHandler.confirm('\nContinue with deployment despite warnings?', true);
857
+ return proceed;
858
+ }
859
+
860
+ /**
861
+ * Save assessment results for post-deploy reference
862
+ */
863
+ async saveAssessmentResults(assessment, servicePath) {
864
+ const assessmentFile = path.join(servicePath, '.clodo-assessment.json');
865
+ try {
866
+ await fs.writeFile(assessmentFile, JSON.stringify({
867
+ ...assessment,
868
+ savedAt: new Date().toISOString(),
869
+ version: '1.0'
870
+ }, null, 2));
871
+ console.log(chalk.gray(` • Assessment results saved to ${assessmentFile}`));
872
+ } catch (error) {
873
+ console.log(chalk.yellow(`āš ļø Failed to save assessment results: ${error.message}`));
874
+ }
875
+ }
876
+
877
+ /**
878
+ * Load previous assessment results
879
+ */
880
+ async loadPreviousAssessment(servicePath) {
881
+ const assessmentFile = path.join(servicePath, '.clodo-assessment.json');
882
+ try {
883
+ const data = await fs.readFile(assessmentFile, 'utf8');
884
+ const assessment = JSON.parse(data);
885
+ console.log(chalk.gray(` • Loaded previous assessment from ${assessmentFile}`));
886
+ return assessment;
887
+ } catch (error) {
888
+ console.log(chalk.yellow(`āš ļø No previous assessment found: ${error.message}`));
889
+ return null;
890
+ }
891
+ }
892
+
893
+ /**
894
+ * Create readline interface for interactive prompts
895
+ */
896
+ createReadlineInterface() {
897
+ // This would create a readline interface for interactive prompts
898
+ // For now, return null as this is handled by the confirmation handler
899
+ return null;
900
+ }
633
901
  }
@@ -4,4 +4,7 @@
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
+ export { AssessmentCache } from './AssessmentCache.js';
9
+ export { CapabilityAssessmentEngine } from './CapabilityAssessmentEngine.js';
10
+ export { ServiceAutoDiscovery } from './ServiceAutoDiscovery.js';
@@ -34,7 +34,20 @@ export class CloudflareAPI {
34
34
  const data = await response.json();
35
35
  if (!response.ok) {
36
36
  const errorMsg = data.errors?.[0]?.message || 'Unknown error';
37
- throw new Error(`Cloudflare API error: ${errorMsg} (${response.status})`);
37
+ const statusCode = response.status;
38
+
39
+ // Provide specific guidance for common authentication/permission errors
40
+ if (statusCode === 401) {
41
+ throw new Error(`Cloudflare API authentication failed (401). Your API token may be invalid or expired. Please check your token at https://dash.cloudflare.com/profile/api-tokens`);
42
+ }
43
+ if (statusCode === 403) {
44
+ // Check if this is a D1-related endpoint to provide specific guidance
45
+ if (endpoint.includes('/d1/')) {
46
+ throw new Error(`Cloudflare API permission denied (403). Your API token lacks D1 database permissions. Required permissions: 'Cloudflare D1:Edit'. Update your token at https://dash.cloudflare.com/profile/api-tokens`);
47
+ }
48
+ throw new Error(`Cloudflare API permission denied (403). Your API token lacks required permissions for this operation. Please check your token permissions at https://dash.cloudflare.com/profile/api-tokens`);
49
+ }
50
+ throw new Error(`Cloudflare API error: ${errorMsg} (${statusCode})`);
38
51
  }
39
52
  if (!data.success) {
40
53
  const errorMsg = data.errors?.[0]?.message || 'Request failed';
@@ -65,6 +78,33 @@ export class CloudflareAPI {
65
78
  }
66
79
  }
67
80
 
81
+ /**
82
+ * Check if API token has D1 database permissions
83
+ * @returns {Promise<Object>} Permission check result
84
+ */
85
+ async checkD1Permissions() {
86
+ try {
87
+ // Try to list D1 databases - this will fail if no D1 permissions
88
+ // We use a dummy account ID that should fail safely if permissions are missing
89
+ await this.request('/accounts/dummy/d1/database');
90
+ return {
91
+ hasPermission: true
92
+ };
93
+ } catch (error) {
94
+ if (error.message.includes('403') || error.message.includes('permission denied')) {
95
+ return {
96
+ hasPermission: false,
97
+ error: 'API token lacks D1 database permissions. Required: Cloudflare D1:Edit'
98
+ };
99
+ }
100
+ // If it's a different error (like invalid account), assume permissions are OK
101
+ // The actual permission check happens during real operations
102
+ return {
103
+ hasPermission: true
104
+ };
105
+ }
106
+ }
107
+
68
108
  /**
69
109
  * List all zones (domains) accessible with this API token
70
110
  * @param {Object} options - Query options
@@ -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
@@ -19,11 +19,13 @@ export class WranglerConfigManager {
19
19
  this.projectRoot = dirname(options);
20
20
  this.dryRun = false;
21
21
  this.verbose = false;
22
+ this.accountId = null;
22
23
  } else {
23
24
  this.projectRoot = options.projectRoot || process.cwd();
24
25
  this.configPath = options.configPath || join(this.projectRoot, 'wrangler.toml');
25
26
  this.dryRun = options.dryRun || false;
26
27
  this.verbose = options.verbose || false;
28
+ this.accountId = options.accountId || null;
27
29
  }
28
30
  }
29
31
 
@@ -52,6 +54,19 @@ export class WranglerConfigManager {
52
54
  }
53
55
  }
54
56
 
57
+ /**
58
+ * Check if wrangler.toml file exists
59
+ * @returns {Promise<boolean>} True if file exists, false otherwise
60
+ */
61
+ async exists() {
62
+ try {
63
+ await access(this.configPath, constants.F_OK);
64
+ return true;
65
+ } catch (error) {
66
+ return false;
67
+ }
68
+ }
69
+
55
70
  /**
56
71
  * Write configuration back to wrangler.toml
57
72
  * @param {Object} config - Configuration object to write
@@ -82,16 +97,26 @@ export class WranglerConfigManager {
82
97
  }
83
98
 
84
99
  /**
85
- * Check if wrangler.toml exists
86
- * @returns {Promise<boolean>}
100
+ * Set account_id in wrangler.toml
101
+ * @param {string} accountId - Cloudflare account ID
102
+ * @returns {Promise<boolean>} True if account_id was set
87
103
  */
88
- async exists() {
89
- try {
90
- await access(this.configPath, constants.F_OK);
91
- return true;
92
- } catch {
104
+ async setAccountId(accountId) {
105
+ if (!accountId) {
93
106
  return false;
94
107
  }
108
+ const config = await this.readConfig();
109
+ if (config.account_id === accountId) {
110
+ if (this.verbose) {
111
+ console.log(` āœ“ account_id already set to ${accountId}`);
112
+ }
113
+ return false;
114
+ }
115
+ console.log(` šŸ“ Setting account_id to ${accountId} in wrangler.toml`);
116
+ config.account_id = accountId;
117
+ await this.writeConfig(config);
118
+ console.log(` āœ… account_id updated in wrangler.toml`);
119
+ return true;
95
120
  }
96
121
 
97
122
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamyla/clodo-framework",
3
- "version": "3.0.11",
3
+ "version": "3.0.13",
4
4
  "description": "Reusable framework for Clodo-style software architecture on Cloudflare Workers + D1",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -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}}'