@kikkimo/claude-launcher 1.0.0 → 2.0.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 +55 -0
- package/README.md +100 -41
- package/claude-launcher +1017 -576
- package/docs/README-zh.md +104 -45
- package/lib/api-manager.js +449 -0
- package/lib/auth/password-input.js +144 -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 +523 -0
- package/lib/i18n/locales/en.js +524 -0
- package/lib/i18n/locales/es.js +523 -0
- package/lib/i18n/locales/fr.js +523 -0
- package/lib/i18n/locales/it.js +523 -0
- package/lib/i18n/locales/ja.js +523 -0
- package/lib/i18n/locales/ko.js +523 -0
- package/lib/i18n/locales/pt.js +523 -0
- package/lib/i18n/locales/ru.js +523 -0
- package/lib/i18n/locales/zh-TW.js +523 -0
- package/lib/i18n/locales/zh.js +523 -0
- package/lib/launcher.js +253 -0
- package/lib/presets/providers.js +104 -0
- package/lib/ui/colors.js +32 -0
- package/lib/ui/interactive-table.js +260 -0
- package/lib/ui/menu.js +314 -0
- package/lib/ui/prompts.js +540 -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,314 @@
|
|
|
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
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Force cleanup stdin state before displaying any menu
|
|
12
|
+
* This ensures clean state and prevents navigation issues
|
|
13
|
+
*/
|
|
14
|
+
function forceCleanupBeforeMenu() {
|
|
15
|
+
try {
|
|
16
|
+
if (process.stdin.isTTY) {
|
|
17
|
+
process.stdin.setRawMode(false);
|
|
18
|
+
process.stdin.removeAllListeners('data');
|
|
19
|
+
process.stdin.removeAllListeners('keypress');
|
|
20
|
+
process.stdin.pause();
|
|
21
|
+
}
|
|
22
|
+
} catch (error) {
|
|
23
|
+
// Ignore cleanup errors - we just want to ensure clean state
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class Menu {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.selectedIndex = 0;
|
|
30
|
+
this.menuOptions = [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Display Claude Code style header
|
|
35
|
+
*/
|
|
36
|
+
displayHeader() {
|
|
37
|
+
// Force cleanup stdin state before any menu display
|
|
38
|
+
forceCleanupBeforeMenu();
|
|
39
|
+
|
|
40
|
+
// Use console.clear and console.log for proper screen clearing
|
|
41
|
+
console.clear();
|
|
42
|
+
console.log('');
|
|
43
|
+
console.log(colors.orange + ' ┌───────────────────────────────────────────────────────────────────────────┐' + colors.reset);
|
|
44
|
+
console.log(colors.orange + ' │' + colors.white + colors.bright + ' Claude Code Launcher ' + colors.orange + '│' + colors.reset);
|
|
45
|
+
console.log(colors.orange + ' └───────────────────────────────────────────────────────────────────────────┘' + colors.reset);
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log(colors.gray + ' ' + i18n.tSync('navigation.use_arrows') + colors.reset);
|
|
48
|
+
console.log('');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Display menu with current selection
|
|
53
|
+
* @param {boolean} clearScreen - Whether to clear screen before displaying (default: true)
|
|
54
|
+
* @param {string} versionInfo - Optional version info to display between banner and navigation
|
|
55
|
+
*/
|
|
56
|
+
displayMenu(clearScreen = true, versionInfo = null) {
|
|
57
|
+
// Clear screen and display header + menu together (like old version)
|
|
58
|
+
if (clearScreen) {
|
|
59
|
+
console.clear();
|
|
60
|
+
}
|
|
61
|
+
console.log('');
|
|
62
|
+
console.log(colors.orange + ' ┌───────────────────────────────────────────────────────────────────────────┐' + colors.reset);
|
|
63
|
+
console.log(colors.orange + ' │' + colors.white + colors.bright + ' Claude Code Launcher ' + colors.orange + '│' + colors.reset);
|
|
64
|
+
console.log(colors.orange + ' └───────────────────────────────────────────────────────────────────────────┘' + colors.reset);
|
|
65
|
+
console.log('');
|
|
66
|
+
|
|
67
|
+
// Display version info if provided (between banner and navigation tips, like Claude Code)
|
|
68
|
+
if (versionInfo) {
|
|
69
|
+
console.log(versionInfo);
|
|
70
|
+
console.log('');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log(colors.gray + ' ' + i18n.tSync('navigation.use_arrows') + colors.reset);
|
|
74
|
+
console.log('');
|
|
75
|
+
|
|
76
|
+
// Display menu options
|
|
77
|
+
this.menuOptions.forEach((option, index) => {
|
|
78
|
+
if (index === this.selectedIndex) {
|
|
79
|
+
// Pad the selected option to ensure complete background coverage
|
|
80
|
+
const paddedOption = padStringToWidth(option, Math.max(40, option.length + 2));
|
|
81
|
+
console.log(colors.orange + ' → ' + colors.black + colors.bgAmber + paddedOption + colors.reset);
|
|
82
|
+
} else {
|
|
83
|
+
console.log(colors.gray + ' ' + option + colors.reset);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
console.log('');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Set menu options
|
|
92
|
+
*/
|
|
93
|
+
setOptions(options) {
|
|
94
|
+
this.menuOptions = options;
|
|
95
|
+
this.selectedIndex = 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Handle keyboard navigation
|
|
100
|
+
* @param {boolean} clearScreen - Whether to clear screen on initial display (default: true)
|
|
101
|
+
* @param {string} versionInfo - Optional version info to display
|
|
102
|
+
*/
|
|
103
|
+
async navigate(clearScreen = true, versionInfo = null) {
|
|
104
|
+
let ctrlCCount = 0;
|
|
105
|
+
this.versionInfo = versionInfo; // Store for redrawing
|
|
106
|
+
|
|
107
|
+
return new Promise((resolve) => {
|
|
108
|
+
this.displayMenu(clearScreen, versionInfo);
|
|
109
|
+
|
|
110
|
+
if (process.stdin.isTTY) {
|
|
111
|
+
process.stdin.setRawMode(true);
|
|
112
|
+
process.stdin.resume();
|
|
113
|
+
process.stdin.setEncoding('utf8');
|
|
114
|
+
|
|
115
|
+
const handleKeyPress = (key) => {
|
|
116
|
+
switch (key) {
|
|
117
|
+
case '\u0003': // Ctrl+C - 2-step exit
|
|
118
|
+
ctrlCCount++;
|
|
119
|
+
if (ctrlCCount === 1) {
|
|
120
|
+
console.log('');
|
|
121
|
+
console.log(colors.yellow + '⚠️ ' + i18n.tSync('messages.prompts.ctrl_c_again') + colors.reset);
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
ctrlCCount = 0; // Reset after 3 seconds
|
|
124
|
+
}, 3000);
|
|
125
|
+
} else if (ctrlCCount >= 2) {
|
|
126
|
+
process.stdin.removeListener('data', handleKeyPress);
|
|
127
|
+
if (process.stdin.isTTY) {
|
|
128
|
+
process.stdin.setRawMode(false);
|
|
129
|
+
}
|
|
130
|
+
console.log('');
|
|
131
|
+
console.log(colors.green + '👋 Goodbye!' + colors.reset);
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
|
|
136
|
+
case '\u001b[A': // Up arrow
|
|
137
|
+
ctrlCCount = 0; // Reset Ctrl+C count on other keys
|
|
138
|
+
this.selectedIndex = (this.selectedIndex - 1 + this.menuOptions.length) % this.menuOptions.length;
|
|
139
|
+
this.displayMenu(true, this.versionInfo);
|
|
140
|
+
break;
|
|
141
|
+
|
|
142
|
+
case '\u001b[B': // Down arrow
|
|
143
|
+
ctrlCCount = 0; // Reset Ctrl+C count on other keys
|
|
144
|
+
this.selectedIndex = (this.selectedIndex + 1) % this.menuOptions.length;
|
|
145
|
+
this.displayMenu(true, this.versionInfo);
|
|
146
|
+
break;
|
|
147
|
+
|
|
148
|
+
case '\r': // Enter
|
|
149
|
+
process.stdin.removeListener('data', handleKeyPress);
|
|
150
|
+
if (process.stdin.isTTY) {
|
|
151
|
+
process.stdin.setRawMode(false);
|
|
152
|
+
}
|
|
153
|
+
process.stdin.pause();
|
|
154
|
+
resolve(this.selectedIndex);
|
|
155
|
+
break;
|
|
156
|
+
|
|
157
|
+
case '\u001b': // Escape
|
|
158
|
+
case 'q':
|
|
159
|
+
case 'Q':
|
|
160
|
+
process.stdin.removeListener('data', handleKeyPress);
|
|
161
|
+
if (process.stdin.isTTY) {
|
|
162
|
+
process.stdin.setRawMode(false);
|
|
163
|
+
}
|
|
164
|
+
process.stdin.pause();
|
|
165
|
+
resolve(-1);
|
|
166
|
+
break;
|
|
167
|
+
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
process.stdin.on('data', handleKeyPress);
|
|
172
|
+
} else {
|
|
173
|
+
// Fallback for non-TTY environments
|
|
174
|
+
const rl = readline.createInterface({
|
|
175
|
+
input: process.stdin,
|
|
176
|
+
output: process.stdout
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
console.log(colors.yellow + ' ' + i18n.tSync('navigation.arrow_keys_not_available', this.menuOptions.length) + colors.reset);
|
|
180
|
+
|
|
181
|
+
rl.on('line', (input) => {
|
|
182
|
+
const choice = parseInt(input.trim());
|
|
183
|
+
if (choice >= 1 && choice <= this.menuOptions.length) {
|
|
184
|
+
rl.close();
|
|
185
|
+
resolve(choice - 1);
|
|
186
|
+
} else if (input.toLowerCase() === 'q' || input.toLowerCase() === 'exit') {
|
|
187
|
+
rl.close();
|
|
188
|
+
resolve(-1);
|
|
189
|
+
} else {
|
|
190
|
+
console.log(colors.red + ' Invalid selection. Please enter 1-' + this.menuOptions.length + '.' + colors.reset);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Display a list for selection
|
|
199
|
+
*/
|
|
200
|
+
async selectFromList(title, items, activeIndex = -1) {
|
|
201
|
+
// Force cleanup stdin state before any list selection
|
|
202
|
+
forceCleanupBeforeMenu();
|
|
203
|
+
|
|
204
|
+
let selectedIndex = activeIndex >= 0 ? activeIndex : 0;
|
|
205
|
+
let ctrlCCount = 0;
|
|
206
|
+
|
|
207
|
+
function displayList() {
|
|
208
|
+
console.clear();
|
|
209
|
+
console.log('');
|
|
210
|
+
console.log(colors.bright + colors.orange + `[*] ${title}` + colors.reset);
|
|
211
|
+
console.log('');
|
|
212
|
+
console.log(colors.gray + ' Use ↑↓ arrow keys to navigate, Enter to select, Esc to cancel' + colors.reset);
|
|
213
|
+
console.log('');
|
|
214
|
+
|
|
215
|
+
items.forEach((item, index) => {
|
|
216
|
+
const isActive = index === activeIndex;
|
|
217
|
+
const prefix = index === selectedIndex ? ' → ' : ' ';
|
|
218
|
+
const activeIndicator = isActive ? ' (ACTIVE)' : '';
|
|
219
|
+
|
|
220
|
+
if (index === selectedIndex) {
|
|
221
|
+
// Pad the selected item to ensure complete background coverage
|
|
222
|
+
const itemText = `${item.name}${activeIndicator}`;
|
|
223
|
+
const paddedItem = padStringToWidth(itemText, Math.max(40, itemText.length + 2));
|
|
224
|
+
console.log(colors.orange + prefix + colors.black + colors.bgAmber + paddedItem + colors.reset);
|
|
225
|
+
if (item.details) {
|
|
226
|
+
item.details.forEach(detail => {
|
|
227
|
+
console.log(colors.gray + ' ' + detail + colors.reset);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
console.log(colors.gray + prefix + `${item.name}${activeIndicator}` + colors.reset);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
console.log('');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return new Promise((resolve) => {
|
|
239
|
+
displayList();
|
|
240
|
+
|
|
241
|
+
if (process.stdin.isTTY) {
|
|
242
|
+
process.stdin.setRawMode(true);
|
|
243
|
+
process.stdin.resume();
|
|
244
|
+
process.stdin.setEncoding('utf8');
|
|
245
|
+
|
|
246
|
+
const handleKeyPress = (key) => {
|
|
247
|
+
switch (key) {
|
|
248
|
+
case '\u0003': // Ctrl+C - 2-step exit
|
|
249
|
+
ctrlCCount++;
|
|
250
|
+
if (ctrlCCount === 1) {
|
|
251
|
+
console.log('');
|
|
252
|
+
console.log(colors.yellow + '⚠️ ' + i18n.tSync('messages.prompts.ctrl_c_again') + colors.reset);
|
|
253
|
+
setTimeout(() => {
|
|
254
|
+
ctrlCCount = 0; // Reset after 3 seconds
|
|
255
|
+
}, 3000);
|
|
256
|
+
} else if (ctrlCCount >= 2) {
|
|
257
|
+
process.stdin.removeListener('data', handleKeyPress);
|
|
258
|
+
if (process.stdin.isTTY) {
|
|
259
|
+
process.stdin.setRawMode(false);
|
|
260
|
+
}
|
|
261
|
+
console.log('');
|
|
262
|
+
console.log(colors.green + '👋 Goodbye!' + colors.reset);
|
|
263
|
+
process.exit(0);
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
266
|
+
|
|
267
|
+
case '\u001b[A': // Up arrow
|
|
268
|
+
ctrlCCount = 0; // Reset Ctrl+C count on other keys
|
|
269
|
+
selectedIndex = (selectedIndex - 1 + items.length) % items.length;
|
|
270
|
+
displayList();
|
|
271
|
+
break;
|
|
272
|
+
|
|
273
|
+
case '\u001b[B': // Down arrow
|
|
274
|
+
ctrlCCount = 0; // Reset Ctrl+C count on other keys
|
|
275
|
+
selectedIndex = (selectedIndex + 1) % items.length;
|
|
276
|
+
displayList();
|
|
277
|
+
break;
|
|
278
|
+
|
|
279
|
+
case '\r': // Enter
|
|
280
|
+
process.stdin.removeListener('data', handleKeyPress);
|
|
281
|
+
if (process.stdin.isTTY) {
|
|
282
|
+
process.stdin.setRawMode(false);
|
|
283
|
+
}
|
|
284
|
+
process.stdin.pause();
|
|
285
|
+
resolve(selectedIndex);
|
|
286
|
+
break;
|
|
287
|
+
|
|
288
|
+
case '\u001b': // Escape
|
|
289
|
+
case 'q':
|
|
290
|
+
case 'Q':
|
|
291
|
+
process.stdin.removeListener('data', handleKeyPress);
|
|
292
|
+
if (process.stdin.isTTY) {
|
|
293
|
+
process.stdin.setRawMode(false);
|
|
294
|
+
}
|
|
295
|
+
process.stdin.pause();
|
|
296
|
+
resolve(null);
|
|
297
|
+
break;
|
|
298
|
+
|
|
299
|
+
default:
|
|
300
|
+
// Any other key resets Ctrl+C count
|
|
301
|
+
ctrlCCount = 0;
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
process.stdin.on('data', handleKeyPress);
|
|
307
|
+
} else {
|
|
308
|
+
resolve(null);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
module.exports = Menu;
|