@tamyla/clodo-framework 4.0.2 → 4.0.4

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,17 @@
1
+ ## [4.0.4](https://github.com/tamylaa/clodo-framework/compare/v4.0.3...v4.0.4) (2025-12-08)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * enhance existing resource handling and API token propagation ([c7b3a22](https://github.com/tamylaa/clodo-framework/commit/c7b3a226b01609f01f6e8cb1cf27b957cd11868e))
7
+
8
+ ## [4.0.3](https://github.com/tamylaa/clodo-framework/compare/v4.0.2...v4.0.3) (2025-12-08)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * Export CLI utilities to enable framework CLI commands ([35f1701](https://github.com/tamylaa/clodo-framework/commit/35f1701653ebe01b8fe41d184d2b88dac4c5b8d4))
14
+
1
15
  ## [4.0.2](https://github.com/tamylaa/clodo-framework/compare/v4.0.1...v4.0.2) (2025-12-08)
2
16
 
3
17
 
@@ -682,6 +682,11 @@ export class DatabaseOrchestrator {
682
682
  } else {
683
683
  command += ` --local`;
684
684
  }
685
+
686
+ // Add API token if available
687
+ if (this.cloudflareToken) {
688
+ command = `CLOUDFLARE_API_TOKEN=${this.cloudflareToken} ${command}`;
689
+ }
685
690
  return command;
686
691
  }
