@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.
@@ -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
+ };