@tamyla/clodo-framework 3.0.13 ā 3.0.15
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 +19 -2
- package/bin/clodo-service.js +94 -1
- package/bin/shared/security/secret-generator.js +7 -2
- package/dist/config/customers.js +8 -2
- package/dist/config/domains.js +8 -2
- package/dist/config/features.js +7 -2
- package/dist/index.js +8 -0
- package/dist/service-management/GenerationEngine.js +23 -14
- package/dist/service-management/InputCollector.js +4 -97
- package/dist/service-management/ServiceCreator.js +9 -1
- package/dist/service-management/ServiceInitializer.js +14 -5
- package/dist/service-management/ServiceOrchestrator.js +1 -269
- package/dist/service-management/index.js +7 -3
- package/dist/shared/security/secret-generator.js +7 -2
- package/dist/worker/integration.js +8 -2
- package/package.json +5 -3
- package/dist/service-management/AssessmentCache.js +0 -303
- package/dist/service-management/CapabilityAssessmentEngine.js +0 -902
- package/dist/service-management/ServiceAutoDiscovery.js +0 -745
|
@@ -17,7 +17,6 @@ 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';
|
|
21
20
|
import chalk from 'chalk';
|
|
22
21
|
import fs from 'fs/promises';
|
|
23
22
|
import path from 'path';
|
|
@@ -61,17 +60,11 @@ export class ServiceOrchestrator {
|
|
|
61
60
|
// Tier 2: Smart confirmations for 15 derived values
|
|
62
61
|
const confirmedValues = await this.confirmationHandler.generateAndConfirm(coreInputs);
|
|
63
62
|
|
|
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
|
-
|
|
69
63
|
// Tier 3: Automated generation of 67 components
|
|
70
64
|
console.log(chalk.yellow('āļø Tier 3: Automated Generation'));
|
|
71
65
|
console.log(chalk.white('Generating 67 configuration files and service components...\n'));
|
|
72
66
|
const generationResult = await this.generationHandler.generateService(coreInputs, confirmedValues, {
|
|
73
|
-
outputPath: this.outputPath
|
|
74
|
-
assessment: finalAssessment // Pass assessment for generation optimization
|
|
67
|
+
outputPath: this.outputPath
|
|
75
68
|
});
|
|
76
69
|
|
|
77
70
|
// Display results
|
|
@@ -631,271 +624,10 @@ export class ServiceOrchestrator {
|
|
|
631
624
|
return [];
|
|
632
625
|
}
|
|
633
626
|
|
|
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
|
-
|
|
693
627
|
/**
|
|
694
628
|
* Escape special regex characters for safe replacement
|
|
695
629
|
*/
|
|
696
630
|
escapeRegExp(string) {
|
|
697
631
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
698
632
|
}
|
|
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
|
-
}
|
|
901
633
|
}
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
export { ServiceCreator, createService } from './ServiceCreator.js';
|
|
7
7
|
export { ServiceInitializer, initializeService } from './ServiceInitializer.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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': {
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { featureManager, COMMON_FEATURES } from '../config/features.js';
|
|
2
2
|
import { getDomainFromEnv, createEnvironmentConfig } from '../config/domains.js';
|
|
3
|
-
|
|
4
|
-
|
|
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.
|
|
3
|
+
"version": "3.0.15",
|
|
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": "
|
|
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": [
|
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Assessment Cache System
|
|
3
|
-
*
|
|
4
|
-
* Caches assessment results to avoid redundant analysis of project artifacts.
|
|
5
|
-
* Provides intelligent cache invalidation based on file changes and time-based expiration.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import crypto from 'crypto';
|
|
9
|
-
import fs from 'fs/promises';
|
|
10
|
-
import path from 'path';
|
|
11
|
-
import { existsSync } from 'fs';
|
|
12
|
-
export class AssessmentCache {
|
|
13
|
-
constructor(options = {}) {
|
|
14
|
-
this.cacheDir = options.cacheDir || './.clodo-cache/assessment';
|
|
15
|
-
this.ttl = options.ttl || 5 * 60 * 1000; // 5 minutes default
|
|
16
|
-
this.maxEntries = options.maxEntries || 50;
|
|
17
|
-
this.enableDiskCache = options.enableDiskCache !== false;
|
|
18
|
-
this.memoryCache = new Map();
|
|
19
|
-
this.initialized = false;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Initialize the cache system
|
|
24
|
-
*/
|
|
25
|
-
async initialize() {
|
|
26
|
-
if (this.initialized) return;
|
|
27
|
-
if (this.enableDiskCache) {
|
|
28
|
-
await this.ensureCacheDirectory();
|
|
29
|
-
await this.loadFromDisk();
|
|
30
|
-
}
|
|
31
|
-
this.initialized = true;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Generate cache key from project state and inputs
|
|
36
|
-
*/
|
|
37
|
-
async generateCacheKey(projectPath, inputs = {}) {
|
|
38
|
-
const keyData = {
|
|
39
|
-
projectPath: path.resolve(projectPath),
|
|
40
|
-
inputs: this.sanitizeInputs(inputs),
|
|
41
|
-
projectFiles: await this.getProjectFileHashes(projectPath)
|
|
42
|
-
};
|
|
43
|
-
const keyString = JSON.stringify(keyData, Object.keys(keyData).sort());
|
|
44
|
-
return crypto.createHash('sha256').update(keyString).digest('hex');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Sanitize inputs for cache key generation (remove sensitive data)
|
|
49
|
-
*/
|
|
50
|
-
sanitizeInputs(inputs) {
|
|
51
|
-
const sanitized = {
|
|
52
|
-
...inputs
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
// Remove sensitive fields but keep their presence for cache invalidation
|
|
56
|
-
const sensitiveFields = ['apiToken', 'cloudflareToken', 'token', 'secret', 'password'];
|
|
57
|
-
sensitiveFields.forEach(field => {
|
|
58
|
-
if (sanitized[field]) {
|
|
59
|
-
sanitized[field] = 'present'; // Just mark presence, not value
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
return sanitized;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Get hashes of relevant project files for cache invalidation
|
|
67
|
-
*/
|
|
68
|
-
async getProjectFileHashes(projectPath) {
|
|
69
|
-
const relevantFiles = ['package.json', 'wrangler.toml', 'src/index.js', 'src/worker.js', 'dist/index.js'];
|
|
70
|
-
const hashes = {};
|
|
71
|
-
for (const file of relevantFiles) {
|
|
72
|
-
const filePath = path.join(projectPath, file);
|
|
73
|
-
try {
|
|
74
|
-
const stats = await fs.stat(filePath);
|
|
75
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
76
|
-
hashes[file] = {
|
|
77
|
-
mtime: stats.mtime.getTime(),
|
|
78
|
-
size: stats.size,
|
|
79
|
-
hash: crypto.createHash('md5').update(content).digest('hex').substring(0, 8)
|
|
80
|
-
};
|
|
81
|
-
} catch (error) {
|
|
82
|
-
// File doesn't exist, that's fine
|
|
83
|
-
hashes[file] = null;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return hashes;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Get cached assessment result
|
|
91
|
-
*/
|
|
92
|
-
async get(cacheKey) {
|
|
93
|
-
await this.initialize();
|
|
94
|
-
|
|
95
|
-
// Check memory cache first
|
|
96
|
-
const memoryEntry = this.memoryCache.get(cacheKey);
|
|
97
|
-
if (memoryEntry && !this.isExpired(memoryEntry)) {
|
|
98
|
-
return memoryEntry.data;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Check disk cache if enabled
|
|
102
|
-
if (this.enableDiskCache) {
|
|
103
|
-
const diskEntry = await this.loadFromDiskCache(cacheKey);
|
|
104
|
-
if (diskEntry && !this.isExpired(diskEntry)) {
|
|
105
|
-
// Restore to memory cache
|
|
106
|
-
this.memoryCache.set(cacheKey, diskEntry);
|
|
107
|
-
return diskEntry.data;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Store assessment result in cache
|
|
115
|
-
*/
|
|
116
|
-
async set(cacheKey, data) {
|
|
117
|
-
await this.initialize();
|
|
118
|
-
const entry = {
|
|
119
|
-
data,
|
|
120
|
-
timestamp: Date.now(),
|
|
121
|
-
key: cacheKey
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
// Store in memory
|
|
125
|
-
this.memoryCache.set(cacheKey, entry);
|
|
126
|
-
|
|
127
|
-
// Store on disk if enabled
|
|
128
|
-
if (this.enableDiskCache) {
|
|
129
|
-
await this.saveToDiskCache(cacheKey, entry);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Maintain cache size limits
|
|
133
|
-
await this.cleanup();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Check if cache entry is expired
|
|
138
|
-
*/
|
|
139
|
-
isExpired(entry) {
|
|
140
|
-
return Date.now() - entry.timestamp > this.ttl;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Clear expired entries and maintain size limits
|
|
145
|
-
*/
|
|
146
|
-
async cleanup() {
|
|
147
|
-
const now = Date.now();
|
|
148
|
-
const validEntries = new Map();
|
|
149
|
-
|
|
150
|
-
// Clean memory cache
|
|
151
|
-
for (const [key, entry] of this.memoryCache) {
|
|
152
|
-
if (!this.isExpired(entry)) {
|
|
153
|
-
validEntries.set(key, entry);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// If still too many entries, remove oldest
|
|
158
|
-
if (validEntries.size > this.maxEntries) {
|
|
159
|
-
const sortedEntries = Array.from(validEntries.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
160
|
-
const toKeep = sortedEntries.slice(-this.maxEntries);
|
|
161
|
-
validEntries.clear();
|
|
162
|
-
toKeep.forEach(([key, entry]) => validEntries.set(key, entry));
|
|
163
|
-
}
|
|
164
|
-
this.memoryCache = validEntries;
|
|
165
|
-
|
|
166
|
-
// Clean disk cache
|
|
167
|
-
if (this.enableDiskCache) {
|
|
168
|
-
await this.cleanupDiskCache();
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Clear all cache entries
|
|
174
|
-
*/
|
|
175
|
-
async clear() {
|
|
176
|
-
this.memoryCache.clear();
|
|
177
|
-
if (this.enableDiskCache) {
|
|
178
|
-
await this.clearDiskCache();
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Get cache statistics
|
|
184
|
-
*/
|
|
185
|
-
async getStats() {
|
|
186
|
-
const now = Date.now();
|
|
187
|
-
const memoryEntries = Array.from(this.memoryCache.values());
|
|
188
|
-
return {
|
|
189
|
-
memory: {
|
|
190
|
-
total: memoryEntries.length,
|
|
191
|
-
valid: memoryEntries.filter(entry => !this.isExpired(entry)).length,
|
|
192
|
-
expired: memoryEntries.filter(entry => this.isExpired(entry)).length
|
|
193
|
-
},
|
|
194
|
-
disk: this.enableDiskCache ? await this.getDiskStats() : null,
|
|
195
|
-
ttl: this.ttl,
|
|
196
|
-
maxEntries: this.maxEntries
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Disk cache implementation
|
|
201
|
-
async ensureCacheDirectory() {
|
|
202
|
-
try {
|
|
203
|
-
await fs.mkdir(this.cacheDir, {
|
|
204
|
-
recursive: true
|
|
205
|
-
});
|
|
206
|
-
} catch (error) {
|
|
207
|
-
// Directory might already exist, ignore
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
getCacheFilePath(key) {
|
|
211
|
-
return path.join(this.cacheDir, `${key}.json`);
|
|
212
|
-
}
|
|
213
|
-
async saveToDiskCache(key, entry) {
|
|
214
|
-
try {
|
|
215
|
-
const filePath = this.getCacheFilePath(key);
|
|
216
|
-
await fs.writeFile(filePath, JSON.stringify(entry, null, 2));
|
|
217
|
-
} catch (error) {
|
|
218
|
-
// Disk cache failure shouldn't break functionality
|
|
219
|
-
console.warn('Failed to save to disk cache:', error.message);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
async loadFromDiskCache(key) {
|
|
223
|
-
try {
|
|
224
|
-
const filePath = this.getCacheFilePath(key);
|
|
225
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
226
|
-
return JSON.parse(content);
|
|
227
|
-
} catch (error) {
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
async loadFromDisk() {
|
|
232
|
-
try {
|
|
233
|
-
const files = await fs.readdir(this.cacheDir);
|
|
234
|
-
const cacheFiles = files.filter(file => file.endsWith('.json'));
|
|
235
|
-
for (const file of cacheFiles) {
|
|
236
|
-
const key = file.replace('.json', '');
|
|
237
|
-
const entry = await this.loadFromDiskCache(key);
|
|
238
|
-
if (entry && !this.isExpired(entry)) {
|
|
239
|
-
this.memoryCache.set(key, entry);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
} catch (error) {
|
|
243
|
-
// Disk cache loading failure is not critical
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
async cleanupDiskCache() {
|
|
247
|
-
try {
|
|
248
|
-
const files = await fs.readdir(this.cacheDir);
|
|
249
|
-
const cacheFiles = files.filter(file => file.endsWith('.json'));
|
|
250
|
-
for (const file of cacheFiles) {
|
|
251
|
-
const key = file.replace('.json', '');
|
|
252
|
-
const entry = await this.loadFromDiskCache(key);
|
|
253
|
-
if (!entry || this.isExpired(entry)) {
|
|
254
|
-
await fs.unlink(path.join(this.cacheDir, file));
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
} catch (error) {
|
|
258
|
-
// Cleanup failure is not critical
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
async clearDiskCache() {
|
|
262
|
-
try {
|
|
263
|
-
const files = await fs.readdir(this.cacheDir);
|
|
264
|
-
for (const file of files) {
|
|
265
|
-
if (file.endsWith('.json')) {
|
|
266
|
-
await fs.unlink(path.join(this.cacheDir, file));
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
} catch (error) {
|
|
270
|
-
// Clear failure is not critical
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
async getDiskStats() {
|
|
274
|
-
try {
|
|
275
|
-
const files = await fs.readdir(this.cacheDir);
|
|
276
|
-
const cacheFiles = files.filter(file => file.endsWith('.json'));
|
|
277
|
-
let valid = 0;
|
|
278
|
-
let expired = 0;
|
|
279
|
-
for (const file of cacheFiles) {
|
|
280
|
-
const key = file.replace('.json', '');
|
|
281
|
-
const entry = await this.loadFromDiskCache(key);
|
|
282
|
-
if (entry) {
|
|
283
|
-
if (this.isExpired(entry)) {
|
|
284
|
-
expired++;
|
|
285
|
-
} else {
|
|
286
|
-
valid++;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
return {
|
|
291
|
-
total: cacheFiles.length,
|
|
292
|
-
valid,
|
|
293
|
-
expired
|
|
294
|
-
};
|
|
295
|
-
} catch (error) {
|
|
296
|
-
return {
|
|
297
|
-
total: 0,
|
|
298
|
-
valid: 0,
|
|
299
|
-
expired: 0
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|