@kikkimo/claude-launcher 2.0.0 → 2.2.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 +112 -0
- package/README.md +9 -6
- package/claude-launcher +57 -3
- package/docs/README-zh.md +9 -6
- package/lib/auth/password-input.js +101 -87
- package/lib/i18n/locales/de.js +17 -2
- package/lib/i18n/locales/en.js +17 -2
- package/lib/i18n/locales/es.js +17 -2
- package/lib/i18n/locales/fr.js +18 -3
- package/lib/i18n/locales/it.js +16 -0
- package/lib/i18n/locales/ja.js +17 -2
- package/lib/i18n/locales/ko.js +17 -2
- package/lib/i18n/locales/pt.js +16 -0
- package/lib/i18n/locales/ru.js +16 -0
- package/lib/i18n/locales/zh-TW.js +17 -2
- package/lib/i18n/locales/zh.js +17 -2
- package/lib/launcher.js +153 -47
- package/lib/presets/providers.js +65 -3
- package/lib/ui/interactive-table.js +102 -24
- package/lib/ui/menu.js +213 -144
- package/lib/ui/prompts.js +116 -85
- package/lib/utils/stdin-manager.js +715 -0
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ const { maskApiToken } = require('../validators');
|
|
|
7
7
|
const { decrypt } = require('../crypto');
|
|
8
8
|
const i18n = require('../i18n');
|
|
9
9
|
const { padStringToWidth } = require('../utils/string-width');
|
|
10
|
+
const stdinManager = require('../utils/stdin-manager');
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Display simple interactive table for API selection
|
|
@@ -127,6 +128,18 @@ async function showApiSelectionTable(apis, title, actionType = 'select', activeI
|
|
|
127
128
|
}
|
|
128
129
|
|
|
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
|
+
|
|
130
143
|
switch (key) {
|
|
131
144
|
case '\u001b[A': // Up arrow
|
|
132
145
|
selectedIndex = (selectedIndex - 1 + apis.length) % apis.length;
|
|
@@ -157,22 +170,17 @@ async function showApiSelectionTable(apis, title, actionType = 'select', activeI
|
|
|
157
170
|
displaySimpleTable();
|
|
158
171
|
|
|
159
172
|
if (process.stdin.isTTY) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
173
|
+
// Use StdinManager for proper state management
|
|
174
|
+
const scope = stdinManager.acquire('raw', {
|
|
175
|
+
id: 'showApiSelectionTable',
|
|
176
|
+
allowNested: true
|
|
177
|
+
});
|
|
165
178
|
|
|
166
179
|
const keyHandler = async (key) => {
|
|
167
180
|
const result = handleKeyPress(key);
|
|
168
181
|
if (result !== undefined) {
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
process.stdin.setRawMode(false);
|
|
172
|
-
}
|
|
173
|
-
process.stdin.removeAllListeners('data');
|
|
174
|
-
process.stdin.removeAllListeners('keypress');
|
|
175
|
-
process.stdin.pause();
|
|
182
|
+
// Release the scope, which automatically restores previous state
|
|
183
|
+
scope.release();
|
|
176
184
|
|
|
177
185
|
// Handle switch mode - activate the selected API
|
|
178
186
|
if (result && actionType === 'switch' && apiManager) {
|
|
@@ -202,7 +210,7 @@ async function showApiSelectionTable(apis, title, actionType = 'select', activeI
|
|
|
202
210
|
}
|
|
203
211
|
};
|
|
204
212
|
|
|
205
|
-
|
|
213
|
+
scope.on('data', keyHandler);
|
|
206
214
|
} else {
|
|
207
215
|
resolve(null);
|
|
208
216
|
}
|
|
@@ -211,12 +219,33 @@ async function showApiSelectionTable(apis, title, actionType = 'select', activeI
|
|
|
211
219
|
|
|
212
220
|
function waitForKeyPress() {
|
|
213
221
|
return new Promise((resolve) => {
|
|
214
|
-
|
|
215
|
-
|
|
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();
|
|
216
244
|
resolve();
|
|
217
245
|
};
|
|
218
|
-
|
|
219
|
-
|
|
246
|
+
|
|
247
|
+
// Use on() instead of once() so Ctrl+C doesn't remove the listener
|
|
248
|
+
scope.on('data', handler);
|
|
220
249
|
});
|
|
221
250
|
}
|
|
222
251
|
|
|
@@ -238,17 +267,66 @@ async function confirmDeletion(api) {
|
|
|
238
267
|
console.log(colors.red + i18n.tSync('ui.general.action_cannot_undone') + colors.reset);
|
|
239
268
|
console.log('');
|
|
240
269
|
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
270
|
+
// Use StdinManager for proper state management
|
|
271
|
+
const scope = stdinManager.acquire('line', {
|
|
272
|
+
id: 'confirmDeletion',
|
|
273
|
+
allowNested: false
|
|
245
274
|
});
|
|
246
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
|
+
|
|
247
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
|
+
|
|
248
321
|
rl.question(colors.red + i18n.tSync('ui.general.confirm_deletion_prompt') + colors.reset, (answer) => {
|
|
249
|
-
rl.close();
|
|
250
322
|
const confirmed = answer.trim().toLowerCase() === 'y';
|
|
251
|
-
|
|
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
|
|
252
330
|
});
|
|
253
331
|
});
|
|
254
332
|
}
|
|
@@ -257,4 +335,4 @@ module.exports = {
|
|
|
257
335
|
showApiSelectionTable,
|
|
258
336
|
waitForKeyPress,
|
|
259
337
|
confirmDeletion
|
|
260
|
-
};
|
|
338
|
+
};
|
package/lib/ui/menu.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
2
|
* Menu Module - Handles menu display and navigation
|
|
3
3
|
*/
|
|
4
4
|
|
|
@@ -6,6 +6,7 @@ const readline = require('readline');
|
|
|
6
6
|
const colors = require('./colors');
|
|
7
7
|
const i18n = require('../i18n');
|
|
8
8
|
const { padStringToWidth } = require('../utils/string-width');
|
|
9
|
+
const stdinManager = require('../utils/stdin-manager');
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Force cleanup stdin state before displaying any menu
|
|
@@ -14,13 +15,20 @@ const { padStringToWidth } = require('../utils/string-width');
|
|
|
14
15
|
function forceCleanupBeforeMenu() {
|
|
15
16
|
try {
|
|
16
17
|
if (process.stdin.isTTY) {
|
|
18
|
+
// Only reset mode, don't remove listeners that might be in use
|
|
17
19
|
process.stdin.setRawMode(false);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
}
|
|
21
26
|
}
|
|
22
27
|
} catch (error) {
|
|
23
|
-
// Ignore cleanup errors
|
|
28
|
+
// Ignore cleanup errors but log for debugging
|
|
29
|
+
if (process.env.DEBUG_STDIN) {
|
|
30
|
+
console.error('[DEBUG] forceCleanupBeforeMenu error:', error.message);
|
|
31
|
+
}
|
|
24
32
|
}
|
|
25
33
|
}
|
|
26
34
|
|
|
@@ -40,9 +48,9 @@ class Menu {
|
|
|
40
48
|
// Use console.clear and console.log for proper screen clearing
|
|
41
49
|
console.clear();
|
|
42
50
|
console.log('');
|
|
43
|
-
console.log(colors.orange + '
|
|
44
|
-
console.log(colors.orange + ' │' + colors.white + colors.bright + '
|
|
45
|
-
console.log(colors.orange + '
|
|
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);
|
|
46
54
|
console.log('');
|
|
47
55
|
console.log(colors.gray + ' ' + i18n.tSync('navigation.use_arrows') + colors.reset);
|
|
48
56
|
console.log('');
|
|
@@ -59,9 +67,9 @@ class Menu {
|
|
|
59
67
|
console.clear();
|
|
60
68
|
}
|
|
61
69
|
console.log('');
|
|
62
|
-
console.log(colors.orange + '
|
|
63
|
-
console.log(colors.orange + ' │' + colors.white + colors.bright + '
|
|
64
|
-
console.log(colors.orange + '
|
|
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);
|
|
65
73
|
console.log('');
|
|
66
74
|
|
|
67
75
|
// Display version info if provided (between banner and navigation tips, like Claude Code)
|
|
@@ -101,90 +109,115 @@ class Menu {
|
|
|
101
109
|
* @param {string} versionInfo - Optional version info to display
|
|
102
110
|
*/
|
|
103
111
|
async navigate(clearScreen = true, versionInfo = null) {
|
|
104
|
-
|
|
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
|
+
|
|
105
118
|
this.versionInfo = versionInfo; // Store for redrawing
|
|
106
119
|
|
|
107
|
-
return new Promise((resolve) => {
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
108
121
|
this.displayMenu(clearScreen, versionInfo);
|
|
109
122
|
|
|
110
123
|
if (process.stdin.isTTY) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
124
|
+
const scope = stdinManager.acquire('raw', {
|
|
125
|
+
id: 'menu_navigate',
|
|
126
|
+
allowNested: true
|
|
127
|
+
});
|
|
114
128
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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;
|
|
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
|
+
};
|
|
167
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);
|
|
168
197
|
}
|
|
169
198
|
};
|
|
170
199
|
|
|
171
|
-
|
|
200
|
+
scope.on('data', handleKeyPress);
|
|
172
201
|
} else {
|
|
173
|
-
//
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
202
|
+
// Non-TTY fallback: use StdinManager for line-based input
|
|
203
|
+
const lineScope = stdinManager.acquire('line', {
|
|
204
|
+
id: 'menu_navigate_nonTTY',
|
|
205
|
+
allowNested: false
|
|
177
206
|
});
|
|
178
207
|
|
|
208
|
+
const rl = lineScope.createReadline();
|
|
209
|
+
|
|
179
210
|
console.log(colors.yellow + ' ' + i18n.tSync('navigation.arrow_keys_not_available', this.menuOptions.length) + colors.reset);
|
|
180
211
|
|
|
181
212
|
rl.on('line', (input) => {
|
|
182
213
|
const choice = parseInt(input.trim());
|
|
183
214
|
if (choice >= 1 && choice <= this.menuOptions.length) {
|
|
184
215
|
rl.close();
|
|
216
|
+
lineScope.release();
|
|
185
217
|
resolve(choice - 1);
|
|
186
218
|
} else if (input.toLowerCase() === 'q' || input.toLowerCase() === 'exit') {
|
|
187
219
|
rl.close();
|
|
220
|
+
lineScope.release();
|
|
188
221
|
resolve(-1);
|
|
189
222
|
} else {
|
|
190
223
|
console.log(colors.red + ' Invalid selection. Please enter 1-' + this.menuOptions.length + '.' + colors.reset);
|
|
@@ -194,17 +227,18 @@ class Menu {
|
|
|
194
227
|
});
|
|
195
228
|
}
|
|
196
229
|
|
|
197
|
-
/**
|
|
198
|
-
* Display a list for selection
|
|
199
|
-
*/
|
|
200
230
|
async selectFromList(title, items, activeIndex = -1) {
|
|
201
|
-
//
|
|
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
|
+
|
|
202
237
|
forceCleanupBeforeMenu();
|
|
203
238
|
|
|
204
239
|
let selectedIndex = activeIndex >= 0 ? activeIndex : 0;
|
|
205
|
-
let ctrlCCount = 0;
|
|
206
240
|
|
|
207
|
-
|
|
241
|
+
const displayList = () => {
|
|
208
242
|
console.clear();
|
|
209
243
|
console.log('');
|
|
210
244
|
console.log(colors.bright + colors.orange + `[*] ${title}` + colors.reset);
|
|
@@ -218,7 +252,6 @@ class Menu {
|
|
|
218
252
|
const activeIndicator = isActive ? ' (ACTIVE)' : '';
|
|
219
253
|
|
|
220
254
|
if (index === selectedIndex) {
|
|
221
|
-
// Pad the selected item to ensure complete background coverage
|
|
222
255
|
const itemText = `${item.name}${activeIndicator}`;
|
|
223
256
|
const paddedItem = padStringToWidth(itemText, Math.max(40, itemText.length + 2));
|
|
224
257
|
console.log(colors.orange + prefix + colors.black + colors.bgAmber + paddedItem + colors.reset);
|
|
@@ -233,82 +266,118 @@ class Menu {
|
|
|
233
266
|
});
|
|
234
267
|
|
|
235
268
|
console.log('');
|
|
236
|
-
}
|
|
269
|
+
};
|
|
237
270
|
|
|
238
|
-
return new Promise((resolve) => {
|
|
271
|
+
return new Promise((resolve, reject) => {
|
|
239
272
|
displayList();
|
|
240
273
|
|
|
241
274
|
if (process.stdin.isTTY) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
+
};
|
|
245
307
|
|
|
246
308
|
const handleKeyPress = (key) => {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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;
|
|
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);
|
|
303
348
|
}
|
|
304
349
|
};
|
|
305
350
|
|
|
306
|
-
|
|
351
|
+
scope.on('data', handleKeyPress);
|
|
307
352
|
} else {
|
|
308
|
-
|
|
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
|
+
});
|
|
309
377
|
}
|
|
310
378
|
});
|
|
311
379
|
}
|
|
380
|
+
|
|
312
381
|
}
|
|
313
382
|
|
|
314
|
-
module.exports = Menu;
|
|
383
|
+
module.exports = Menu;
|