@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +7 -0
  3. package/dist/cli/commands/create.js +2 -1
  4. package/dist/index.js +5 -0
  5. package/dist/middleware/Composer.js +38 -0
  6. package/dist/middleware/Registry.js +14 -0
  7. package/dist/middleware/index.js +3 -0
  8. package/dist/middleware/shared/basicAuth.js +21 -0
  9. package/dist/middleware/shared/cors.js +28 -0
  10. package/dist/middleware/shared/index.js +3 -0
  11. package/dist/middleware/shared/logging.js +14 -0
  12. package/dist/monitoring/HealthChecker.js +286 -0
  13. package/dist/orchestration/modules/StateManager.js +11 -3
  14. package/dist/security/ConfigurationValidator.js +216 -0
  15. package/dist/service-management/GenerationEngine.js +13 -2
  16. package/dist/service-management/ServiceOrchestrator.js +6 -2
  17. package/dist/service-management/generators/BaseGenerator.js +31 -6
  18. package/dist/service-management/generators/code/ServiceMiddlewareGenerator.js +156 -10
  19. package/dist/service-management/generators/code/WorkerIndexGenerator.js +75 -9
  20. package/dist/service-management/generators/utils/FileWriter.js +13 -1
  21. package/dist/services/ServiceClient.js +239 -0
  22. package/dist/simple-api.js +32 -1
  23. package/dist/utils/CircuitBreaker.js +192 -0
  24. package/dist/utils/EnvironmentValidator.js +147 -0
  25. package/dist/utils/TemplateRuntime.js +291 -0
  26. package/dist/utils/deployment/secret-generator.js +37 -26
  27. package/dist/version/FrameworkInfo.js +104 -0
  28. package/dist/worker/integration.js +13 -1
  29. package/docs/MIDDLEWARE_MIGRATION_SUMMARY.md +121 -0
  30. package/package.json +8 -1
  31. package/scripts/DEPLOY_COMMAND_NEW.js +128 -0
  32. package/scripts/README-automated-testing-suite.md +356 -0
  33. package/scripts/README-test-clodo-deployment.md +157 -0
  34. package/scripts/README.md +50 -0
  35. package/scripts/analyze-imports.ps1 +104 -0
  36. package/scripts/analyze-mixed-code.js +163 -0
  37. package/scripts/analyze-mixed-rationale.js +149 -0
  38. package/scripts/automated-testing-suite.js +776 -0
  39. package/scripts/check-templates.js +105 -0
  40. package/scripts/debug-generate-worker.js +58 -0
  41. package/scripts/deployment/README.md +31 -0
  42. package/scripts/deployment/deploy-domain.ps1 +449 -0
  43. package/scripts/deployment/deploy-staging.js +120 -0
  44. package/scripts/deployment/validate-staging.js +166 -0
  45. package/scripts/diagnose-imports.js +362 -0
  46. package/scripts/framework-diagnostic.js +368 -0
  47. package/scripts/migration/migrate-middleware-legacy-to-contract.js +64 -0
  48. package/scripts/post-publish-test.js +663 -0
  49. package/scripts/scan-worker-issues.js +52 -0
  50. package/scripts/service-management/README.md +27 -0
  51. package/scripts/service-management/setup-interactive.ps1 +693 -0
  52. package/scripts/test-clodo-deployment.js +588 -0
  53. package/scripts/test-downstream-install.js +237 -0
  54. package/scripts/test-local-package.ps1 +126 -0
  55. package/scripts/test-local-package.sh +166 -0
  56. package/scripts/test-package.js +339 -0
  57. package/scripts/testing/README.md +49 -0
  58. package/scripts/testing/test-first.ps1 +0 -0
  59. package/scripts/testing/test-first50.ps1 +0 -0
  60. package/scripts/testing/test.ps1 +0 -0
  61. package/scripts/utilities/README.md +61 -0
  62. package/scripts/utilities/check-bin.js +8 -0
  63. package/scripts/utilities/check-bundle.js +23 -0
  64. package/scripts/utilities/check-dist-imports.js +65 -0
  65. package/scripts/utilities/check-import-paths.js +191 -0
  66. package/scripts/utilities/cleanup-cli.js +159 -0
  67. package/scripts/utilities/deployment-helpers.ps1 +199 -0
  68. package/scripts/utilities/fix-dist-imports.js +135 -0
  69. package/scripts/utilities/generate-secrets.js +159 -0
  70. package/scripts/utilities/safe-push.ps1 +51 -0
  71. package/scripts/utilities/setup-helpers.ps1 +206 -0
  72. package/scripts/utilities/test-packaged-artifact.js +92 -0
  73. package/scripts/utilities/validate-dist-imports.js +189 -0
  74. package/scripts/utilities/validate-schema.js +102 -0
  75. package/scripts/verify-exports.js +193 -0
  76. package/scripts/verify-persistence-config.js +45 -0
  77. package/scripts/verify-worker-safety.js +73 -0
  78. package/templates/generic/src/config/.config-is-sample +1 -0
  79. package/templates/static-site/.env.example +1 -1
  80. package/templates/static-site/src/config/.config-is-sample +1 -0
  81. package/templates/static-site/src/config/domains.js +3 -0
  82. 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,3 @@
1
+ export { MiddlewareRegistry } from './Registry.js';
2
+ export { MiddlewareComposer } from './Composer.js';
3
+ export * as Shared from './shared/index.js';
@@ -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,3 @@
1
+ export { cors } from './cors.js';
2
+ export { logging } from './logging.js';
3
+ export { basicAuth } from './basicAuth.js';
@@ -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
- this.logDirectory = options.logDirectory || 'deployments';
15
- this.enablePersistence = options.enablePersistence !== false;
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
- if (!this.enablePersistence) return;
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 {