@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.
@@ -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
+ };