687
692
  buildBackupCommand(databaseName, environment, backupFile, isRemote) {
@@ -200,10 +200,10 @@ export class DeploymentDatabaseManager {
200
200
  /**
201
201
  * Run database migrations
202
202
  */
203
- async runDatabaseMigrations(config) {
203
+ async runDatabaseMigrations(config, credentials = {}) {
204
204
  console.log('\nšŸ”„ Running database migrations...');
205
205
  try {
206
- await runMigrations(config.database.name);
206
+ await runMigrations(config.database.name, 'production', credentials);
207
207
  console.log(' āœ… Database migrations completed');
208
208
  this.state.migrationsRun = true;
209
209
  } catch (error) {
@@ -154,11 +154,28 @@ export async function getCloudflareToken(requiredPermissions = ['api_access']) {
154
154
  }
155
155
  throw new Error('No valid Cloudflare API token found with required permissions');
156
156
  }
157
- export async function listWorkers() {
157
+ export async function listWorkers(options = {}) {
158
+ const {
159
+ apiToken,
160
+ accountId
161
+ } = options;
162
+
163
+ // Use API-based operation if credentials provided
164
+ if (apiToken && accountId) {
165
+ const {
166
+ CloudflareAPI
167
+ } = await import('@tamyla/clodo-framework/utils/cloudflare');
168
+ const cf = new CloudflareAPI(apiToken);
169
+ return await cf.listWorkers(accountId);
170
+ }
171
+
172
+ // Fallback to CLI-based operation
158
173
  try {
174
+ const envVars = apiToken ? `CLOUDFLARE_API_TOKEN=${apiToken}` : '';
175
+ const command = `${envVars} npx wrangler list`;
159
176
  const {
160
177
  stdout: list
161
- } = await executeWithRateLimit('npx wrangler list', 'workers');
178
+ } = await executeWithRateLimit(command, 'workers');
162
179
  return list;
163
180
  } catch (error) {
164
181
  throw new Error(`Failed to list workers: ${error.message}`);
@@ -207,26 +224,57 @@ export async function deployWorker(env = 'production') {
207
224
  throw new Error(`Worker deployment failed after retries: ${error.message}`);
208
225
  }
209
226
  }
210
- export async function deploySecret(key, value, env = 'production') {
211
- const command = process.platform === 'win32' ? `powershell -Command "Write-Output '${value}' | npx wrangler secret put ${key} --env ${env}"` : `echo "${value}" | npx wrangler secret put ${key} --env ${env}`;
227
+ export async function deploySecret(key, value, env = 'production', options = {}) {
228
+ const {
229
+ apiToken,
230
+ accountId,
231
+ scriptName
232
+ } = options;
233
+
234
+ // Use API-based operation if credentials provided
235
+ if (apiToken && accountId && scriptName) {
236
+ const {
237
+ CloudflareAPI
238
+ } = await import('@tamyla/clodo-framework/utils/cloudflare');
239
+ const cf = new CloudflareAPI(apiToken);
240
+ await cf.putWorkerSecret(accountId, scriptName, key, {
241
+ text: value
242
+ });
243
+ return;
244
+ }
245
+
246
+ // Fallback to CLI-based operation with API token if available
247
+ const envVars = apiToken ? `CLOUDFLARE_API_TOKEN=${apiToken}` : '';
248
+ const command = process.platform === 'win32' ? `${envVars} powershell -Command "Write-Output '${value}' | npx wrangler secret put ${key} --env ${env}"` : `${envVars} echo "${value}" | npx wrangler secret put ${key} --env ${env}`;
212
249
  try {
213
- await executeWithRateLimit(command, 'workers', 3); // Lower retries for secrets
250
+ // Increase retries for secrets due to higher timeout likelihood
251
+ await executeWithRateLimit(command, 'workers', 5);
214
252
  } catch (error) {
215
253
  throw new Error(`Secret deployment failed: ${error.message}`);
216
254
  }
217
255
  }
218
- export async function deleteSecret(key, env = 'production') {
256
+ export async function deleteSecret(key, env = 'production', options = {}) {
257
+ const {
258
+ apiToken
259
+ } = options;
260
+ const envVars = apiToken ? `CLOUDFLARE_API_TOKEN=${apiToken}` : '';
261
+ const command = `${envVars} npx wrangler secret delete ${key} --env ${env}`;
219
262
  try {
220
- await executeWithRateLimit(`npx wrangler secret delete ${key} --env ${env}`, 'workers');
263
+ await executeWithRateLimit(command, 'workers');
221
264
  } catch (error) {
222
265
  throw new Error(`Secret deletion failed: ${error.message}`);
223
266
  }
224
267
  }
225
- export async function listSecrets(env = 'production') {
268
+ export async function listSecrets(env = 'production', options = {}) {
269
+ const {
270
+ apiToken
271
+ } = options;
272
+ const envVars = apiToken ? `CLOUDFLARE_API_TOKEN=${apiToken}` : '';
273
+ const command = `${envVars} npx wrangler secret list --env ${env}`;
226
274
  try {
227
275
  const {
228
276
  stdout: list
229
- } = await executeWithRateLimit(`npx wrangler secret list --env ${env}`, 'workers');
277
+ } = await executeWithRateLimit(command, 'workers');
230
278
  return list;
231
279
  } catch (error) {
232
280
  throw new Error(`Failed to list secrets: ${error.message}`);
@@ -298,9 +346,11 @@ export async function createDatabase(name, options = {}) {
298
346
 
299
347
  // Fallback to CLI-based operation
300
348
  try {
349
+ const envVars = apiToken ? `CLOUDFLARE_API_TOKEN=${apiToken}` : '';
350
+ const command = `${envVars} npx wrangler d1 create ${name}`;
301
351
  const {
302
352
  stdout: output
303
- } = await executeWithRateLimit(`npx wrangler d1 create ${name}`, 'd1');
353
+ } = await executeWithRateLimit(command, 'd1');
304
354
  const idMatch = output.match(/database_id = "([^"]+)"/);
305
355
  if (!idMatch) {
306
356
  throw new Error('Could not extract database ID from creation output');
@@ -310,19 +360,29 @@ export async function createDatabase(name, options = {}) {
310
360
  throw new Error(`Database creation failed: ${error.message}`);
311
361
  }
312
362
  }
313
- export async function deleteDatabase(name) {
363
+ export async function deleteDatabase(name, options = {}) {
364
+ const {
365
+ apiToken
366
+ } = options;
367
+ const envVars = apiToken ? `CLOUDFLARE_API_TOKEN=${apiToken}` : '';
368
+ const command = `${envVars} npx wrangler d1 delete ${name} --skip-confirmation`;
314
369
  try {
315
- await executeWithRateLimit(`npx wrangler d1 delete ${name} --skip-confirmation`, 'd1');
370
+ await executeWithRateLimit(command, 'd1');
316
371
  } catch (error) {
317
372
  throw new Error(`Database deletion failed: ${error.message}`);
318
373
  }
319
374
  }
320
- export async function runMigrations(databaseName, env = 'production') {
375
+ export async function runMigrations(databaseName, env = 'production', options = {}) {
376
+ const {
377
+ apiToken
378
+ } = options;
321
379
  const startTime = Date.now();
322
380
  try {
323
381
  await ensureMonitoringInitialized();
324
382
  const result = await errorRecovery.executeWithRecovery(async () => {
325
- await executeWithRateLimit(`npx wrangler d1 migrations apply ${databaseName} --env ${env} --remote`, 'd1');
383
+ const envVars = apiToken ? `CLOUDFLARE_API_TOKEN=${apiToken}` : '';
384
+ const command = `${envVars} npx wrangler d1 migrations apply ${databaseName} --env ${env} --remote`;
385
+ await executeWithRateLimit(command, 'd1');
326
386
  return true;
327
387
  }, {
328
388
  operationId: `runMigrations_${databaseName}_${env}`
@@ -21,9 +21,11 @@ export class InteractiveDatabaseWorkflow {
21
21
  /**
22
22
  * @param {Object} options - Configuration options
23
23
  * @param {Array} options.rollbackActions - Array to track rollback actions
24
+ * @param {boolean} options.dryRun - Dry run mode
24
25
  */
25
26
  constructor(options = {}) {
26
27
  this.rollbackActions = options.rollbackActions || [];
28
+ this.dryRun = options.dryRun || false;
27
29
  }
28
30
 
29
31
  /**
@@ -49,9 +51,15 @@ export class InteractiveDatabaseWorkflow {
49
51
  const databaseName = await this.promptForDatabaseName(suggestedName, options.interactive);
50
52
 
51
53
  // Check if database exists
52
- const existingInfo = await this.checkExistingDatabase(databaseName);
54
+ const existingInfo = await this.checkExistingDatabase(databaseName, {
55
+ apiToken: options.apiToken,
56
+ accountId: options.accountId
57
+ });
53
58
  if (existingInfo.exists) {
54
- return await this.handleExistingDatabase(databaseName, existingInfo, options.interactive);
59
+ return await this.handleExistingDatabase(databaseName, existingInfo, options.interactive, {
60
+ apiToken: options.apiToken,
61
+ accountId: options.accountId
62
+ });
55
63
  } else {
56
64
  return await this.createNewDatabase(databaseName, options.interactive, {
57
65
  apiToken: options.apiToken,
@@ -85,16 +93,36 @@ export class InteractiveDatabaseWorkflow {
85
93
  * @param {string} name - Database name
86
94
  * @returns {Promise<Object>} { exists: boolean, id?: string, error?: string }
87
95
  */
88
- async checkExistingDatabase(name) {
96
+ async checkExistingDatabase(name, credentials = {}) {
89
97
  console.log('\nšŸ” Checking for existing database...');
90
98
  try {
91
- const exists = await databaseExists(name);
99
+ const exists = await databaseExists(name, credentials);
92
100
  if (exists) {
93
101
  console.log(` šŸ“‹ Database '${name}' already exists`);
94
102
 
95
- // Extract database ID from the list command
103
+ // If using API, get the database ID from API
104
+ if (credentials.apiToken && credentials.accountId) {
105
+ try {
106
+ const {
107
+ CloudflareAPI
108
+ } = await import('@tamyla/clodo-framework/utils/cloudflare');
109
+ const cf = new CloudflareAPI(credentials.apiToken);
110
+ const dbInfo = await cf.getD1Database(credentials.accountId, name);
111
+ return {
112
+ exists: true,
113
+ id: dbInfo.uuid
114
+ };
115
+ } catch (apiError) {
116
+ console.log(` āš ļø Could not get database ID from API: ${apiError.message}`);
117
+ // Fall back to CLI method for ID extraction
118
+ }
119
+ }
120
+
121
+ // Extract database ID from the CLI list command (fallback)
96
122
  try {
97
- const dbListResult = await execAsync('npx wrangler d1 list');
123
+ const envVars = credentials.apiToken ? `CLOUDFLARE_API_TOKEN=${credentials.apiToken}` : '';
124
+ const command = `${envVars} npx wrangler d1 list`;
125
+ const dbListResult = await execAsync(command);
98
126
  const lines = dbListResult.stdout.split('\n');
99
127
  for (const line of lines) {
100
128
  if (line.includes(name)) {
@@ -107,11 +135,14 @@ export class InteractiveDatabaseWorkflow {
107
135
  }
108
136
  }
109
137
  }
110
- } catch (error) {
111
- console.log(' āš ļø Could not extract database ID');
138
+ } catch (cliError) {
139
+ console.log(` āš ļø Could not extract database ID: ${cliError.message}`);
112
140
  }
141
+
142
+ // Return exists=true but no ID if we can't get it
113
143
  return {
114
- exists: true
144
+ exists: true,
145
+ id: null
115
146
  };
116
147
  } else {
117
148
  console.log(` āœ… Database '${name}' does not exist`);
@@ -120,10 +151,10 @@ export class InteractiveDatabaseWorkflow {
120
151
  };
121
152
  }
122
153
  } catch (error) {
123
- console.log(' āš ļø Could not check existing databases');
154
+ console.log(` āš ļø Error checking database existence: ${error.message}`);
155
+ console.log(' ā„¹ļø Assuming database does not exist');
124
156
  return {
125
- exists: false,
126
- error: error.message
157
+ exists: false
127
158
  };
128
159
  }
129
160
  }
@@ -134,9 +165,10 @@ export class InteractiveDatabaseWorkflow {
134
165
  * @param {string} name - Database name
135
166
  * @param {Object} existingInfo - Information about existing database
136
167
  * @param {boolean} interactive - Enable interactive prompts
168
+ * @param {Object} credentials - Cloudflare API credentials
137
169
  * @returns {Promise<Object>} Database configuration
138
170
  */
139
- async handleExistingDatabase(name, existingInfo, interactive = true) {
171
+ async handleExistingDatabase(name, existingInfo, interactive = true, credentials = {}) {
140
172
  if (!interactive) {
141
173
  // Non-interactive mode: reuse existing
142
174
  console.log(` āœ… Using existing database: ${name}`);
@@ -176,7 +208,7 @@ export class InteractiveDatabaseWorkflow {
176
208
  throw new Error('Database deletion cancelled');
177
209
  }
178
210
  console.log(`\nšŸ—‘ļø Deleting existing database...`);
179
- await deleteDatabase(name);
211
+ await deleteDatabase(name, credentials);
180
212
  return await this.createNewDatabase(name, interactive);
181
213
  }
182
214
  default:
@@ -197,6 +229,18 @@ export class InteractiveDatabaseWorkflow {
197
229
  async createNewDatabase(name, interactive = true, credentials = {}) {
198
230
  console.log(`\nšŸ†• Creating new database: ${name}`);
199
231
 
232
+ // Check dry-run mode
233
+ if (this.dryRun) {
234
+ console.log(' šŸ” DRY RUN: Would create database but skipping actual creation');
235
+ return {
236
+ name,
237
+ id: 'dry-run-database-id',
238
+ created: false,
239
+ reused: false,
240
+ dryRun: true
241
+ };
242
+ }
243
+
200
244
  // Log which method will be used
201
245
  if (credentials.apiToken && credentials.accountId) {
202
246
  console.log(' ā„¹ļø Using Cloudflare API for database creation');
@@ -68,10 +68,13 @@ export class InteractiveDeploymentCoordinator {
68
68
  configCache: null // TODO: Add config cache if available
69
69
  }),
70
70
  databaseWorkflow: new InteractiveDatabaseWorkflow({
71
- interactive: true
71
+ interactive: true,
72
+ dryRun: this.options.dryRun
72
73
  }),
73
74
  secretWorkflow: new InteractiveSecretWorkflow({
74
- interactive: true
75
+ interactive: true,
76
+ dryRun: this.options.dryRun,
77
+ credentials: this.options.credentials
75
78
  }),
76
79
  validation: new InteractiveValidationWorkflow({
77
80
  interactive: true
@@ -170,9 +173,10 @@ export class InteractiveDeploymentCoordinator {
170
173
  async configureSecrets() {
171
174
  console.log('šŸ” Phase 3: Configuring Secrets & Credentials');
172
175
  console.log('─'.repeat(50));
173
- const workerName = `${this.deploymentState.config.domain}-data-service`;
176
+ const workerName = this.deploymentState.config.worker?.name || `${this.deploymentState.config.domain}-data-service`;
174
177
  this.deploymentState.resources.secrets = await this.workflows.secretWorkflow.handleSecretManagement(this.deploymentState.config.domain, this.deploymentState.config.environment, workerName, {
175
- interactive: true
178
+ interactive: true,
179
+ credentials: this.deploymentState.config.credentials
176
180
  });
177
181
  console.log('āœ… Secrets configuration complete\n');
178
182
  }
@@ -92,7 +92,10 @@ export class InteractiveDomainInfoGatherer {
92
92
  // Generate default worker configuration
93
93
  config.worker = config.worker || {};
94
94
  config.worker.name = `${config.domain}-data-service`;
95
- config.worker.url = `https://${config.worker.name}.tamylatrading.workers.dev`;
95
+
96
+ // Use actual zone name from credentials if available, otherwise fallback
97
+ const zoneName = config.credentials?.zoneName || 'tamylatrading.workers.dev';
98
+ config.worker.url = `https://${config.worker.name}.${zoneName}`;
96
99
  console.log(`\nšŸ”§ Generated Configuration:`);
97
100
  console.log(` Worker Name: ${config.worker.name}`);
98
101
  console.log(` Worker URL: ${config.worker.url}`);
@@ -101,8 +104,18 @@ export class InteractiveDomainInfoGatherer {
101
104
  }
102
105
  const confirmWorkerConfig = await askYesNo('Is this worker configuration correct?', 'y');
103
106
  if (!confirmWorkerConfig) {
104
- config.worker.name = await askUser('Enter custom worker name', config.worker.name);
105
- config.worker.url = `https://${config.worker.name}.tamylatrading.workers.dev`;
107
+ let customWorkerName = await askUser('Enter custom worker name', config.worker.name);
108
+
109
+ // Validate worker name - should not be a full URL
110
+ if (customWorkerName.includes('.') || customWorkerName.includes('/') || customWorkerName.includes('://')) {
111
+ console.log(' āš ļø Worker name should be just the name, not a full URL');
112
+ console.log(' šŸ’” Example: "my-service" not "my-service.workers.dev"');
113
+ customWorkerName = customWorkerName.split('.')[0]; // Extract just the first part
114
+ console.log(` šŸ”§ Using extracted name: ${customWorkerName}`);
115
+ }
116
+ config.worker.name = customWorkerName;
117
+ const zoneName = config.credentials?.zoneName || 'tamylatrading.workers.dev';
118
+ config.worker.url = `https://${config.worker.name}.${zoneName}`;
106
119
  console.log(`\nāœ… Updated worker configuration`);
107
120
  }
108
121
  }
@@ -21,9 +21,13 @@ export class InteractiveSecretWorkflow {
21
21
  /**
22
22
  * @param {Object} options - Configuration options
23
23
  * @param {Array} options.rollbackActions - Array to track rollback actions
24
+ * @param {boolean} options.dryRun - Dry run mode
25
+ * @param {Object} options.credentials - Cloudflare credentials
24
26
  */
25
27
  constructor(options = {}) {
26
28
  this.rollbackActions = options.rollbackActions || [];
29
+ this.dryRun = options.dryRun || false;
30
+ this.credentials = options.credentials || {};
27
31
  }
28
32
 
29
33
  /**
@@ -161,6 +165,12 @@ export class InteractiveSecretWorkflow {
161
165
  */
162
166
  async deploySecrets(secrets, workerName, environment, interactive = true) {
163
167
  console.log('\nā˜ļø Deploying secrets to Cloudflare Workers...');
168
+
169
+ // Check dry-run mode
170
+ if (this.dryRun) {
171
+ console.log(` šŸ” DRY RUN: Would deploy ${Object.keys(secrets).length} secrets to worker '${workerName}' but skipping actual deployment`);
172
+ return;
173
+ }
164
174
  if (interactive) {
165
175
  const deployConfirmed = await askYesNo(`Deploy ${Object.keys(secrets).length} secrets to worker '${workerName}'?`, 'y');
166
176
  if (!deployConfirmed) {
@@ -170,7 +180,11 @@ export class InteractiveSecretWorkflow {
170
180
  for (const [key, value] of Object.entries(secrets)) {
171
181
  console.log(` šŸ”‘ Deploying ${key}...`);
172
182
  try {
173
- await deploySecret(key, value, environment);
183
+ await deploySecret(key, value, environment, {
184
+ apiToken: this.credentials?.token,
185
+ accountId: this.credentials?.accountId,
186
+ scriptName: workerName
187
+ });
174
188
  console.log(` āœ… ${key} deployed`);
175
189
 
176
190
  // Add to rollback actions
@@ -35,8 +35,8 @@ export class InteractiveValidationWorkflow {
35
35
  // Check prerequisites
36
36
  await this.validatePrerequisites();
37
37
 
38
- // Check authentication
39
- await this.validateAuthentication();
38
+ // Check authentication - pass existing credentials if available
39
+ await this.validateAuthentication(config);
40
40
 
41
41
  // Check for existing deployments
42
42
  await this.checkExistingDeployments(config);
@@ -68,9 +68,16 @@ export class InteractiveValidationWorkflow {
68
68
  /**
69
69
  * Validate Cloudflare authentication
70
70
  *
71
+ * @param {Object} config - Deployment configuration with credentials
71
72
  * @returns {Promise<void>}
72
73
  */
73
- async validateAuthentication() {
74
+ async validateAuthentication(config = {}) {
75
+ // If credentials are already collected, skip authentication check
76
+ if (config.credentials?.token && config.credentials?.accountId) {
77
+ console.log('\nšŸ” Using collected Cloudflare credentials...');
78
+ console.log('āœ… Authentication validated via deployment workflow');
79
+ return;
80
+ }
74
81
  console.log('\nšŸ” Checking Cloudflare authentication...');
75
82
  const isAuthenticated = await checkAuth();
76
83
  if (!isAuthenticated) {
@@ -104,17 +111,144 @@ export class InteractiveValidationWorkflow {
104
111
  console.log(' ā„¹ļø Non-interactive mode: will overwrite existing worker');
105
112
  return true;
106
113
  }
107
- const shouldOverwrite = await askYesNo('Do you want to overwrite the existing worker?', 'n');
108
- if (!shouldOverwrite) {
109
- throw new Error('Deployment cancelled - worker already exists');
110
- }
111
- return true;
114
+ return await this.handleExistingWorker(config);
112
115
  } else {
113
116
  console.log(` āœ… Worker name '${config.worker.name}' is available`);
114
117
  return false;
115
118
  }
116
119
  }
117
120
 
121
+ /**
122
+ * Handle existing worker with multiple options
123
+ *
124
+ * @param {Object} config - Deployment configuration
125
+ * @returns {Promise<boolean>} True if deployment should proceed
126
+ */
127
+ async handleExistingWorker(config) {
128
+ console.log('\nšŸ”„ Existing Worker Options:');
129
+ console.log('===========================');
130
+ const {
131
+ askChoice
132
+ } = await import('../utils/prompt-utils.js');
133
+ const choice = await askChoice('How would you like to handle the existing worker?', ['šŸ”„ Overwrite/Update - Deploy new version (recommended)', 'šŸ“ Rename - Create with a different worker name', 'šŸ” Compare - Show differences before deciding', 'šŸ’¾ Backup & Update - Create backup before overwriting', 'āŒ Cancel - Stop deployment'], 0);
134
+ switch (choice) {
135
+ case 0:
136
+ // Overwrite/Update
137
+ console.log(' āœ… Will overwrite existing worker with new deployment');
138
+ return true;
139
+ case 1:
140
+ // Rename
141
+ const newName = await this.promptForNewWorkerName(config.worker.name);
142
+ config.worker.name = newName;
143
+ config.worker.url = `https://${newName}.${config.credentials?.zoneName || 'tamylatrading.workers.dev'}`;
144
+ console.log(` āœ… Will deploy as new worker: ${newName}`);
145
+ return true;
146
+ case 2:
147
+ // Compare
148
+ await this.compareExistingWorker(config);
149
+ // After comparison, ask again
150
+ return await this.handleExistingWorker(config);
151
+ case 3:
152
+ // Backup & Update
153
+ await this.backupExistingWorker(config);
154
+ console.log(' āœ… Backup created, will now overwrite existing worker');
155
+ return true;
156
+ case 4: // Cancel
157
+ default:
158
+ throw new Error('Deployment cancelled by user');
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Prompt for a new worker name
164
+ *
165
+ * @param {string} currentName - Current worker name
166
+ * @returns {Promise<string>} New worker name
167
+ */
168
+ async promptForNewWorkerName(currentName) {
169
+ const {
170
+ askUser
171
+ } = await import('../utils/prompt-utils.js');
172
+ let newName = await askUser(`Enter new worker name (current: ${currentName})`);
173
+ newName = newName.trim();
174
+ if (!newName) {
175
+ throw new Error('Worker name cannot be empty');
176
+ }
177
+
178
+ // Basic validation
179
+ if (newName.length < 3) {
180
+ throw new Error('Worker name must be at least 3 characters long');
181
+ }
182
+ if (!/^[a-zA-Z0-9_-]+$/.test(newName)) {
183
+ throw new Error('Worker name can only contain letters, numbers, hyphens, and underscores');
184
+ }
185
+
186
+ // Check if the new name also exists
187
+ const exists = await workerExists(newName);
188
+ if (exists) {
189
+ console.log(` āš ļø Worker '${newName}' also exists!`);
190
+ const useAnyway = await askUser('Use this name anyway? (y/n)', 'n');
191
+ if (useAnyway.toLowerCase() !== 'y' && useAnyway.toLowerCase() !== 'yes') {
192
+ return await this.promptForNewWorkerName(currentName);
193
+ }
194
+ }
195
+ return newName;
196
+ }
197
+
198
+ /**
199
+ * Compare existing worker configuration
200
+ *
201
+ * @param {Object} config - Deployment configuration
202
+ */
203
+ async compareExistingWorker(config) {
204
+ console.log('\nšŸ” Comparing Worker Configurations:');
205
+ console.log('=====================================');
206
+ try {
207
+ // This is a simplified comparison - in a real implementation,
208
+ // you might fetch the existing worker's configuration
209
+ console.log(` šŸ“‹ Existing Worker: ${config.worker.name}`);
210
+ console.log(` 🌐 URL: https://${config.worker.name}.${config.credentials?.zoneName || 'tamylatrading.workers.dev'}`);
211
+ console.log(` šŸ“… Last deployed: Unknown (would need API call to check)`);
212
+ console.log(` šŸ”§ Environment: ${config.environment || 'production'}`);
213
+ console.log('\n šŸ“‹ New Deployment:');
214
+ console.log(` šŸ“‹ Worker: ${config.worker.name}`);
215
+ console.log(` 🌐 URL: ${config.worker.url}`);
216
+ console.log(` šŸ”§ Environment: ${config.environment || 'production'}`);
217
+ console.log(` šŸ—„ļø Database: ${config.database?.name || 'None'}`);
218
+ console.log(` šŸ” Secrets: ${Object.keys(config.secrets || {}).length} configured`);
219
+ console.log('\n ā„¹ļø Note: Full comparison would require additional API calls to fetch existing worker details');
220
+ } catch (error) {
221
+ console.log(` āš ļø Could not compare configurations: ${error.message}`);
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Create a backup of existing worker
227
+ *
228
+ * @param {Object} config - Deployment configuration
229
+ */
230
+ async backupExistingWorker(config) {
231
+ console.log('\nšŸ’¾ Creating Worker Backup:');
232
+ console.log('===========================');
233
+ try {
234
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
235
+ const backupName = `${config.worker.name}-backup-${timestamp}`;
236
+ console.log(` šŸ“‹ Creating backup worker: ${backupName}`);
237
+
238
+ // Note: This is a placeholder - actual backup would require
239
+ // downloading the existing worker code and creating a new worker
240
+ // This would be complex and might not be worth implementing
241
+ // since Cloudflare doesn't provide direct worker code access
242
+
243
+ console.log(` āš ļø Worker backup not fully implemented yet`);
244
+ console.log(` šŸ’” Consider manually backing up important worker code before overwriting`);
245
+ console.log(` šŸ’” You can also use version control to track changes`);
246
+ } catch (error) {
247
+ console.log(` āš ļø Backup failed: ${error.message}`);
248
+ console.log(` šŸ”„ Continuing with deployment anyway...`);
249
+ }
250
+ }
251
+
118
252
  /**
119
253
  * Execute comprehensive validation
120
254
  *
@@ -242,6 +242,22 @@ export class CloudflareAPI {
242
242
  }));
243
243
  }
244
244
 
245
+ /**
246
+ * Put a secret for a worker
247
+ * @param {string} accountId - Account ID
248
+ * @param {string} scriptName - Worker script name
249
+ * @param {string} secretName - Secret name
250
+ * @param {Object} secretValue - Secret value object with 'text' property
251
+ * @returns {Promise<Object>} API response
252
+ */
253
+ async putWorkerSecret(accountId, scriptName, secretName, secretValue) {
254
+ const data = await this.request(`/accounts/${accountId}/workers/scripts/${scriptName}/secrets/${secretName}`, {
255
+ method: 'PUT',
256
+ body: JSON.stringify(secretValue)
257
+ });
258
+ return data;
259
+ }
260
+
245
261
  /**
246
262
  * List D1 databases for an account
247
263
  * @param {string} accountId - Account ID
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamyla/clodo-framework",
3
- "version": "4.0.2",
3
+ "version": "4.0.4",
4
4
  "description": "Reusable framework for Clodo-style software architecture on Cloudflare Workers + D1",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -100,6 +100,7 @@
100
100
  "check:bundle": "node scripts/utilities/check-bundle.js || echo 'Bundle check passed'",
101
101
  "check:imports": "node scripts/utilities/check-import-paths.js",
102
102
  "check:all": "npm run type-check && npm run test",
103
+ "diagnose": "node scripts/framework-diagnostic.js",
103
104
  "analyze:bundle": "echo 'Bundle analysis not implemented yet'",
104
105
  "docs": "echo 'JSDoc documentation generation not configured yet'",
105
106
  "validate": "npm run check:all",