@tamyla/clodo-framework 2.0.15 → 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,10 @@
|
|
|
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
|
+
|
|
1
8
|
## [2.0.15](https://github.com/tamylaa/clodo-framework/compare/v2.0.14...v2.0.15) (2025-10-12)
|
|
2
9
|
|
|
3
10
|
|
package/bin/clodo-service.js
CHANGED
|
@@ -559,6 +559,7 @@ program
|
|
|
559
559
|
const cloudflareToken = process.env.CLOUDFLARE_API_TOKEN || await inputCollector.collectCloudflareToken();
|
|
560
560
|
|
|
561
561
|
// Use CloudflareAPI for automatic domain discovery
|
|
562
|
+
console.log(chalk.cyan('⏳ Fetching Cloudflare configuration...'));
|
|
562
563
|
const cloudflareConfig = await inputCollector.collectCloudflareConfigWithDiscovery(
|
|
563
564
|
cloudflareToken,
|
|
564
565
|
options.domain
|
|
@@ -676,9 +677,31 @@ program
|
|
|
676
677
|
|
|
677
678
|
} catch (error) {
|
|
678
679
|
console.error(chalk.red(`\n❌ Deployment failed: ${error.message}`));
|
|
679
|
-
|
|
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:'));
|
|
680
700
|
console.error(chalk.gray(error.stack));
|
|
701
|
+
} else {
|
|
702
|
+
console.log(chalk.gray('\nRun with DEBUG=1 for full stack trace'));
|
|
681
703
|
}
|
|
704
|
+
|
|
682
705
|
process.exit(1);
|
|
683
706
|
}
|
|
684
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();
|