@kikkimo/claude-launcher 2.0.0 → 2.1.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/CHANGELOG.md +91 -0
- package/README.md +9 -6
- package/claude-launcher +57 -3
- package/docs/README-zh.md +9 -6
- package/lib/auth/password-input.js +101 -87
- package/lib/i18n/locales/de.js +17 -2
- package/lib/i18n/locales/en.js +17 -2
- package/lib/i18n/locales/es.js +17 -2
- package/lib/i18n/locales/fr.js +18 -3
- package/lib/i18n/locales/it.js +16 -0
- package/lib/i18n/locales/ja.js +17 -2
- package/lib/i18n/locales/ko.js +17 -2
- package/lib/i18n/locales/pt.js +16 -0
- package/lib/i18n/locales/ru.js +16 -0
- package/lib/i18n/locales/zh-TW.js +17 -2
- package/lib/i18n/locales/zh.js +17 -2
- package/lib/launcher.js +153 -47
- package/lib/presets/providers.js +46 -2
- package/lib/ui/interactive-table.js +102 -24
- package/lib/ui/menu.js +213 -144
- package/lib/ui/prompts.js +116 -85
- package/lib/utils/stdin-manager.js +715 -0
- package/package.json +1 -1
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
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
} catch (error) {
|
|
55
|
-
// Ignore cleanup errors
|
|
59
|
+
return;
|
|
56
60
|
}
|
|
57
|
-
};
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
+
scope.on('data', handleKeyPress);
|
|
100
100
|
} else {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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' ||
|
|
316
|
+
if (selectedProvider.id === 'anthropic' || selectedProvider.id === 'deepseek' ||
|
|
317
|
+
selectedProvider.id === 'moonshot' || 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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
//
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
506
|
+
|
|
507
|
+
const yes = key.toString().trim().toLowerCase() === 'y';
|
|
508
|
+
scope.release();
|
|
509
|
+
resolve(yes);
|
|
481
510
|
});
|
|
482
511
|
} else {
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
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
|
+
};
|