@kikkimo/claude-launcher 1.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 +146 -0
- package/README.md +103 -41
- package/claude-launcher +1071 -576
- package/docs/README-zh.md +107 -45
- package/lib/api-manager.js +449 -0
- package/lib/auth/password-input.js +158 -0
- package/lib/auth/password-strength.js +154 -0
- package/lib/auth/password-validator.js +255 -0
- package/lib/crypto.js +85 -0
- package/lib/i18n/formatter.js +62 -0
- package/lib/i18n/index.js +218 -0
- package/lib/i18n/language-manager.js +160 -0
- package/lib/i18n/locales/de.js +538 -0
- package/lib/i18n/locales/en.js +539 -0
- package/lib/i18n/locales/es.js +538 -0
- package/lib/i18n/locales/fr.js +538 -0
- package/lib/i18n/locales/it.js +539 -0
- package/lib/i18n/locales/ja.js +538 -0
- package/lib/i18n/locales/ko.js +538 -0
- package/lib/i18n/locales/pt.js +539 -0
- package/lib/i18n/locales/ru.js +539 -0
- package/lib/i18n/locales/zh-TW.js +538 -0
- package/lib/i18n/locales/zh.js +538 -0
- package/lib/launcher.js +359 -0
- package/lib/presets/providers.js +148 -0
- package/lib/ui/colors.js +32 -0
- package/lib/ui/interactive-table.js +338 -0
- package/lib/ui/menu.js +383 -0
- package/lib/ui/prompts.js +571 -0
- package/lib/utils/stdin-manager.js +715 -0
- package/lib/utils/string-width.js +180 -0
- package/lib/utils/version-checker.js +240 -0
- package/lib/validators.js +130 -0
- package/package.json +2 -2
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompts Module - User input prompts and interactions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const colors = require('./colors');
|
|
7
|
+
const { getAllProviders } = require('../presets/providers');
|
|
8
|
+
const { validateBaseUrl, validateAuthToken, validateModel } = require('../validators');
|
|
9
|
+
const i18n = require('../i18n');
|
|
10
|
+
const stdinManager = require('../utils/stdin-manager');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Simple input using readline via StdinManager
|
|
14
|
+
*/
|
|
15
|
+
async function simpleInput(prompt) {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
const scope = stdinManager.acquire('line', {
|
|
18
|
+
id: 'simpleInput',
|
|
19
|
+
allowNested: false
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const rl = scope.createReadline();
|
|
23
|
+
|
|
24
|
+
rl.question(prompt, (answer) => {
|
|
25
|
+
rl.close();
|
|
26
|
+
scope.release();
|
|
27
|
+
resolve(answer.trim());
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get provider choice with ESC key support
|
|
34
|
+
*/
|
|
35
|
+
async function getProviderChoice(prompt) {
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
if (process.stdin.isTTY) {
|
|
38
|
+
process.stdout.write(colors.green + prompt + colors.reset);
|
|
39
|
+
|
|
40
|
+
let input = '';
|
|
41
|
+
const scope = stdinManager.acquire('raw', {
|
|
42
|
+
id: 'getProviderChoice',
|
|
43
|
+
allowNested: true
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const handleKeyPress = (key) => {
|
|
47
|
+
const keyCode = key.charCodeAt(0);
|
|
48
|
+
|
|
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
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
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
|
+
}
|
|
67
|
+
|
|
68
|
+
switch (keyCode) {
|
|
69
|
+
case 27: // ESC key
|
|
70
|
+
scope.release();
|
|
71
|
+
process.stdout.write('\n');
|
|
72
|
+
resolve(null);
|
|
73
|
+
return;
|
|
74
|
+
|
|
75
|
+
case 13: // Enter key
|
|
76
|
+
scope.release();
|
|
77
|
+
process.stdout.write('\n');
|
|
78
|
+
resolve(input);
|
|
79
|
+
return;
|
|
80
|
+
|
|
81
|
+
case 127: // Backspace
|
|
82
|
+
case 8: // Backspace (some terminals)
|
|
83
|
+
if (input.length > 0) {
|
|
84
|
+
input = input.slice(0, -1);
|
|
85
|
+
process.stdout.write('\b \b');
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
|
|
89
|
+
default:
|
|
90
|
+
// Only accept printable ASCII characters
|
|
91
|
+
if (keyCode >= 32 && keyCode < 127) {
|
|
92
|
+
input += key;
|
|
93
|
+
process.stdout.write(key);
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
scope.on('data', handleKeyPress);
|
|
100
|
+
} else {
|
|
101
|
+
const scope = stdinManager.acquire('line', {
|
|
102
|
+
id: 'getProviderChoice_nonTTY',
|
|
103
|
+
allowNested: true
|
|
104
|
+
});
|
|
105
|
+
const rl = scope.createReadline();
|
|
106
|
+
rl.question(colors.green + prompt + colors.reset, (answer) => {
|
|
107
|
+
rl.close();
|
|
108
|
+
scope.release();
|
|
109
|
+
resolve(answer.trim());
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Wait for any key press
|
|
118
|
+
*/
|
|
119
|
+
async function waitForKey(message = 'Press any key to continue...') {
|
|
120
|
+
console.log(colors.gray + message + colors.reset);
|
|
121
|
+
|
|
122
|
+
return new Promise((resolve) => {
|
|
123
|
+
if (process.stdin.isTTY) {
|
|
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
|
+
}
|
|
143
|
+
|
|
144
|
+
// If waiting for second Ctrl+C, any other key cancels it
|
|
145
|
+
if (stdinManager.isCtrlCPending()) {
|
|
146
|
+
stdinManager.cancelCtrlC();
|
|
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();
|
|
153
|
+
resolve();
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Use on() instead of once() so Ctrl+C doesn't remove the listener
|
|
157
|
+
scope.on('data', handler);
|
|
158
|
+
} else {
|
|
159
|
+
// For non-TTY environments, use readline directly
|
|
160
|
+
const scope = stdinManager.acquire('line', {
|
|
161
|
+
id: 'waitForKey_nonTTY',
|
|
162
|
+
allowNested: true
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const rl = scope.createReadline();
|
|
166
|
+
rl.question('', () => {
|
|
167
|
+
rl.close();
|
|
168
|
+
scope.release();
|
|
169
|
+
resolve();
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Prompt for third-party API configuration with enhanced guidance
|
|
177
|
+
*/
|
|
178
|
+
async function promptForThirdPartyApi() {
|
|
179
|
+
try {
|
|
180
|
+
// Step 1: Show information and wait for acknowledgment
|
|
181
|
+
console.clear();
|
|
182
|
+
console.log('');
|
|
183
|
+
console.log(colors.bright + colors.orange + i18n.tSync('ui.general.add_new_api_title') + colors.reset);
|
|
184
|
+
console.log('');
|
|
185
|
+
|
|
186
|
+
// Security and privacy information
|
|
187
|
+
console.log(colors.yellow + i18n.tSync('ui.general.security_privacy_info') + colors.reset);
|
|
188
|
+
const securityItems = i18n.tSync('ui.general.security_items');
|
|
189
|
+
securityItems.forEach(item => {
|
|
190
|
+
console.log(colors.bright + colors.green + ' • ' + item + colors.reset);
|
|
191
|
+
});
|
|
192
|
+
console.log('');
|
|
193
|
+
|
|
194
|
+
console.log(colors.yellow + i18n.tSync('ui.general.configuration_tips') + colors.reset);
|
|
195
|
+
const configTips = i18n.tSync('ui.general.config_tip_items');
|
|
196
|
+
configTips.forEach(tip => {
|
|
197
|
+
console.log(colors.gray + ' • ' + tip + colors.reset);
|
|
198
|
+
});
|
|
199
|
+
console.log(colors.gray + ' • ' + i18n.tSync('ui.general.type_exit_cancel') + colors.reset);
|
|
200
|
+
console.log('');
|
|
201
|
+
|
|
202
|
+
console.log(colors.yellow + i18n.tSync('ui.general.all_providers_compatible') + colors.reset);
|
|
203
|
+
console.log('');
|
|
204
|
+
|
|
205
|
+
await waitForKey(i18n.tSync('ui.general.press_continue_provider_selection'));
|
|
206
|
+
|
|
207
|
+
// Step 2: Show provider selection menu
|
|
208
|
+
console.clear();
|
|
209
|
+
console.log('');
|
|
210
|
+
console.log(colors.bright + colors.orange + i18n.tSync('ui.general.add_new_api_title') + colors.reset);
|
|
211
|
+
console.log('');
|
|
212
|
+
|
|
213
|
+
// Show available providers
|
|
214
|
+
console.log(colors.cyan + i18n.tSync('ui.general.compatible_providers_title') + colors.reset);
|
|
215
|
+
console.log('');
|
|
216
|
+
const providers = getAllProviders();
|
|
217
|
+
providers.forEach((provider, index) => {
|
|
218
|
+
const compatIcon = provider.compatibility === 'native' ? '🎯' : '✅';
|
|
219
|
+
console.log(colors.gray + ` ${index + 1}. ${compatIcon} ${provider.name}` + colors.reset);
|
|
220
|
+
console.log(colors.dim + ` ${provider.description}` + colors.reset);
|
|
221
|
+
});
|
|
222
|
+
console.log('');
|
|
223
|
+
|
|
224
|
+
// Select provider or custom with validation
|
|
225
|
+
let selectedProvider = null;
|
|
226
|
+
let baseUrl = '';
|
|
227
|
+
let suggestedModels = [];
|
|
228
|
+
|
|
229
|
+
while (true) {
|
|
230
|
+
const selectPrompt = i18n.tSync('ui.general.select_provider_prompt').replace('{0}', providers.length);
|
|
231
|
+
const providerChoice = await getProviderChoice(selectPrompt);
|
|
232
|
+
|
|
233
|
+
if (providerChoice === null) {
|
|
234
|
+
// User cancelled with ESC
|
|
235
|
+
throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (providerChoice.toLowerCase() === 'exit' || providerChoice.toLowerCase() === 'quit') {
|
|
239
|
+
throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Require non-empty input
|
|
243
|
+
if (!providerChoice || providerChoice.trim() === '') {
|
|
244
|
+
console.log(colors.red + i18n.tSync('ui.general.provider_selection_required', providers.length) + colors.reset);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Validate numeric input
|
|
249
|
+
if (isNaN(providerChoice)) {
|
|
250
|
+
console.log(colors.red + i18n.tSync('ui.general.invalid_provider_selection').replace('{0}', providers.length) + colors.reset);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const index = parseInt(providerChoice) - 1;
|
|
255
|
+
if (index < 0 || index >= providers.length) {
|
|
256
|
+
console.log(colors.red + i18n.tSync('ui.general.invalid_provider_number').replace('{0}', providers.length) + colors.reset);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Valid selection
|
|
261
|
+
selectedProvider = providers[index];
|
|
262
|
+
baseUrl = selectedProvider.baseUrl;
|
|
263
|
+
suggestedModels = selectedProvider.models || [];
|
|
264
|
+
|
|
265
|
+
console.log('');
|
|
266
|
+
console.log(colors.green + i18n.tSync('ui.general.selected_provider', selectedProvider.name) + colors.reset);
|
|
267
|
+
if (selectedProvider.note) {
|
|
268
|
+
if (selectedProvider.id === 'custom') {
|
|
269
|
+
console.log(colors.yellow + ' ' + i18n.tSync('ui.general.replace_url_model_note') + colors.reset);
|
|
270
|
+
} else {
|
|
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);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
console.log('');
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Input base URL - different handling for custom vs specific providers
|
|
285
|
+
if (selectedProvider && selectedProvider.id === 'custom') {
|
|
286
|
+
// Custom provider - show reference URL and require manual input
|
|
287
|
+
console.log(colors.gray + ` ` + i18n.tSync('ui.general.reference_base_url', baseUrl) + colors.reset);
|
|
288
|
+
console.log('');
|
|
289
|
+
|
|
290
|
+
while (true) {
|
|
291
|
+
const inputUrl = await simpleInput(colors.green + i18n.tSync('ui.general.api_base_url_prompt') + colors.reset);
|
|
292
|
+
|
|
293
|
+
if (inputUrl.toLowerCase() === 'exit' || inputUrl.toLowerCase() === 'quit') {
|
|
294
|
+
throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!inputUrl || inputUrl.trim() === '') {
|
|
298
|
+
console.log(colors.red + i18n.tSync('ui.general.base_url_required') + colors.reset);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const validation = validateBaseUrl(inputUrl);
|
|
303
|
+
if (!validation.valid) {
|
|
304
|
+
console.log(colors.red + `❌ ${validation.error}` + colors.reset);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
baseUrl = validation.value;
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
} else if (selectedProvider && !baseUrl.includes('{')) {
|
|
311
|
+
// Specific providers - show recommended URL with option to use default
|
|
312
|
+
console.log(colors.gray + ` ` + i18n.tSync('ui.general.recommended_base_url', baseUrl) + colors.reset);
|
|
313
|
+
|
|
314
|
+
// For all known providers, show the recommended URL in the prompt
|
|
315
|
+
let prompt;
|
|
316
|
+
if (selectedProvider.id === 'anthropic' || selectedProvider.id === 'deepseek' ||
|
|
317
|
+
selectedProvider.id === 'moonshot' || selectedProvider.id === 'zhipu' || selectedProvider.id === 'zai') {
|
|
318
|
+
prompt = colors.green + i18n.tSync('ui.general.press_enter_default_url') + `${colors.yellow}${baseUrl}${colors.green}` + colors.reset;
|
|
319
|
+
console.log(colors.gray + ' ' + i18n.tSync('ui.general.edit_url_hint') + colors.reset);
|
|
320
|
+
} else {
|
|
321
|
+
prompt = colors.green + i18n.tSync('ui.general.press_enter_default_url') + colors.reset;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const customUrl = await simpleInput(prompt);
|
|
325
|
+
if (customUrl) {
|
|
326
|
+
if (customUrl.toLowerCase() === 'exit' || customUrl.toLowerCase() === 'quit') {
|
|
327
|
+
throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
|
|
328
|
+
}
|
|
329
|
+
baseUrl = customUrl;
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
// Fallback case
|
|
333
|
+
while (true) {
|
|
334
|
+
const inputUrl = await simpleInput(colors.green + i18n.tSync('ui.general.api_base_url_prompt') + colors.reset);
|
|
335
|
+
|
|
336
|
+
if (inputUrl.toLowerCase() === 'exit' || inputUrl.toLowerCase() === 'quit') {
|
|
337
|
+
throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const validation = validateBaseUrl(inputUrl);
|
|
341
|
+
if (!validation.valid) {
|
|
342
|
+
console.log(colors.red + `❌ ${validation.error}` + colors.reset);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
baseUrl = validation.value;
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Input auth token
|
|
351
|
+
let authToken;
|
|
352
|
+
console.log('');
|
|
353
|
+
|
|
354
|
+
// Simplified API token input
|
|
355
|
+
if (selectedProvider) {
|
|
356
|
+
console.log(colors.gray + ` ` + i18n.tSync('ui.general.expected_format', selectedProvider.authTokenFormat) + colors.reset);
|
|
357
|
+
}
|
|
358
|
+
console.log(colors.gray + ' ' + i18n.tSync('ui.general.type_exit_cancel_setup') + colors.reset);
|
|
359
|
+
console.log('');
|
|
360
|
+
|
|
361
|
+
while (true) {
|
|
362
|
+
const token = await simpleInput(colors.green + i18n.tSync('ui.general.auth_token_prompt') + colors.reset);
|
|
363
|
+
|
|
364
|
+
if (token.toLowerCase() === 'exit' || token.toLowerCase() === 'quit') {
|
|
365
|
+
throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const validation = validateAuthToken(token);
|
|
369
|
+
if (!validation.valid) {
|
|
370
|
+
console.log(colors.red + `❌ ${validation.error}` + colors.reset);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
authToken = validation.value;
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Input model - different handling for custom vs specific providers
|
|
378
|
+
let model;
|
|
379
|
+
console.log('');
|
|
380
|
+
|
|
381
|
+
if (selectedProvider && selectedProvider.id === 'custom') {
|
|
382
|
+
// Custom provider - always require manual input, no suggested models
|
|
383
|
+
while (true) {
|
|
384
|
+
const inputModel = await simpleInput(colors.green + i18n.tSync('ui.general.model_name_prompt') + colors.reset);
|
|
385
|
+
|
|
386
|
+
if (inputModel.toLowerCase() === 'exit' || inputModel.toLowerCase() === 'quit') {
|
|
387
|
+
throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const validation = validateModel(inputModel);
|
|
391
|
+
if (!validation.valid) {
|
|
392
|
+
console.log(colors.red + `❌ ${validation.error}` + colors.reset);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
model = validation.value;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
} else if (suggestedModels.length > 0) {
|
|
399
|
+
// Specific providers - show suggested models
|
|
400
|
+
console.log(colors.cyan + ' ' + i18n.tSync('ui.general.suggested_models') + colors.reset);
|
|
401
|
+
suggestedModels.forEach((m, i) => {
|
|
402
|
+
console.log(colors.gray + ` ${i + 1}. ${m}` + colors.reset);
|
|
403
|
+
});
|
|
404
|
+
console.log('');
|
|
405
|
+
|
|
406
|
+
while (true) {
|
|
407
|
+
const modelPrompt = i18n.tSync('ui.general.select_model_prompt').replace('{0}', suggestedModels.length);
|
|
408
|
+
const modelChoice = await simpleInput(colors.green + modelPrompt + colors.reset);
|
|
409
|
+
|
|
410
|
+
if (modelChoice.toLowerCase() === 'exit' || modelChoice.toLowerCase() === 'quit') {
|
|
411
|
+
throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Check if it's a number selection
|
|
415
|
+
if (!isNaN(modelChoice) && modelChoice.trim() !== '') {
|
|
416
|
+
const index = parseInt(modelChoice) - 1;
|
|
417
|
+
if (index >= 0 && index < suggestedModels.length) {
|
|
418
|
+
model = suggestedModels[index];
|
|
419
|
+
break;
|
|
420
|
+
} else {
|
|
421
|
+
console.log(colors.red + i18n.tSync('ui.general.invalid_model_selection').replace('{0}', suggestedModels.length) + colors.reset);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// If not a number, validate as custom model name
|
|
427
|
+
const validation = validateModel(modelChoice);
|
|
428
|
+
if (!validation.valid) {
|
|
429
|
+
console.log(colors.red + `❌ ${validation.error}` + colors.reset);
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
model = validation.value;
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
} else {
|
|
436
|
+
// Fallback - manual input
|
|
437
|
+
while (true) {
|
|
438
|
+
const inputModel = await simpleInput(colors.green + i18n.tSync('ui.general.model_name_prompt') + colors.reset);
|
|
439
|
+
|
|
440
|
+
if (inputModel.toLowerCase() === 'exit' || inputModel.toLowerCase() === 'quit') {
|
|
441
|
+
throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const validation = validateModel(inputModel);
|
|
445
|
+
if (!validation.valid) {
|
|
446
|
+
console.log(colors.red + `❌ ${validation.error}` + colors.reset);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
model = validation.value;
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Input name
|
|
455
|
+
const name = await simpleInput(colors.green + i18n.tSync('ui.general.api_name_prompt') + colors.reset);
|
|
456
|
+
if (name.toLowerCase() === 'exit' || name.toLowerCase() === 'quit') {
|
|
457
|
+
throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return {
|
|
461
|
+
baseUrl,
|
|
462
|
+
authToken,
|
|
463
|
+
model,
|
|
464
|
+
name: name || undefined,
|
|
465
|
+
provider: selectedProvider?.id || 'custom'
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
} catch (error) {
|
|
469
|
+
// Let the upper layer handle error display to avoid duplicate messages
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Confirm action prompt
|
|
476
|
+
*/
|
|
477
|
+
async function confirmAction(message) {
|
|
478
|
+
console.log(colors.yellow + message + colors.reset);
|
|
479
|
+
console.log(colors.gray + i18n.tSync('ui.general.press_y_confirm') + colors.reset);
|
|
480
|
+
|
|
481
|
+
return new Promise((resolve) => {
|
|
482
|
+
if (process.stdin.isTTY) {
|
|
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
|
+
}
|
|
501
|
+
|
|
502
|
+
// If waiting for second Ctrl+C, any other key cancels it
|
|
503
|
+
if (stdinManager.isCtrlCPending()) {
|
|
504
|
+
stdinManager.cancelCtrlC();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const yes = key.toString().trim().toLowerCase() === 'y';
|
|
508
|
+
scope.release();
|
|
509
|
+
resolve(yes);
|
|
510
|
+
});
|
|
511
|
+
} else {
|
|
512
|
+
const scope = stdinManager.acquire('line', {
|
|
513
|
+
id: 'confirmAction_nonTTY',
|
|
514
|
+
allowNested: true
|
|
515
|
+
});
|
|
516
|
+
const rl = scope.createReadline();
|
|
517
|
+
rl.question('', (answer) => {
|
|
518
|
+
rl.close();
|
|
519
|
+
scope.release();
|
|
520
|
+
resolve(answer.toLowerCase() === 'y');
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Display success message
|
|
528
|
+
*/
|
|
529
|
+
function showSuccess(title, details = []) {
|
|
530
|
+
console.log('');
|
|
531
|
+
console.log(colors.bright + colors.green + `✓ ${title}` + colors.reset);
|
|
532
|
+
details.forEach(detail => {
|
|
533
|
+
console.log(colors.gray + ` ${detail}` + colors.reset);
|
|
534
|
+
});
|
|
535
|
+
console.log('');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Display error message
|
|
540
|
+
*/
|
|
541
|
+
function showError(title, details = []) {
|
|
542
|
+
console.log('');
|
|
543
|
+
console.log(colors.bright + colors.red + `❌ ${title}` + colors.reset);
|
|
544
|
+
details.forEach(detail => {
|
|
545
|
+
console.log(colors.gray + ` ${detail}` + colors.reset);
|
|
546
|
+
});
|
|
547
|
+
console.log('');
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Display info message
|
|
552
|
+
*/
|
|
553
|
+
function showInfo(title, details = []) {
|
|
554
|
+
console.log('');
|
|
555
|
+
console.log(colors.bright + colors.cyan + `ℹ️ ${title}` + colors.reset);
|
|
556
|
+
details.forEach(detail => {
|
|
557
|
+
console.log(colors.gray + ` ${detail}` + colors.reset);
|
|
558
|
+
});
|
|
559
|
+
console.log('');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
module.exports = {
|
|
564
|
+
simpleInput,
|
|
565
|
+
waitForKey,
|
|
566
|
+
promptForThirdPartyApi,
|
|
567
|
+
confirmAction,
|
|
568
|
+
showSuccess,
|
|
569
|
+
showError,
|
|
570
|
+
showInfo
|
|
571
|
+
};
|