@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
package/lib/ui/menu.js
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Menu Module - Handles menu display and navigation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const colors = require('./colors');
|
|
7
|
+
const i18n = require('../i18n');
|
|
8
|
+
const { padStringToWidth } = require('../utils/string-width');
|
|
9
|
+
const stdinManager = require('../utils/stdin-manager');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Force cleanup stdin state before displaying any menu
|
|
13
|
+
* This ensures clean state and prevents navigation issues
|
|
14
|
+
*/
|
|
15
|
+
function forceCleanupBeforeMenu() {
|
|
16
|
+
try {
|
|
17
|
+
if (process.stdin.isTTY) {
|
|
18
|
+
// Only reset mode, don't remove listeners that might be in use
|
|
19
|
+
process.stdin.setRawMode(false);
|
|
20
|
+
// NOTE: Removed removeAllListeners to prevent conflicts with other modules
|
|
21
|
+
// The menu will manage its own listeners
|
|
22
|
+
// Only pause if not already paused
|
|
23
|
+
if (typeof process.stdin.isPaused !== 'function' || !process.stdin.isPaused()) {
|
|
24
|
+
process.stdin.pause();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
} catch (error) {
|
|
28
|
+
// Ignore cleanup errors but log for debugging
|
|
29
|
+
if (process.env.DEBUG_STDIN) {
|
|
30
|
+
console.error('[DEBUG] forceCleanupBeforeMenu error:', error.message);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class Menu {
|
|
36
|
+
constructor() {
|
|
37
|
+
this.selectedIndex = 0;
|
|
38
|
+
this.menuOptions = [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Display Claude Code style header
|
|
43
|
+
*/
|
|
44
|
+
displayHeader() {
|
|
45
|
+
// Force cleanup stdin state before any menu display
|
|
46
|
+
forceCleanupBeforeMenu();
|
|
47
|
+
|
|
48
|
+
// Use console.clear and console.log for proper screen clearing
|
|
49
|
+
console.clear();
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log(colors.orange + ' ┌──────────────────────────────────────────────────────┐' + colors.reset);
|
|
52
|
+
console.log(colors.orange + ' │' + colors.white + colors.bright + ' Claude Code Launcher ' + colors.orange + '│' + colors.reset);
|
|
53
|
+
console.log(colors.orange + ' └──────────────────────────────────────────────────────┘' + colors.reset);
|
|
54
|
+
console.log('');
|
|
55
|
+
console.log(colors.gray + ' ' + i18n.tSync('navigation.use_arrows') + colors.reset);
|
|
56
|
+
console.log('');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Display menu with current selection
|
|
61
|
+
* @param {boolean} clearScreen - Whether to clear screen before displaying (default: true)
|
|
62
|
+
* @param {string} versionInfo - Optional version info to display between banner and navigation
|
|
63
|
+
*/
|
|
64
|
+
displayMenu(clearScreen = true, versionInfo = null) {
|
|
65
|
+
// Clear screen and display header + menu together (like old version)
|
|
66
|
+
if (clearScreen) {
|
|
67
|
+
console.clear();
|
|
68
|
+
}
|
|
69
|
+
console.log('');
|
|
70
|
+
console.log(colors.orange + ' ┌──────────────────────────────────────────────────────┐' + colors.reset);
|
|
71
|
+
console.log(colors.orange + ' │' + colors.white + colors.bright + ' Claude Code Launcher ' + colors.orange + '│' + colors.reset);
|
|
72
|
+
console.log(colors.orange + ' └──────────────────────────────────────────────────────┘' + colors.reset);
|
|
73
|
+
console.log('');
|
|
74
|
+
|
|
75
|
+
// Display version info if provided (between banner and navigation tips, like Claude Code)
|
|
76
|
+
if (versionInfo) {
|
|
77
|
+
console.log(versionInfo);
|
|
78
|
+
console.log('');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(colors.gray + ' ' + i18n.tSync('navigation.use_arrows') + colors.reset);
|
|
82
|
+
console.log('');
|
|
83
|
+
|
|
84
|
+
// Display menu options
|
|
85
|
+
this.menuOptions.forEach((option, index) => {
|
|
86
|
+
if (index === this.selectedIndex) {
|
|
87
|
+
// Pad the selected option to ensure complete background coverage
|
|
88
|
+
const paddedOption = padStringToWidth(option, Math.max(40, option.length + 2));
|
|
89
|
+
console.log(colors.orange + ' → ' + colors.black + colors.bgAmber + paddedOption + colors.reset);
|
|
90
|
+
} else {
|
|
91
|
+
console.log(colors.gray + ' ' + option + colors.reset);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
console.log('');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Set menu options
|
|
100
|
+
*/
|
|
101
|
+
setOptions(options) {
|
|
102
|
+
this.menuOptions = options;
|
|
103
|
+
this.selectedIndex = 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Handle keyboard navigation
|
|
108
|
+
* @param {boolean} clearScreen - Whether to clear screen on initial display (default: true)
|
|
109
|
+
* @param {string} versionInfo - Optional version info to display
|
|
110
|
+
*/
|
|
111
|
+
async navigate(clearScreen = true, versionInfo = null) {
|
|
112
|
+
// Guard against empty menu to prevent NaN from modulo operations
|
|
113
|
+
if (!this.menuOptions || this.menuOptions.length === 0) {
|
|
114
|
+
console.log(colors.yellow + ' Warning: No menu options available' + colors.reset);
|
|
115
|
+
return -1; // Return cancel/exit code
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.versionInfo = versionInfo; // Store for redrawing
|
|
119
|
+
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
this.displayMenu(clearScreen, versionInfo);
|
|
122
|
+
|
|
123
|
+
if (process.stdin.isTTY) {
|
|
124
|
+
const scope = stdinManager.acquire('raw', {
|
|
125
|
+
id: 'menu_navigate',
|
|
126
|
+
allowNested: true
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
let resolved = false;
|
|
130
|
+
let cleanedUp = false;
|
|
131
|
+
|
|
132
|
+
const cleanup = () => {
|
|
133
|
+
if (cleanedUp) return;
|
|
134
|
+
cleanedUp = true;
|
|
135
|
+
try {
|
|
136
|
+
scope.removeListener('data', handleKeyPress);
|
|
137
|
+
scope.release();
|
|
138
|
+
} catch (error) {
|
|
139
|
+
// Ignore cleanup errors to prevent masking original error
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const safeResolve = (value) => {
|
|
144
|
+
if (resolved) return;
|
|
145
|
+
resolved = true;
|
|
146
|
+
cleanup();
|
|
147
|
+
resolve(value);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const safeReject = (error) => {
|
|
151
|
+
if (resolved) return;
|
|
152
|
+
resolved = true;
|
|
153
|
+
cleanup();
|
|
154
|
+
reject(error);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const handleKeyPress = (key) => {
|
|
158
|
+
try {
|
|
159
|
+
// Handle Ctrl+C first
|
|
160
|
+
if (key === '\u0003') {
|
|
161
|
+
stdinManager.handleCtrlC();
|
|
162
|
+
return; // Don't process further
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// If waiting for second Ctrl+C, any other key cancels it
|
|
166
|
+
if (stdinManager.isCtrlCPending()) {
|
|
167
|
+
stdinManager.cancelCtrlC();
|
|
168
|
+
// Continue to process this key normally
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
switch (key) {
|
|
172
|
+
case '\u001b[A': // Up arrow
|
|
173
|
+
this.selectedIndex = (this.selectedIndex - 1 + this.menuOptions.length) % this.menuOptions.length;
|
|
174
|
+
this.displayMenu(true, this.versionInfo);
|
|
175
|
+
break;
|
|
176
|
+
|
|
177
|
+
case '\u001b[B': // Down arrow
|
|
178
|
+
this.selectedIndex = (this.selectedIndex + 1) % this.menuOptions.length;
|
|
179
|
+
this.displayMenu(true, this.versionInfo);
|
|
180
|
+
break;
|
|
181
|
+
|
|
182
|
+
case '\r': // Enter
|
|
183
|
+
safeResolve(this.selectedIndex);
|
|
184
|
+
break;
|
|
185
|
+
|
|
186
|
+
case '\u001b': // Escape
|
|
187
|
+
case 'q':
|
|
188
|
+
case 'Q':
|
|
189
|
+
safeResolve(-1);
|
|
190
|
+
break;
|
|
191
|
+
|
|
192
|
+
default:
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
} catch (error) {
|
|
196
|
+
safeReject(error);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
scope.on('data', handleKeyPress);
|
|
201
|
+
} else {
|
|
202
|
+
// Non-TTY fallback: use StdinManager for line-based input
|
|
203
|
+
const lineScope = stdinManager.acquire('line', {
|
|
204
|
+
id: 'menu_navigate_nonTTY',
|
|
205
|
+
allowNested: false
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const rl = lineScope.createReadline();
|
|
209
|
+
|
|
210
|
+
console.log(colors.yellow + ' ' + i18n.tSync('navigation.arrow_keys_not_available', this.menuOptions.length) + colors.reset);
|
|
211
|
+
|
|
212
|
+
rl.on('line', (input) => {
|
|
213
|
+
const choice = parseInt(input.trim());
|
|
214
|
+
if (choice >= 1 && choice <= this.menuOptions.length) {
|
|
215
|
+
rl.close();
|
|
216
|
+
lineScope.release();
|
|
217
|
+
resolve(choice - 1);
|
|
218
|
+
} else if (input.toLowerCase() === 'q' || input.toLowerCase() === 'exit') {
|
|
219
|
+
rl.close();
|
|
220
|
+
lineScope.release();
|
|
221
|
+
resolve(-1);
|
|
222
|
+
} else {
|
|
223
|
+
console.log(colors.red + ' Invalid selection. Please enter 1-' + this.menuOptions.length + '.' + colors.reset);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async selectFromList(title, items, activeIndex = -1) {
|
|
231
|
+
// Guard against empty items list to prevent NaN from modulo operations
|
|
232
|
+
if (!items || items.length === 0) {
|
|
233
|
+
console.log(colors.yellow + ' Warning: No items available to select' + colors.reset);
|
|
234
|
+
return null; // Return null to indicate no selection
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
forceCleanupBeforeMenu();
|
|
238
|
+
|
|
239
|
+
let selectedIndex = activeIndex >= 0 ? activeIndex : 0;
|
|
240
|
+
|
|
241
|
+
const displayList = () => {
|
|
242
|
+
console.clear();
|
|
243
|
+
console.log('');
|
|
244
|
+
console.log(colors.bright + colors.orange + `[*] ${title}` + colors.reset);
|
|
245
|
+
console.log('');
|
|
246
|
+
console.log(colors.gray + ' Use ↑↓ arrow keys to navigate, Enter to select, Esc to cancel' + colors.reset);
|
|
247
|
+
console.log('');
|
|
248
|
+
|
|
249
|
+
items.forEach((item, index) => {
|
|
250
|
+
const isActive = index === activeIndex;
|
|
251
|
+
const prefix = index === selectedIndex ? ' → ' : ' ';
|
|
252
|
+
const activeIndicator = isActive ? ' (ACTIVE)' : '';
|
|
253
|
+
|
|
254
|
+
if (index === selectedIndex) {
|
|
255
|
+
const itemText = `${item.name}${activeIndicator}`;
|
|
256
|
+
const paddedItem = padStringToWidth(itemText, Math.max(40, itemText.length + 2));
|
|
257
|
+
console.log(colors.orange + prefix + colors.black + colors.bgAmber + paddedItem + colors.reset);
|
|
258
|
+
if (item.details) {
|
|
259
|
+
item.details.forEach(detail => {
|
|
260
|
+
console.log(colors.gray + ' ' + detail + colors.reset);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
console.log(colors.gray + prefix + `${item.name}${activeIndicator}` + colors.reset);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
console.log('');
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
return new Promise((resolve, reject) => {
|
|
272
|
+
displayList();
|
|
273
|
+
|
|
274
|
+
if (process.stdin.isTTY) {
|
|
275
|
+
const scope = stdinManager.acquire('raw', {
|
|
276
|
+
id: 'menu_selectFromList',
|
|
277
|
+
allowNested: true
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
let resolved = false;
|
|
281
|
+
let cleanedUp = false;
|
|
282
|
+
|
|
283
|
+
const cleanup = () => {
|
|
284
|
+
if (cleanedUp) return;
|
|
285
|
+
cleanedUp = true;
|
|
286
|
+
try {
|
|
287
|
+
scope.removeListener('data', handleKeyPress);
|
|
288
|
+
scope.release();
|
|
289
|
+
} catch (error) {
|
|
290
|
+
// Ignore cleanup errors to prevent masking original error
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const safeResolve = (value) => {
|
|
295
|
+
if (resolved) return;
|
|
296
|
+
resolved = true;
|
|
297
|
+
cleanup();
|
|
298
|
+
resolve(value);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const safeReject = (error) => {
|
|
302
|
+
if (resolved) return;
|
|
303
|
+
resolved = true;
|
|
304
|
+
cleanup();
|
|
305
|
+
reject(error);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const handleKeyPress = (key) => {
|
|
309
|
+
try {
|
|
310
|
+
// Handle Ctrl+C first
|
|
311
|
+
if (key === '\u0003') {
|
|
312
|
+
stdinManager.handleCtrlC();
|
|
313
|
+
return; // Don't process further
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// If waiting for second Ctrl+C, any other key cancels it
|
|
317
|
+
if (stdinManager.isCtrlCPending()) {
|
|
318
|
+
stdinManager.cancelCtrlC();
|
|
319
|
+
// Continue to process this key normally
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
switch (key) {
|
|
323
|
+
case '\u001b[A': // Up arrow
|
|
324
|
+
selectedIndex = (selectedIndex - 1 + items.length) % items.length;
|
|
325
|
+
displayList();
|
|
326
|
+
break;
|
|
327
|
+
|
|
328
|
+
case '\u001b[B': // Down arrow
|
|
329
|
+
selectedIndex = (selectedIndex + 1) % items.length;
|
|
330
|
+
displayList();
|
|
331
|
+
break;
|
|
332
|
+
|
|
333
|
+
case '\r': // Enter
|
|
334
|
+
safeResolve(selectedIndex);
|
|
335
|
+
break;
|
|
336
|
+
|
|
337
|
+
case '\u001b': // Escape
|
|
338
|
+
case 'q':
|
|
339
|
+
case 'Q':
|
|
340
|
+
safeResolve(null);
|
|
341
|
+
break;
|
|
342
|
+
|
|
343
|
+
default:
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
} catch (error) {
|
|
347
|
+
safeReject(error);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
scope.on('data', handleKeyPress);
|
|
352
|
+
} else {
|
|
353
|
+
// Non-TTY fallback: use line-based input
|
|
354
|
+
const lineScope = stdinManager.acquire('line', {
|
|
355
|
+
id: 'menu_selectFromList_nonTTY',
|
|
356
|
+
allowNested: false
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const rl = lineScope.createReadline();
|
|
360
|
+
|
|
361
|
+
console.log(colors.yellow + ' Arrow keys not available. Enter item number (1-' + items.length + ') or q to cancel:' + colors.reset);
|
|
362
|
+
|
|
363
|
+
rl.on('line', (input) => {
|
|
364
|
+
const choice = parseInt(input.trim());
|
|
365
|
+
if (choice >= 1 && choice <= items.length) {
|
|
366
|
+
rl.close();
|
|
367
|
+
lineScope.release();
|
|
368
|
+
resolve(choice - 1);
|
|
369
|
+
} else if (input.toLowerCase() === 'q') {
|
|
370
|
+
rl.close();
|
|
371
|
+
lineScope.release();
|
|
372
|
+
resolve(null);
|
|
373
|
+
} else {
|
|
374
|
+
console.log(colors.red + ' Invalid selection. Please enter 1-' + items.length + '.' + colors.reset);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
module.exports = Menu;
|