@tamyla/clodo-framework 3.1.12 → 3.1.13

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 CHANGED
@@ -1,3 +1,11 @@
1
+ ## [3.1.13](https://github.com/tamylaa/clodo-framework/compare/v3.1.12...v3.1.13) (2025-10-27)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * add comprehensive test coverage for service validation ([393e6d2](https://github.com/tamylaa/clodo-framework/commit/393e6d268348cc2926c80f0d3f6cd5377c2875a2))
7
+ * add intelligent Cloudflare service detection and validation ([a680006](https://github.com/tamylaa/clodo-framework/commit/a6800063e2df8f8d6fa7660079cd22eb3f9a4a97))
8
+
1
9
  ## [3.1.12](https://github.com/tamylaa/clodo-framework/compare/v3.1.11...v3.1.12) (2025-10-27)
2
10
 
3
11
 
@@ -2,46 +2,39 @@
2
2
  * Deploy Command - Smart minimal input deployment with service detection
3
3
  *
4
4
  * Input Strategy: SMART MINIMAL
5
- * - Detects if project is a service (clodo-service-manifest.json)
6
- * - Gathers credentials smartly: env vars → flags → fail with helpful message (never prompt)
5
+ * - Detects Clodo services OR legacy services (wrangler.toml)
6
+ * - Supports multiple manifest locations
7
+ * - Gathers credentials smartly: env vars → flags → fail with helpful message
7
8
  * - Integrates with modular-enterprise-deploy.js for clean CLI-based deployment
8
9
  */
9
10
 
10
11
  import chalk from 'chalk';
11
- import { existsSync, readFileSync } from 'fs';
12
- import { resolve, join } from 'path';
12
+ import { resolve } from 'path';
13
+ import { ManifestLoader } from '../shared/config/manifest-loader.js';
14
+ import { CloudflareServiceValidator } from '../shared/config/cloudflare-service-validator.js';
13
15
  export function registerDeployCommand(program) {
14
16
  program.command('deploy').description('Deploy a Clodo service with smart credential handling').option('--token <token>', 'Cloudflare API token').option('--account-id <id>', 'Cloudflare account ID').option('--zone-id <id>', 'Cloudflare zone ID').option('--dry-run', 'Simulate deployment without making changes').option('--quiet', 'Quiet mode - minimal output').option('--service-path <path>', 'Path to service directory', '.').action(async options => {
15
17
  try {
16
18
  console.log(chalk.cyan('\n🚀 Clodo Service Deployment\n'));
17
19
 
18
- // Step 1: Detect if this is a service project
20
+ // Step 1: Load and validate service configuration
19
21
  const servicePath = resolve(options.servicePath);
20
- const manifestPath = join(servicePath, 'clodo-service-manifest.json');
21
- if (!existsSync(manifestPath)) {
22
- console.error(chalk.red('❌ This is not a Clodo service project'));
23
- console.error(chalk.yellow('\nExpected to find: clodo-service-manifest.json'));
24
- console.error(chalk.cyan('\nAre you trying to deploy the framework itself?'));
25
- console.error(chalk.white('The clodo-framework repository is a library, not deployable.'));
26
- console.error(chalk.white('Create a service first: npx clodo-service create'));
22
+ const serviceConfig = await ManifestLoader.loadAndValidateCloudflareService(servicePath);
23
+ if (!serviceConfig.manifest) {
24
+ if (serviceConfig.error === 'NOT_A_CLOUDFLARE_SERVICE') {
25
+ ManifestLoader.printNotCloudflareServiceError(servicePath);
26
+ } else if (serviceConfig.error === 'CLOUDFLARE_SERVICE_INVALID') {
27
+ ManifestLoader.printValidationErrors(serviceConfig.validationResult);
28
+ }
27
29
  process.exit(1);
28
30
  }
29
31
 
30
- // Read service manifest
31
- let manifest;
32
- try {
33
- const manifestContent = readFileSync(manifestPath, 'utf8');
34
- manifest = JSON.parse(manifestContent);
35
- } catch (err) {
36
- console.error(chalk.red('❌ Failed to read service manifest'));
37
- console.error(chalk.yellow(`Error: ${err.message}`));
38
- process.exit(1);
32
+ // Print service info and validation results
33
+ if (serviceConfig.validationResult) {
34
+ CloudflareServiceValidator.printValidationReport(serviceConfig.validationResult.validation);
39
35
  }
40
- const serviceName = manifest.serviceName || 'unknown-service';
41
- const serviceType = manifest.serviceType || 'unknown-type';
42
- console.log(chalk.white(`Service: ${chalk.bold(serviceName)}`));
43
- console.log(chalk.white(`Type: ${serviceType}`));
44
- console.log(chalk.white(`Path: ${servicePath}\n`));
36
+ ManifestLoader.printManifestInfo(serviceConfig.manifest);
37
+ console.log(chalk.gray(`Configuration loaded from: ${serviceConfig.foundAt}\n`));
45
38
 
46
39
  // Step 2: Smart credential gathering
47
40
  // Priority: flags → environment variables → fail with helpful message
@@ -84,11 +77,12 @@ export function registerDeployCommand(program) {
84
77
  }
85
78
 
86
79
  // Step 3: Extract configuration from manifest
87
- const config = manifest.configuration || {};
88
- const domain = config.domain || config.domainName;
89
- if (!domain) {
90
- console.error(chalk.red('❌ No domain configured in service manifest'));
91
- console.error(chalk.yellow('Update clodo-service-manifest.json with domain configuration'));
80
+ const manifest = serviceConfig.manifest;
81
+ const config = manifest.deployment || manifest.configuration || {};
82
+ const domains = config.domains || [];
83
+ if (domains.length === 0) {
84
+ console.error(chalk.red(' No domains configured in service manifest'));
85
+ console.error(chalk.yellow('Add domain configuration to: clodo-service-manifest.json'));
92
86
  process.exit(1);
93
87
  }
94
88
  console.log(chalk.cyan('📋 Deployment Plan:'));
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Cloudflare Service Validator
3
+ *
4
+ * Validates that a directory is a properly formed Cloudflare Workers service
5
+ * and detects quality issues before deployment attempts
6
+ */
7
+
8
+ import { existsSync, readFileSync } from 'fs';
9
+ import { join } from 'path';
10
+ import chalk from 'chalk';
11
+ import TOML from '@iarna/toml';
12
+ export class CloudflareServiceValidator {
13
+ /**
14
+ * Check if directory has basic Cloudflare service signatures
15
+ * Returns: { isCloudflareService, wranglerPath, packagePath, errors }
16
+ */
17
+ static detectCloudflareService(servicePath) {
18
+ const wranglerPath = join(servicePath, 'wrangler.toml');
19
+ const packagePath = join(servicePath, 'package.json');
20
+ const hasWrangler = existsSync(wranglerPath);
21
+ const hasPackage = existsSync(packagePath);
22
+ const errors = [];
23
+ if (!hasWrangler) errors.push('Missing wrangler.toml');
24
+ if (!hasPackage) errors.push('Missing package.json');
25
+ return {
26
+ isCloudflareService: hasWrangler && hasPackage,
27
+ wranglerPath: hasWrangler ? wranglerPath : null,
28
+ packagePath: hasPackage ? packagePath : null,
29
+ errors
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Validate service has proper entry points and structure
35
+ * Returns: { isValid, issues, warnings }
36
+ */
37
+ static validateServiceStructure(servicePath, wranglerPath, packagePath) {
38
+ const issues = [];
39
+ const warnings = [];
40
+ try {
41
+ // Parse files
42
+ const wranglerContent = readFileSync(wranglerPath, 'utf8');
43
+ const wranglerConfig = TOML.parse(wranglerContent);
44
+ const packageContent = readFileSync(packagePath, 'utf8');
45
+ const packageJson = JSON.parse(packageContent);
46
+
47
+ // Check wrangler.toml quality
48
+ if (!wranglerConfig.name) {
49
+ issues.push('wrangler.toml: Missing "name" field');
50
+ }
51
+ if (!wranglerConfig.main) {
52
+ warnings.push('wrangler.toml: No "main" entry point specified (will use default)');
53
+ }
54
+
55
+ // Check package.json quality
56
+ if (!packageJson.name) {
57
+ issues.push('package.json: Missing "name" field');
58
+ }
59
+ if (!packageJson.dependencies && !packageJson.devDependencies) {
60
+ warnings.push('package.json: No dependencies defined');
61
+ }
62
+
63
+ // Check for common entry points
64
+ const possibleEntryPoints = ['src/worker/index.js', 'src/index.js', 'index.js', 'dist/index.js', wranglerConfig.main].filter(Boolean);
65
+ const hasEntryPoint = possibleEntryPoints.some(ep => existsSync(join(servicePath, ep)));
66
+ if (!hasEntryPoint) {
67
+ warnings.push(`No recognizable entry point found. Checked: ${possibleEntryPoints.join(', ')}`);
68
+ }
69
+
70
+ // Check for Cloudflare environment
71
+ const env = wranglerConfig.env || {};
72
+ if (Object.keys(env).length === 0) {
73
+ warnings.push('wrangler.toml: No environments configured (production, staging, etc)');
74
+ }
75
+
76
+ // Check for routes configuration
77
+ const routes = wranglerConfig.routes;
78
+ if (!routes) {
79
+ warnings.push('wrangler.toml: No routes defined (service may not be accessible)');
80
+ }
81
+ return {
82
+ isValid: issues.length === 0,
83
+ issues,
84
+ warnings,
85
+ wranglerConfig,
86
+ packageJson
87
+ };
88
+ } catch (err) {
89
+ return {
90
+ isValid: false,
91
+ issues: [`Failed to parse config files: ${err.message}`],
92
+ warnings: [],
93
+ error: err
94
+ };
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Check if service has existing deployment history
100
+ */
101
+ static detectExistingDeployments(servicePath) {
102
+ const possibleLocations = [join(servicePath, '.wrangler'), join(servicePath, 'dist'), join(servicePath, 'build'), join(servicePath, '.deployments')];
103
+ const existingDeployments = possibleLocations.filter(loc => existsSync(loc));
104
+ return {
105
+ hasDeploymentHistory: existingDeployments.length > 0,
106
+ deploymentMarkers: existingDeployments
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Print validation report
112
+ */
113
+ static printValidationReport(validation) {
114
+ if (validation.issues.length > 0) {
115
+ console.error(chalk.red('\n❌ Service Configuration Issues:\n'));
116
+ validation.issues.forEach(issue => {
117
+ console.error(chalk.red(` • ${issue}`));
118
+ });
119
+ }
120
+ if (validation.warnings.length > 0) {
121
+ console.warn(chalk.yellow('\n⚠️ Service Configuration Warnings:\n'));
122
+ validation.warnings.forEach(warning => {
123
+ console.warn(chalk.yellow(` • ${warning}`));
124
+ });
125
+ }
126
+ if (validation.isValid && validation.warnings.length === 0) {
127
+ console.log(chalk.green('\n✅ Service structure looks good!\n'));
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Interactive validation: ask user if they want to continue despite warnings
133
+ */
134
+ static async askContinueDespiteWarnings() {
135
+ // This would be called from interactive context
136
+ // For now, return a promise that can be used in async/await
137
+ return new Promise(resolve => {
138
+ // In actual implementation, would use readline or similar
139
+ // For this stub, we'll indicate the method exists
140
+ resolve(true);
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Comprehensive service validation workflow
146
+ */
147
+ static validateForDeployment(servicePath) {
148
+ // Step 1: Check for basic signatures
149
+ const detection = this.detectCloudflareService(servicePath);
150
+ if (!detection.isCloudflareService) {
151
+ return {
152
+ canDeploy: false,
153
+ reason: 'NOT_A_CLOUDFLARE_SERVICE',
154
+ detection,
155
+ details: 'This directory does not have the required files for a Cloudflare Workers service.'
156
+ };
157
+ }
158
+
159
+ // Step 2: Validate structure
160
+ const validation = this.validateServiceStructure(servicePath, detection.wranglerPath, detection.packagePath);
161
+ if (!validation.isValid) {
162
+ return {
163
+ canDeploy: false,
164
+ reason: 'SERVICE_CONFIGURATION_INVALID',
165
+ validation,
166
+ details: 'The service configuration is invalid and may not deploy correctly.'
167
+ };
168
+ }
169
+
170
+ // Step 3: Check deployment history
171
+ const deploymentHistory = this.detectExistingDeployments(servicePath);
172
+ return {
173
+ canDeploy: true,
174
+ reason: validation.warnings.length > 0 ? 'SERVICE_VALID_WITH_WARNINGS' : 'SERVICE_VALID',
175
+ detection,
176
+ validation,
177
+ deploymentHistory,
178
+ details: 'Service is ready for deployment.'
179
+ };
180
+ }
181
+ }
182
+ export default CloudflareServiceValidator;
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Manifest Loader - Flexible service manifest detection and loading
3
+ *
4
+ * Supports:
5
+ * - Clodo Framework generated manifests (service-manifest.json)
6
+ * - Legacy services (wrangler.toml + package.json)
7
+ * - Custom manifest locations
8
+ * - Environment variable overrides
9
+ */
10
+
11
+ import { existsSync, readFileSync } from 'fs';
12
+ import { join, resolve } from 'path';
13
+ import chalk from 'chalk';
14
+ import { CloudflareServiceValidator } from './cloudflare-service-validator.js';
15
+
16
+ /**
17
+ * Possible manifest locations (in order of priority)
18
+ */
19
+ const MANIFEST_LOCATIONS = ['clodo-service-manifest.json',
20
+ // Standard location (root)
21
+ '.clodo/service-manifest.json',
22
+ // Hidden config directory
23
+ 'config/service-manifest.json' // Config subdirectory
24
+ ];
25
+
26
+ /**
27
+ * Fallback configuration builders for legacy services
28
+ */
29
+ class LegacyServiceDetector {
30
+ /**
31
+ * Build manifest-like config from wrangler.toml + package.json
32
+ */
33
+ static async buildFromWrangler(servicePath) {
34
+ const wranglerPath = join(servicePath, 'wrangler.toml');
35
+ const packagePath = join(servicePath, 'package.json');
36
+ if (!existsSync(wranglerPath) || !existsSync(packagePath)) {
37
+ return null;
38
+ }
39
+ try {
40
+ const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
41
+ return {
42
+ _source: 'legacy-wrangler',
43
+ _legacyNote: 'This is a legacy service without a service-manifest.json. Consider running: npx clodo-service init',
44
+ serviceName: packageJson.name || 'unknown-service',
45
+ serviceType: 'legacy-workers-project',
46
+ version: packageJson.version || '1.0.0',
47
+ isClodoService: false,
48
+ isLegacyService: true,
49
+ deployment: {
50
+ framework: 'wrangler',
51
+ ready: true,
52
+ configFiles: {
53
+ wrangler: './wrangler.toml',
54
+ package: './package.json'
55
+ }
56
+ },
57
+ metadata: {
58
+ detectedAt: new Date().toISOString(),
59
+ framework: 'legacy-wrangler'
60
+ }
61
+ };
62
+ } catch (err) {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Build manifest-like config from package.json alone
69
+ */
70
+ static async buildFromPackageJson(servicePath) {
71
+ const packagePath = join(servicePath, 'package.json');
72
+ if (!existsSync(packagePath)) {
73
+ return null;
74
+ }
75
+ try {
76
+ const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
77
+ return {
78
+ _source: 'legacy-package-only',
79
+ _legacyNote: 'This project has a package.json but no deployment configuration.',
80
+ serviceName: packageJson.name || 'unknown-service',
81
+ serviceType: 'nodejs-project',
82
+ version: packageJson.version || '1.0.0',
83
+ isClodoService: false,
84
+ isLegacyService: true,
85
+ deployment: {
86
+ framework: 'nodejs',
87
+ ready: false,
88
+ configFiles: {
89
+ package: './package.json'
90
+ }
91
+ },
92
+ metadata: {
93
+ detectedAt: new Date().toISOString(),
94
+ framework: 'legacy-nodejs'
95
+ }
96
+ };
97
+ } catch (err) {
98
+ return null;
99
+ }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Main manifest loader
105
+ */
106
+ export class ManifestLoader {
107
+ /**
108
+ * Load service configuration with intelligent Cloudflare service validation
109
+ *
110
+ * Returns:
111
+ * - Clodo Framework manifest (if found)
112
+ * - Validated Cloudflare service config (wrangler.toml + package.json)
113
+ * - Error details if not a valid Cloudflare service
114
+ */
115
+ static async loadAndValidateCloudflareService(servicePath = '.') {
116
+ const resolvedPath = resolve(servicePath);
117
+
118
+ // Step 1: Try loading Clodo manifest first
119
+ for (const location of MANIFEST_LOCATIONS) {
120
+ const manifestPath = join(resolvedPath, location);
121
+ if (existsSync(manifestPath)) {
122
+ try {
123
+ const content = readFileSync(manifestPath, 'utf8');
124
+ const manifest = JSON.parse(content);
125
+ manifest._source = 'clodo-manifest';
126
+ manifest._location = location;
127
+ manifest.isClodoService = true;
128
+ manifest.isValidCloudflareService = true;
129
+ return {
130
+ manifest,
131
+ foundAt: manifestPath,
132
+ isClodo: true,
133
+ validationResult: null
134
+ };
135
+ } catch (err) {
136
+ throw new Error(`Failed to parse manifest at ${manifestPath}: ${err.message}`);
137
+ }
138
+ }
139
+ }
140
+
141
+ // Step 2: Validate as Cloudflare service (requires wrangler.toml + package.json)
142
+ const validationResult = CloudflareServiceValidator.validateForDeployment(resolvedPath);
143
+ if (!validationResult.canDeploy && validationResult.reason === 'NOT_A_CLOUDFLARE_SERVICE') {
144
+ return {
145
+ manifest: null,
146
+ foundAt: null,
147
+ isClodo: false,
148
+ validationResult,
149
+ error: 'NOT_A_CLOUDFLARE_SERVICE'
150
+ };
151
+ }
152
+
153
+ // Step 3: Build manifest-like config from Cloudflare service
154
+ if (validationResult.canDeploy || validationResult.reason === 'SERVICE_VALID_WITH_WARNINGS') {
155
+ const manifest = this.buildFromCloudflareService(validationResult, resolvedPath);
156
+ return {
157
+ manifest,
158
+ foundAt: 'detected-cloudflare-service',
159
+ isClodo: false,
160
+ validationResult,
161
+ isValidCloudflareService: true
162
+ };
163
+ }
164
+
165
+ // Service is Cloudflare but has issues
166
+ return {
167
+ manifest: null,
168
+ foundAt: null,
169
+ isClodo: false,
170
+ validationResult,
171
+ error: 'CLOUDFLARE_SERVICE_INVALID'
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Build manifest-like config from valid Cloudflare service files
177
+ */
178
+ static buildFromCloudflareService(validationResult, servicePath) {
179
+ const wranglerConfig = validationResult.validation.wranglerConfig;
180
+ const packageJson = validationResult.validation.packageJson;
181
+ return {
182
+ _source: 'cloudflare-service-detected',
183
+ _legacyNote: 'This is a Cloudflare Workers service. Consider creating it with: npx clodo-service create',
184
+ serviceName: packageJson.name || wranglerConfig.name || 'unknown-service',
185
+ serviceType: 'cloudflare-workers-service',
186
+ version: packageJson.version || '1.0.0',
187
+ isClodoService: false,
188
+ isValidCloudflareService: true,
189
+ deployment: {
190
+ framework: 'wrangler',
191
+ ready: validationResult.deploymentHistory.hasDeploymentHistory,
192
+ configFiles: {
193
+ wrangler: './wrangler.toml',
194
+ package: './package.json'
195
+ },
196
+ hasExistingDeployments: validationResult.deploymentHistory.hasDeploymentHistory
197
+ },
198
+ metadata: {
199
+ detectedAt: new Date().toISOString(),
200
+ framework: 'wrangler',
201
+ detectionReason: validationResult.reason
202
+ }
203
+ };
204
+ }
205
+
206
+ /**
207
+ * Print helpful error message for non-Cloudflare services
208
+ */
209
+ static printNotCloudflareServiceError(servicePath = '.') {
210
+ console.error(chalk.red('\n❌ Not a Cloudflare Workers Service\n'));
211
+ console.error(chalk.white('This directory is missing required Cloudflare files:'));
212
+ console.error(chalk.gray(' - wrangler.toml'));
213
+ console.error(chalk.gray(' - package.json'));
214
+ console.error(chalk.cyan('\n📋 To deploy, you need either:\n'));
215
+ console.error(chalk.white('Option 1: Create a new Clodo service'));
216
+ console.error(chalk.green(' npx clodo-service create'));
217
+ console.error(chalk.gray(' (Creates: service-manifest.json + full structure)\n'));
218
+ console.error(chalk.white('Option 2: Initialize an existing project'));
219
+ console.error(chalk.green(' npx clodo-service init'));
220
+ console.error(chalk.gray(' (Creates: service-manifest.json from existing files)\n'));
221
+ console.error(chalk.white('Option 3: Create a standard Cloudflare project'));
222
+ console.error(chalk.green(' npm init wrangler@latest'));
223
+ console.error(chalk.gray(' (Creates: wrangler.toml + package.json)\n'));
224
+ }
225
+
226
+ /**
227
+ * Print validation issues for malformed services
228
+ */
229
+ static printValidationErrors(validationResult) {
230
+ if (validationResult.validation.issues.length > 0) {
231
+ console.error(chalk.red('\n❌ Service Configuration Issues:\n'));
232
+ validationResult.validation.issues.forEach(issue => {
233
+ console.error(chalk.red(` • ${issue}`));
234
+ });
235
+ }
236
+ if (validationResult.validation.warnings.length > 0) {
237
+ console.warn(chalk.yellow('\n⚠️ Service Configuration Warnings:\n'));
238
+ validationResult.validation.warnings.forEach(warning => {
239
+ console.warn(chalk.yellow(` • ${warning}`));
240
+ });
241
+ console.warn(chalk.cyan('\nContinue with deployment? (May fail or behave unexpectedly)'));
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Validate manifest has required fields for deployment
247
+ */
248
+ static validateDeploymentReady(manifest) {
249
+ const errors = [];
250
+
251
+ // Required for both Clodo and legacy services
252
+ if (!manifest.serviceName) errors.push('Missing: serviceName');
253
+ if (!manifest.deployment) errors.push('Missing: deployment configuration');
254
+
255
+ // For Clodo services
256
+ if (manifest.isClodoService) {
257
+ if (!manifest.deployment.domains || manifest.deployment.domains.length === 0) {
258
+ errors.push('Missing: deployment.domains (required for multi-domain deployment)');
259
+ }
260
+ }
261
+ return {
262
+ isValid: errors.length === 0,
263
+ errors
264
+ };
265
+ }
266
+
267
+ /**
268
+ * Print manifest info for debugging
269
+ */
270
+ static printManifestInfo(manifest) {
271
+ console.log(chalk.cyan('\n📋 Service Configuration:\n'));
272
+ console.log(chalk.white(`Service Name: ${chalk.bold(manifest.serviceName)}`));
273
+ console.log(chalk.white(`Service Type: ${chalk.bold(manifest.serviceType)}`));
274
+ console.log(chalk.white(`Source: ${chalk.gray(manifest._source)}`));
275
+ if (manifest._legacyNote) {
276
+ console.log(chalk.yellow(`\nℹ️ ${manifest._legacyNote}`));
277
+ }
278
+ if (manifest.deployment.domains) {
279
+ console.log(chalk.white(`\nDomains:`));
280
+ manifest.deployment.domains.forEach(domain => {
281
+ console.log(chalk.gray(` - ${domain.name} (${domain.environment})`));
282
+ });
283
+ }
284
+ console.log('');
285
+ }
286
+ }
287
+ export default ManifestLoader;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamyla/clodo-framework",
3
- "version": "3.1.12",
3
+ "version": "3.1.13",
4
4
  "description": "Reusable framework for Clodo-style software architecture on Cloudflare Workers + D1",
5
5
  "type": "module",
6
6
  "sideEffects": [