@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
 
@@ -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
- if (error.stack && process.env.DEBUG) {
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
- // Fix for PowerShell double-echo issue
23
- const isPowerShell = process.env.PSModulePath !== undefined;
24
- this.rl = this.interactive ? createInterface({
25
- input: process.stdin,
26
- output: process.stdout,
27
- terminal: !isPowerShell // Disable terminal mode in PowerShell to prevent double echo
28
- }) : null;
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.rl.question(prompt, answer => {
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
- // Format zones for display
393
- const formatted = formatZonesForDisplay(zones);
394
- formatted.forEach((line, index) => {
395
- console.log(chalk.white(` ${index + 1}. ${line}`));
396
- });
397
-
398
- // Let user select a domain
399
- const selection = await this.prompt('\nSelect domain (enter number or name): ');
400
- const selectedIndex = parseZoneSelection(selection, zones);
401
- if (selectedIndex === -1) {
402
- throw new Error('Invalid domain selection');
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.setRawMode(true);
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
- process.stdin.setRawMode(false);
116
- process.stdin.pause();
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
- resolve(password);
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamyla/clodo-framework",
3
- "version": "2.0.15",
3
+ "version": "2.0.16",
4
4
  "description": "Reusable framework for Clodo-style software architecture on Cloudflare Workers + D1",
5
5
  "type": "module",
6
6
  "sideEffects": [