@tamyla/clodo-framework 2.0.18 → 2.0.20

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,363 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wrangler Configuration Manager
5
+ * Manages wrangler.toml configuration files for Cloudflare Workers deployment
6
+ * Handles environment sections, D1 database bindings, and other Cloudflare resources
7
+ *
8
+ * @module WranglerConfigManager
9
+ */
10
+ import { readFile, writeFile, access, mkdir } from 'fs/promises';
11
+ import { join, dirname } from 'path';
12
+ import { parse as parseToml, stringify as stringifyToml } from '@iarna/toml';
13
+ import { constants } from 'fs';
14
+ export class WranglerConfigManager {
15
+ constructor(options = {}) {
16
+ // Handle both string path and options object
17
+ if (typeof options === 'string') {
18
+ this.configPath = options;
19
+ this.projectRoot = dirname(options);
20
+ this.dryRun = false;
21
+ this.verbose = false;
22
+ } else {
23
+ this.projectRoot = options.projectRoot || process.cwd();
24
+ this.configPath = options.configPath || join(this.projectRoot, 'wrangler.toml');
25
+ this.dryRun = options.dryRun || false;
26
+ this.verbose = options.verbose || false;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Read and parse wrangler.toml file
32
+ * @returns {Promise<Object>} Parsed TOML configuration
33
+ */
34
+ async readConfig() {
35
+ try {
36
+ const content = await readFile(this.configPath, 'utf-8');
37
+ return parseToml(content);
38
+ } catch (error) {
39
+ if (error.code === 'ENOENT') {
40
+ // File doesn't exist - return minimal default config
41
+ if (this.verbose) {
42
+ console.log(` ℹ️ wrangler.toml not found at ${this.configPath}`);
43
+ }
44
+ return {
45
+ name: 'worker',
46
+ main: 'src/index.js',
47
+ compatibility_date: new Date().toISOString().split('T')[0],
48
+ env: {}
49
+ };
50
+ }
51
+ throw new Error(`Failed to read wrangler.toml: ${error.message}`);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Write configuration back to wrangler.toml
57
+ * @param {Object} config - Configuration object to write
58
+ */
59
+ async writeConfig(config) {
60
+ if (this.dryRun) {
61
+ console.log(` 🔍 DRY RUN: Would write wrangler.toml`);
62
+ console.log(stringifyToml(config));
63
+ return;
64
+ }
65
+ try {
66
+ // Ensure directory exists
67
+ await mkdir(dirname(this.configPath), {
68
+ recursive: true
69
+ });
70
+
71
+ // Convert to TOML string
72
+ const tomlContent = stringifyToml(config);
73
+
74
+ // Write to file
75
+ await writeFile(this.configPath, tomlContent, 'utf-8');
76
+ if (this.verbose) {
77
+ console.log(` ✅ Updated wrangler.toml at ${this.configPath}`);
78
+ }
79
+ } catch (error) {
80
+ throw new Error(`Failed to write wrangler.toml: ${error.message}`);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Check if wrangler.toml exists
86
+ * @returns {Promise<boolean>}
87
+ */
88
+ async exists() {
89
+ try {
90
+ await access(this.configPath, constants.F_OK);
91
+ return true;
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Ensure environment section exists in wrangler.toml
99
+ * @param {string} environment - Environment name (development, staging, production)
100
+ * @returns {Promise<boolean>} True if environment was added, false if already existed
101
+ */
102
+ async ensureEnvironment(environment) {
103
+ const config = await this.readConfig();
104
+
105
+ // For production, check top-level only
106
+ if (environment === 'production') {
107
+ // Production config is typically at top level
108
+ if (!config.name) {
109
+ console.log(` ⚠️ wrangler.toml missing 'name' field`);
110
+ return false;
111
+ }
112
+ return false; // Already exists (implicitly)
113
+ }
114
+
115
+ // For non-production environments, check env.{environment} section
116
+ if (!config.env) {
117
+ config.env = {};
118
+ }
119
+ if (!config.env[environment]) {
120
+ console.log(` 📝 Adding [env.${environment}] section to wrangler.toml`);
121
+
122
+ // Create environment section with basic structure
123
+ config.env[environment] = {
124
+ name: config.name || 'worker'
125
+ // Don't add workers_dev by default - let wrangler decide
126
+ };
127
+ await this.writeConfig(config);
128
+ return true;
129
+ }
130
+ if (this.verbose) {
131
+ console.log(` ✓ [env.${environment}] section already exists`);
132
+ }
133
+ return false;
134
+ }
135
+
136
+ /**
137
+ * Add D1 database binding to wrangler.toml
138
+ * @param {string} environment - Environment name
139
+ * @param {Object} databaseInfo - Database information
140
+ * @param {string} databaseInfo.binding - Binding name (e.g., 'DB')
141
+ * @param {string} databaseInfo.database_name - Database name
142
+ * @param {string} databaseInfo.database_id - Database ID
143
+ * @returns {Promise<boolean>} True if binding was added
144
+ */
145
+ async addDatabaseBinding(environment, databaseInfo) {
146
+ // Support both camelCase and snake_case for flexibility
147
+ const binding = databaseInfo.binding || 'DB';
148
+ const databaseName = databaseInfo.database_name || databaseInfo.databaseName;
149
+ const databaseId = databaseInfo.database_id || databaseInfo.databaseId;
150
+ if (!databaseName || !databaseId) {
151
+ throw new Error('Database name and ID are required');
152
+ }
153
+ const config = await this.readConfig();
154
+ console.log(` 📝 Adding D1 database binding to wrangler.toml`);
155
+ console.log(` Environment: ${environment}`);
156
+ console.log(` Binding: ${binding}`);
157
+ console.log(` Database: ${databaseName}`);
158
+ console.log(` ID: ${databaseId}`);
159
+ const dbBinding = {
160
+ binding,
161
+ database_name: databaseName,
162
+ database_id: databaseId
163
+ };
164
+ if (environment === 'production') {
165
+ // Add to top-level d1_databases array
166
+ if (!config.d1_databases) {
167
+ config.d1_databases = [];
168
+ }
169
+
170
+ // Check if binding already exists
171
+ const existingIndex = config.d1_databases.findIndex(db => db.binding === binding || db.database_name === databaseName);
172
+ if (existingIndex >= 0) {
173
+ console.log(` 🔄 Updating existing database binding`);
174
+ config.d1_databases[existingIndex] = dbBinding;
175
+ } else {
176
+ console.log(` ➕ Adding new database binding`);
177
+ config.d1_databases.push(dbBinding);
178
+ }
179
+ } else {
180
+ // Add to environment-specific section
181
+ if (!config.env) {
182
+ config.env = {};
183
+ }
184
+ if (!config.env[environment]) {
185
+ config.env[environment] = {
186
+ name: config.name || 'worker'
187
+ };
188
+ }
189
+ if (!config.env[environment].d1_databases) {
190
+ config.env[environment].d1_databases = [];
191
+ }
192
+
193
+ // Check if binding already exists
194
+ const existingIndex = config.env[environment].d1_databases.findIndex(db => db.binding === binding || db.database_name === databaseName);
195
+ if (existingIndex >= 0) {
196
+ console.log(` 🔄 Updating existing database binding`);
197
+ config.env[environment].d1_databases[existingIndex] = dbBinding;
198
+ } else {
199
+ console.log(` ➕ Adding new database binding`);
200
+ config.env[environment].d1_databases.push(dbBinding);
201
+ }
202
+ }
203
+ await this.writeConfig(config);
204
+ console.log(` ✅ D1 database binding added successfully`);
205
+ return true;
206
+ }
207
+
208
+ /**
209
+ * Remove D1 database binding from wrangler.toml
210
+ * @param {string} environment - Environment name
211
+ * @param {string} bindingOrName - Binding name or database name
212
+ * @returns {Promise<boolean>} True if binding was removed
213
+ */
214
+ async removeDatabaseBinding(environment, bindingOrName) {
215
+ const config = await this.readConfig();
216
+ let removed = false;
217
+ if (environment === 'production') {
218
+ if (config.d1_databases) {
219
+ const initialLength = config.d1_databases.length;
220
+ config.d1_databases = config.d1_databases.filter(db => db.binding !== bindingOrName && db.database_name !== bindingOrName);
221
+ removed = config.d1_databases.length < initialLength;
222
+ }
223
+ } else {
224
+ if (config.env?.[environment]?.d1_databases) {
225
+ const initialLength = config.env[environment].d1_databases.length;
226
+ config.env[environment].d1_databases = config.env[environment].d1_databases.filter(db => db.binding !== bindingOrName && db.database_name !== bindingOrName);
227
+ removed = config.env[environment].d1_databases.length < initialLength;
228
+ }
229
+ }
230
+ if (removed) {
231
+ await this.writeConfig(config);
232
+ console.log(` ✅ Removed database binding: ${bindingOrName}`);
233
+ } else {
234
+ console.log(` ℹ️ Database binding not found: ${bindingOrName}`);
235
+ }
236
+ return removed;
237
+ }
238
+
239
+ /**
240
+ * Get all database bindings for an environment
241
+ * @param {string} environment - Environment name
242
+ * @returns {Promise<Array>} Array of database bindings
243
+ */
244
+ async getDatabaseBindings(environment) {
245
+ const config = await this.readConfig();
246
+ if (environment === 'production') {
247
+ return config.d1_databases || [];
248
+ } else {
249
+ return config.env?.[environment]?.d1_databases || [];
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Create minimal wrangler.toml if it doesn't exist
255
+ * @param {string} name - Worker name
256
+ * @param {string} environment - Initial environment to create (optional)
257
+ * @param {Object} options - Additional configuration options
258
+ * @returns {Promise<Object>} The created config object
259
+ */
260
+ async createMinimalConfig(name = 'worker', environment = null, options = {}) {
261
+ const exists = await this.exists();
262
+ if (exists) {
263
+ console.log(` ✓ wrangler.toml already exists`);
264
+ return await this.readConfig();
265
+ }
266
+ console.log(` 📝 Creating minimal wrangler.toml`);
267
+ const today = new Date().toISOString().split('T')[0];
268
+ const minimalConfig = {
269
+ name: name,
270
+ main: options.main || 'src/index.js',
271
+ compatibility_date: options.compatibility_date || today,
272
+ // Environment sections will be added as needed
273
+ env: {}
274
+ };
275
+
276
+ // Add initial environment if specified
277
+ if (environment && environment !== 'production') {
278
+ minimalConfig.env[environment] = {};
279
+ }
280
+ await this.writeConfig(minimalConfig);
281
+ console.log(` ✅ Created wrangler.toml at ${this.configPath}`);
282
+ return minimalConfig;
283
+ }
284
+
285
+ /**
286
+ * Validate wrangler.toml configuration
287
+ * @returns {Promise<Object>} Validation result with errors and warnings arrays
288
+ */
289
+ async validate() {
290
+ const errors = [];
291
+ const warnings = [];
292
+
293
+ // Check if file exists
294
+ const exists = await this.exists();
295
+ if (!exists) {
296
+ errors.push('wrangler.toml file not found');
297
+ return {
298
+ valid: false,
299
+ errors,
300
+ warnings
301
+ };
302
+ }
303
+ try {
304
+ const config = await this.readConfig();
305
+
306
+ // Check required fields
307
+ if (!config.name) {
308
+ errors.push('Missing required field: name');
309
+ }
310
+ if (!config.main) {
311
+ warnings.push('Missing main field (entry point)');
312
+ }
313
+ if (!config.compatibility_date) {
314
+ warnings.push('Missing compatibility_date field');
315
+ }
316
+ return {
317
+ valid: errors.length === 0,
318
+ errors,
319
+ warnings,
320
+ config
321
+ };
322
+ } catch (error) {
323
+ errors.push(`Invalid TOML syntax: ${error.message}`);
324
+ return {
325
+ valid: false,
326
+ errors,
327
+ warnings
328
+ };
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Display configuration summary
334
+ * @param {string} environment - Environment to display (optional)
335
+ */
336
+ async displaySummary(environment = null) {
337
+ const config = await this.readConfig();
338
+ console.log(`\n📋 Wrangler Configuration Summary`);
339
+ console.log(` File: ${this.configPath}`);
340
+ console.log(` Worker Name: ${config.name || 'N/A'}`);
341
+ console.log(` Entry Point: ${config.main || 'N/A'}`);
342
+ console.log(` Compatibility: ${config.compatibility_date || 'N/A'}`);
343
+ if (environment) {
344
+ console.log(`\n Environment: ${environment}`);
345
+ const bindings = await this.getDatabaseBindings(environment);
346
+ if (bindings.length > 0) {
347
+ console.log(` D1 Databases (${bindings.length}):`);
348
+ bindings.forEach(db => {
349
+ console.log(` • ${db.binding}: ${db.database_name} (${db.database_id})`);
350
+ });
351
+ } else {
352
+ console.log(` D1 Databases: None configured`);
353
+ }
354
+ } else {
355
+ // Show all environments
356
+ if (config.env && Object.keys(config.env).length > 0) {
357
+ console.log(`\n Environments: ${Object.keys(config.env).join(', ')}`);
358
+ }
359
+ }
360
+ console.log('');
361
+ }
362
+ }
363
+ export default WranglerConfigManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamyla/clodo-framework",
3
- "version": "2.0.18",
3
+ "version": "2.0.20",
4
4
  "description": "Reusable framework for Clodo-style software architecture on Cloudflare Workers + D1",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -20,7 +20,8 @@
20
20
  "./handlers": "./dist/handlers/GenericRouteHandler.js",
21
21
  "./config": "./dist/config/index.js",
22
22
  "./config/discovery": "./dist/config/discovery/domain-discovery.js",
23
- "./config/customer-loader": "./dist/config/customer-config-loader.js",
23
+ "./config/customers": "./dist/config/customers.js",
24
+ "./utils/config": "./dist/utils/config/unified-config-manager.js",
24
25
  "./worker": "./dist/worker/index.js",
25
26
  "./utils": "./dist/utils/index.js",
26
27
  "./utils/deployment": "./dist/utils/deployment/index.js",
@@ -38,15 +39,13 @@
38
39
  "./service-management": "./dist/service-management/index.js",
39
40
  "./service-management/create": "./dist/service-management/ServiceCreator.js",
40
41
  "./service-management/init": "./dist/service-management/ServiceInitializer.js",
41
- "./config/cli": "./dist/config/CustomerConfigCLI.js",
42
42
  "./modules/security": "./dist/modules/security.js"
43
43
  },
44
44
  "bin": {
45
45
  "clodo-service": "./bin/clodo-service.js",
46
46
  "clodo-create-service": "./bin/service-management/create-service.js",
47
47
  "clodo-init-service": "./bin/service-management/init-service.js",
48
- "clodo-security": "./bin/security/security-cli.js",
49
- "clodo-customer-config": "./bin/shared/config/customer-cli.js"
48
+ "clodo-security": "./bin/security/security-cli.js"
50
49
  },
51
50
  "publishConfig": {
52
51
  "access": "public"
@@ -1,182 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Customer Configuration Management CLI
5
- * Manages multi-environment, multi-customer configuration structure
6
- * Integrates with Clodo Framework domain and feature flag systems
7
- */
8
-
9
- import { CustomerConfigCLI } from '../../../dist/config/CustomerConfigCLI.js';
10
- import { resolve } from 'path';
11
-
12
- // Parse command line arguments
13
- const argv = process.argv.slice(2);
14
- let configDir = null;
15
- let command = null;
16
- let args = [];
17
-
18
- // Extract --config-dir parameter if present
19
- for (let i = 0; i < argv.length; i++) {
20
- if (argv[i] === '--config-dir' && i + 1 < argv.length) {
21
- configDir = resolve(argv[i + 1]);
22
- i++; // Skip the next argument (the path)
23
- } else if (!command) {
24
- command = argv[i];
25
- } else {
26
- args.push(argv[i]);
27
- }
28
- }
29
-
30
- // Default to current working directory if not specified
31
- if (!configDir) {
32
- configDir = resolve(process.cwd(), 'config');
33
- }
34
-
35
- async function main() {
36
- const cli = new CustomerConfigCLI({ configDir });
37
- await cli.initialize();
38
-
39
- try {
40
- switch (command) {
41
- case 'create-customer':
42
- const [customerName, domain] = args;
43
- const result = await cli.createCustomer(customerName, domain);
44
- if (result.success) {
45
- console.log(`\n🎉 Customer ${customerName} configuration created successfully!`);
46
- console.log(`\n📋 Customer Details:`);
47
- console.log(` Name: ${result.customer.name}`);
48
- console.log(` Domain: ${result.customer.domain || 'Not specified'}`);
49
- console.log(` Config Path: ${result.customer.configPath}`);
50
- console.log(` Environments: ${result.customer.environments.join(', ')}`);
51
- console.log(`\n📋 Next steps:`);
52
- console.log(`1. Review generated configs in: config/customers/${customerName}/`);
53
- console.log(`2. Update domain-specific URLs if needed`);
54
- console.log(`3. Generate production secrets: npm run security:generate-key ${customerName}`);
55
- console.log(`4. Set production secrets: wrangler secret put KEY_NAME --env production`);
56
- } else {
57
- console.error(`❌ Failed to create customer: ${result.error}`);
58
- process.exit(1);
59
- }
60
- break;
61
-
62
- case 'validate':
63
- const validateResult = await cli.validateConfigurations();
64
- if (validateResult.valid) {
65
- console.log('✅ All customer configurations are valid');
66
- } else {
67
- console.log('❌ Configuration validation failed');
68
- validateResult.errors.forEach(error => console.log(` - ${error}`));
69
- process.exit(1);
70
- }
71
- break;
72
-
73
- case 'show':
74
- const [customerNameShow, environment] = args;
75
- const showResult = cli.showConfiguration(customerNameShow, environment);
76
- if (showResult.success) {
77
- console.log(`🔍 Effective configuration: ${customerNameShow}/${environment}\n`);
78
- if (showResult.config.variables?.base) {
79
- console.log('📋 Base variables:');
80
- Object.entries(showResult.config.variables.base).slice(0, 10).forEach(([key, value]) => {
81
- console.log(` ${key}=${value}`);
82
- });
83
- if (Object.keys(showResult.config.variables.base).length > 10) {
84
- console.log(' ...');
85
- }
86
- console.log('');
87
- }
88
- if (showResult.config.variables?.customer) {
89
- console.log(`📋 Customer ${environment} variables:`);
90
- Object.entries(showResult.config.variables.customer).slice(0, 15).forEach(([key, value]) => {
91
- console.log(` ${key}=${value}`);
92
- });
93
- if (Object.keys(showResult.config.variables.customer).length > 15) {
94
- console.log(' ...');
95
- }
96
- console.log('');
97
- }
98
- if (showResult.config.features && Object.keys(showResult.config.features).length > 0) {
99
- console.log('🚩 Customer features:');
100
- Object.entries(showResult.config.features).forEach(([feature, enabled]) => {
101
- console.log(` ${feature}: ${enabled ? '✅' : '❌'}`);
102
- });
103
- }
104
- } else {
105
- console.error(`❌ Failed to show configuration: ${showResult.error}`);
106
- process.exit(1);
107
- }
108
- break;
109
-
110
- case 'deploy-command':
111
- const [customerNameDeploy, environmentDeploy] = args;
112
- const deployResult = cli.getDeployCommand(customerNameDeploy, environmentDeploy);
113
- if (deployResult.success) {
114
- console.log(`📋 Deploy command for ${customerNameDeploy}/${environmentDeploy}:`);
115
- console.log(` ${deployResult.command}`);
116
- console.log(`\n💡 Ensure customer config is loaded: ${deployResult.configPath}`);
117
- } else {
118
- console.error(`❌ Failed to get deploy command: ${deployResult.error}`);
119
- process.exit(1);
120
- }
121
- break;
122
-
123
- case 'list':
124
- const listResult = cli.listCustomers();
125
- if (listResult.success && listResult.customers.length > 0) {
126
- console.log('📋 Configured customers:\n');
127
- listResult.customers.forEach(customer => {
128
- console.log(`🏢 ${customer.name}`);
129
- console.log(` Domain: ${customer.customerDomain || customer.domain || 'Not configured'}`);
130
- console.log(` Account ID: ${customer.accountId ? `${customer.accountId.substring(0, 8)}...${customer.accountId.substring(24)}` : 'Not configured'}`);
131
- console.log(` Zone ID: ${customer.zoneId ? `${customer.zoneId.substring(0, 8)}...` : 'Not configured'}`);
132
- if (customer.databaseId) {
133
- console.log(` Database: ${customer.databaseName || 'Unnamed'} (${customer.databaseId.substring(0, 8)}...)`);
134
- }
135
- console.log(` Secrets: ${customer.hasSecrets ? '✅ Managed via wrangler secret commands' : '❌ Not configured'}`);
136
- console.log(` Environments: ${customer.environments.join(', ')}`);
137
- console.log(` Config Path: config/customers/${customer.name}/`);
138
- console.log('');
139
- });
140
- } else if (listResult.success) {
141
- console.log('📋 No customers configured');
142
- console.log('\n💡 Tip: Run "clodo-customer-config create-customer <name>" to create your first customer');
143
- } else {
144
- console.error(`❌ Failed to list customers: ${listResult.error}`);
145
- process.exit(1);
146
- }
147
- break;
148
-
149
- default:
150
- console.log('Customer Configuration Management Tool\n');
151
- console.log('Usage:');
152
- console.log(' clodo-customer-config [--config-dir <path>] <command> [args]\n');
153
- console.log('Options:');
154
- console.log(' --config-dir <path> - Path to config directory (default: ./config)\n');
155
- console.log('Available commands:');
156
- console.log(' create-customer <name> [domain] - Create new customer config from template');
157
- console.log(' validate - Validate configuration structure');
158
- console.log(' show <customer> <environment> - Show effective configuration');
159
- console.log(' deploy-command <customer> <env> - Get deployment command');
160
- console.log(' list - List all configured customers');
161
- console.log('\nExamples:');
162
- console.log(' clodo-customer-config create-customer acmecorp acmecorp.com');
163
- console.log(' clodo-customer-config validate');
164
- console.log(' clodo-customer-config show acmecorp production');
165
- console.log(' clodo-customer-config list');
166
- console.log(' clodo-customer-config --config-dir /path/to/service/config validate');
167
- console.log('\nIntegration:');
168
- console.log(' This tool integrates with Clodo Framework domain and feature flag systems.');
169
- console.log(' Customer configurations are automatically registered as domains.');
170
- console.log(' When run from a service directory, it uses ./config by default.');
171
- break;
172
- }
173
- } catch (error) {
174
- console.error(`❌ Error: ${error.message}`);
175
- process.exit(1);
176
- }
177
- }
178
-
179
- main().catch(error => {
180
- console.error(`❌ Unexpected error: ${error.message}`);
181
- process.exit(1);
182
- });