@tamyla/clodo-framework 4.0.14 → 4.1.0
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 -0
- package/dist/config/service-schema-config.js +22 -0
- package/dist/index.js +5 -0
- package/dist/monitoring/HealthChecker.js +286 -0
- package/dist/orchestration/modules/StateManager.js +11 -3
- package/dist/programmatic/createService.js +9 -0
- package/dist/security/ConfigurationValidator.js +216 -0
- package/dist/service-management/ServiceOrchestrator.js +48 -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/validation/payloadValidation.js +159 -0
- package/dist/version/FrameworkInfo.js +104 -0
- package/dist/worker/integration.js +13 -1
- package/package.json +7 -2
- package/scripts/check-templates.js +105 -0
- package/scripts/debug-generate-worker.js +58 -0
- package/scripts/debug_validate_payload.mjs +12 -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,22 @@
|
|
|
1
|
+
# [4.1.0](https://github.com/tamylaa/clodo-framework/compare/v4.0.15...v4.1.0) (2026-01-31)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **lint:** remove unnecessary escape in serviceName regex to satisfy no-useless-escape ([ac4dcda](https://github.com/tamylaa/clodo-framework/commit/ac4dcdabb39dda3dba3f4e06022f28936a53d645))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **programmatic-service-api:** programmatic createService + configurable validation ([#4](https://github.com/tamylaa/clodo-framework/issues/4)) ([040f68b](https://github.com/tamylaa/clodo-framework/commit/040f68b9115b604b3f27d7baa14c63122494c5f3))
|
|
12
|
+
|
|
13
|
+
## [4.0.15](https://github.com/tamylaa/clodo-framework/compare/v4.0.14...v4.0.15) (2026-01-31)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* **lint:** remove unnecessary escape in semver regex ([72751bd](https://github.com/tamylaa/clodo-framework/commit/72751bdf9a98b2f6b1f97706b175ea31f3aa0df1))
|
|
19
|
+
|
|
1
20
|
## [4.0.14](https://github.com/tamylaa/clodo-framework/compare/v4.0.13...v4.0.14) (2026-01-19)
|
|
2
21
|
|
|
3
22
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Centralized configuration for service payload schema values
|
|
2
|
+
// Allows runtime overrides for enums and features to keep validation flexible and configurable
|
|
3
|
+
|
|
4
|
+
let config = {
|
|
5
|
+
serviceTypes: ['api-service', 'data-service', 'worker', 'pages', 'gateway', 'generic'],
|
|
6
|
+
features: ['d1', 'upstash', 'r2', 'pages', 'ws', 'durableObject', 'cron', 'metrics']
|
|
7
|
+
};
|
|
8
|
+
export function getConfig() {
|
|
9
|
+
return {
|
|
10
|
+
...config
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function setConfig(updates = {}) {
|
|
14
|
+
if (updates.serviceTypes) config.serviceTypes = [...updates.serviceTypes];
|
|
15
|
+
if (updates.features) config.features = [...updates.features];
|
|
16
|
+
}
|
|
17
|
+
export function resetConfig() {
|
|
18
|
+
config = {
|
|
19
|
+
serviceTypes: ['api-service', 'data-service', 'worker', 'pages', 'gateway', 'generic'],
|
|
20
|
+
features: ['d1', 'upstash', 'r2', 'pages', 'ws', 'durableObject', 'cron', 'metrics']
|
|
21
|
+
};
|
|
22
|
+
}
|
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 {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ServiceOrchestrator } from '../service-management/ServiceOrchestrator.js';
|
|
2
|
+
export async function createServiceProgrammatic(payload, options = {}) {
|
|
3
|
+
const orchestrator = new ServiceOrchestrator({
|
|
4
|
+
interactive: false,
|
|
5
|
+
outputPath: options.outputDir || '.'
|
|
6
|
+
});
|
|
7
|
+
return await orchestrator.createService(payload, options);
|
|
8
|
+
}
|
|
9
|
+
export const createService = createServiceProgrammatic; // Backwards-compatible alias
|
|
@@ -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
|
*/
|
|
@@ -99,6 +99,54 @@ export class ServiceOrchestrator {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Programmatic service creation API
|
|
104
|
+
* Accepts a ServicePayload and performs validation, generation and returns structured result
|
|
105
|
+
*/
|
|
106
|
+
async createService(payload, options = {}) {
|
|
107
|
+
// Validate payload using new validator
|
|
108
|
+
const {
|
|
109
|
+
validateServicePayload
|
|
110
|
+
} = await import('../validation/payloadValidation.js');
|
|
111
|
+
const validation = validateServicePayload(payload);
|
|
112
|
+
if (!validation.valid && !options.force) {
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
errors: validation.errors,
|
|
116
|
+
warnings: validation.warnings || []
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Map payload to coreInputs expected by generation handlers
|
|
121
|
+
const coreInputs = {
|
|
122
|
+
serviceName: payload.serviceName,
|
|
123
|
+
serviceType: payload.serviceType,
|
|
124
|
+
domainName: payload.domain,
|
|
125
|
+
environment: options.environment || 'development'
|
|
126
|
+
};
|
|
127
|
+
try {
|
|
128
|
+
// Generate derived confirmations and run generation
|
|
129
|
+
const confirmedValues = await this.confirmationHandler.generateAndConfirm(coreInputs);
|
|
130
|
+
const generationResult = await this.generationHandler.generateService(coreInputs, confirmedValues, {
|
|
131
|
+
outputPath: options.outputDir || this.outputPath,
|
|
132
|
+
middlewareStrategy: this.middlewareStrategy
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
success: true,
|
|
136
|
+
serviceId: generationResult.serviceId || null,
|
|
137
|
+
servicePath: generationResult.servicePath || null,
|
|
138
|
+
serviceName: generationResult.serviceName || coreInputs.serviceName,
|
|
139
|
+
fileCount: generationResult.fileCount || (generationResult.generatedFiles || []).length,
|
|
140
|
+
generatedFiles: generationResult.generatedFiles || [],
|
|
141
|
+
warnings: validation.warnings || []
|
|
142
|
+
};
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return {
|
|
145
|
+
success: false,
|
|
146
|
+
errors: [error.message]
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
102
150
|
/**
|
|
103
151
|
* Validate an existing service configuration
|
|
104
152
|
*/
|