@tamyla/clodo-framework 4.0.13 → 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 +18 -0
- package/README.md +7 -0
- package/dist/cli/commands/create.js +2 -1
- package/dist/index.js +5 -0
- package/dist/middleware/Composer.js +38 -0
- package/dist/middleware/Registry.js +14 -0
- package/dist/middleware/index.js +3 -0
- package/dist/middleware/shared/basicAuth.js +21 -0
- package/dist/middleware/shared/cors.js +28 -0
- package/dist/middleware/shared/index.js +3 -0
- package/dist/middleware/shared/logging.js +14 -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/GenerationEngine.js +13 -2
- package/dist/service-management/ServiceOrchestrator.js +6 -2
- package/dist/service-management/generators/BaseGenerator.js +31 -6
- package/dist/service-management/generators/code/ServiceMiddlewareGenerator.js +156 -10
- package/dist/service-management/generators/code/WorkerIndexGenerator.js +75 -9
- package/dist/service-management/generators/utils/FileWriter.js +13 -1
- package/dist/services/ServiceClient.js +239 -0
- package/dist/simple-api.js +32 -1
- 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/docs/MIDDLEWARE_MIGRATION_SUMMARY.md +121 -0
- package/package.json +8 -1
- package/scripts/DEPLOY_COMMAND_NEW.js +128 -0
- package/scripts/README-automated-testing-suite.md +356 -0
- package/scripts/README-test-clodo-deployment.md +157 -0
- package/scripts/README.md +50 -0
- package/scripts/analyze-imports.ps1 +104 -0
- package/scripts/analyze-mixed-code.js +163 -0
- package/scripts/analyze-mixed-rationale.js +149 -0
- package/scripts/automated-testing-suite.js +776 -0
- package/scripts/check-templates.js +105 -0
- package/scripts/debug-generate-worker.js +58 -0
- package/scripts/deployment/README.md +31 -0
- package/scripts/deployment/deploy-domain.ps1 +449 -0
- package/scripts/deployment/deploy-staging.js +120 -0
- package/scripts/deployment/validate-staging.js +166 -0
- package/scripts/diagnose-imports.js +362 -0
- package/scripts/framework-diagnostic.js +368 -0
- package/scripts/migration/migrate-middleware-legacy-to-contract.js +64 -0
- package/scripts/post-publish-test.js +663 -0
- package/scripts/scan-worker-issues.js +52 -0
- package/scripts/service-management/README.md +27 -0
- package/scripts/service-management/setup-interactive.ps1 +693 -0
- package/scripts/test-clodo-deployment.js +588 -0
- package/scripts/test-downstream-install.js +237 -0
- package/scripts/test-local-package.ps1 +126 -0
- package/scripts/test-local-package.sh +166 -0
- package/scripts/test-package.js +339 -0
- package/scripts/testing/README.md +49 -0
- package/scripts/testing/test-first.ps1 +0 -0
- package/scripts/testing/test-first50.ps1 +0 -0
- package/scripts/testing/test.ps1 +0 -0
- package/scripts/utilities/README.md +61 -0
- package/scripts/utilities/check-bin.js +8 -0
- package/scripts/utilities/check-bundle.js +23 -0
- package/scripts/utilities/check-dist-imports.js +65 -0
- package/scripts/utilities/check-import-paths.js +191 -0
- package/scripts/utilities/cleanup-cli.js +159 -0
- package/scripts/utilities/deployment-helpers.ps1 +199 -0
- package/scripts/utilities/fix-dist-imports.js +135 -0
- package/scripts/utilities/generate-secrets.js +159 -0
- package/scripts/utilities/safe-push.ps1 +51 -0
- package/scripts/utilities/setup-helpers.ps1 +206 -0
- package/scripts/utilities/test-packaged-artifact.js +92 -0
- package/scripts/utilities/validate-dist-imports.js +189 -0
- package/scripts/utilities/validate-schema.js +102 -0
- package/scripts/verify-exports.js +193 -0
- package/scripts/verify-persistence-config.js +45 -0
- package/scripts/verify-worker-safety.js +73 -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/types/middleware.d.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
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
|
+
|
|
8
|
+
## [4.0.14](https://github.com/tamylaa/clodo-framework/compare/v4.0.13...v4.0.14) (2026-01-19)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* middleware packaging, generators, tests, and packaged-artifact smoke test ([b001434](https://github.com/tamylaa/clodo-framework/commit/b001434697da2145e8778b5c0afd02275cd1604e))
|
|
14
|
+
* resolve Windows filesystem timing issues in tests ([7092ca6](https://github.com/tamylaa/clodo-framework/commit/7092ca6ffc230a19d25c96333c0313ead1739f15))
|
|
15
|
+
|
|
1
16
|
## [4.0.13](https://github.com/tamylaa/clodo-framework/compare/v4.0.12...v4.0.13) (2025-12-17)
|
|
2
17
|
|
|
3
18
|
|
|
@@ -173,6 +188,9 @@ Benefits:
|
|
|
173
188
|
- Moved 13 root-level documentation files to appropriate i-docs categories
|
|
174
189
|
- Root directory now contains only essential project files (config, package.json, README, etc.)
|
|
175
190
|
|
|
191
|
+
* **Documentation**: Added `docs/HOWTO_CONSUME_CLODO_FRAMEWORK.md` — a concise consumer guide covering CLI usage, public exports, packaging troubleshooting, and recommended consumption patterns.
|
|
192
|
+
* **Middleware Architecture**: Introduce contract-first middleware generation (v4.1 candidate) with `MiddlewareRegistry` and `MiddlewareComposer` to reduce duplication and improve testability. Added a migration tool `scripts/migration/migrate-middleware-legacy-to-contract.js` and a `--middleware-strategy` CLI flag to opt into `legacy` generator output.
|
|
193
|
+
|
|
176
194
|
* **Configuration Management**: Eliminated hard-coded values from source code
|
|
177
195
|
- Moved domain defaults from ServiceCreator to `validation-config.json`
|
|
178
196
|
- Added configuration hierarchy: CLI option → config file → fallback default
|
package/README.md
CHANGED
|
@@ -199,6 +199,13 @@ import { CloudflareAPI } from '@tamyla/clodo-framework/utils/cloudflare';
|
|
|
199
199
|
|
|
200
200
|
If you need functionality that's currently only in `bin/`, please open an issue - we'll consider adding it to the public API.
|
|
201
201
|
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## How to consume (quick)
|
|
205
|
+
A short guide and best practices for consuming `@tamyla/clodo-framework` are available in the docs: [docs/HOWTO_CONSUME_CLODO_FRAMEWORK.md](./docs/HOWTO_CONSUME_CLODO_FRAMEWORK.md). This file includes quickstart steps, public export guidance, CLI usage, and troubleshooting tips for packaged artifacts.
|
|
206
|
+
|
|
207
|
+
If you'd like this information added to `clodo.dev`, I can prepare a small website PR as well.
|
|
208
|
+
|
|
202
209
|
## Project Structure
|
|
203
210
|
|
|
204
211
|
The project is organized for maximum clarity and maintainability:
|
|
@@ -9,7 +9,7 @@ import chalk from 'chalk';
|
|
|
9
9
|
import { Clodo, ConfigLoader } from '@tamyla/clodo-framework';
|
|
10
10
|
import { StandardOptions } from '../../lib/shared/utils/cli-options.js';
|
|
11
11
|
export function registerCreateCommand(program) {
|
|
12
|
-
const command = program.command('create').description('Create a new Clodo service with conversational setup').option('-n, --non-interactive', 'Run in non-interactive mode with all required parameters').option('--service-name <name>', 'Service name (required in non-interactive mode)').option('--service-type <type>', 'Service type: data-service, auth-service, content-service, api-gateway, generic', 'generic').option('--domain-name <domain>', 'Domain name (required in non-interactive mode)').option('--cloudflare-token <token>', 'Cloudflare API token (required in non-interactive mode)').option('--cloudflare-account-id <id>', 'Cloudflare account ID (required in non-interactive mode)').option('--cloudflare-zone-id <id>', 'Cloudflare zone ID (required in non-interactive mode)').option('--environment <env>', 'Target environment: development, staging, production', 'development').option('--output-path <path>', 'Output directory for generated service', '.').option('--template-path <path>', 'Path to service templates', './templates').option('--force', 'Skip confirmation prompts').option('--validate', 'Validate service after creation');
|
|
12
|
+
const command = program.command('create').description('Create a new Clodo service with conversational setup').option('-n, --non-interactive', 'Run in non-interactive mode with all required parameters').option('--service-name <name>', 'Service name (required in non-interactive mode)').option('--service-type <type>', 'Service type: data-service, auth-service, content-service, api-gateway, generic', 'generic').option('--domain-name <domain>', 'Domain name (required in non-interactive mode)').option('--cloudflare-token <token>', 'Cloudflare API token (required in non-interactive mode)').option('--cloudflare-account-id <id>', 'Cloudflare account ID (required in non-interactive mode)').option('--cloudflare-zone-id <id>', 'Cloudflare zone ID (required in non-interactive mode)').option('--environment <env>', 'Target environment: development, staging, production', 'development').option('--output-path <path>', 'Output directory for generated service', '.').option('--template-path <path>', 'Path to service templates', './templates').option('--middleware-strategy <strategy>', 'Middleware generation strategy: contract|legacy', 'contract').option('--force', 'Skip confirmation prompts').option('--validate', 'Validate service after creation');
|
|
13
13
|
|
|
14
14
|
// Add standard options (--verbose, --quiet, --json, --no-color, --config-file)
|
|
15
15
|
StandardOptions.define(command).action(async options => {
|
|
@@ -40,6 +40,7 @@ export function registerCreateCommand(program) {
|
|
|
40
40
|
domain: mergedOptions.domainName,
|
|
41
41
|
environment: mergedOptions.environment,
|
|
42
42
|
outputPath: mergedOptions.outputPath,
|
|
43
|
+
middlewareStrategy: mergedOptions.middlewareStrategy,
|
|
43
44
|
interactive: !mergedOptions.nonInteractive,
|
|
44
45
|
credentials: {
|
|
45
46
|
token: mergedOptions.cloudflareToken,
|
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,38 @@
|
|
|
1
|
+
// Middleware composer that runs middleware in sequence and supports short-circuiting
|
|
2
|
+
export class MiddlewareComposer {
|
|
3
|
+
static compose(...middlewares) {
|
|
4
|
+
const chain = middlewares.filter(Boolean);
|
|
5
|
+
return {
|
|
6
|
+
async execute(request, handler) {
|
|
7
|
+
// Preprocess/auth/validate phases
|
|
8
|
+
for (const m of chain) {
|
|
9
|
+
if (typeof m.preprocess === 'function') {
|
|
10
|
+
const res = await m.preprocess(request);
|
|
11
|
+
if (res) return res;
|
|
12
|
+
}
|
|
13
|
+
if (typeof m.authenticate === 'function') {
|
|
14
|
+
const res = await m.authenticate(request);
|
|
15
|
+
if (res) return res;
|
|
16
|
+
}
|
|
17
|
+
if (typeof m.validate === 'function') {
|
|
18
|
+
const res = await m.validate(request);
|
|
19
|
+
if (res) return res;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Call the final handler
|
|
24
|
+
let response = await handler(request);
|
|
25
|
+
|
|
26
|
+
// Postprocess in reverse order
|
|
27
|
+
for (const m of chain.slice().reverse()) {
|
|
28
|
+
if (typeof m.postprocess === 'function') {
|
|
29
|
+
const updated = await m.postprocess(response);
|
|
30
|
+
// Allow middleware to replace response
|
|
31
|
+
if (updated instanceof Response) response = updated;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return response;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Registry for service-specific middleware implementations
|
|
2
|
+
export class MiddlewareRegistry {
|
|
3
|
+
static implementations = new Map();
|
|
4
|
+
static register(serviceName, middleware) {
|
|
5
|
+
if (!serviceName) throw new Error('serviceName is required');
|
|
6
|
+
this.implementations.set(serviceName, middleware);
|
|
7
|
+
}
|
|
8
|
+
static get(serviceName) {
|
|
9
|
+
return this.implementations.get(serviceName) || null;
|
|
10
|
+
}
|
|
11
|
+
static clear() {
|
|
12
|
+
this.implementations.clear();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function basicAuth({
|
|
2
|
+
realm = 'Restricted'
|
|
3
|
+
} = {}) {
|
|
4
|
+
return {
|
|
5
|
+
authenticate(request) {
|
|
6
|
+
const auth = request.headers.get('Authorization');
|
|
7
|
+
if (!auth) {
|
|
8
|
+
return new Response(JSON.stringify({
|
|
9
|
+
error: 'Unauthorized'
|
|
10
|
+
}), {
|
|
11
|
+
status: 401,
|
|
12
|
+
headers: {
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
'WWW-Authenticate': `Basic realm="${realm}"`
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function cors(options = {}) {
|
|
2
|
+
const origin = options.origin || '*';
|
|
3
|
+
const allowMethods = options.methods || 'GET, POST, PUT, DELETE, OPTIONS';
|
|
4
|
+
const allowHeaders = options.headers || 'Content-Type, Authorization';
|
|
5
|
+
return {
|
|
6
|
+
preprocess(request) {
|
|
7
|
+
if (request.method === 'OPTIONS') {
|
|
8
|
+
const headers = new Headers();
|
|
9
|
+
headers.set('Access-Control-Allow-Origin', origin);
|
|
10
|
+
headers.set('Access-Control-Allow-Methods', allowMethods);
|
|
11
|
+
headers.set('Access-Control-Allow-Headers', allowHeaders);
|
|
12
|
+
return new Response(null, {
|
|
13
|
+
status: 204,
|
|
14
|
+
headers
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
},
|
|
19
|
+
postprocess(response) {
|
|
20
|
+
const headers = new Headers(response.headers);
|
|
21
|
+
headers.set('Access-Control-Allow-Origin', origin);
|
|
22
|
+
return new Response(response.body, {
|
|
23
|
+
...response,
|
|
24
|
+
headers
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function logging(options = {}) {
|
|
2
|
+
const level = options.level || 'info';
|
|
3
|
+
return {
|
|
4
|
+
preprocess(request) {
|
|
5
|
+
try {
|
|
6
|
+
const path = new URL(request.url).pathname;
|
|
7
|
+
console.log(`[${new Date().toISOString()}] ${level.toUpperCase()} ${request.method} ${path}`);
|
|
8
|
+
} catch (e) {
|
|
9
|
+
// Best-effort logging - do not throw
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -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 {
|