@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 +14 -0
- package/dist/database/database-orchestrator.js +5 -0
- package/dist/lib/database/deployment-db-manager.js +2 -2
- package/dist/lib/shared/cloudflare/ops.js +74 -14
- package/dist/lib/shared/deployment/workflows/interactive-database-workflow.js +58 -14
- package/dist/lib/shared/deployment/workflows/interactive-deployment-coordinator.js +8 -4
- package/dist/lib/shared/deployment/workflows/interactive-domain-info-gatherer.js +16 -3
- package/dist/lib/shared/deployment/workflows/interactive-secret-workflow.js +15 -1
- package/dist/lib/shared/deployment/workflows/interactive-validation.js +142 -8
- package/dist/utils/cloudflare/api.js +16 -0
- package/package.json +2 -1
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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 (
|
|
111
|
-
console.log(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|