@tamyla/clodo-framework 2.0.14 ā 2.0.16
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
|
+
## [2.0.16](https://github.com/tamylaa/clodo-framework/compare/v2.0.15...v2.0.16) (2025-10-12)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* comprehensive readline state management and stdin restoration ([a0716fe](https://github.com/tamylaa/clodo-framework/commit/a0716fef9a5db8211b851ab7861bf2419f31e7fa))
|
|
7
|
+
|
|
8
|
+
## [2.0.15](https://github.com/tamylaa/clodo-framework/compare/v2.0.14...v2.0.15) (2025-10-12)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* improve customer selection UX with number support ([2e10d56](https://github.com/tamylaa/clodo-framework/commit/2e10d562061c478ecc238e1185e2514af453d00e))
|
|
14
|
+
|
|
1
15
|
## [2.0.14](https://github.com/tamylaa/clodo-framework/compare/v2.0.13...v2.0.14) (2025-10-12)
|
|
2
16
|
|
|
3
17
|
|
package/bin/clodo-service.js
CHANGED
|
@@ -516,8 +516,8 @@ program
|
|
|
516
516
|
const customers = configPersistence.getConfiguredCustomers();
|
|
517
517
|
if (customers.length > 0) {
|
|
518
518
|
console.log(chalk.cyan('š” Configured customers:'));
|
|
519
|
-
customers.forEach(customer => {
|
|
520
|
-
console.log(chalk.white(`
|
|
519
|
+
customers.forEach((customer, index) => {
|
|
520
|
+
console.log(chalk.white(` ${index + 1}. ${customer}`));
|
|
521
521
|
});
|
|
522
522
|
console.log('');
|
|
523
523
|
}
|
|
@@ -527,8 +527,30 @@ program
|
|
|
527
527
|
if (!coreInputs.cloudflareAccountId) {
|
|
528
528
|
console.log(chalk.cyan('š Tier 1: Core Input Collection\n'));
|
|
529
529
|
|
|
530
|
-
// Collect basic info
|
|
531
|
-
|
|
530
|
+
// Collect basic info with smart customer selection
|
|
531
|
+
let customer = options.customer;
|
|
532
|
+
if (!customer) {
|
|
533
|
+
const customers = configPersistence.getConfiguredCustomers();
|
|
534
|
+
if (customers.length > 0) {
|
|
535
|
+
const selection = await inputCollector.question('Select customer (enter number or name): ');
|
|
536
|
+
|
|
537
|
+
// Try to parse as number first
|
|
538
|
+
const num = parseInt(selection);
|
|
539
|
+
if (!isNaN(num) && num >= 1 && num <= customers.length) {
|
|
540
|
+
customer = customers[num - 1];
|
|
541
|
+
console.log(chalk.green(`ā Selected: ${customer}\n`));
|
|
542
|
+
} else if (customers.includes(selection)) {
|
|
543
|
+
customer = selection;
|
|
544
|
+
console.log(chalk.green(`ā Selected: ${customer}\n`));
|
|
545
|
+
} else {
|
|
546
|
+
// New customer name
|
|
547
|
+
customer = selection;
|
|
548
|
+
console.log(chalk.yellow(`ā ļø Creating new customer: ${customer}\n`));
|
|
549
|
+
}
|
|
550
|
+
} else {
|
|
551
|
+
customer = await inputCollector.question('Customer name: ');
|
|
552
|
+
}
|
|
553
|
+
}
|
|
532
554
|
const environment = options.env || await inputCollector.collectEnvironment();
|
|
533
555
|
const serviceName = await inputCollector.collectServiceName();
|
|
534
556
|
const serviceType = await inputCollector.collectServiceType();
|
|
@@ -537,6 +559,7 @@ program
|
|
|
537
559
|
const cloudflareToken = process.env.CLOUDFLARE_API_TOKEN || await inputCollector.collectCloudflareToken();
|
|
538
560
|
|
|
539
561
|
// Use CloudflareAPI for automatic domain discovery
|
|
562
|
+
console.log(chalk.cyan('ā³ Fetching Cloudflare configuration...'));
|
|
540
563
|
const cloudflareConfig = await inputCollector.collectCloudflareConfigWithDiscovery(
|
|
541
564
|
cloudflareToken,
|
|
542
565
|
options.domain
|
|
@@ -654,9 +677,31 @@ program
|
|
|
654
677
|
|
|
655
678
|
} catch (error) {
|
|
656
679
|
console.error(chalk.red(`\nā Deployment failed: ${error.message}`));
|
|
657
|
-
|
|
680
|
+
|
|
681
|
+
// Show helpful context based on error type
|
|
682
|
+
if (error.message.includes('timeout')) {
|
|
683
|
+
console.log(chalk.yellow('\nš” Troubleshooting Tips:'));
|
|
684
|
+
console.log(chalk.white(' ⢠Use non-interactive mode: npx clodo-service deploy --customer=NAME --env=ENV --non-interactive'));
|
|
685
|
+
console.log(chalk.white(' ⢠Set DEBUG=1 for detailed logs: DEBUG=1 npx clodo-service deploy'));
|
|
686
|
+
console.log(chalk.white(' ⢠Check your terminal supports readline'));
|
|
687
|
+
} else if (error.message.includes('domain')) {
|
|
688
|
+
console.log(chalk.yellow('\nš” Domain Issues:'));
|
|
689
|
+
console.log(chalk.white(' ⢠Verify domain exists in Cloudflare dashboard'));
|
|
690
|
+
console.log(chalk.white(' ⢠Check API token has zone:read permissions'));
|
|
691
|
+
console.log(chalk.white(' ⢠Try specifying domain: --domain=example.com'));
|
|
692
|
+
} else if (error.message.includes('readline')) {
|
|
693
|
+
console.log(chalk.yellow('\nš” Terminal Issues:'));
|
|
694
|
+
console.log(chalk.white(' ⢠Try a different terminal (cmd, bash, powershell)'));
|
|
695
|
+
console.log(chalk.white(' ⢠Use --non-interactive with config file'));
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (process.env.DEBUG) {
|
|
699
|
+
console.error(chalk.gray('\nFull Stack Trace:'));
|
|
658
700
|
console.error(chalk.gray(error.stack));
|
|
701
|
+
} else {
|
|
702
|
+
console.log(chalk.gray('\nRun with DEBUG=1 for full stack trace'));
|
|
659
703
|
}
|
|
704
|
+
|
|
660
705
|
process.exit(1);
|
|
661
706
|
}
|
|
662
707
|
});
|
|
@@ -18,14 +18,27 @@ import { uiStructuresLoader } from '../utils/ui-structures-loader.js';
|
|
|
18
18
|
export class InputCollector {
|
|
19
19
|
constructor(options = {}) {
|
|
20
20
|
this.interactive = options.interactive !== false;
|
|
21
|
+
this.isPowerShell = process.env.PSModulePath !== undefined;
|
|
21
22
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
this.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
// Don't create readline immediately - lazy initialize
|
|
24
|
+
this.rl = null;
|
|
25
|
+
this.options = options;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Ensure readline is initialized and healthy
|
|
30
|
+
* Creates new instance if needed (e.g., after password input corrupted state)
|
|
31
|
+
*/
|
|
32
|
+
ensureReadline() {
|
|
33
|
+
if (!this.rl || this.rl.closed) {
|
|
34
|
+
// Fix for PowerShell double-echo issue
|
|
35
|
+
this.rl = this.interactive ? createInterface({
|
|
36
|
+
input: process.stdin,
|
|
37
|
+
output: process.stdout,
|
|
38
|
+
terminal: !this.isPowerShell // Disable terminal mode in PowerShell
|
|
39
|
+
}) : null;
|
|
40
|
+
}
|
|
41
|
+
return this.rl;
|
|
29
42
|
}
|
|
30
43
|
|
|
31
44
|
/**
|
|
@@ -236,11 +249,32 @@ export class InputCollector {
|
|
|
236
249
|
}
|
|
237
250
|
|
|
238
251
|
/**
|
|
239
|
-
* Promisified readline question
|
|
252
|
+
* Promisified readline question with timeout protection
|
|
253
|
+
* Automatically recreates readline if needed
|
|
240
254
|
*/
|
|
241
|
-
question(prompt) {
|
|
242
|
-
return new Promise(resolve => {
|
|
243
|
-
this.
|
|
255
|
+
question(prompt, timeout = 120000) {
|
|
256
|
+
return new Promise((resolve, reject) => {
|
|
257
|
+
const rl = this.ensureReadline();
|
|
258
|
+
if (!rl) {
|
|
259
|
+
reject(new Error('Readline interface not initialized - running in non-interactive mode?'));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Verify stdin is readable
|
|
264
|
+
if (!process.stdin.readable) {
|
|
265
|
+
reject(new Error('stdin not readable - terminal may be in broken state'));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Set timeout to detect hangs (default 2 minutes)
|
|
270
|
+
const timer = setTimeout(() => {
|
|
271
|
+
console.log(chalk.red('\n\nā ļø Input timeout - readline may be blocked'));
|
|
272
|
+
console.log(chalk.yellow('This can happen in some terminal environments.'));
|
|
273
|
+
console.log(chalk.white('Try running with explicit parameters: npx clodo-service deploy --customer=NAME --env=ENV\n'));
|
|
274
|
+
reject(new Error('Input timeout after ' + timeout / 1000 + ' seconds'));
|
|
275
|
+
}, timeout);
|
|
276
|
+
rl.question(prompt, answer => {
|
|
277
|
+
clearTimeout(timer);
|
|
244
278
|
resolve(answer.trim());
|
|
245
279
|
});
|
|
246
280
|
});
|
|
@@ -388,21 +422,29 @@ export class InputCollector {
|
|
|
388
422
|
throw new Error('No domains available');
|
|
389
423
|
}
|
|
390
424
|
console.log(chalk.green(`ā Found ${zones.length} domain(s)\n`));
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
425
|
+
let selectedZone;
|
|
426
|
+
|
|
427
|
+
// Auto-select if only one domain
|
|
428
|
+
if (zones.length === 1) {
|
|
429
|
+
selectedZone = zones[0];
|
|
430
|
+
console.log(chalk.white(` 1. ā
${zones[0].name} (${zones[0].plan?.name || 'Free'}) - Account: ${zones[0].account?.name || 'N/A'}`));
|
|
431
|
+
console.log(chalk.green(`\nā Auto-selected: ${selectedZone.name} (only domain available)\n`));
|
|
432
|
+
} else {
|
|
433
|
+
// Format zones for display
|
|
434
|
+
const formatted = formatZonesForDisplay(zones);
|
|
435
|
+
formatted.forEach((line, index) => {
|
|
436
|
+
console.log(chalk.white(` ${index + 1}. ${line}`));
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// Let user select a domain
|
|
440
|
+
const selection = await this.prompt('\nSelect domain (enter number or name): ');
|
|
441
|
+
const selectedIndex = parseZoneSelection(selection, zones);
|
|
442
|
+
if (selectedIndex === -1) {
|
|
443
|
+
throw new Error('Invalid domain selection');
|
|
444
|
+
}
|
|
445
|
+
selectedZone = zones[selectedIndex];
|
|
446
|
+
console.log(chalk.green(`\nā Selected: ${selectedZone.name}`));
|
|
403
447
|
}
|
|
404
|
-
const selectedZone = zones[selectedIndex];
|
|
405
|
-
console.log(chalk.green(`\nā Selected: ${selectedZone.name}`));
|
|
406
448
|
|
|
407
449
|
// Get full zone details
|
|
408
450
|
const zoneDetails = await cfApi.getZoneDetails(selectedZone.id);
|
|
@@ -515,11 +557,12 @@ export class InputCollector {
|
|
|
515
557
|
}
|
|
516
558
|
|
|
517
559
|
/**
|
|
518
|
-
* Close readline interface
|
|
560
|
+
* Close readline interface and clean up
|
|
519
561
|
*/
|
|
520
562
|
close() {
|
|
521
|
-
if (this.rl) {
|
|
563
|
+
if (this.rl && !this.rl.closed) {
|
|
522
564
|
this.rl.close();
|
|
565
|
+
this.rl = null; // Clear reference so ensureReadline() can create new one if needed
|
|
523
566
|
}
|
|
524
567
|
}
|
|
525
568
|
}
|
|
@@ -98,31 +98,58 @@ export function showProgress(message, steps = ['ā³', 'ā”', 'ā
']) {
|
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
100
|
* Ask for sensitive input (like API tokens) with hidden input
|
|
101
|
+
* CRITICAL: Properly restores stdin state for subsequent readline operations
|
|
101
102
|
*/
|
|
102
103
|
export function askPassword(question) {
|
|
103
104
|
return new Promise(resolve => {
|
|
104
105
|
const prompt = `${question}: `;
|
|
105
106
|
process.stdout.write(prompt);
|
|
106
107
|
|
|
108
|
+
// Save original state
|
|
109
|
+
const wasRaw = process.stdin.isRaw;
|
|
110
|
+
const wasPaused = process.stdin.isPaused();
|
|
111
|
+
|
|
107
112
|
// Hide input for sensitive data
|
|
108
|
-
process.stdin.
|
|
113
|
+
if (process.stdin.isTTY) {
|
|
114
|
+
process.stdin.setRawMode(true);
|
|
115
|
+
}
|
|
109
116
|
process.stdin.resume();
|
|
110
117
|
let password = '';
|
|
111
118
|
const onData = char => {
|
|
112
119
|
const charCode = char[0];
|
|
113
|
-
if (charCode === 13) {
|
|
114
|
-
// Enter key
|
|
115
|
-
|
|
116
|
-
process.stdin.
|
|
120
|
+
if (charCode === 13 || charCode === 10) {
|
|
121
|
+
// Enter key (CR or LF)
|
|
122
|
+
// Restore original state BEFORE resolving
|
|
123
|
+
if (process.stdin.isTTY) {
|
|
124
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Important: Resume stdin so readline can use it
|
|
128
|
+
if (!wasPaused) {
|
|
129
|
+
process.stdin.resume();
|
|
130
|
+
} else {
|
|
131
|
+
process.stdin.pause();
|
|
132
|
+
}
|
|
117
133
|
process.stdin.removeListener('data', onData);
|
|
118
134
|
process.stdout.write('\n');
|
|
119
|
-
|
|
135
|
+
|
|
136
|
+
// Small delay to let stdin stabilize before next readline operation
|
|
137
|
+
setTimeout(() => resolve(password), 50);
|
|
120
138
|
} else if (charCode === 127 || charCode === 8) {
|
|
121
139
|
// Backspace
|
|
122
140
|
if (password.length > 0) {
|
|
123
141
|
password = password.slice(0, -1);
|
|
124
142
|
process.stdout.write('\b \b');
|
|
125
143
|
}
|
|
144
|
+
} else if (charCode === 3) {
|
|
145
|
+
// Ctrl+C
|
|
146
|
+
// Restore state and exit gracefully
|
|
147
|
+
if (process.stdin.isTTY) {
|
|
148
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
149
|
+
}
|
|
150
|
+
process.stdin.removeListener('data', onData);
|
|
151
|
+
process.stdout.write('\n');
|
|
152
|
+
process.exit(0);
|
|
126
153
|
} else if (charCode >= 32 && charCode <= 126) {
|
|
127
154
|
// Printable characters
|
|
128
155
|
password += char.toString();
|