@kikkimo/claude-launcher 2.0.0 → 2.2.0

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/lib/ui/prompts.js CHANGED
@@ -7,19 +7,23 @@ const colors = require('./colors');
7
7
  const { getAllProviders } = require('../presets/providers');
8
8
  const { validateBaseUrl, validateAuthToken, validateModel } = require('../validators');
9
9
  const i18n = require('../i18n');
10
+ const stdinManager = require('../utils/stdin-manager');
10
11
 
11
12
  /**
12
- * Simple input using readline
13
+ * Simple input using readline via StdinManager
13
14
  */
14
15
  async function simpleInput(prompt) {
15
16
  return new Promise((resolve) => {
16
- const rl = readline.createInterface({
17
- input: process.stdin,
18
- output: process.stdout
17
+ const scope = stdinManager.acquire('line', {
18
+ id: 'simpleInput',
19
+ allowNested: false
19
20
  });
20
21
 
22
+ const rl = scope.createReadline();
23
+
21
24
  rl.question(prompt, (answer) => {
22
25
  rl.close();
26
+ scope.release();
23
27
  resolve(answer.trim());
24
28
  });
25
29
  });
@@ -31,43 +35,45 @@ async function simpleInput(prompt) {
31
35
  async function getProviderChoice(prompt) {
32
36
  return new Promise((resolve) => {
33
37
  if (process.stdin.isTTY) {
34
- // Use raw mode to capture ESC key - this is necessary for interactive input
35
38
  process.stdout.write(colors.green + prompt + colors.reset);
36
39
 
37
40
  let input = '';
41
+ const scope = stdinManager.acquire('raw', {
42
+ id: 'getProviderChoice',
43
+ allowNested: true
44
+ });
38
45
 
39
- // Save original state
40
- const originalRawMode = process.stdin.isRaw;
41
- const originalPaused = process.stdin.isPaused();
42
-
43
- process.stdin.setRawMode(true);
44
- process.stdin.resume();
45
- process.stdin.setEncoding('utf8');
46
+ const handleKeyPress = (key) => {
47
+ const keyCode = key.charCodeAt(0);
46
48
 
47
- const cleanup = () => {
48
- try {
49
- process.stdin.setRawMode(false);
50
- if (originalPaused) {
51
- process.stdin.pause();
49
+ // Handle Ctrl+C first
50
+ if (key === '\u0003') {
51
+ scope.release();
52
+ // handleCtrlC() returns false on first Ctrl+C, or exits on second.
53
+ // Resolve with null to indicate cancellation (same as ESC key).
54
+ const exited = stdinManager.handleCtrlC();
55
+ if (exited === false) {
56
+ process.stdout.write('\n');
57
+ resolve(null); // User cancelled with Ctrl+C
52
58
  }
53
- process.stdin.removeAllListeners('data');
54
- } catch (error) {
55
- // Ignore cleanup errors
59
+ return;
56
60
  }
57
- };
58
61
 
59
- const handleKeyPress = (key) => {
60
- const keyCode = key.charCodeAt(0);
62
+ // If waiting for second Ctrl+C, any other key cancels it
63
+ if (stdinManager.isCtrlCPending()) {
64
+ stdinManager.cancelCtrlC();
65
+ // Continue to process this key normally
66
+ }
61
67
 
62
68
  switch (keyCode) {
63
69
  case 27: // ESC key
64
- cleanup();
70
+ scope.release();
65
71
  process.stdout.write('\n');
66
72
  resolve(null);
67
73
  return;
68
74
 
69
75
  case 13: // Enter key
70
- cleanup();
76
+ scope.release();
71
77
  process.stdout.write('\n');
72
78
  resolve(input);
73
79
  return;
@@ -80,14 +86,8 @@ async function getProviderChoice(prompt) {
80
86
  }
81
87
  return;
82
88
 
83
- case 3: // Ctrl+C
84
- cleanup();
85
- process.stdout.write('\n');
86
- resolve(null);
87
- return;
88
-
89
89
  default:
90
- // Only accept printable characters
90
+ // Only accept printable ASCII characters
91
91
  if (keyCode >= 32 && keyCode < 127) {
92
92
  input += key;
93
93
  process.stdout.write(key);
@@ -96,16 +96,16 @@ async function getProviderChoice(prompt) {
96
96
  }
97
97
  };
98
98
 
99
- process.stdin.on('data', handleKeyPress);
99
+ scope.on('data', handleKeyPress);
100
100
  } else {
101
- // Fallback for non-TTY environments
102
- const rl = readline.createInterface({
103
- input: process.stdin,
104
- output: process.stdout
101
+ const scope = stdinManager.acquire('line', {
102
+ id: 'getProviderChoice_nonTTY',
103
+ allowNested: true
105
104
  });
106
-
105
+ const rl = scope.createReadline();
107
106
  rl.question(colors.green + prompt + colors.reset, (answer) => {
108
107
  rl.close();
108
+ scope.release();
109
109
  resolve(answer.trim());
110
110
  });
111
111
  }
@@ -121,35 +121,51 @@ async function waitForKey(message = 'Press any key to continue...') {
121
121
 
122
122
  return new Promise((resolve) => {
123
123
  if (process.stdin.isTTY) {
124
- // Force complete cleanup before setting up new listeners
125
- try {
126
- process.stdin.removeAllListeners('data');
127
- process.stdin.removeAllListeners('keypress');
128
- } catch (error) {
129
- // Ignore cleanup errors
130
- }
124
+ // Use StdinManager for proper state management
125
+ const scope = stdinManager.acquire('raw', {
126
+ id: 'waitForKey',
127
+ allowNested: true
128
+ });
129
+
130
+ const handler = (key) => {
131
+ // Handle Ctrl+C first
132
+ if (key === '\u0003') {
133
+ scope.removeListener('data', handler);
134
+ scope.release();
135
+ // handleCtrlC() returns false on first Ctrl+C, or exits on second.
136
+ // Resolve to allow caller to continue (waitForKey doesn't have cancellation).
137
+ const exited = stdinManager.handleCtrlC();
138
+ if (exited === false) {
139
+ resolve(); // Continue after first Ctrl+C warning
140
+ }
141
+ return;
142
+ }
131
143
 
132
- process.stdin.setRawMode(true);
133
- process.stdin.resume();
134
- process.stdin.once('data', () => {
135
- // Complete cleanup after key press
136
- try {
137
- process.stdin.setRawMode(false);
138
- process.stdin.removeAllListeners('data');
139
- process.stdin.removeAllListeners('keypress');
140
- process.stdin.pause();
141
- } catch (error) {
142
- // Ignore cleanup errors
144
+ // If waiting for second Ctrl+C, any other key cancels it
145
+ if (stdinManager.isCtrlCPending()) {
146
+ stdinManager.cancelCtrlC();
143
147
  }
148
+
149
+ // Manually remove listener before resolving
150
+ scope.removeListener('data', handler);
151
+ // Release the scope, which automatically restores previous state
152
+ scope.release();
144
153
  resolve();
145
- });
154
+ };
155
+
156
+ // Use on() instead of once() so Ctrl+C doesn't remove the listener
157
+ scope.on('data', handler);
146
158
  } else {
147
- const rl = readline.createInterface({
148
- input: process.stdin,
149
- output: process.stdout
159
+ // For non-TTY environments, use readline directly
160
+ const scope = stdinManager.acquire('line', {
161
+ id: 'waitForKey_nonTTY',
162
+ allowNested: true
150
163
  });
164
+
165
+ const rl = scope.createReadline();
151
166
  rl.question('', () => {
152
167
  rl.close();
168
+ scope.release();
153
169
  resolve();
154
170
  });
155
171
  }
@@ -252,7 +268,13 @@ async function promptForThirdPartyApi() {
252
268
  if (selectedProvider.id === 'custom') {
253
269
  console.log(colors.yellow + ' ' + i18n.tSync('ui.general.replace_url_model_note') + colors.reset);
254
270
  } else {
255
- console.log(colors.yellow + ` Note: ${selectedProvider.note}` + colors.reset);
271
+ // Try to get note from i18n, fallback to provider.note if not found
272
+ const noteKey = `provider.notes.${selectedProvider.id}`;
273
+ const noteText = i18n.tSync(noteKey);
274
+ // If i18n returns the key itself, it means translation not found, use original note
275
+ const displayNote = noteText === noteKey ? selectedProvider.note : noteText;
276
+ const notePrefix = i18n.tSync('provider.note_prefix');
277
+ console.log(colors.yellow + ` ${notePrefix}: ${displayNote}` + colors.reset);
256
278
  }
257
279
  }
258
280
  console.log('');
@@ -291,7 +313,8 @@ async function promptForThirdPartyApi() {
291
313
 
292
314
  // For all known providers, show the recommended URL in the prompt
293
315
  let prompt;
294
- if (selectedProvider.id === 'anthropic' || selectedProvider.id === 'deepseek' || selectedProvider.id === 'moonshot') {
316
+ if (selectedProvider.id === 'anthropic' || selectedProvider.id === 'deepseek' ||
317
+ selectedProvider.id === 'moonshot' || selectedProvider.id === 'kimi_for_coding' || selectedProvider.id === 'zhipu' || selectedProvider.id === 'zai') {
295
318
  prompt = colors.green + i18n.tSync('ui.general.press_enter_default_url') + `${colors.yellow}${baseUrl}${colors.green}` + colors.reset;
296
319
  console.log(colors.gray + ' ' + i18n.tSync('ui.general.edit_url_hint') + colors.reset);
297
320
  } else {
@@ -457,35 +480,43 @@ async function confirmAction(message) {
457
480
 
458
481
  return new Promise((resolve) => {
459
482
  if (process.stdin.isTTY) {
460
- // Force complete cleanup before setting up new listeners
461
- try {
462
- process.stdin.removeAllListeners('data');
463
- process.stdin.removeAllListeners('keypress');
464
- } catch (error) {
465
- // Ignore cleanup errors
466
- }
483
+ const scope = stdinManager.acquire('raw', {
484
+ id: 'confirmAction',
485
+ allowNested: true
486
+ });
487
+ scope.once('data', (key) => {
488
+ // Handle Ctrl+C first
489
+ if (key === '\u0003') {
490
+ scope.release();
491
+ // handleCtrlC() returns false on first Ctrl+C (shows warning),
492
+ // or calls process.exit(0) on second Ctrl+C (terminates process).
493
+ // If it returns (first Ctrl+C), resolve with false to indicate cancellation.
494
+ const exited = stdinManager.handleCtrlC();
495
+ if (exited === false) {
496
+ resolve(false); // User cancelled with Ctrl+C
497
+ }
498
+ // If handleCtrlC() didn't return, process.exit(0) was called
499
+ return;
500
+ }
467
501
 
468
- process.stdin.setRawMode(true);
469
- process.stdin.resume();
470
- process.stdin.once('data', (key) => {
471
- // Complete cleanup after key press
472
- try {
473
- process.stdin.setRawMode(false);
474
- process.stdin.removeAllListeners('data');
475
- process.stdin.removeAllListeners('keypress');
476
- process.stdin.pause();
477
- } catch (error) {
478
- // Ignore cleanup errors
502
+ // If waiting for second Ctrl+C, any other key cancels it
503
+ if (stdinManager.isCtrlCPending()) {
504
+ stdinManager.cancelCtrlC();
479
505
  }
480
- resolve(key.toString().toLowerCase() === 'y');
506
+
507
+ const yes = key.toString().trim().toLowerCase() === 'y';
508
+ scope.release();
509
+ resolve(yes);
481
510
  });
482
511
  } else {
483
- const rl = readline.createInterface({
484
- input: process.stdin,
485
- output: process.stdout
512
+ const scope = stdinManager.acquire('line', {
513
+ id: 'confirmAction_nonTTY',
514
+ allowNested: true
486
515
  });
516
+ const rl = scope.createReadline();
487
517
  rl.question('', (answer) => {
488
518
  rl.close();
519
+ scope.release();
489
520
  resolve(answer.toLowerCase() === 'y');
490
521
  });
491
522
  }
@@ -537,4 +568,4 @@ module.exports = {
537
568
  showSuccess,
538
569
  showError,
539
570
  showInfo
540
- };
571
+ };