@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,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple Interactive Table Test - Minimal version for testing clearing
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const colors = require('./colors');
|
|
6
|
+
const { maskApiToken } = require('../validators');
|
|
7
|
+
const { decrypt } = require('../crypto');
|
|
8
|
+
const i18n = require('../i18n');
|
|
9
|
+
const { padStringToWidth } = require('../utils/string-width');
|
|
10
|
+
const stdinManager = require('../utils/stdin-manager');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Display simple interactive table for API selection
|
|
14
|
+
*/
|
|
15
|
+
async function showApiSelectionTable(apis, title, actionType = 'select', activeIndex = -1, apiManager = null) {
|
|
16
|
+
if (apis.length === 0) {
|
|
17
|
+
console.clear();
|
|
18
|
+
console.log('');
|
|
19
|
+
console.log(colors.yellow + 'ℹ️ ' + i18n.tSync('messages.info.no_apis_info_title') + colors.reset);
|
|
20
|
+
console.log(colors.gray + ' ' + i18n.tSync('messages.info.apis_removed_or_none') + colors.reset);
|
|
21
|
+
console.log('');
|
|
22
|
+
console.log(colors.gray + i18n.tSync('messages.info.press_return_menu') + colors.reset);
|
|
23
|
+
|
|
24
|
+
await waitForKeyPress();
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let selectedIndex = 0;
|
|
29
|
+
if (actionType === 'switch' && activeIndex >= 0 && activeIndex < apis.length) {
|
|
30
|
+
selectedIndex = activeIndex;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function displaySimpleTable() {
|
|
34
|
+
// Header info
|
|
35
|
+
console.log('');
|
|
36
|
+
console.log(colors.cyan + title + colors.reset);
|
|
37
|
+
console.log('');
|
|
38
|
+
|
|
39
|
+
// Show current active API for switch mode
|
|
40
|
+
if (actionType === 'switch' && activeIndex >= 0 && activeIndex < apis.length) {
|
|
41
|
+
const activeApi = apis[activeIndex];
|
|
42
|
+
console.log(colors.gray + i18n.tSync('ui.general.currently_active_api') + colors.reset);
|
|
43
|
+
console.log(colors.gray + ` Name: ${activeApi.name}` + colors.reset);
|
|
44
|
+
console.log(colors.gray + ` Provider: ${activeApi.provider}` + colors.reset);
|
|
45
|
+
console.log(colors.gray + ` Usage Count: ${activeApi.usageCount || 0}` + colors.reset);
|
|
46
|
+
console.log('');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Table header with 3-column layout
|
|
50
|
+
console.log(colors.bright + colors.orange +
|
|
51
|
+
'┌────┬─────────────────────────┬────────────────────────────────────────────────────────────────────────┐' + colors.reset);
|
|
52
|
+
console.log(colors.bright + colors.orange +
|
|
53
|
+
'│ No.│ Name │ Detail │' + colors.reset);
|
|
54
|
+
console.log(colors.bright + colors.orange +
|
|
55
|
+
'├────┼─────────────────────────┼────────────────────────────────────────────────────────────────────────┤' + colors.reset);
|
|
56
|
+
|
|
57
|
+
// Testing with multi-row display loop
|
|
58
|
+
apis.forEach((api, index) => {
|
|
59
|
+
const num = (index + 1).toString().padStart(2, ' ');
|
|
60
|
+
|
|
61
|
+
// Check if this is the currently active API
|
|
62
|
+
const isActiveApi = activeIndex === index;
|
|
63
|
+
const activeMarker = isActiveApi ? '●' : ' ';
|
|
64
|
+
|
|
65
|
+
// Format name with active marker
|
|
66
|
+
const nameWithMarker = `${activeMarker} ${api.name}`;
|
|
67
|
+
const displayName = nameWithMarker.padEnd(23, ' ');
|
|
68
|
+
|
|
69
|
+
// Test decrypt and maskApiToken functions
|
|
70
|
+
const decryptedToken = decrypt(api.authToken);
|
|
71
|
+
const displayToken = decryptedToken.success ? maskApiToken(decryptedToken.value) : '***ERROR***';
|
|
72
|
+
|
|
73
|
+
// Create 6 detail lines (full version)
|
|
74
|
+
const details = [
|
|
75
|
+
`Provider: ${api.provider}`,
|
|
76
|
+
`URL: ${api.baseUrl}`,
|
|
77
|
+
`Model: ${api.model}`,
|
|
78
|
+
`Token: ${displayToken}`,
|
|
79
|
+
`Usage: ${api.usageCount || 0} times`,
|
|
80
|
+
`Last Used: ${api.lastUsed ? new Date(api.lastUsed).toLocaleString() : 'Never'}`
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
// Pad each detail line to exactly 70 characters
|
|
84
|
+
const paddedDetails = details.map(detail => padStringToWidth(detail, 70));
|
|
85
|
+
|
|
86
|
+
// Color selection based on active state and selection
|
|
87
|
+
const nameColor = isActiveApi ? colors.green : (index === selectedIndex ? colors.white : colors.gray);
|
|
88
|
+
const detailColor = isActiveApi ? colors.green : (index === selectedIndex ? colors.white : colors.gray);
|
|
89
|
+
const bgColor = index === selectedIndex ? colors.bgAmber : '';
|
|
90
|
+
const textBg = index === selectedIndex ? colors.black : '';
|
|
91
|
+
|
|
92
|
+
// Display 6 rows for each API, with No. and Name centered on row 3 (index 2)
|
|
93
|
+
for (let i = 0; i < paddedDetails.length; i++) {
|
|
94
|
+
if (i === 2) {
|
|
95
|
+
// Middle row (3rd row) - show No. and Name for vertical centering
|
|
96
|
+
console.log(colors.orange + '│' + textBg + bgColor + nameColor +
|
|
97
|
+
` ${num} ` + colors.reset + colors.orange + '│' + textBg + bgColor + nameColor +
|
|
98
|
+
` ${displayName} ` + colors.reset + colors.orange + '│' + textBg + bgColor + detailColor +
|
|
99
|
+
` ${paddedDetails[i]} ` + colors.reset + colors.orange + '│' + colors.reset);
|
|
100
|
+
} else {
|
|
101
|
+
// Other rows - empty No. and Name columns
|
|
102
|
+
console.log(colors.orange + '│' + textBg + bgColor + colors.gray +
|
|
103
|
+
' ' + colors.reset + colors.orange + '│' + textBg + bgColor + colors.gray +
|
|
104
|
+
' ' + colors.reset + colors.orange + '│' + textBg + bgColor + detailColor +
|
|
105
|
+
' ' + paddedDetails[i] + ' ' + colors.reset + colors.orange + '│' + colors.reset);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Add separator line after each API except the last one
|
|
110
|
+
if (index < apis.length - 1) {
|
|
111
|
+
console.log(colors.bright + colors.orange +
|
|
112
|
+
'├────┼─────────────────────────┼────────────────────────────────────────────────────────────────────────┤' + colors.reset);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
console.log(colors.bright + colors.orange +
|
|
117
|
+
'└────┴─────────────────────────┴────────────────────────────────────────────────────────────────────────┘' + colors.reset);
|
|
118
|
+
console.log('');
|
|
119
|
+
|
|
120
|
+
if (actionType === 'switch' && activeIndex >= 0) {
|
|
121
|
+
console.log(colors.green + ' ● = ' + i18n.tSync('ui.general.currently_active_api') + colors.reset);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Different action prompts for different functionality
|
|
125
|
+
const actionText = actionType === 'remove' ? 'remove' : (actionType === 'switch' ? 'switch' : 'select');
|
|
126
|
+
console.log(colors.amber + ' ' + i18n.tSync('navigation.use_arrows_esc', actionText) + colors.reset);
|
|
127
|
+
console.log('');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function handleKeyPress(key) {
|
|
131
|
+
// Handle Ctrl+C first
|
|
132
|
+
if (key === '\u0003') {
|
|
133
|
+
stdinManager.handleCtrlC();
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// If waiting for second Ctrl+C, any other key cancels it
|
|
138
|
+
if (stdinManager.isCtrlCPending()) {
|
|
139
|
+
stdinManager.cancelCtrlC();
|
|
140
|
+
// Continue to process this key normally
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
switch (key) {
|
|
144
|
+
case '\u001b[A': // Up arrow
|
|
145
|
+
selectedIndex = (selectedIndex - 1 + apis.length) % apis.length;
|
|
146
|
+
console.clear(); // Force clear screen
|
|
147
|
+
displaySimpleTable();
|
|
148
|
+
break;
|
|
149
|
+
|
|
150
|
+
case '\u001b[B': // Down arrow
|
|
151
|
+
selectedIndex = (selectedIndex + 1) % apis.length;
|
|
152
|
+
console.clear(); // Force clear screen
|
|
153
|
+
displaySimpleTable();
|
|
154
|
+
break;
|
|
155
|
+
|
|
156
|
+
case '\r': // Enter
|
|
157
|
+
return apis[selectedIndex];
|
|
158
|
+
|
|
159
|
+
case '\u001b': // Escape
|
|
160
|
+
case 'q':
|
|
161
|
+
case 'Q':
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return new Promise((resolve) => {
|
|
168
|
+
// Initial display
|
|
169
|
+
console.clear();
|
|
170
|
+
displaySimpleTable();
|
|
171
|
+
|
|
172
|
+
if (process.stdin.isTTY) {
|
|
173
|
+
// Use StdinManager for proper state management
|
|
174
|
+
const scope = stdinManager.acquire('raw', {
|
|
175
|
+
id: 'showApiSelectionTable',
|
|
176
|
+
allowNested: true
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const keyHandler = async (key) => {
|
|
180
|
+
const result = handleKeyPress(key);
|
|
181
|
+
if (result !== undefined) {
|
|
182
|
+
// Release the scope, which automatically restores previous state
|
|
183
|
+
scope.release();
|
|
184
|
+
|
|
185
|
+
// Handle switch mode - activate the selected API
|
|
186
|
+
if (result && actionType === 'switch' && apiManager) {
|
|
187
|
+
const selectedIndex = apis.findIndex(api => api.id === result.id);
|
|
188
|
+
const switchedApi = apiManager.setActiveApi(selectedIndex);
|
|
189
|
+
|
|
190
|
+
console.clear();
|
|
191
|
+
console.log('');
|
|
192
|
+
console.log(colors.bright + colors.green + `✓ ${i18n.tSync('messages.success.api_switched')}` + colors.reset);
|
|
193
|
+
console.log(colors.gray + ` ${i18n.tSync('api.actions.switch_success', switchedApi.name)}` + colors.reset);
|
|
194
|
+
console.log(colors.gray + ` ${i18n.tSync('api.details.provider')}: ${switchedApi.provider}` + colors.reset);
|
|
195
|
+
console.log(colors.gray + ` ${i18n.tSync('api.details.url')}: ${switchedApi.baseUrl}` + colors.reset);
|
|
196
|
+
console.log(colors.gray + ` ${i18n.tSync('api.details.model')}: ${switchedApi.model}` + colors.reset);
|
|
197
|
+
console.log('');
|
|
198
|
+
|
|
199
|
+
// Wait for user key press
|
|
200
|
+
console.log(colors.gray + i18n.tSync('messages.prompts.press_any_key') + colors.reset);
|
|
201
|
+
await waitForKeyPress();
|
|
202
|
+
} else {
|
|
203
|
+
console.clear();
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log(colors.green + '✓ Selection completed: ' + (result ? result.name : 'Cancelled') + colors.reset);
|
|
206
|
+
console.log('');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
resolve(result);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
scope.on('data', keyHandler);
|
|
214
|
+
} else {
|
|
215
|
+
resolve(null);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function waitForKeyPress() {
|
|
221
|
+
return new Promise((resolve) => {
|
|
222
|
+
// Use StdinManager for proper state management
|
|
223
|
+
const scope = stdinManager.acquire('raw', {
|
|
224
|
+
id: 'waitForKeyPress',
|
|
225
|
+
allowNested: true
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const handler = (key) => {
|
|
229
|
+
// Handle Ctrl+C first
|
|
230
|
+
if (key === '\u0003') {
|
|
231
|
+
stdinManager.handleCtrlC();
|
|
232
|
+
return; // Keep listener active for subsequent keys
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// If waiting for second Ctrl+C, any other key cancels it
|
|
236
|
+
if (stdinManager.isCtrlCPending()) {
|
|
237
|
+
stdinManager.cancelCtrlC();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Manually remove listener before resolving
|
|
241
|
+
scope.removeListener('data', handler);
|
|
242
|
+
// Release the scope, which automatically restores previous state
|
|
243
|
+
scope.release();
|
|
244
|
+
resolve();
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// Use on() instead of once() so Ctrl+C doesn't remove the listener
|
|
248
|
+
scope.on('data', handler);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function confirmDeletion(api) {
|
|
253
|
+
console.clear();
|
|
254
|
+
console.log('');
|
|
255
|
+
console.log(colors.red + colors.bright + '[!] ' + i18n.tSync('messages.prompts.confirm_deletion') + colors.reset);
|
|
256
|
+
console.log('');
|
|
257
|
+
console.log(colors.yellow + i18n.tSync('ui.general.confirm_delete_api') + colors.reset);
|
|
258
|
+
console.log('');
|
|
259
|
+
console.log(colors.gray + `Name: ${api.name}` + colors.reset);
|
|
260
|
+
console.log(colors.gray + `Provider: ${api.provider}` + colors.reset);
|
|
261
|
+
console.log(colors.gray + `Base URL: ${api.baseUrl}` + colors.reset);
|
|
262
|
+
console.log(colors.gray + `Model: ${api.model}` + colors.reset);
|
|
263
|
+
const decryptedToken = decrypt(api.authToken);
|
|
264
|
+
const displayToken = decryptedToken.success ? maskApiToken(decryptedToken.value) : '***ERROR***';
|
|
265
|
+
console.log(colors.gray + `Token: ${displayToken}` + colors.reset);
|
|
266
|
+
console.log('');
|
|
267
|
+
console.log(colors.red + i18n.tSync('ui.general.action_cannot_undone') + colors.reset);
|
|
268
|
+
console.log('');
|
|
269
|
+
|
|
270
|
+
// Use StdinManager for proper state management
|
|
271
|
+
const scope = stdinManager.acquire('line', {
|
|
272
|
+
id: 'confirmDeletion',
|
|
273
|
+
allowNested: false
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
let rl;
|
|
277
|
+
try {
|
|
278
|
+
rl = scope.createReadline();
|
|
279
|
+
} catch (error) {
|
|
280
|
+
console.error('[ERROR] Failed to create readline interface:', error.message);
|
|
281
|
+
scope.release();
|
|
282
|
+
return false; // Default to not deleting if we can't get user confirmation
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return new Promise((resolve) => {
|
|
286
|
+
let released = false;
|
|
287
|
+
let timeoutId = null;
|
|
288
|
+
|
|
289
|
+
// Centralized cleanup helper to prevent double-release
|
|
290
|
+
const cleanup = (result) => {
|
|
291
|
+
if (released) return; // Guard against multiple calls
|
|
292
|
+
released = true;
|
|
293
|
+
|
|
294
|
+
// Clear timeout if it exists
|
|
295
|
+
if (timeoutId !== null) {
|
|
296
|
+
clearTimeout(timeoutId);
|
|
297
|
+
timeoutId = null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Remove all readline listeners to prevent subsequent handlers
|
|
301
|
+
if (rl) {
|
|
302
|
+
rl.removeAllListeners('error');
|
|
303
|
+
rl.removeAllListeners('line');
|
|
304
|
+
// Don't remove 'close' listeners - readline needs them for cleanup
|
|
305
|
+
rl.close();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Release the scope
|
|
309
|
+
scope.release();
|
|
310
|
+
|
|
311
|
+
// Resolve the promise
|
|
312
|
+
resolve(result);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// Set a timeout to prevent infinite waiting
|
|
316
|
+
timeoutId = setTimeout(() => {
|
|
317
|
+
console.log('\n' + colors.yellow + '[!] Confirmation timeout - operation cancelled' + colors.reset);
|
|
318
|
+
cleanup(false); // Timeout means no deletion
|
|
319
|
+
}, 60000); // 60 second timeout
|
|
320
|
+
|
|
321
|
+
rl.question(colors.red + i18n.tSync('ui.general.confirm_deletion_prompt') + colors.reset, (answer) => {
|
|
322
|
+
const confirmed = answer.trim().toLowerCase() === 'y';
|
|
323
|
+
cleanup(confirmed);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Handle readline errors
|
|
327
|
+
rl.on('error', (error) => {
|
|
328
|
+
console.error('[ERROR] Readline error:', error.message);
|
|
329
|
+
cleanup(false); // Error means no deletion
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = {
|
|
335
|
+
showApiSelectionTable,
|
|
336
|
+
waitForKeyPress,
|
|
337
|
+
confirmDeletion
|
|
338
|
+
};
|