@tamyla/clodo-framework 2.0.8 → 2.0.9
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 +20 -0
- package/bin/clodo-service.js +25 -10
- package/dist/config/ConfigurationManager.js +1 -1
- package/dist/orchestration/multi-domain-orchestrator.js +20 -15
- package/dist/service-management/InputCollector.js +93 -5
- package/dist/utils/deployment/index.js +1 -1
- package/package.json +1 -1
- package/dist/utils/deployment/interactive-prompts.js +0 -97
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## [2.0.9](https://github.com/tamylaa/clodo-framework/compare/v2.0.8...v2.0.9) (2025-10-12)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* re-release v2.0.8 features that were missing from npm package ([33cf712](https://github.com/tamylaa/clodo-framework/commit/33cf71267c07610e04bd8c752db4385c6f5ca603))
|
|
7
|
+
|
|
8
|
+
## [2.0.8](https://github.com/tamylaa/clodo-framework/compare/v2.0.7...v2.0.8) (2025-10-12)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* clean up skipped security validation test with proper TODO ([722afbe](https://github.com/tamylaa/clodo-framework/commit/722afbe056be6f8ccb623acbbd3ab9ac4ce75caa))
|
|
14
|
+
* Enhanced customer config, CloudflareAPI utility, and code consolidation ([447ed9b](https://github.com/tamylaa/clodo-framework/commit/447ed9b5d8a2bb806386fc334e2e8bf4efeff43b))
|
|
15
|
+
* implement missing deployment phase methods in MultiDomainOrchestrator ([b0cb1e8](https://github.com/tamylaa/clodo-framework/commit/b0cb1e828679df61d9ebde2551599f47ddeb20d2))
|
|
16
|
+
* integrate CloudflareAPI auto-discovery and cleanup duplicates ([8166d18](https://github.com/tamylaa/clodo-framework/commit/8166d189bbe1e5b78db8f24d56221f0e18e72021))
|
|
17
|
+
* resolve ESLint no-undef errors in cloudflare index.js ([1a2bb7f](https://github.com/tamylaa/clodo-framework/commit/1a2bb7fa175bab142f3d5be451cb44ef1b9d8747))
|
|
18
|
+
|
|
1
19
|
## [2.0.8](https://github.com/tamylaa/clodo-framework/compare/v2.0.7...v2.0.8) (2025-10-12)
|
|
2
20
|
|
|
3
21
|
|
|
@@ -5,6 +23,8 @@
|
|
|
5
23
|
|
|
6
24
|
* clean up skipped security validation test with proper TODO ([722afbe](https://github.com/tamylaa/clodo-framework/commit/722afbe056be6f8ccb623acbbd3ab9ac4ce75caa))
|
|
7
25
|
* Enhanced customer config, CloudflareAPI utility, and code consolidation ([447ed9b](https://github.com/tamylaa/clodo-framework/commit/447ed9b5d8a2bb806386fc334e2e8bf4efeff43b))
|
|
26
|
+
* implement missing deployment phase methods in MultiDomainOrchestrator ([b0cb1e8](https://github.com/tamylaa/clodo-framework/commit/b0cb1e828679df61d9ebde2551599f47ddeb20d2))
|
|
27
|
+
* integrate CloudflareAPI auto-discovery and cleanup duplicates ([8166d18](https://github.com/tamylaa/clodo-framework/commit/8166d189bbe1e5b78db8f24d56221f0e18e72021))
|
|
8
28
|
* resolve ESLint no-undef errors in cloudflare index.js ([1a2bb7f](https://github.com/tamylaa/clodo-framework/commit/1a2bb7fa175bab142f3d5be451cb44ef1b9d8747))
|
|
9
29
|
|
|
10
30
|
## [2.0.7](https://github.com/tamylaa/clodo-framework/compare/v2.0.6...v2.0.7) (2025-10-12)
|
package/bin/clodo-service.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Clodo Framework - Unified Three-Tier Service Management CLI
|
|
@@ -479,16 +479,31 @@ program
|
|
|
479
479
|
if (!coreInputs.cloudflareAccountId) {
|
|
480
480
|
console.log(chalk.cyan('📊 Tier 1: Core Input Collection\n'));
|
|
481
481
|
|
|
482
|
-
//
|
|
482
|
+
// Collect basic info
|
|
483
|
+
const customer = options.customer || await inputCollector.question('Customer name: ');
|
|
484
|
+
const environment = options.env || await inputCollector.collectEnvironment();
|
|
485
|
+
const serviceName = await inputCollector.collectServiceName();
|
|
486
|
+
const serviceType = await inputCollector.collectServiceType();
|
|
487
|
+
|
|
488
|
+
// Collect Cloudflare token
|
|
489
|
+
const cloudflareToken = process.env.CLOUDFLARE_API_TOKEN || await inputCollector.collectCloudflareToken();
|
|
490
|
+
|
|
491
|
+
// Use CloudflareAPI for automatic domain discovery
|
|
492
|
+
const cloudflareConfig = await inputCollector.collectCloudflareConfigWithDiscovery(
|
|
493
|
+
cloudflareToken,
|
|
494
|
+
options.domain
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
// Combine all inputs
|
|
483
498
|
coreInputs = {
|
|
484
|
-
customer
|
|
485
|
-
environment
|
|
486
|
-
serviceName
|
|
487
|
-
serviceType
|
|
488
|
-
domainName:
|
|
489
|
-
cloudflareToken
|
|
490
|
-
cloudflareAccountId:
|
|
491
|
-
cloudflareZoneId:
|
|
499
|
+
customer,
|
|
500
|
+
environment,
|
|
501
|
+
serviceName,
|
|
502
|
+
serviceType,
|
|
503
|
+
domainName: cloudflareConfig.domainName,
|
|
504
|
+
cloudflareToken,
|
|
505
|
+
cloudflareAccountId: cloudflareConfig.accountId,
|
|
506
|
+
cloudflareZoneId: cloudflareConfig.zoneId
|
|
492
507
|
};
|
|
493
508
|
|
|
494
509
|
source = 'interactive';
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* User input-driven configuration setup for deployment workflows
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { askChoice, askUser } from '
|
|
6
|
+
import { askChoice, askUser } from '../../bin/shared/utils/interactive-prompts.js';
|
|
7
7
|
export class InteractiveDeploymentConfigurator {
|
|
8
8
|
/**
|
|
9
9
|
* Generate configuration from user input
|
|
@@ -189,43 +189,48 @@ export class MultiDomainOrchestrator {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
/**
|
|
192
|
-
*
|
|
193
|
-
* @deprecated Use deploymentCoordinator.initializeDomainDeployment() instead
|
|
192
|
+
* Initialize domain deployment (placeholder implementation)
|
|
194
193
|
*/
|
|
195
194
|
async initializeDomainDeployment(domain) {
|
|
196
|
-
|
|
195
|
+
// Placeholder: Add actual initialization logic here
|
|
196
|
+
console.log(` 🔧 Initializing deployment for ${domain}`);
|
|
197
|
+
return true;
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
/**
|
|
200
|
-
*
|
|
201
|
-
* @deprecated Use deploymentCoordinator.setupDomainDatabase() instead
|
|
201
|
+
* Setup domain database (placeholder implementation)
|
|
202
202
|
*/
|
|
203
203
|
async setupDomainDatabase(domain) {
|
|
204
|
-
|
|
204
|
+
// Placeholder: Add actual database setup logic here
|
|
205
|
+
console.log(` 🗄️ Setting up database for ${domain}`);
|
|
206
|
+
return true;
|
|
205
207
|
}
|
|
206
208
|
|
|
207
209
|
/**
|
|
208
|
-
*
|
|
209
|
-
* @deprecated Use deploymentCoordinator.handleDomainSecrets() instead
|
|
210
|
+
* Handle domain secrets (placeholder implementation)
|
|
210
211
|
*/
|
|
211
212
|
async handleDomainSecrets(domain) {
|
|
212
|
-
|
|
213
|
+
// Placeholder: Add actual secrets handling logic here
|
|
214
|
+
console.log(` 🔐 Handling secrets for ${domain}`);
|
|
215
|
+
return true;
|
|
213
216
|
}
|
|
214
217
|
|
|
215
218
|
/**
|
|
216
|
-
*
|
|
217
|
-
* @deprecated Use deploymentCoordinator.deployDomainWorker() instead
|
|
219
|
+
* Deploy domain worker (placeholder implementation)
|
|
218
220
|
*/
|
|
219
221
|
async deployDomainWorker(domain) {
|
|
220
|
-
|
|
222
|
+
// Placeholder: Add actual worker deployment logic here
|
|
223
|
+
console.log(` 🚀 Deploying worker for ${domain}`);
|
|
224
|
+
return true;
|
|
221
225
|
}
|
|
222
226
|
|
|
223
227
|
/**
|
|
224
|
-
*
|
|
225
|
-
* @deprecated Use deploymentCoordinator.validateDomainDeployment() instead
|
|
228
|
+
* Validate domain deployment (placeholder implementation)
|
|
226
229
|
*/
|
|
227
230
|
async validateDomainDeployment(domain) {
|
|
228
|
-
|
|
231
|
+
// Placeholder: Add actual deployment validation logic here
|
|
232
|
+
console.log(` ✅ Validating deployment for ${domain}`);
|
|
233
|
+
return true;
|
|
229
234
|
}
|
|
230
235
|
|
|
231
236
|
/**
|
|
@@ -342,16 +342,88 @@ export class InputCollector {
|
|
|
342
342
|
const token = await this.prompt('Cloudflare API Token: ');
|
|
343
343
|
if (token && token.length > 20) {
|
|
344
344
|
// Basic length validation
|
|
345
|
-
|
|
345
|
+
// Verify token with CloudflareAPI
|
|
346
|
+
try {
|
|
347
|
+
const {
|
|
348
|
+
CloudflareAPI
|
|
349
|
+
} = await import('../utils/cloudflare/api.js');
|
|
350
|
+
const cfApi = new CloudflareAPI(token);
|
|
351
|
+
const isValid = await cfApi.verifyToken();
|
|
352
|
+
if (isValid) {
|
|
353
|
+
console.log(chalk.green('✓ API token verified successfully'));
|
|
354
|
+
return token;
|
|
355
|
+
}
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.log(chalk.red(`✗ Token verification failed: ${error.message}`));
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
346
360
|
}
|
|
347
361
|
console.log(chalk.red('✗ Invalid API token format.'));
|
|
348
362
|
}
|
|
349
363
|
}
|
|
350
364
|
|
|
351
365
|
/**
|
|
352
|
-
* Collect Cloudflare
|
|
366
|
+
* Collect Cloudflare configuration with automatic domain discovery
|
|
367
|
+
* Returns { accountId, zoneId, domainName }
|
|
353
368
|
*/
|
|
354
|
-
async
|
|
369
|
+
async collectCloudflareConfigWithDiscovery(apiToken, preferredDomain = null) {
|
|
370
|
+
try {
|
|
371
|
+
const {
|
|
372
|
+
CloudflareAPI
|
|
373
|
+
} = await import('../utils/cloudflare/api.js');
|
|
374
|
+
const {
|
|
375
|
+
formatZonesForDisplay,
|
|
376
|
+
parseZoneSelection
|
|
377
|
+
} = await import('../utils/cloudflare/api.js');
|
|
378
|
+
const cfApi = new CloudflareAPI(apiToken);
|
|
379
|
+
console.log(chalk.cyan('\n🔍 Discovering your Cloudflare domains...'));
|
|
380
|
+
const zones = await cfApi.listZones();
|
|
381
|
+
if (!zones || zones.length === 0) {
|
|
382
|
+
console.log(chalk.yellow('⚠️ No domains found in your Cloudflare account.'));
|
|
383
|
+
console.log(chalk.white('Please add a domain to Cloudflare first.'));
|
|
384
|
+
throw new Error('No domains available');
|
|
385
|
+
}
|
|
386
|
+
console.log(chalk.green(`✓ Found ${zones.length} domain(s)\n`));
|
|
387
|
+
|
|
388
|
+
// Format zones for display
|
|
389
|
+
const formatted = formatZonesForDisplay(zones);
|
|
390
|
+
console.log(formatted);
|
|
391
|
+
|
|
392
|
+
// Let user select a domain
|
|
393
|
+
const selection = await this.prompt('\nSelect domain (enter number or name): ');
|
|
394
|
+
const selectedZone = parseZoneSelection(selection, zones);
|
|
395
|
+
if (!selectedZone) {
|
|
396
|
+
throw new Error('Invalid domain selection');
|
|
397
|
+
}
|
|
398
|
+
console.log(chalk.green(`\n✓ Selected: ${selectedZone.name}`));
|
|
399
|
+
|
|
400
|
+
// Get full zone details
|
|
401
|
+
const zoneDetails = await cfApi.getZoneDetails(selectedZone.id);
|
|
402
|
+
return {
|
|
403
|
+
domainName: zoneDetails.name,
|
|
404
|
+
zoneId: zoneDetails.id,
|
|
405
|
+
accountId: zoneDetails.account.id,
|
|
406
|
+
accountName: zoneDetails.account.name,
|
|
407
|
+
nameServers: zoneDetails.name_servers,
|
|
408
|
+
status: zoneDetails.status
|
|
409
|
+
};
|
|
410
|
+
} catch (error) {
|
|
411
|
+
console.log(chalk.red(`\n✗ Domain discovery failed: ${error.message}`));
|
|
412
|
+
console.log(chalk.yellow('Falling back to manual entry...\n'));
|
|
413
|
+
|
|
414
|
+
// Fallback to manual entry
|
|
415
|
+
return {
|
|
416
|
+
accountId: await this.collectCloudflareAccountIdManual(),
|
|
417
|
+
zoneId: await this.collectCloudflareZoneIdManual(),
|
|
418
|
+
domainName: preferredDomain
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Manual Cloudflare Account ID collection (fallback)
|
|
425
|
+
*/
|
|
426
|
+
async collectCloudflareAccountIdManual() {
|
|
355
427
|
console.log(chalk.white('Find your Account ID in the right sidebar of your Cloudflare dashboard.'));
|
|
356
428
|
console.log('');
|
|
357
429
|
for (;;) {
|
|
@@ -365,9 +437,9 @@ export class InputCollector {
|
|
|
365
437
|
}
|
|
366
438
|
|
|
367
439
|
/**
|
|
368
|
-
*
|
|
440
|
+
* Manual Cloudflare Zone ID collection (fallback)
|
|
369
441
|
*/
|
|
370
|
-
async
|
|
442
|
+
async collectCloudflareZoneIdManual() {
|
|
371
443
|
console.log(chalk.white('Find your Zone ID in the Overview tab of your domain in Cloudflare.'));
|
|
372
444
|
console.log('');
|
|
373
445
|
for (;;) {
|
|
@@ -380,6 +452,22 @@ export class InputCollector {
|
|
|
380
452
|
}
|
|
381
453
|
}
|
|
382
454
|
|
|
455
|
+
/**
|
|
456
|
+
* Collect Cloudflare Account ID (kept for backward compatibility)
|
|
457
|
+
* @deprecated Use collectCloudflareConfigWithDiscovery instead
|
|
458
|
+
*/
|
|
459
|
+
async collectCloudflareAccountId() {
|
|
460
|
+
return this.collectCloudflareAccountIdManual();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Collect Cloudflare Zone ID (kept for backward compatibility)
|
|
465
|
+
* @deprecated Use collectCloudflareConfigWithDiscovery instead
|
|
466
|
+
*/
|
|
467
|
+
async collectCloudflareZoneId() {
|
|
468
|
+
return this.collectCloudflareZoneIdManual();
|
|
469
|
+
}
|
|
470
|
+
|
|
383
471
|
/**
|
|
384
472
|
* Collect target environment
|
|
385
473
|
*/
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
|
|
4
4
|
export { ConfigurationCacheManager } from './config-cache.js';
|
|
5
5
|
export { EnhancedSecretManager } from './secret-generator.js';
|
|
6
|
-
export { askUser, askYesNo, askChoice, closePrompts } from '
|
|
6
|
+
export { askUser, askYesNo, askChoice, closePrompts } from '../../../bin/shared/utils/interactive-prompts.js';
|
package/package.json
CHANGED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Interactive Prompts Module
|
|
3
|
-
* Standardized user input functions for all scripts
|
|
4
|
-
*
|
|
5
|
-
* Replaces duplicate prompt code across 10+ scripts
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import readline from 'readline';
|
|
9
|
-
|
|
10
|
-
// Create readline interface (singleton)
|
|
11
|
-
let rl = null;
|
|
12
|
-
function getReadlineInterface() {
|
|
13
|
-
if (!rl) {
|
|
14
|
-
rl = readline.createInterface({
|
|
15
|
-
input: process.stdin,
|
|
16
|
-
output: process.stdout
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
return rl;
|
|
20
|
-
}
|
|
21
|
-
export function askUser(question, defaultValue = null) {
|
|
22
|
-
return new Promise(resolve => {
|
|
23
|
-
const prompt = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
|
|
24
|
-
getReadlineInterface().question(prompt, answer => {
|
|
25
|
-
resolve(answer.trim() || defaultValue);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
export function askYesNo(question, defaultValue = 'n') {
|
|
30
|
-
return new Promise(resolve => {
|
|
31
|
-
const prompt = `${question} [y/N]: `;
|
|
32
|
-
getReadlineInterface().question(prompt, answer => {
|
|
33
|
-
const response = answer.trim().toLowerCase() || defaultValue;
|
|
34
|
-
resolve(response === 'y' || response === 'yes');
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
export function askChoice(question, choices, defaultIndex = 0) {
|
|
39
|
-
return new Promise(resolve => {
|
|
40
|
-
console.log(`\n${question}`);
|
|
41
|
-
choices.forEach((choice, index) => {
|
|
42
|
-
const marker = index === defaultIndex ? '>' : ' ';
|
|
43
|
-
console.log(`${marker} ${index + 1}. ${choice}`);
|
|
44
|
-
});
|
|
45
|
-
getReadlineInterface().question(`\nSelect option [1-${choices.length}]: `, answer => {
|
|
46
|
-
const choice = parseInt(answer) - 1;
|
|
47
|
-
if (isNaN(choice) || choice < 0 || choice >= choices.length) {
|
|
48
|
-
resolve(defaultIndex);
|
|
49
|
-
} else {
|
|
50
|
-
resolve(choice);
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
export function askMultiChoice(question, choices, defaultIndices = []) {
|
|
56
|
-
return new Promise(resolve => {
|
|
57
|
-
console.log(`\n${question}`);
|
|
58
|
-
console.log('(Enter comma-separated numbers, e.g., "1,3,5")');
|
|
59
|
-
choices.forEach((choice, index) => {
|
|
60
|
-
const marker = defaultIndices.includes(index) ? '>' : ' ';
|
|
61
|
-
console.log(`${marker} ${index + 1}. ${choice}`);
|
|
62
|
-
});
|
|
63
|
-
const defaultStr = defaultIndices.map(i => i + 1).join(',');
|
|
64
|
-
getReadlineInterface().question(`\nSelect options [${defaultStr}]: `, answer => {
|
|
65
|
-
if (!answer.trim()) {
|
|
66
|
-
resolve(defaultIndices);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
const selected = answer.split(',').map(s => parseInt(s.trim()) - 1).filter(i => !isNaN(i) && i >= 0 && i < choices.length);
|
|
70
|
-
resolve(selected.length > 0 ? selected : defaultIndices);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
export function closePrompts() {
|
|
75
|
-
if (rl) {
|
|
76
|
-
rl.close();
|
|
77
|
-
rl = null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Utility function for progress indicators
|
|
82
|
-
export function showProgress(message, steps = ['⏳', '⚡', '✅']) {
|
|
83
|
-
return new Promise(resolve => {
|
|
84
|
-
let stepIndex = 0;
|
|
85
|
-
const interval = setInterval(() => {
|
|
86
|
-
process.stdout.write(`\r${steps[stepIndex]} ${message}`);
|
|
87
|
-
stepIndex = (stepIndex + 1) % steps.length;
|
|
88
|
-
}, 500);
|
|
89
|
-
|
|
90
|
-
// Auto-resolve after 2 seconds or when manually resolved
|
|
91
|
-
setTimeout(() => {
|
|
92
|
-
clearInterval(interval);
|
|
93
|
-
process.stdout.write(`\r✅ ${message}\n`);
|
|
94
|
-
resolve();
|
|
95
|
-
}, 2000);
|
|
96
|
-
});
|
|
97
|
-
}
|