@kikkimo/claude-launcher 2.5.0 → 3.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/lib/ui/prompts.js CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  const readline = require('readline');
6
6
  const colors = require('./colors');
7
+ const screen = require('./screen');
7
8
  const { getAllProviders } = require('../presets/providers');
8
9
  const { validateBaseUrl, validateAuthToken, validateModel } = require('../validators');
9
10
  const i18n = require('../i18n');
@@ -20,10 +21,14 @@ async function simpleInput(prompt) {
20
21
  });
21
22
 
22
23
  const rl = scope.createReadline();
24
+ screen.showCursor();
25
+ screen.setReadlineActive(true);
23
26
 
24
27
  rl.question(prompt, (answer) => {
25
28
  rl.close();
26
29
  scope.release();
30
+ screen.setReadlineActive(false);
31
+ screen.hideCursor();
27
32
  resolve(answer.trim());
28
33
  });
29
34
  });
@@ -35,7 +40,8 @@ async function simpleInput(prompt) {
35
40
  async function getProviderChoice(prompt) {
36
41
  return new Promise((resolve) => {
37
42
  if (process.stdin.isTTY) {
38
- process.stdout.write(colors.green + prompt + colors.reset);
43
+ screen.showCursor();
44
+ screen.write(colors.green + prompt + colors.reset);
39
45
 
40
46
  let input = '';
41
47
  const scope = stdinManager.acquire('raw', {
@@ -53,7 +59,8 @@ async function getProviderChoice(prompt) {
53
59
  // Resolve with null to indicate cancellation (same as ESC key).
54
60
  const exited = stdinManager.handleCtrlC();
55
61
  if (exited === false) {
56
- process.stdout.write('\n');
62
+ screen.write('\n');
63
+ screen.hideCursor();
57
64
  resolve(null); // User cancelled with Ctrl+C
58
65
  }
59
66
  return;
@@ -68,13 +75,15 @@ async function getProviderChoice(prompt) {
68
75
  switch (keyCode) {
69
76
  case 27: // ESC key
70
77
  scope.release();
71
- process.stdout.write('\n');
78
+ screen.write('\n');
79
+ screen.hideCursor();
72
80
  resolve(null);
73
81
  return;
74
82
 
75
83
  case 13: // Enter key
76
84
  scope.release();
77
- process.stdout.write('\n');
85
+ screen.write('\n');
86
+ screen.hideCursor();
78
87
  resolve(input);
79
88
  return;
80
89
 
@@ -82,7 +91,7 @@ async function getProviderChoice(prompt) {
82
91
  case 8: // Backspace (some terminals)
83
92
  if (input.length > 0) {
84
93
  input = input.slice(0, -1);
85
- process.stdout.write('\b \b');
94
+ screen.write('\b \b');
86
95
  }
87
96
  return;
88
97
 
@@ -90,7 +99,7 @@ async function getProviderChoice(prompt) {
90
99
  // Only accept printable ASCII characters
91
100
  if (keyCode >= 32 && keyCode < 127) {
92
101
  input += key;
93
- process.stdout.write(key);
102
+ screen.write(key);
94
103
  }
95
104
  return;
96
105
  }
@@ -117,7 +126,7 @@ async function getProviderChoice(prompt) {
117
126
  * Wait for any key press
118
127
  */
119
128
  async function waitForKey(message = 'Press any key to continue...') {
120
- console.log(colors.gray + message + colors.reset);
129
+ screen.write(colors.gray + message + colors.reset + '\n');
121
130
 
122
131
  return new Promise((resolve) => {
123
132
  if (process.stdin.isTTY) {
@@ -173,119 +182,141 @@ async function waitForKey(message = 'Press any key to continue...') {
173
182
  }
174
183
 
175
184
  /**
176
- * Prompt for third-party API configuration with enhanced guidance
185
+ * Display provider selection list and return selected provider
186
+ * @param {Object} options - { title: string|null, showNote: boolean }
187
+ * @returns {Object|null} Selected provider { id, name, baseUrl, models, note } or null on cancel
177
188
  */
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('');
189
+ async function selectProvider({ title = null, showNote = true } = {}) {
190
+ const lines = [];
191
+ if (title) {
192
+ lines.push(colors.cyan + title + colors.reset);
193
+ lines.push('');
194
+ }
193
195
 
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('');
196
+ const providers = getAllProviders();
197
+ providers.forEach((provider, index) => {
198
+ const compatIcon = provider.compatibility === 'native' ? '🎯' : '✅';
199
+ lines.push(colors.gray + ` ${index + 1}. ${compatIcon} ${provider.name}` + colors.reset);
200
+ lines.push(colors.dim + ` ${provider.description}` + colors.reset);
201
+ });
202
+ lines.push('');
203
+ screen.render(lines);
201
204
 
202
- console.log(colors.yellow + i18n.tSync('ui.general.all_providers_compatible') + colors.reset);
203
- console.log('');
205
+ while (true) {
206
+ const selectPrompt = i18n.tSync('ui.general.select_provider_prompt').replace('{0}', providers.length);
207
+ const providerChoice = await getProviderChoice(selectPrompt);
204
208
 
205
- await waitForKey(i18n.tSync('ui.general.press_continue_provider_selection'));
209
+ if (providerChoice === null) {
210
+ return null; // Esc cancel
211
+ }
206
212
 
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('');
213
+ if (providerChoice.toLowerCase() === 'exit' || providerChoice.toLowerCase() === 'quit') {
214
+ return null; // exit/quit cancel
215
+ }
223
216
 
224
- // Select provider or custom with validation
225
- let selectedProvider = null;
226
- let baseUrl = '';
227
- let suggestedModels = [];
217
+ if (!providerChoice || providerChoice.trim() === '') {
218
+ screen.write(colors.red + i18n.tSync('ui.general.provider_selection_required', providers.length) + colors.reset + '\n');
219
+ continue;
220
+ }
228
221
 
229
- while (true) {
230
- const selectPrompt = i18n.tSync('ui.general.select_provider_prompt').replace('{0}', providers.length);
231
- const providerChoice = await getProviderChoice(selectPrompt);
222
+ if (isNaN(providerChoice)) {
223
+ screen.write(colors.red + i18n.tSync('ui.general.invalid_provider_selection').replace('{0}', providers.length) + colors.reset + '\n');
224
+ continue;
225
+ }
232
226
 
233
- if (providerChoice === null) {
234
- // User cancelled with ESC
235
- throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
236
- }
227
+ const index = parseInt(providerChoice) - 1;
228
+ if (index < 0 || index >= providers.length) {
229
+ screen.write(colors.red + i18n.tSync('ui.general.invalid_provider_number').replace('{0}', providers.length) + colors.reset + '\n');
230
+ continue;
231
+ }
237
232
 
238
- if (providerChoice.toLowerCase() === 'exit' || providerChoice.toLowerCase() === 'quit') {
239
- throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
240
- }
233
+ const selectedProvider = providers[index];
241
234
 
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;
235
+ screen.write('\n');
236
+ screen.write(colors.green + i18n.tSync('ui.general.selected_provider', selectedProvider.name) + colors.reset + '\n');
237
+ if (showNote && selectedProvider.note) {
238
+ if (selectedProvider.id === 'custom') {
239
+ screen.write(colors.yellow + ' ' + i18n.tSync('ui.general.replace_url_model_note') + colors.reset + '\n');
240
+ } else {
241
+ const noteKey = `provider.notes.${selectedProvider.id}`;
242
+ const noteText = i18n.tSync(noteKey);
243
+ const displayNote = noteText === noteKey ? selectedProvider.note : noteText;
244
+ const notePrefix = i18n.tSync('provider.note_prefix');
245
+ screen.write(colors.yellow + ` ${notePrefix}: ${displayNote}` + colors.reset + '\n');
246
246
  }
247
+ }
248
+ screen.write('\n');
247
249
 
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
- }
250
+ return {
251
+ id: selectedProvider.id,
252
+ name: selectedProvider.name,
253
+ baseUrl: selectedProvider.baseUrl,
254
+ models: selectedProvider.models || [],
255
+ note: selectedProvider.note || null
256
+ };
257
+ }
258
+ }
253
259
 
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
- }
260
+ /**
261
+ * Prompt for third-party API configuration with enhanced guidance
262
+ */
263
+ async function promptForThirdPartyApi() {
264
+ try {
265
+ // Step 1: Show information and wait for acknowledgment
266
+ {
267
+ const lines = [];
268
+ lines.push('');
269
+ lines.push(colors.bright + colors.orange + i18n.tSync('ui.general.add_new_api_title') + colors.reset);
270
+ lines.push('');
271
+
272
+ // Security and privacy information
273
+ lines.push(colors.yellow + i18n.tSync('ui.general.security_privacy_info') + colors.reset);
274
+ const securityItems = i18n.tSync('ui.general.security_items');
275
+ securityItems.forEach(item => {
276
+ lines.push(colors.bright + colors.green + ' • ' + item + colors.reset);
277
+ });
278
+ lines.push('');
259
279
 
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;
280
+ lines.push(colors.yellow + i18n.tSync('ui.general.configuration_tips') + colors.reset);
281
+ const configTips = i18n.tSync('ui.general.config_tip_items');
282
+ configTips.forEach(tip => {
283
+ lines.push(colors.gray + ' • ' + tip + colors.reset);
284
+ });
285
+ lines.push(colors.gray + '' + i18n.tSync('ui.general.type_exit_cancel') + colors.reset);
286
+ lines.push('');
287
+
288
+ lines.push(colors.yellow + i18n.tSync('ui.general.all_providers_compatible') + colors.reset);
289
+ lines.push('');
290
+ screen.render(lines);
291
+ }
292
+
293
+ await waitForKey(i18n.tSync('ui.general.press_continue_provider_selection'));
294
+
295
+ // Step 2: Show provider selection menu
296
+ screen.render([
297
+ '',
298
+ colors.bright + colors.orange + i18n.tSync('ui.general.add_new_api_title') + colors.reset,
299
+ ''
300
+ ]);
301
+
302
+ const selectedProviderResult = await selectProvider({
303
+ title: i18n.tSync('ui.general.compatible_providers_title'),
304
+ showNote: true
305
+ });
306
+
307
+ if (!selectedProviderResult) {
308
+ throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
282
309
  }
283
310
 
311
+ const selectedProvider = selectedProviderResult;
312
+ let baseUrl = selectedProvider.baseUrl;
313
+ let suggestedModels = selectedProvider.models;
314
+
284
315
  // Input base URL - different handling for custom vs specific providers
285
316
  if (selectedProvider && selectedProvider.id === 'custom') {
286
317
  // 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('');
318
+ screen.write(colors.gray + ` ` + i18n.tSync('ui.general.reference_base_url', baseUrl) + colors.reset + '\n');
319
+ screen.write('\n');
289
320
 
290
321
  while (true) {
291
322
  const inputUrl = await simpleInput(colors.green + i18n.tSync('ui.general.api_base_url_prompt') + colors.reset);
@@ -295,13 +326,13 @@ async function promptForThirdPartyApi() {
295
326
  }
296
327
 
297
328
  if (!inputUrl || inputUrl.trim() === '') {
298
- console.log(colors.red + i18n.tSync('ui.general.base_url_required') + colors.reset);
329
+ screen.write(colors.red + i18n.tSync('ui.general.base_url_required') + colors.reset + '\n');
299
330
  continue;
300
331
  }
301
332
 
302
333
  const validation = validateBaseUrl(inputUrl);
303
334
  if (!validation.valid) {
304
- console.log(colors.red + `❌ ${validation.error}` + colors.reset);
335
+ screen.write(colors.red + `❌ ${validation.error}` + colors.reset + '\n');
305
336
  continue;
306
337
  }
307
338
  baseUrl = validation.value;
@@ -309,14 +340,14 @@ async function promptForThirdPartyApi() {
309
340
  }
310
341
  } else if (selectedProvider && !baseUrl.includes('{')) {
311
342
  // 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);
343
+ screen.write(colors.gray + ` ` + i18n.tSync('ui.general.recommended_base_url', baseUrl) + colors.reset + '\n');
313
344
 
314
345
  // For all known providers, show the recommended URL in the prompt
315
346
  let prompt;
316
347
  if (selectedProvider.id === 'anthropic' || selectedProvider.id === 'deepseek' ||
317
348
  selectedProvider.id === 'moonshot' || selectedProvider.id === 'kimi_for_coding' || selectedProvider.id === 'zhipu' || selectedProvider.id === 'zai') {
318
349
  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);
350
+ screen.write(colors.gray + ' ' + i18n.tSync('ui.general.edit_url_hint') + colors.reset + '\n');
320
351
  } else {
321
352
  prompt = colors.green + i18n.tSync('ui.general.press_enter_default_url') + colors.reset;
322
353
  }
@@ -339,7 +370,7 @@ async function promptForThirdPartyApi() {
339
370
 
340
371
  const validation = validateBaseUrl(inputUrl);
341
372
  if (!validation.valid) {
342
- console.log(colors.red + `❌ ${validation.error}` + colors.reset);
373
+ screen.write(colors.red + `❌ ${validation.error}` + colors.reset + '\n');
343
374
  continue;
344
375
  }
345
376
  baseUrl = validation.value;
@@ -349,14 +380,14 @@ async function promptForThirdPartyApi() {
349
380
 
350
381
  // Input auth token
351
382
  let authToken;
352
- console.log('');
383
+ screen.write('\n');
353
384
 
354
385
  // Simplified API token input
355
386
  if (selectedProvider) {
356
- console.log(colors.gray + ` ` + i18n.tSync('ui.general.expected_format', selectedProvider.authTokenFormat) + colors.reset);
387
+ screen.write(colors.gray + ` ` + i18n.tSync('ui.general.expected_format', selectedProvider.authTokenFormat) + colors.reset + '\n');
357
388
  }
358
- console.log(colors.gray + ' ' + i18n.tSync('ui.general.type_exit_cancel_setup') + colors.reset);
359
- console.log('');
389
+ screen.write(colors.gray + ' ' + i18n.tSync('ui.general.type_exit_cancel_setup') + colors.reset + '\n');
390
+ screen.write('\n');
360
391
 
361
392
  while (true) {
362
393
  const token = await simpleInput(colors.green + i18n.tSync('ui.general.auth_token_prompt') + colors.reset);
@@ -367,7 +398,7 @@ async function promptForThirdPartyApi() {
367
398
 
368
399
  const validation = validateAuthToken(token);
369
400
  if (!validation.valid) {
370
- console.log(colors.red + `❌ ${validation.error}` + colors.reset);
401
+ screen.write(colors.red + `❌ ${validation.error}` + colors.reset + '\n');
371
402
  continue;
372
403
  }
373
404
  authToken = validation.value;
@@ -376,7 +407,7 @@ async function promptForThirdPartyApi() {
376
407
 
377
408
  // Input model - different handling for custom vs specific providers
378
409
  let model;
379
- console.log('');
410
+ screen.write('\n');
380
411
 
381
412
  if (selectedProvider && selectedProvider.id === 'custom') {
382
413
  // Custom provider - always require manual input, no suggested models
@@ -389,7 +420,7 @@ async function promptForThirdPartyApi() {
389
420
 
390
421
  const validation = validateModel(inputModel);
391
422
  if (!validation.valid) {
392
- console.log(colors.red + `❌ ${validation.error}` + colors.reset);
423
+ screen.write(colors.red + `❌ ${validation.error}` + colors.reset + '\n');
393
424
  continue;
394
425
  }
395
426
  model = validation.value;
@@ -397,11 +428,11 @@ async function promptForThirdPartyApi() {
397
428
  }
398
429
  } else if (suggestedModels.length > 0) {
399
430
  // Specific providers - show suggested models
400
- console.log(colors.cyan + ' ' + i18n.tSync('ui.general.suggested_models') + colors.reset);
431
+ screen.write(colors.cyan + ' ' + i18n.tSync('ui.general.suggested_models') + colors.reset + '\n');
401
432
  suggestedModels.forEach((m, i) => {
402
- console.log(colors.gray + ` ${i + 1}. ${m}` + colors.reset);
433
+ screen.write(colors.gray + ` ${i + 1}. ${m}` + colors.reset + '\n');
403
434
  });
404
- console.log('');
435
+ screen.write('\n');
405
436
 
406
437
  while (true) {
407
438
  const modelPrompt = i18n.tSync('ui.general.select_model_prompt').replace('{0}', suggestedModels.length);
@@ -418,7 +449,7 @@ async function promptForThirdPartyApi() {
418
449
  model = suggestedModels[index];
419
450
  break;
420
451
  } else {
421
- console.log(colors.red + i18n.tSync('ui.general.invalid_model_selection').replace('{0}', suggestedModels.length) + colors.reset);
452
+ screen.write(colors.red + i18n.tSync('ui.general.invalid_model_selection').replace('{0}', suggestedModels.length) + colors.reset + '\n');
422
453
  continue;
423
454
  }
424
455
  }
@@ -426,7 +457,7 @@ async function promptForThirdPartyApi() {
426
457
  // If not a number, validate as custom model name
427
458
  const validation = validateModel(modelChoice);
428
459
  if (!validation.valid) {
429
- console.log(colors.red + `❌ ${validation.error}` + colors.reset);
460
+ screen.write(colors.red + `❌ ${validation.error}` + colors.reset + '\n');
430
461
  continue;
431
462
  }
432
463
  model = validation.value;
@@ -443,7 +474,7 @@ async function promptForThirdPartyApi() {
443
474
 
444
475
  const validation = validateModel(inputModel);
445
476
  if (!validation.valid) {
446
- console.log(colors.red + `❌ ${validation.error}` + colors.reset);
477
+ screen.write(colors.red + `❌ ${validation.error}` + colors.reset + '\n');
447
478
  continue;
448
479
  }
449
480
  model = validation.value;
@@ -475,8 +506,8 @@ async function promptForThirdPartyApi() {
475
506
  * Confirm action prompt
476
507
  */
477
508
  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);
509
+ screen.write(colors.yellow + message + colors.reset + '\n');
510
+ screen.write(colors.gray + i18n.tSync('ui.general.press_y_confirm') + colors.reset + '\n');
480
511
 
481
512
  return new Promise((resolve) => {
482
513
  if (process.stdin.isTTY) {
@@ -527,36 +558,33 @@ async function confirmAction(message) {
527
558
  * Display success message
528
559
  */
529
560
  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('');
561
+ const lines = [''];
562
+ lines.push(colors.bright + colors.green + `✓ ${title}` + colors.reset);
563
+ details.forEach(detail => lines.push(colors.gray + ` ${detail}` + colors.reset));
564
+ lines.push('');
565
+ screen.render(lines);
536
566
  }
537
567
 
538
568
  /**
539
569
  * Display error message
540
570
  */
541
571
  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('');
572
+ const lines = [''];
573
+ lines.push(colors.bright + colors.red + `❌ ${title}` + colors.reset);
574
+ details.forEach(detail => lines.push(colors.gray + ` ${detail}` + colors.reset));
575
+ lines.push('');
576
+ screen.render(lines);
548
577
  }
549
578
 
550
579
  /**
551
580
  * Display info message
552
581
  */
553
582
  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('');
583
+ const lines = [''];
584
+ lines.push(colors.bright + colors.cyan + `ℹ️ ${title}` + colors.reset);
585
+ details.forEach(detail => lines.push(colors.gray + ` ${detail}` + colors.reset));
586
+ lines.push('');
587
+ screen.render(lines);
560
588
  }
561
589
 
562
590
 
@@ -564,6 +592,7 @@ module.exports = {
564
592
  simpleInput,
565
593
  waitForKey,
566
594
  promptForThirdPartyApi,
595
+ selectProvider,
567
596
  confirmAction,
568
597
  showSuccess,
569
598
  showError,
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Screen Singleton - ANSI terminal rendering layer
3
+ *
4
+ * All UI output goes through this module. Eliminates position drift
5
+ * by using absolute cursor positioning (cursorHome + clearScreen)
6
+ * and alternate screen buffer for isolation.
7
+ */
8
+
9
+ const ANSI = {
10
+ enterAltScreen: '\x1b[?1049h',
11
+ exitAltScreen: '\x1b[?1049l',
12
+ cursorHome: '\x1b[H',
13
+ clearScreen: '\x1b[2J',
14
+ cursorHide: '\x1b[?25l',
15
+ cursorShow: '\x1b[?25h',
16
+ reset: '\x1b[0m',
17
+ };
18
+
19
+ class Screen {
20
+ constructor() {
21
+ this.inAltScreen = false;
22
+ this.isTTY = process.stdout.isTTY || false;
23
+ this.noAlt = process.env.SCREEN_NO_ALT === '1';
24
+ this.testMode = process.env.SCREEN_TEST === '1';
25
+ this.readlineActive = false;
26
+ this.currentTag = null;
27
+ this._log = [];
28
+ }
29
+
30
+ enter() {
31
+ if (!this.isTTY) return;
32
+ if (this.inAltScreen) return;
33
+ if (!this.noAlt) {
34
+ this._rawWrite(ANSI.enterAltScreen);
35
+ }
36
+ this._rawWrite(ANSI.cursorHide);
37
+ this.inAltScreen = true;
38
+ }
39
+
40
+ exit() {
41
+ if (!this.inAltScreen) return;
42
+ this._rawWrite(ANSI.cursorShow);
43
+ if (!this.noAlt) {
44
+ this._rawWrite(ANSI.exitAltScreen);
45
+ }
46
+ this.inAltScreen = false;
47
+ }
48
+
49
+ exitForHandoff() {
50
+ if (!this.inAltScreen) return;
51
+ this._rawWrite(ANSI.cursorShow);
52
+ this._rawWrite(ANSI.reset);
53
+ if (!this.noAlt) {
54
+ this._rawWrite(ANSI.exitAltScreen);
55
+ }
56
+ if (this.isTTY && process.stdin.isTTY) {
57
+ try { process.stdin.setRawMode(false); } catch (_) {}
58
+ }
59
+ this.inAltScreen = false;
60
+ }
61
+
62
+ render(lines) {
63
+ this.currentTag = 'render';
64
+ if (this.isTTY) {
65
+ this._rawWrite(ANSI.cursorHome + ANSI.clearScreen);
66
+ }
67
+ for (const line of lines) {
68
+ this._rawWrite(line + '\n');
69
+ }
70
+ this.currentTag = null;
71
+ }
72
+
73
+ write(text) {
74
+ this.currentTag = 'write';
75
+ this._rawWrite(text);
76
+ this.currentTag = null;
77
+ }
78
+
79
+ showCursor() {
80
+ if (this.isTTY) {
81
+ this._rawWrite(ANSI.cursorShow);
82
+ }
83
+ }
84
+
85
+ hideCursor() {
86
+ if (this.isTTY) {
87
+ this._rawWrite(ANSI.cursorHide);
88
+ }
89
+ }
90
+
91
+ isActive() {
92
+ return this.inAltScreen;
93
+ }
94
+
95
+ debug(message) {
96
+ if (this.inAltScreen) return; // Suppress during alt-screen
97
+ this._rawStderr(message + '\n');
98
+ }
99
+
100
+ setReadlineActive(active) {
101
+ this.readlineActive = active;
102
+ this.currentTag = active ? 'readline' : null;
103
+ }
104
+
105
+ getLog() {
106
+ return this._log;
107
+ }
108
+
109
+ _rawWrite(data) {
110
+ if (this.testMode) {
111
+ const tag = this.readlineActive ? 'readline' : (this.currentTag || 'untagged');
112
+ this._log.push({ channel: 'stdout', tag, data: data.substring(0, 80), time: Date.now() });
113
+ }
114
+ process.stdout.write(data);
115
+ }
116
+
117
+ _rawStderr(data) {
118
+ if (this.testMode) {
119
+ this._log.push({ channel: 'stderr', tag: 'debug', data: data.substring(0, 80), time: Date.now() });
120
+ }
121
+ process.stderr.write(data);
122
+ }
123
+ }
124
+
125
+ module.exports = new Screen();