@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/CHANGELOG.md +70 -0
- package/README.md +23 -13
- package/claude-launcher +1244 -432
- package/docs/README-zh.md +23 -13
- package/lib/api-manager.js +629 -70
- package/lib/auth/password-input.js +8 -4
- package/lib/auth/password-validator.js +83 -48
- package/lib/i18n/index.js +4 -3
- package/lib/i18n/language-manager.js +4 -3
- package/lib/i18n/locales/de.js +229 -13
- package/lib/i18n/locales/en.js +235 -13
- package/lib/i18n/locales/es.js +229 -13
- package/lib/i18n/locales/fr.js +229 -13
- package/lib/i18n/locales/it.js +229 -13
- package/lib/i18n/locales/ja.js +229 -13
- package/lib/i18n/locales/ko.js +229 -13
- package/lib/i18n/locales/pt.js +229 -13
- package/lib/i18n/locales/ru.js +229 -13
- package/lib/i18n/locales/zh-TW.js +229 -13
- package/lib/i18n/locales/zh.js +235 -13
- package/lib/launcher.js +167 -110
- package/lib/presets/providers.js +143 -39
- package/lib/ui/api-editor.js +668 -0
- package/lib/ui/i18n-labels.js +16 -0
- package/lib/ui/interactive-table.js +216 -99
- package/lib/ui/menu.js +79 -62
- package/lib/ui/prompts.js +168 -139
- package/lib/ui/screen.js +125 -0
- package/lib/utils/stdin-manager.js +11 -9
- package/lib/utils/version-checker.js +65 -4
- package/lib/validators.js +102 -1
- package/package.json +2 -2
- package/docs/superpowers/plans/2026-03-31-update-models-and-auto-mode.md +0 -1414
- package/docs/superpowers/specs/2026-03-31-update-models-and-auto-mode-design.md +0 -187
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
209
|
+
if (providerChoice === null) {
|
|
210
|
+
return null; // Esc cancel
|
|
211
|
+
}
|
|
206
212
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
239
|
-
throw new Error(i18n.tSync('errors.general.cancelled_by_user'));
|
|
240
|
-
}
|
|
233
|
+
const selectedProvider = providers[index];
|
|
241
234
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
383
|
+
screen.write('\n');
|
|
353
384
|
|
|
354
385
|
// Simplified API token input
|
|
355
386
|
if (selectedProvider) {
|
|
356
|
-
|
|
387
|
+
screen.write(colors.gray + ` ` + i18n.tSync('ui.general.expected_format', selectedProvider.authTokenFormat) + colors.reset + '\n');
|
|
357
388
|
}
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
431
|
+
screen.write(colors.cyan + ' ' + i18n.tSync('ui.general.suggested_models') + colors.reset + '\n');
|
|
401
432
|
suggestedModels.forEach((m, i) => {
|
|
402
|
-
|
|
433
|
+
screen.write(colors.gray + ` ${i + 1}. ${m}` + colors.reset + '\n');
|
|
403
434
|
});
|
|
404
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
479
|
-
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
details.forEach(detail => {
|
|
533
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
details.forEach(detail => {
|
|
545
|
-
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
details.forEach(detail => {
|
|
557
|
-
|
|
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,
|
package/lib/ui/screen.js
ADDED
|
@@ -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();
|