@tamyla/clodo-framework 2.0.0 → 2.0.2

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.
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Command Configuration Manager
3
+ * Loads and manages configurable system commands from validation-config.json
4
+ *
5
+ * Ensures all commands are configurable and platform-specific
6
+ * No hardcoded commands in the codebase
7
+ */
8
+
9
+ import { readFileSync } from 'fs';
10
+ import { join } from 'path';
11
+
12
+ export class CommandConfigManager {
13
+ constructor(configPath = null) {
14
+ this.configPath = configPath || join(process.cwd(), 'validation-config.json');
15
+ this.config = null;
16
+ this.loadConfig();
17
+ }
18
+
19
+ /**
20
+ * Load configuration from JSON file
21
+ */
22
+ loadConfig() {
23
+ try {
24
+ const configData = readFileSync(this.configPath, 'utf-8');
25
+ this.config = JSON.parse(configData);
26
+ console.log('šŸ“‹ Loaded command configuration from validation-config.json');
27
+ } catch (error) {
28
+ console.log('āš ļø Could not load command config, using defaults');
29
+ this.config = this.getDefaultConfig();
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Get default configuration fallback
35
+ */
36
+ getDefaultConfig() {
37
+ return {
38
+ requiredCommands: {
39
+ npx: 'npx',
40
+ node: 'node',
41
+ npm: 'npm',
42
+ wrangler: 'npx wrangler'
43
+ },
44
+ cloudflareCommands: {
45
+ whoami: 'npx wrangler whoami',
46
+ auth_login: 'npx wrangler auth login',
47
+ deployments_list: 'npx wrangler deployments list',
48
+ list_workers: 'npx wrangler dev --help',
49
+ deploy: 'npx wrangler deploy',
50
+ list_zones: 'npx wrangler zone list',
51
+ worker_status: 'npx wrangler status'
52
+ },
53
+ systemCommands: {
54
+ powershell_web_request: 'powershell -Command "try { Invoke-WebRequest -Uri \'{url}\' -TimeoutSec 10 -UseBasicParsing | Out-Null } catch { exit 1 }"',
55
+ curl_test: 'curl -s --connect-timeout 10 {url} -o /dev/null'
56
+ }
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Get a Cloudflare command
62
+ */
63
+ getCloudflareCommand(commandName, params = {}) {
64
+ const command = this.config?.cloudflareCommands?.[commandName];
65
+ if (!command) {
66
+ throw new Error(`Cloudflare command '${commandName}' not found in configuration`);
67
+ }
68
+
69
+ return this.interpolateParams(command, params);
70
+ }
71
+
72
+ /**
73
+ * Get a system command
74
+ */
75
+ getSystemCommand(commandName, params = {}) {
76
+ const command = this.config?.systemCommands?.[commandName];
77
+ if (!command) {
78
+ throw new Error(`System command '${commandName}' not found in configuration`);
79
+ }
80
+
81
+ return this.interpolateParams(command, params);
82
+ }
83
+
84
+ /**
85
+ * Get a required command
86
+ */
87
+ getRequiredCommand(commandName) {
88
+ const command = this.config?.requiredCommands?.[commandName];
89
+ if (!command) {
90
+ throw new Error(`Required command '${commandName}' not found in configuration`);
91
+ }
92
+
93
+ return command;
94
+ }
95
+
96
+ /**
97
+ * Get network test command for current platform
98
+ */
99
+ getNetworkTestCommand(url) {
100
+ const isWindows = process.platform === 'win32';
101
+ const commandName = isWindows ? 'powershell_web_request' : 'curl_test';
102
+
103
+ return this.getSystemCommand(commandName, { url });
104
+ }
105
+
106
+ /**
107
+ * Interpolate parameters in command strings
108
+ */
109
+ interpolateParams(command, params) {
110
+ let result = command;
111
+
112
+ for (const [key, value] of Object.entries(params)) {
113
+ const placeholder = `{${key}}`;
114
+ result = result.replace(new RegExp(placeholder.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'g'), value);
115
+ }
116
+
117
+ return result;
118
+ }
119
+
120
+ /**
121
+ * Get all available commands for debugging
122
+ */
123
+ getAllCommands() {
124
+ return {
125
+ requiredCommands: this.config?.requiredCommands || {},
126
+ cloudflareCommands: this.config?.cloudflareCommands || {},
127
+ systemCommands: this.config?.systemCommands || {}
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Validate command configuration
133
+ */
134
+ validateConfig() {
135
+ const errors = [];
136
+
137
+ // Check required sections
138
+ const requiredSections = ['requiredCommands', 'cloudflareCommands', 'systemCommands'];
139
+
140
+ for (const section of requiredSections) {
141
+ if (!this.config[section] || typeof this.config[section] !== 'object') {
142
+ errors.push(`Missing or invalid section: ${section}`);
143
+ }
144
+ }
145
+
146
+ // Check essential Cloudflare commands
147
+ const essentialCfCommands = ['whoami', 'auth_login', 'deployments_list'];
148
+
149
+ for (const cmd of essentialCfCommands) {
150
+ if (!this.config?.cloudflareCommands?.[cmd]) {
151
+ errors.push(`Missing essential Cloudflare command: ${cmd}`);
152
+ }
153
+ }
154
+
155
+ if (errors.length > 0) {
156
+ throw new Error(`Command configuration validation failed:\\n${errors.join('\\n')}`);
157
+ }
158
+
159
+ return true;
160
+ }
161
+
162
+ /**
163
+ * Reload configuration
164
+ */
165
+ reload() {
166
+ this.loadConfig();
167
+ this.validateConfig();
168
+ }
169
+ }
170
+
171
+ // Singleton instance
172
+ let commandConfigInstance = null;
173
+
174
+ /**
175
+ * Get singleton instance of command config manager
176
+ */
177
+ export function getCommandConfig(configPath = null) {
178
+ if (!commandConfigInstance || configPath) {
179
+ commandConfigInstance = new CommandConfigManager(configPath);
180
+ }
181
+ return commandConfigInstance;
182
+ }
183
+
184
+ export default CommandConfigManager;
@@ -6,7 +6,7 @@
6
6
  * Integrates with Clodo Framework domain and feature flag systems
7
7
  */
8
8
 
9
- import { CustomerConfigCLI } from '../../../src/config/CustomerConfigCLI.js';
9
+ import { CustomerConfigCLI } from '../../../dist/config/CustomerConfigCLI.js';
10
10
 
11
11
  const command = process.argv[2];
12
12
  const args = process.argv.slice(3);
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Configuration Module
3
+ * Exports all configuration management utilities
4
+ */
5
+
6
+ export { ConfigCache } from './cache.js';
7
+ export { ConfigManager } from './manager.js';
8
+ export { CommandConfigManager } from './command-config-manager.js';
9
+ export { CustomerConfigurationManager } from '../../../src/config/customers.js';
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Configuration Manager Module
3
+ * Configuration file management and validation
4
+ *
5
+ * Consolidates config management across 34+ scripts
6
+ */
7
+
8
+ import { readFileSync, writeFileSync, existsSync, copyFileSync, readdirSync, statSync } from 'fs';
9
+ import { join } from 'path';
10
+
11
+ export function updateWranglerConfig(updates, configPath = 'wrangler.toml') {
12
+ if (!existsSync(configPath)) {
13
+ throw new Error(`Configuration file not found: ${configPath}`);
14
+ }
15
+
16
+ let config = readFileSync(configPath, 'utf8');
17
+ let changesMade = [];
18
+
19
+ // Apply updates systematically
20
+ for (const [key, value] of Object.entries(updates)) {
21
+ switch (key) {
22
+ case 'workerName':
23
+ // Update main worker name
24
+ if (config.match(/^name = "[^"]*"/m)) {
25
+ config = config.replace(/^name = "[^"]*"/m, `name = "${value}"`);
26
+ changesMade.push(`Updated main worker name to: ${value}`);
27
+ }
28
+
29
+ // Update production environment name
30
+ if (config.match(/^\[env\.production\]\s*\nname = "[^"]*"/m)) {
31
+ config = config.replace(/^\[env\.production\]\s*\nname = "[^"]*"/m, `[env.production]\nname = "${value}"`);
32
+ changesMade.push(`Updated production worker name to: ${value}`);
33
+ }
34
+ break;
35
+
36
+ case 'databaseName': {
37
+ const dbNameRegex = /database_name = "[^"]*"/g;
38
+ if (config.match(dbNameRegex)) {
39
+ config = config.replace(dbNameRegex, `database_name = "${value}"`);
40
+ changesMade.push(`Updated database name to: ${value}`);
41
+ }
42
+ break;
43
+ }
44
+
45
+ case 'databaseId': {
46
+ const dbIdRegex = /database_id = "[^"]*"/g;
47
+ if (config.match(dbIdRegex)) {
48
+ config = config.replace(dbIdRegex, `database_id = "${value}"`);
49
+ changesMade.push(`Updated database ID to: ${value}`);
50
+ }
51
+ break;
52
+ }
53
+
54
+ case 'serviceDomain': {
55
+ const domainRegex = /SERVICE_DOMAIN = "[^"]*"/g;
56
+ if (config.match(domainRegex)) {
57
+ config = config.replace(domainRegex, `SERVICE_DOMAIN = "${value}"`);
58
+ changesMade.push(`Updated service domain to: ${value}`);
59
+ }
60
+ break;
61
+ }
62
+
63
+ case 'compatibilityDate': {
64
+ const dateRegex = /^compatibility_date = "[^"]*"/m;
65
+ if (config.match(dateRegex)) {
66
+ config = config.replace(dateRegex, `compatibility_date = "${value}"`);
67
+ changesMade.push(`Updated compatibility date to: ${value}`);
68
+ }
69
+ break;
70
+ }
71
+
72
+ default:
73
+ console.warn(`Unknown config update key: ${key}`);
74
+ }
75
+ }
76
+
77
+ writeFileSync(configPath, config);
78
+
79
+ return {
80
+ configPath,
81
+ changesMade,
82
+ success: changesMade.length > 0
83
+ };
84
+ }
85
+
86
+ export function backupConfig(configPath = 'wrangler.toml', suffix = null) {
87
+ if (!existsSync(configPath)) {
88
+ throw new Error(`Configuration file not found: ${configPath}`);
89
+ }
90
+
91
+ const timestamp = suffix || new Date().toISOString().replace(/[:.]/g, '-');
92
+ const backupPath = `${configPath}.backup.${timestamp}`;
93
+
94
+ copyFileSync(configPath, backupPath);
95
+
96
+ return {
97
+ originalPath: configPath,
98
+ backupPath,
99
+ timestamp: new Date().toISOString()
100
+ };
101
+ }
102
+
103
+ export function validateConfig(configPath = 'wrangler.toml') {
104
+ if (!existsSync(configPath)) {
105
+ throw new Error(`Configuration file not found: ${configPath}`);
106
+ }
107
+
108
+ const config = readFileSync(configPath, 'utf8');
109
+ const issues = [];
110
+ const warnings = [];
111
+
112
+ // Required fields validation
113
+ const requiredFields = [
114
+ { pattern: /^name = "/m, description: 'Worker name' },
115
+ { pattern: /^compatibility_date = "/m, description: 'Compatibility date' },
116
+ { pattern: /^main = "/m, description: 'Main entry point' }
117
+ ];
118
+
119
+ requiredFields.forEach(field => {
120
+ if (!config.match(field.pattern)) {
121
+ issues.push(`Missing required field: ${field.description}`);
122
+ }
123
+ });
124
+
125
+ // Database configuration validation
126
+ const hasDatabaseBinding = config.includes('[[d1_databases]]');
127
+ if (hasDatabaseBinding) {
128
+ if (!config.includes('database_name =')) {
129
+ issues.push('Database binding found but missing database_name');
130
+ }
131
+ if (!config.includes('database_id =')) {
132
+ issues.push('Database binding found but missing database_id');
133
+ }
134
+ }
135
+
136
+ // Environment validation
137
+ const hasProductionEnv = config.includes('[env.production]');
138
+ if (hasProductionEnv) {
139
+ const prodSection = config.split('[env.production]')[1];
140
+ if (prodSection && !prodSection.includes('name =')) {
141
+ warnings.push('Production environment missing explicit name');
142
+ }
143
+ }
144
+
145
+ // Compatibility date validation
146
+ const compatMatch = config.match(/compatibility_date = "([^"]+)"/);
147
+ if (compatMatch) {
148
+ const date = new Date(compatMatch[1]);
149
+ const now = new Date();
150
+ const daysDiff = (now - date) / (1000 * 60 * 60 * 24);
151
+
152
+ if (daysDiff > 365) {
153
+ warnings.push(`Compatibility date is ${Math.floor(daysDiff)} days old`);
154
+ }
155
+ }
156
+
157
+ return {
158
+ isValid: issues.length === 0,
159
+ issues,
160
+ warnings,
161
+ summary: {
162
+ errors: issues.length,
163
+ warnings: warnings.length,
164
+ status: issues.length === 0 ? 'valid' : 'invalid'
165
+ }
166
+ };
167
+ }
168
+
169
+ export function parseWranglerConfig(configPath = 'wrangler.toml') {
170
+ if (!existsSync(configPath)) {
171
+ throw new Error(`Configuration file not found: ${configPath}`);
172
+ }
173
+
174
+ const config = readFileSync(configPath, 'utf8');
175
+ const parsed = {};
176
+
177
+ // Extract main fields
178
+ const extractField = (pattern, key) => {
179
+ const match = config.match(pattern);
180
+ if (match) parsed[key] = match[1];
181
+ };
182
+
183
+ extractField(/^name = "([^"]+)"/m, 'name');
184
+ extractField(/^compatibility_date = "([^"]+)"/m, 'compatibilityDate');
185
+ extractField(/^main = "([^"]+)"/m, 'main');
186
+
187
+ // Extract database configuration
188
+ if (config.includes('[[d1_databases]]')) {
189
+ parsed.database = {};
190
+ extractField(/database_name = "([^"]+)"/m, 'name');
191
+ extractField(/database_id = "([^"]+)"/m, 'id');
192
+
193
+ if (parsed.name) parsed.database.name = parsed.name;
194
+ if (parsed.id) parsed.database.id = parsed.id;
195
+ delete parsed.name;
196
+ delete parsed.id;
197
+ }
198
+
199
+ // Extract environment configurations
200
+ parsed.environments = {};
201
+ const envMatches = config.matchAll(/\[env\.([^\]]+)\]/g);
202
+ for (const match of envMatches) {
203
+ const envName = match[1];
204
+ parsed.environments[envName] = {};
205
+
206
+ // Extract env-specific settings (simplified parsing)
207
+ const envSection = config.split(`[env.${envName}]`)[1];
208
+ if (envSection) {
209
+ const nextEnv = envSection.indexOf('[env.');
210
+ const sectionContent = nextEnv > 0 ? envSection.substring(0, nextEnv) : envSection;
211
+
212
+ const nameMatch = sectionContent.match(/name = "([^"]+)"/);
213
+ if (nameMatch) parsed.environments[envName].name = nameMatch[1];
214
+ }
215
+ }
216
+
217
+ return parsed;
218
+ }
219
+
220
+ export function generateWranglerConfig(options) {
221
+ const {
222
+ name,
223
+ main = 'src/worker/index.js',
224
+ compatibilityDate = new Date().toISOString().split('T')[0],
225
+ database = null,
226
+ environments = {},
227
+ vars = {},
228
+ secrets = []
229
+ } = options;
230
+
231
+ let config = `# Generated Wrangler Configuration
232
+ # Timestamp: ${new Date().toISOString()}
233
+
234
+ name = "${name}"
235
+ main = "${main}"
236
+ compatibility_date = "${compatibilityDate}"
237
+ `;
238
+
239
+ // Add variables
240
+ if (Object.keys(vars).length > 0) {
241
+ config += '\n[vars]\n';
242
+ Object.entries(vars).forEach(([key, value]) => {
243
+ config += `${key} = "${value}"\n`;
244
+ });
245
+ }
246
+
247
+ // Add database configuration
248
+ if (database) {
249
+ config += `
250
+ [[d1_databases]]
251
+ binding = "DB"
252
+ database_name = "${database.name}"
253
+ database_id = "${database.id}"
254
+ `;
255
+ }
256
+
257
+ // Add environments
258
+ Object.entries(environments).forEach(([envName, envConfig]) => {
259
+ config += `\n[env.${envName}]\n`;
260
+ if (envConfig.name) config += `name = "${envConfig.name}"\n`;
261
+
262
+ if (envConfig.vars) {
263
+ Object.entries(envConfig.vars).forEach(([key, value]) => {
264
+ config += `${key} = "${value}"\n`;
265
+ });
266
+ }
267
+
268
+ if (envConfig.database) {
269
+ config += `
270
+ [[env.${envName}.d1_databases]]
271
+ binding = "DB"
272
+ database_name = "${envConfig.database.name}"
273
+ database_id = "${envConfig.database.id}"
274
+ `;
275
+ }
276
+ });
277
+
278
+ return config;
279
+ }
280
+
281
+ export function listConfigFiles(directory = '.') {
282
+ const configFiles = [];
283
+ const patterns = ['wrangler.toml', 'package.json', '.env*'];
284
+
285
+ try {
286
+ const items = readdirSync(directory);
287
+
288
+ items.forEach(item => {
289
+ const path = join(directory, item);
290
+ const stat = statSync(path);
291
+
292
+ if (stat.isFile()) {
293
+ const matches = patterns.some(pattern => {
294
+ if (pattern.includes('*')) {
295
+ return item.startsWith(pattern.replace('*', ''));
296
+ }
297
+ return item === pattern;
298
+ });
299
+
300
+ if (matches) {
301
+ configFiles.push({
302
+ name: item,
303
+ path,
304
+ size: stat.size,
305
+ modified: stat.mtime
306
+ });
307
+ }
308
+ }
309
+ });
310
+
311
+ return configFiles.sort((a, b) => a.name.localeCompare(b.name));
312
+ } catch (error) {
313
+ return [];
314
+ }
315
+ }
@@ -270,6 +270,9 @@ export class DeploymentValidator {
270
270
 
271
271
  // Validate environment configuration
272
272
  await this.validateEnvironmentConfig();
273
+
274
+ // Validate D1 database bindings
275
+ await this.validateD1Configuration();
273
276
  this.results.categories.configuration = 'passed';
274
277
  this.addResult('configuration', 'Configuration files validated', 'success');
275
278
  } catch (error) {
@@ -457,6 +460,112 @@ export class DeploymentValidator {
457
460
  console.log(` āœ… Environment: ${this.environment} configuration valid`);
458
461
  this.addResult('environment', `${this.environment} environment validated`, 'info');
459
462
  }
463
+
464
+ /**
465
+ * Validate D1 database configuration across all services
466
+ */
467
+ async validateD1Configuration() {
468
+ console.log(' šŸ—„ļø Validating D1 database configuration...');
469
+ try {
470
+ // Import WranglerDeployer for D1 validation capabilities
471
+ const {
472
+ WranglerDeployer
473
+ } = await import('../../../src/deployment/wrangler-deployer.js');
474
+
475
+ // Check if this is a framework-level validation (no specific service)
476
+ if (!this.options?.servicePath) {
477
+ console.log(' ā­ļø Skipping D1 validation (framework-level validation)');
478
+ this.addResult('d1-config', 'D1 validation skipped for framework-level validation', 'info');
479
+ return;
480
+ }
481
+
482
+ // Create deployer instance for the specific service
483
+ const deployer = new WranglerDeployer({
484
+ cwd: this.options.servicePath,
485
+ environment: this.environment
486
+ });
487
+
488
+ // Discover deployment configuration
489
+ const deployConfig = await deployer.discoverDeploymentConfig(this.environment);
490
+ if (!deployConfig.configPath) {
491
+ console.log(' ā­ļø No wrangler.toml found, skipping D1 validation');
492
+ this.addResult('d1-config', 'No wrangler.toml found', 'info');
493
+ return;
494
+ }
495
+
496
+ // Validate D1 bindings
497
+ const d1Validation = await deployer.validateD1Bindings(deployConfig);
498
+ if (d1Validation.valid) {
499
+ if (d1Validation.bindings.length === 0) {
500
+ console.log(' āœ… No D1 databases configured');
501
+ this.addResult('d1-config', 'No D1 databases configured', 'info');
502
+ } else {
503
+ console.log(` āœ… All ${d1Validation.summary.total} D1 database bindings valid`);
504
+ this.addResult('d1-config', `${d1Validation.summary.total} D1 bindings validated`, 'success');
505
+
506
+ // Log details about each binding
507
+ d1Validation.bindings.forEach(binding => {
508
+ if (binding.valid) {
509
+ console.log(` āœ… ${binding.binding}: ${binding.database_name}`);
510
+ }
511
+ });
512
+ }
513
+ } else {
514
+ console.log(` āŒ D1 validation failed: ${d1Validation.summary.invalid} invalid bindings`);
515
+
516
+ // Log details about invalid bindings
517
+ d1Validation.invalidBindings.forEach(binding => {
518
+ console.log(` āŒ ${binding.binding || 'unnamed'}: ${binding.issues.join(', ')}`);
519
+ });
520
+
521
+ // Add warning with suggestions
522
+ const suggestions = this.generateD1Suggestions(d1Validation);
523
+ this.addWarning('d1-config', `D1 configuration issues found. ${suggestions}`);
524
+
525
+ // Don't fail validation entirely for D1 issues if not in strict mode
526
+ if (!this.strictMode) {
527
+ console.log(' āš ļø Continuing with warnings (non-strict mode)');
528
+ this.addResult('d1-config', 'D1 issues found but continuing', 'warning');
529
+ } else {
530
+ throw new Error(`D1 validation failed: ${d1Validation.summary.invalid} invalid bindings`);
531
+ }
532
+ }
533
+ } catch (error) {
534
+ if (error.message.includes('D1 validation failed')) {
535
+ throw error; // Re-throw D1 validation errors
536
+ }
537
+
538
+ // Handle other errors (import errors, etc.)
539
+ console.log(` āš ļø D1 validation error: ${error.message}`);
540
+ this.addWarning('d1-config', `D1 validation error: ${error.message}`);
541
+ if (this.strictMode) {
542
+ throw new Error(`D1 validation failed: ${error.message}`);
543
+ }
544
+ }
545
+ }
546
+
547
+ /**
548
+ * Generate helpful suggestions for D1 configuration issues
549
+ * @param {Object} d1Validation - D1 validation results
550
+ * @returns {string} Suggestions text
551
+ */
552
+ generateD1Suggestions(d1Validation) {
553
+ const suggestions = [];
554
+ d1Validation.invalidBindings.forEach(binding => {
555
+ binding.issues.forEach(issue => {
556
+ if (issue.includes('not found')) {
557
+ suggestions.push('Run "wrangler d1 list" to see available databases');
558
+ suggestions.push('Create missing database with "wrangler d1 create <name>"');
559
+ } else if (issue.includes('Missing')) {
560
+ suggestions.push('Check wrangler.toml [[d1_databases]] section completeness');
561
+ }
562
+ });
563
+ });
564
+
565
+ // Remove duplicates
566
+ const uniqueSuggestions = [...new Set(suggestions)];
567
+ return uniqueSuggestions.length > 0 ? `Suggestions: ${uniqueSuggestions.slice(0, 2).join('; ')}` : 'Check D1 database configuration in wrangler.toml';
568
+ }
460
569
  async validateDomainEndpoints(domain) {
461
570
  console.log(` Validating endpoints for ${domain}...`);
462
571
  console.log(` šŸ”§ DEBUG: skipEndpointCheck = ${this.options?.skipEndpointCheck}`);
@@ -538,6 +647,9 @@ export class DeploymentValidator {
538
647
  this.results.errors.push(message);
539
648
  }
540
649
  }
650
+ addWarning(category, message) {
651
+ this.addResult(category, message, 'warning');
652
+ }
541
653
  printValidationSummary() {
542
654
  console.log('\nšŸ“Š VALIDATION SUMMARY');
543
655
  console.log('====================');