@tamyla/clodo-framework 4.0.14 → 4.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 +7 -0
- package/dist/index.js +5 -0
- package/dist/monitoring/HealthChecker.js +286 -0
- package/dist/orchestration/modules/StateManager.js +11 -3
- package/dist/security/ConfigurationValidator.js +216 -0
- package/dist/service-management/generators/BaseGenerator.js +31 -6
- package/dist/service-management/generators/utils/FileWriter.js +13 -1
- package/dist/services/ServiceClient.js +239 -0
- package/dist/utils/CircuitBreaker.js +192 -0
- package/dist/utils/EnvironmentValidator.js +147 -0
- package/dist/utils/TemplateRuntime.js +291 -0
- package/dist/utils/deployment/secret-generator.js +37 -26
- package/dist/version/FrameworkInfo.js +104 -0
- package/dist/worker/integration.js +13 -1
- package/package.json +5 -1
- package/scripts/check-templates.js +105 -0
- package/scripts/debug-generate-worker.js +58 -0
- package/scripts/migration/migrate-middleware-legacy-to-contract.js +18 -1
- package/scripts/verify-persistence-config.js +45 -0
- package/templates/generic/src/config/.config-is-sample +1 -0
- package/templates/static-site/.env.example +1 -1
- package/templates/static-site/src/config/.config-is-sample +1 -0
- package/templates/static-site/src/config/domains.js +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [4.0.15](https://github.com/tamylaa/clodo-framework/compare/v4.0.14...v4.0.15) (2026-01-31)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **lint:** remove unnecessary escape in semver regex ([72751bd](https://github.com/tamylaa/clodo-framework/commit/72751bdf9a98b2f6b1f97706b175ea31f3aa0df1))
|
|
7
|
+
|
|
1
8
|
## [4.0.14](https://github.com/tamylaa/clodo-framework/compare/v4.0.13...v4.0.14) (2026-01-19)
|
|
2
9
|
|
|
3
10
|
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ export * from './schema/SchemaManager.js';
|
|
|
21
21
|
export * from './modules/ModuleManager.js';
|
|
22
22
|
export * from './routing/EnhancedRouter.js';
|
|
23
23
|
export * from './handlers/GenericRouteHandler.js';
|
|
24
|
+
export { ServiceClient } from './services/ServiceClient.js';
|
|
24
25
|
|
|
25
26
|
// Deployment components
|
|
26
27
|
export { DeploymentValidator } from './deployment/validator.js';
|
|
@@ -32,6 +33,7 @@ export { DeploymentAuditor } from './deployment/auditor.js';
|
|
|
32
33
|
export { SecurityCLI } from './security/SecurityCLI.js';
|
|
33
34
|
export { ConfigurationValidator } from './security/ConfigurationValidator.js';
|
|
34
35
|
export { SecretGenerator } from './security/SecretGenerator.js';
|
|
36
|
+
export { EnvironmentValidator } from './utils/EnvironmentValidator.js';
|
|
35
37
|
|
|
36
38
|
// Service management components
|
|
37
39
|
export { ServiceCreator } from './service-management/ServiceCreator.js';
|
|
@@ -48,6 +50,9 @@ export { verifyWorkerDeployment, healthCheckWithBackoff, checkHealth } from './l
|
|
|
48
50
|
export { classifyError, getRecoverySuggestions } from './lib/shared/error-handling/error-classifier.js';
|
|
49
51
|
|
|
50
52
|
// Framework version info
|
|
53
|
+
export { FrameworkInfo } from './version/FrameworkInfo.js';
|
|
54
|
+
export { TemplateRuntime } from './utils/TemplateRuntime.js';
|
|
55
|
+
export { HealthChecker } from './monitoring/HealthChecker.js';
|
|
51
56
|
export const FRAMEWORK_VERSION = '1.0.0';
|
|
52
57
|
export const FRAMEWORK_NAME = 'Clodo Framework';
|
|
53
58
|
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime Health Checker
|
|
3
|
+
* Provides health validation and monitoring for services
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Runtime Health Checker for service monitoring
|
|
8
|
+
*/
|
|
9
|
+
export class HealthChecker {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.options = {
|
|
12
|
+
timeout: options.timeout || 5000,
|
|
13
|
+
interval: options.interval || 30000,
|
|
14
|
+
retries: options.retries || 2,
|
|
15
|
+
...options
|
|
16
|
+
};
|
|
17
|
+
this.checks = new Map();
|
|
18
|
+
this.lastCheck = null;
|
|
19
|
+
this.overallStatus = 'unknown';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Add a health check
|
|
24
|
+
* @param {string} name - Check name
|
|
25
|
+
* @param {Function} checkFunction - Async function that returns health status
|
|
26
|
+
* @param {Object} options - Check options
|
|
27
|
+
*/
|
|
28
|
+
addCheck(name, checkFunction, options = {}) {
|
|
29
|
+
this.checks.set(name, {
|
|
30
|
+
name,
|
|
31
|
+
checkFunction,
|
|
32
|
+
options: {
|
|
33
|
+
timeout: options.timeout || this.options.timeout,
|
|
34
|
+
critical: options.critical !== false,
|
|
35
|
+
...options
|
|
36
|
+
},
|
|
37
|
+
lastResult: null,
|
|
38
|
+
lastCheck: null
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Remove a health check
|
|
44
|
+
* @param {string} name - Check name
|
|
45
|
+
*/
|
|
46
|
+
removeCheck(name) {
|
|
47
|
+
this.checks.delete(name);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Run all health checks
|
|
52
|
+
* @returns {Promise<Object>} Health status
|
|
53
|
+
*/
|
|
54
|
+
async runChecks() {
|
|
55
|
+
const results = {
|
|
56
|
+
status: 'healthy',
|
|
57
|
+
timestamp: new Date().toISOString(),
|
|
58
|
+
checks: {},
|
|
59
|
+
summary: {
|
|
60
|
+
total: this.checks.size,
|
|
61
|
+
healthy: 0,
|
|
62
|
+
unhealthy: 0,
|
|
63
|
+
unknown: 0
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const checkPromises = Array.from(this.checks.entries()).map(async ([name, check]) => {
|
|
67
|
+
try {
|
|
68
|
+
const startTime = Date.now();
|
|
69
|
+
const result = await this.runSingleCheck(check);
|
|
70
|
+
const duration = Date.now() - startTime;
|
|
71
|
+
results.checks[name] = {
|
|
72
|
+
status: result.status,
|
|
73
|
+
duration,
|
|
74
|
+
timestamp: new Date().toISOString(),
|
|
75
|
+
...result.details
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Update summary
|
|
79
|
+
results.summary[result.status === 'healthy' ? 'healthy' : 'unhealthy']++;
|
|
80
|
+
|
|
81
|
+
// Update overall status
|
|
82
|
+
if (result.status !== 'healthy' && check.options.critical) {
|
|
83
|
+
results.status = 'unhealthy';
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
results.checks[name] = {
|
|
87
|
+
status: 'unhealthy',
|
|
88
|
+
error: error.message,
|
|
89
|
+
timestamp: new Date().toISOString()
|
|
90
|
+
};
|
|
91
|
+
results.summary.unhealthy++;
|
|
92
|
+
if (check.options.critical) {
|
|
93
|
+
results.status = 'unhealthy';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
await Promise.all(checkPromises);
|
|
98
|
+
this.lastCheck = results;
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Run a single health check with timeout and retries
|
|
104
|
+
* @param {Object} check - Check configuration
|
|
105
|
+
* @returns {Promise<Object>} Check result
|
|
106
|
+
*/
|
|
107
|
+
async runSingleCheck(check) {
|
|
108
|
+
let lastError;
|
|
109
|
+
for (let attempt = 0; attempt <= this.options.retries; attempt++) {
|
|
110
|
+
try {
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
const timeoutId = setTimeout(() => controller.abort(), check.options.timeout);
|
|
113
|
+
const result = await check.checkFunction({
|
|
114
|
+
signal: controller.signal
|
|
115
|
+
});
|
|
116
|
+
clearTimeout(timeoutId);
|
|
117
|
+
check.lastResult = result;
|
|
118
|
+
check.lastCheck = new Date();
|
|
119
|
+
return {
|
|
120
|
+
status: 'healthy',
|
|
121
|
+
details: result
|
|
122
|
+
};
|
|
123
|
+
} catch (error) {
|
|
124
|
+
lastError = error;
|
|
125
|
+
if (attempt === this.options.retries) {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Wait before retry
|
|
130
|
+
await this.sleep(1000 * attempt);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
throw lastError;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get the last health check results
|
|
138
|
+
* @returns {Object|null} Last check results
|
|
139
|
+
*/
|
|
140
|
+
getLastResults() {
|
|
141
|
+
return this.lastCheck;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get overall health status
|
|
146
|
+
* @returns {string} Health status
|
|
147
|
+
*/
|
|
148
|
+
getStatus() {
|
|
149
|
+
return this.lastCheck?.status || 'unknown';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check if service is healthy
|
|
154
|
+
* @returns {boolean} True if healthy
|
|
155
|
+
*/
|
|
156
|
+
isHealthy() {
|
|
157
|
+
return this.getStatus() === 'healthy';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Add common health checks
|
|
162
|
+
* @param {Object} env - Cloudflare Worker environment
|
|
163
|
+
*/
|
|
164
|
+
addCommonChecks(env) {
|
|
165
|
+
// Database connectivity check
|
|
166
|
+
if (env.DATABASE) {
|
|
167
|
+
this.addCheck('database', async () => {
|
|
168
|
+
await env.DATABASE.prepare('SELECT 1').first();
|
|
169
|
+
return {
|
|
170
|
+
message: 'Database connection successful'
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// KV connectivity check
|
|
176
|
+
if (env.KV_CACHE) {
|
|
177
|
+
this.addCheck('kv', async () => {
|
|
178
|
+
await env.KV_CACHE.put('health-check', 'ok', {
|
|
179
|
+
expirationTtl: 60
|
|
180
|
+
});
|
|
181
|
+
return {
|
|
182
|
+
message: 'KV connection successful'
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// R2 connectivity check
|
|
188
|
+
if (env.R2_STORAGE) {
|
|
189
|
+
this.addCheck('r2', async () => {
|
|
190
|
+
// Try to list objects (will work if bucket exists and is accessible)
|
|
191
|
+
try {
|
|
192
|
+
const objects = await env.R2_STORAGE.list({
|
|
193
|
+
limit: 1
|
|
194
|
+
});
|
|
195
|
+
return {
|
|
196
|
+
message: 'R2 connection successful'
|
|
197
|
+
};
|
|
198
|
+
} catch (error) {
|
|
199
|
+
// If list fails, try a simple head request on a test object
|
|
200
|
+
await env.R2_STORAGE.head('health-check-test');
|
|
201
|
+
return {
|
|
202
|
+
message: 'R2 connection successful'
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Memory usage check
|
|
209
|
+
this.addCheck('memory', async () => {
|
|
210
|
+
// In Cloudflare Workers, memory is managed automatically
|
|
211
|
+
// This is just a placeholder for custom memory checks
|
|
212
|
+
return {
|
|
213
|
+
message: 'Memory check passed'
|
|
214
|
+
};
|
|
215
|
+
}, {
|
|
216
|
+
critical: false
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Start periodic health checking
|
|
222
|
+
* @param {Object} env - Cloudflare Worker environment
|
|
223
|
+
*/
|
|
224
|
+
startPeriodicChecks(env) {
|
|
225
|
+
// Add common checks if not already added
|
|
226
|
+
if (this.checks.size === 0) {
|
|
227
|
+
this.addCommonChecks(env);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Run initial check
|
|
231
|
+
this.runChecks().catch(error => {
|
|
232
|
+
console.error('Initial health check failed:', error);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Set up periodic checks
|
|
236
|
+
if (this.options.interval > 0) {
|
|
237
|
+
this.intervalId = setInterval(async () => {
|
|
238
|
+
try {
|
|
239
|
+
await this.runChecks();
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.error('Periodic health check failed:', error);
|
|
242
|
+
}
|
|
243
|
+
}, this.options.interval);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Stop periodic health checking
|
|
249
|
+
*/
|
|
250
|
+
stopPeriodicChecks() {
|
|
251
|
+
if (this.intervalId) {
|
|
252
|
+
clearInterval(this.intervalId);
|
|
253
|
+
this.intervalId = null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get health check summary
|
|
259
|
+
* @returns {Object} Summary of health status
|
|
260
|
+
*/
|
|
261
|
+
getSummary() {
|
|
262
|
+
if (!this.lastCheck) {
|
|
263
|
+
return {
|
|
264
|
+
status: 'unknown',
|
|
265
|
+
message: 'No health checks run yet'
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const summary = {
|
|
269
|
+
status: this.lastCheck.status,
|
|
270
|
+
timestamp: this.lastCheck.timestamp,
|
|
271
|
+
totalChecks: this.lastCheck.summary.total,
|
|
272
|
+
healthyChecks: this.lastCheck.summary.healthy,
|
|
273
|
+
unhealthyChecks: this.lastCheck.summary.unhealthy
|
|
274
|
+
};
|
|
275
|
+
return summary;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Sleep utility
|
|
280
|
+
* @param {number} ms - Milliseconds to sleep
|
|
281
|
+
* @returns {Promise<void>}
|
|
282
|
+
*/
|
|
283
|
+
sleep(ms) {
|
|
284
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -11,8 +11,11 @@ export class StateManager {
|
|
|
11
11
|
constructor(options = {}) {
|
|
12
12
|
this.environment = options.environment || 'production';
|
|
13
13
|
this.dryRun = options.dryRun || false;
|
|
14
|
-
|
|
15
|
-
this.
|
|
14
|
+
// Default to ephemeral cache directory to avoid polluting workspace root with artifacts
|
|
15
|
+
this.logDirectory = options.logDirectory || '.clodo-cache/deployments';
|
|
16
|
+
// Persistence must be explicitly enabled via options or environment variable to avoid
|
|
17
|
+
// accidentally creating deployment artifacts in developer workspaces.
|
|
18
|
+
this.enablePersistence = options.enablePersistence ?? process.env.CLODO_ENABLE_PERSISTENCE === '1';
|
|
16
19
|
this.rollbackEnabled = options.rollbackEnabled !== false;
|
|
17
20
|
|
|
18
21
|
// Initialize portfolio state
|
|
@@ -258,7 +261,12 @@ export class StateManager {
|
|
|
258
261
|
* @returns {Promise<void>}
|
|
259
262
|
*/
|
|
260
263
|
async saveAuditLog() {
|
|
261
|
-
|
|
264
|
+
// Respect persistence opt-in: do not write orchestration artifacts unless enabled
|
|
265
|
+
if (!this.enablePersistence) {
|
|
266
|
+
// Avoid creating files during tests or normal developer runs
|
|
267
|
+
if (this.verbose) console.log(' ℹ️ Persistence disabled: skipping saveAuditLog');
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
262
270
|
try {
|
|
263
271
|
// Ensure log directory exists
|
|
264
272
|
try {
|
|
@@ -460,6 +460,222 @@ export class ConfigurationValidator {
|
|
|
460
460
|
return config;
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
+
/**
|
|
464
|
+
* Validate service configuration consistency between manifest and wrangler config
|
|
465
|
+
* @param {string} manifestPath - Path to manifest.json
|
|
466
|
+
* @param {string} wranglerPath - Path to wrangler.toml
|
|
467
|
+
* @returns {Object} Validation result with issues array
|
|
468
|
+
*/
|
|
469
|
+
static validateServiceConfig(manifestPath, wranglerPath) {
|
|
470
|
+
const issues = [];
|
|
471
|
+
try {
|
|
472
|
+
// Read manifest.json
|
|
473
|
+
const manifestContent = readFileSync(manifestPath, 'utf8');
|
|
474
|
+
const manifest = JSON.parse(manifestContent);
|
|
475
|
+
|
|
476
|
+
// Read wrangler.toml (basic parsing - could be enhanced with a TOML parser)
|
|
477
|
+
const wranglerContent = readFileSync(wranglerPath, 'utf8');
|
|
478
|
+
const wranglerConfig = this.parseWranglerToml(wranglerContent);
|
|
479
|
+
|
|
480
|
+
// Validate D1 database consistency
|
|
481
|
+
this.validateD1Consistency(manifest, wranglerConfig, issues);
|
|
482
|
+
|
|
483
|
+
// Validate KV namespace consistency
|
|
484
|
+
this.validateKVConsistency(manifest, wranglerConfig, issues);
|
|
485
|
+
|
|
486
|
+
// Validate R2 bucket consistency
|
|
487
|
+
this.validateR2Consistency(manifest, wranglerConfig, issues);
|
|
488
|
+
|
|
489
|
+
// Validate environment variables
|
|
490
|
+
this.validateEnvironmentConsistency(manifest, wranglerConfig, issues);
|
|
491
|
+
} catch (error) {
|
|
492
|
+
issues.push({
|
|
493
|
+
type: 'error',
|
|
494
|
+
message: `Configuration validation failed: ${error.message}`,
|
|
495
|
+
severity: 'critical'
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
valid: issues.length === 0,
|
|
500
|
+
issues: issues
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Parse wrangler.toml content (basic implementation)
|
|
506
|
+
* @param {string} content - TOML content
|
|
507
|
+
* @returns {Object} Parsed configuration
|
|
508
|
+
*/
|
|
509
|
+
static parseWranglerToml(content) {
|
|
510
|
+
const config = {
|
|
511
|
+
d1_databases: [],
|
|
512
|
+
kv_namespaces: [],
|
|
513
|
+
r2_buckets: [],
|
|
514
|
+
vars: {}
|
|
515
|
+
};
|
|
516
|
+
if (!content || !content.trim()) {
|
|
517
|
+
return config;
|
|
518
|
+
}
|
|
519
|
+
const lines = content.split('\n');
|
|
520
|
+
let currentSection = null;
|
|
521
|
+
for (const line of lines) {
|
|
522
|
+
const trimmed = line.trim();
|
|
523
|
+
|
|
524
|
+
// Section headers
|
|
525
|
+
if (trimmed.startsWith('[[')) {
|
|
526
|
+
if (trimmed.includes('d1_databases')) {
|
|
527
|
+
currentSection = 'd1_databases';
|
|
528
|
+
config.d1_databases.push({});
|
|
529
|
+
} else if (trimmed.includes('kv_namespaces')) {
|
|
530
|
+
currentSection = 'kv_namespaces';
|
|
531
|
+
config.kv_namespaces.push({});
|
|
532
|
+
} else if (trimmed.includes('r2_buckets')) {
|
|
533
|
+
currentSection = 'r2_buckets';
|
|
534
|
+
config.r2_buckets.push({});
|
|
535
|
+
}
|
|
536
|
+
} else if (trimmed.startsWith('[') && !trimmed.startsWith('[[')) {
|
|
537
|
+
currentSection = trimmed.replace(/\[|\]/g, '');
|
|
538
|
+
// Support both table and array-of-tables syntaxes by creating a container
|
|
539
|
+
if (Array.isArray(config[currentSection]) && config[currentSection].length === 0) {
|
|
540
|
+
config[currentSection].push({});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// Key-value pairs
|
|
544
|
+
else if (trimmed.includes('=') && currentSection) {
|
|
545
|
+
const [key, ...valueParts] = trimmed.split('=');
|
|
546
|
+
const value = valueParts.join('=').trim().replace(/"/g, '');
|
|
547
|
+
if (currentSection === 'vars') {
|
|
548
|
+
config.vars[key.trim()] = value;
|
|
549
|
+
} else if (Array.isArray(config[currentSection])) {
|
|
550
|
+
const currentItem = config[currentSection][config[currentSection].length - 1];
|
|
551
|
+
if (currentItem) {
|
|
552
|
+
currentItem[key.trim()] = value;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return config;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Validate D1 database consistency
|
|
562
|
+
*/
|
|
563
|
+
static validateD1Consistency(manifest, wranglerConfig, issues) {
|
|
564
|
+
const manifestD1 = manifest.d1 || false;
|
|
565
|
+
const wranglerD1 = wranglerConfig.d1_databases || [];
|
|
566
|
+
if (manifestD1 === true && wranglerD1.length === 0) {
|
|
567
|
+
issues.push({
|
|
568
|
+
type: 'mismatch',
|
|
569
|
+
message: 'Manifest declares D1=true but wrangler.toml has no [[d1_databases]] configured',
|
|
570
|
+
severity: 'critical',
|
|
571
|
+
manifest: {
|
|
572
|
+
d1: manifestD1
|
|
573
|
+
},
|
|
574
|
+
wrangler: {
|
|
575
|
+
d1_databases: wranglerD1
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
} else if (manifestD1 === false && wranglerD1.length > 0) {
|
|
579
|
+
issues.push({
|
|
580
|
+
type: 'mismatch',
|
|
581
|
+
message: 'Manifest declares D1=false but wrangler.toml has [[d1_databases]] configured',
|
|
582
|
+
severity: 'critical',
|
|
583
|
+
manifest: {
|
|
584
|
+
d1: manifestD1
|
|
585
|
+
},
|
|
586
|
+
wrangler: {
|
|
587
|
+
d1_databases: wranglerD1
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Validate KV namespace consistency
|
|
595
|
+
*/
|
|
596
|
+
static validateKVConsistency(manifest, wranglerConfig, issues) {
|
|
597
|
+
const manifestKV = manifest.kv || false;
|
|
598
|
+
const wranglerKV = wranglerConfig.kv_namespaces || [];
|
|
599
|
+
if (manifestKV === true && wranglerKV.length === 0) {
|
|
600
|
+
issues.push({
|
|
601
|
+
type: 'mismatch',
|
|
602
|
+
message: 'Manifest declares KV=true but wrangler.toml has no [[kv_namespaces]] configured',
|
|
603
|
+
severity: 'critical',
|
|
604
|
+
manifest: {
|
|
605
|
+
kv: manifestKV
|
|
606
|
+
},
|
|
607
|
+
wrangler: {
|
|
608
|
+
kv_namespaces: wranglerKV
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
} else if (manifestKV === false && wranglerKV.length > 0) {
|
|
612
|
+
issues.push({
|
|
613
|
+
type: 'mismatch',
|
|
614
|
+
message: 'Manifest declares KV=false but wrangler.toml has [[kv_namespaces]] configured',
|
|
615
|
+
severity: 'critical',
|
|
616
|
+
manifest: {
|
|
617
|
+
kv: manifestKV
|
|
618
|
+
},
|
|
619
|
+
wrangler: {
|
|
620
|
+
kv_namespaces: wranglerKV
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Validate R2 bucket consistency
|
|
628
|
+
*/
|
|
629
|
+
static validateR2Consistency(manifest, wranglerConfig, issues) {
|
|
630
|
+
const manifestR2 = manifest.r2 || false;
|
|
631
|
+
const wranglerR2 = wranglerConfig.r2_buckets || [];
|
|
632
|
+
if (manifestR2 === true && wranglerR2.length === 0) {
|
|
633
|
+
issues.push({
|
|
634
|
+
type: 'mismatch',
|
|
635
|
+
message: 'Manifest declares R2=true but wrangler.toml has no [[r2_buckets]] configured',
|
|
636
|
+
severity: 'critical',
|
|
637
|
+
manifest: {
|
|
638
|
+
r2: manifestR2
|
|
639
|
+
},
|
|
640
|
+
wrangler: {
|
|
641
|
+
r2_buckets: wranglerR2
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
} else if (manifestR2 === false && wranglerR2.length > 0) {
|
|
645
|
+
issues.push({
|
|
646
|
+
type: 'mismatch',
|
|
647
|
+
message: 'Manifest declares R2=false but wrangler.toml has [[r2_buckets]] configured',
|
|
648
|
+
severity: 'critical',
|
|
649
|
+
manifest: {
|
|
650
|
+
r2: manifestR2
|
|
651
|
+
},
|
|
652
|
+
wrangler: {
|
|
653
|
+
r2_buckets: wranglerR2
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Validate environment variable consistency
|
|
661
|
+
*/
|
|
662
|
+
static validateEnvironmentConsistency(manifest, wranglerConfig, issues) {
|
|
663
|
+
const manifestEnv = manifest.environment || {};
|
|
664
|
+
const wranglerEnv = wranglerConfig.vars || {};
|
|
665
|
+
|
|
666
|
+
// Check for required environment variables declared in manifest but missing in wrangler
|
|
667
|
+
for (const [key, value] of Object.entries(manifestEnv)) {
|
|
668
|
+
if (value === 'required' && !(key in wranglerEnv)) {
|
|
669
|
+
issues.push({
|
|
670
|
+
type: 'missing_env',
|
|
671
|
+
message: `Manifest declares ${key} as required but not found in wrangler.toml vars`,
|
|
672
|
+
severity: 'warning',
|
|
673
|
+
key: key
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
463
679
|
/**
|
|
464
680
|
* Log validation results
|
|
465
681
|
*/
|
|
@@ -180,6 +180,9 @@ export class BaseGenerator {
|
|
|
180
180
|
const fullPath = path.join(this.servicePath, relativePath);
|
|
181
181
|
const overwrite = options.overwrite !== false; // Default to true
|
|
182
182
|
|
|
183
|
+
// Debug: log the target file path for diagnosis
|
|
184
|
+
this.logger.info(`BaseGenerator: writing file -> ${fullPath}`);
|
|
185
|
+
|
|
183
186
|
// Check if file exists and overwrite is disabled
|
|
184
187
|
try {
|
|
185
188
|
await fs.access(fullPath);
|
|
@@ -197,13 +200,35 @@ export class BaseGenerator {
|
|
|
197
200
|
recursive: true
|
|
198
201
|
});
|
|
199
202
|
|
|
200
|
-
// Write file
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
203
|
+
// Write file with retry logic for filesystem stability
|
|
204
|
+
const maxRetries = 3;
|
|
205
|
+
let lastError;
|
|
206
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
207
|
+
try {
|
|
208
|
+
this.logger.info(`BaseGenerator: attempt ${attempt} - writing to ${fullPath}`);
|
|
209
|
+
await fs.writeFile(fullPath, content, 'utf8');
|
|
210
|
+
|
|
211
|
+
// Add a small delay to let file system settle, especially on Windows
|
|
212
|
+
if (attempt === 1) {
|
|
213
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Verify the file was written successfully by checking it exists
|
|
217
|
+
await fs.access(fullPath, fs.constants.F_OK);
|
|
218
|
+
this.logger.info(`Generated: ${relativePath}`);
|
|
219
|
+
return; // Success
|
|
220
|
+
} catch (error) {
|
|
221
|
+
lastError = error;
|
|
222
|
+
this.logger.warn(`Write attempt ${attempt}/${maxRetries} failed for ${relativePath}: ${error.message}`);
|
|
223
|
+
if (attempt < maxRetries) {
|
|
224
|
+
// Wait before retry (exponential backoff)
|
|
225
|
+
await new Promise(resolve => setTimeout(resolve, 50 * attempt));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
206
228
|
}
|
|
229
|
+
|
|
230
|
+
// If all retries failed, throw the last error
|
|
231
|
+
throw new Error(`Failed to write and verify file '${relativePath}' after ${maxRetries} attempts: ${lastError.message}`);
|
|
207
232
|
}
|
|
208
233
|
|
|
209
234
|
/**
|
|
@@ -69,12 +69,22 @@ export class FileWriter {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// Ensure parent directory exists
|
|
72
|
-
|
|
72
|
+
const dirPath = path.dirname(fullPath);
|
|
73
|
+
await this.ensureDirectory(dirPath);
|
|
73
74
|
|
|
74
75
|
// Write file atomically (write to temp file, then rename)
|
|
75
76
|
const tempPath = `${fullPath}.tmp`;
|
|
76
77
|
try {
|
|
78
|
+
// Write to temp file first
|
|
77
79
|
await fs.writeFile(tempPath, content, encoding);
|
|
80
|
+
|
|
81
|
+
// Small delay to ensure file is fully written on Windows
|
|
82
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
83
|
+
|
|
84
|
+
// Verify temp file exists before rename (prevents ENOENT)
|
|
85
|
+
await fs.access(tempPath);
|
|
86
|
+
|
|
87
|
+
// Rename to final destination
|
|
78
88
|
await fs.rename(tempPath, fullPath);
|
|
79
89
|
this.writtenFiles.push(fullPath);
|
|
80
90
|
return {
|
|
@@ -106,6 +116,8 @@ export class FileWriter {
|
|
|
106
116
|
await fs.mkdir(dirPath, {
|
|
107
117
|
recursive: true
|
|
108
118
|
});
|
|
119
|
+
// Verify the directory was created
|
|
120
|
+
await fs.access(dirPath);
|
|
109
121
|
} catch (error) {
|
|
110
122
|
// Ignore error if directory already exists
|
|
111
123
|
if (error.code !== 'EEXIST') {
|