@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.
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Launcher Module - Handles Claude Code launching with various configurations
3
+ */
4
+
5
+ const { spawn } = require('child_process');
6
+ const colors = require('./ui/colors');
7
+ const i18n = require('./i18n');
8
+
9
+ /**
10
+ * Launch Claude Code with specified environment variables
11
+ */
12
+ function launchClaude(command, envVars = {}, disableAuthTokens = false) {
13
+ console.log('');
14
+ console.log(colors.yellow + '🚀 ' + i18n.tSync('launch.starting') + colors.reset);
15
+ console.log(colors.gray + i18n.tSync('launch.command', command) + colors.reset);
16
+
17
+ if (Object.keys(envVars).length > 0) {
18
+ console.log(colors.gray + i18n.tSync('launch.environment_variables') + colors.reset);
19
+ for (const [key, value] of Object.entries(envVars)) {
20
+ if (key.includes('TOKEN') || key.includes('KEY')) {
21
+ console.log(colors.gray + ` ${key}=***` + colors.reset);
22
+ } else {
23
+ console.log(colors.gray + ` ${key}=${value}` + colors.reset);
24
+ }
25
+ }
26
+ }
27
+
28
+ console.log('');
29
+ console.log(colors.green + '✓ ' + i18n.tSync('launch.run_in_terminal') + colors.reset);
30
+ console.log(colors.gray + ' ' + i18n.tSync('launch.launcher_exit') + colors.reset);
31
+ console.log('');
32
+
33
+ // Prepare clean environment
34
+ const env = { ...process.env, ...envVars };
35
+
36
+ // Disable conflicting auth tokens when using third-party API
37
+ if (disableAuthTokens) {
38
+ // Only delete CLAUDE_CODE_OAUTH_TOKEN - keep ANTHROPIC_AUTH_TOKEN that we just set
39
+ delete env.CLAUDE_CODE_OAUTH_TOKEN;
40
+ console.log(colors.gray + ' Disabled: CLAUDE_CODE_OAUTH_TOKEN' + colors.reset);
41
+ }
42
+
43
+ // Parse command and arguments
44
+ const args = command.split(' ');
45
+ const cmd = args.shift();
46
+
47
+ try {
48
+ // Clean up terminal state before launching Claude
49
+ if (process.stdin.isTTY) {
50
+ process.stdin.setRawMode(false);
51
+ process.stdin.pause();
52
+ }
53
+
54
+ // Remove all event listeners to avoid conflicts
55
+ process.stdin.removeAllListeners('data');
56
+ process.stdin.removeAllListeners('keypress');
57
+ process.removeAllListeners('SIGINT');
58
+ process.removeAllListeners('SIGTERM');
59
+
60
+ // Launch Claude in current terminal, let it inherit everything
61
+ const child = spawn(cmd, args, {
62
+ stdio: 'inherit',
63
+ env: env,
64
+ cwd: process.cwd(),
65
+ shell: true
66
+ });
67
+
68
+ // Don't exit immediately, wait for Claude to exit then exit launcher
69
+ child.on('close', (code) => {
70
+ process.exit(code || 0);
71
+ });
72
+
73
+ child.on('error', (error) => {
74
+ console.log(colors.red + '❌ Error running Claude: ' + error.message + colors.reset);
75
+ process.exit(1);
76
+ });
77
+
78
+ } catch (error) {
79
+ console.log(colors.red + '❌ Error launching Claude Code: ' + error.message + colors.reset);
80
+ console.log(colors.gray + i18n.tSync('ui.general.press_key_return_menu') + colors.reset);
81
+ process.stdin.setRawMode(true);
82
+ process.stdin.resume();
83
+ process.stdin.once('data', () => {
84
+ process.stdin.setRawMode(false);
85
+ // Note: Caller should handle menu display
86
+ });
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Launch Claude with default settings
92
+ */
93
+ function launchClaudeDefault() {
94
+ launchClaude('claude');
95
+ }
96
+
97
+ /**
98
+ * Launch Claude with skip permissions flag
99
+ */
100
+ function launchClaudeSkipPermissions() {
101
+ launchClaude('claude --dangerously-skip-permissions');
102
+ }
103
+
104
+ /**
105
+ * Get environment variables based on provider type
106
+ */
107
+ function getProviderEnvVars(api) {
108
+ // Decrypt the auth token (all tokens are stored encrypted)
109
+ const { decrypt } = require('./crypto');
110
+ const decrypted = decrypt(api.authToken);
111
+
112
+ if (!decrypted.success) {
113
+ console.error('Failed to decrypt auth token:', decrypted.error);
114
+ throw new Error('Failed to decrypt API auth token. Please check your configuration.');
115
+ }
116
+
117
+ const authToken = decrypted.value;
118
+
119
+ const baseEnvVars = {
120
+ ANTHROPIC_BASE_URL: api.baseUrl,
121
+ ANTHROPIC_AUTH_TOKEN: authToken,
122
+ ANTHROPIC_MODEL: api.model,
123
+ ANTHROPIC_SMALL_FAST_MODEL: api.smallFastModel || api.model
124
+ };
125
+
126
+ // Add provider-specific environment variables
127
+ switch (api.provider) {
128
+ case 'deepseek':
129
+ return {
130
+ ...baseEnvVars,
131
+ API_TIMEOUT_MS: '600000',
132
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1'
133
+ };
134
+
135
+ case 'moonshot':
136
+ case 'anthropic':
137
+ case 'custom':
138
+ default:
139
+ return baseEnvVars;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Launch Claude with third-party API configuration
145
+ */
146
+ function launchClaudeWithApi(api, skipPermissions = false) {
147
+ const command = skipPermissions
148
+ ? 'claude --dangerously-skip-permissions'
149
+ : 'claude';
150
+
151
+ const envVars = getProviderEnvVars(api);
152
+
153
+ console.log('');
154
+ console.log(colors.bright + colors.orange + '🔗 ' + i18n.tSync('launch.using_third_party_api') + colors.reset);
155
+ console.log(colors.gray + ` Provider: ${api.provider || 'Custom'}` + colors.reset);
156
+ console.log(colors.gray + ` API: ${api.name}` + colors.reset);
157
+ console.log(colors.gray + ` Base URL: ${api.baseUrl}` + colors.reset);
158
+ console.log(colors.gray + ` Model: ${api.model}` + colors.reset);
159
+
160
+ // Show provider-specific optimizations
161
+ if (api.provider === 'deepseek') {
162
+ console.log(colors.yellow + ' ⚡ ' + i18n.tSync('launch.deepseek_optimizations') + colors.reset);
163
+ console.log(colors.gray + ' • ' + i18n.tSync('launch.extended_timeout') + colors.reset);
164
+ console.log(colors.gray + ' • ' + i18n.tSync('launch.non_essential_disabled') + colors.reset);
165
+ }
166
+
167
+ console.log('');
168
+
169
+ launchClaude(command, envVars, true);
170
+ }
171
+
172
+ /**
173
+ * Test API connection
174
+ */
175
+ async function testApiConnection(api) {
176
+ console.log(colors.yellow + '🔍 Testing API connection...' + colors.reset);
177
+
178
+ try {
179
+ // Handle both plaintext (during testing) and encrypted tokens (from stored APIs)
180
+ let authToken = api.authToken;
181
+
182
+ // Try to decrypt if it looks like an encrypted token
183
+ if (typeof authToken === 'string' && authToken.includes(':')) {
184
+ const { decrypt } = require('./crypto');
185
+ const decrypted = decrypt(authToken);
186
+
187
+ if (decrypted.success) {
188
+ authToken = decrypted.value;
189
+ }
190
+ // If decryption fails but token contains ':', it might be encrypted but corrupted
191
+ else if (authToken.split(':').length === 3) {
192
+ console.error('Failed to decrypt auth token for testing:', decrypted.error);
193
+ return { success: false, error: 'Failed to decrypt auth token' };
194
+ }
195
+ // Otherwise, treat as plaintext token
196
+ }
197
+
198
+ // Try to make a simple request to test the connection
199
+ const https = require('https');
200
+ const url = new URL(api.baseUrl);
201
+
202
+ return new Promise((resolve) => {
203
+ const options = {
204
+ hostname: url.hostname,
205
+ port: url.port || 443,
206
+ path: url.pathname,
207
+ method: 'GET',
208
+ timeout: 5000,
209
+ headers: {
210
+ 'Authorization': `Bearer ${authToken}`
211
+ }
212
+ };
213
+
214
+ const req = https.request(options, (res) => {
215
+ if (res.statusCode === 401) {
216
+ console.log(colors.yellow + '⚠️ API returned 401 - Check your auth token' + colors.reset);
217
+ resolve({ success: false, error: 'Authentication failed' });
218
+ } else if (res.statusCode >= 200 && res.statusCode < 500) {
219
+ console.log(colors.green + '✓ API is reachable' + colors.reset);
220
+ resolve({ success: true });
221
+ } else {
222
+ console.log(colors.red + `❌ API returned status ${res.statusCode}` + colors.reset);
223
+ resolve({ success: false, error: `HTTP ${res.statusCode}` });
224
+ }
225
+ });
226
+
227
+ req.on('error', (error) => {
228
+ console.log(colors.red + `❌ Connection failed: ${error.message}` + colors.reset);
229
+ resolve({ success: false, error: error.message });
230
+ });
231
+
232
+ req.on('timeout', () => {
233
+ console.log(colors.red + '❌ Connection timeout' + colors.reset);
234
+ req.destroy();
235
+ resolve({ success: false, error: 'Timeout' });
236
+ });
237
+
238
+ req.end();
239
+ });
240
+ } catch (error) {
241
+ console.log(colors.red + `❌ Test failed: ${error.message}` + colors.reset);
242
+ return { success: false, error: error.message };
243
+ }
244
+ }
245
+
246
+ module.exports = {
247
+ launchClaude,
248
+ launchClaudeDefault,
249
+ launchClaudeSkipPermissions,
250
+ launchClaudeWithApi,
251
+ getProviderEnvVars,
252
+ testApiConnection
253
+ };
@@ -0,0 +1,104 @@
1
+ /**
2
+ * API Providers Presets - Claude Code compatible API providers
3
+ *
4
+ * Note: Only includes APIs that are compatible with Claude Code's Anthropic API format
5
+ */
6
+
7
+ const providers = {
8
+ anthropic: {
9
+ name: 'Anthropic (Official)',
10
+ baseUrl: 'https://api.anthropic.com',
11
+ models: [
12
+ 'claude-3-5-haiku-20241022',
13
+ 'claude-3.7-sonnet',
14
+ 'claude-sonnet-4',
15
+ 'claude-opus-4',
16
+ 'claude-opus-4.1'
17
+ ],
18
+ authTokenFormat: 'sk-ant-api03-...',
19
+ description: 'Official Anthropic API - Fully compatible',
20
+ requiresToken: true,
21
+ compatibility: 'native'
22
+ },
23
+ moonshot: {
24
+ name: 'Moonshot AI (Kimi-K2)',
25
+ baseUrl: 'https://api.moonshot.cn/anthropic',
26
+ models: [
27
+ 'kimi-k2-0711-preview',
28
+ 'kimi-k2-0905-preview',
29
+ 'kimi-k2-turbo-preview'
30
+ ],
31
+ authTokenFormat: 'sk-...',
32
+ description: 'Moonshot AI - Provides Anthropic-compatible API',
33
+ requiresToken: true,
34
+ compatibility: 'anthropic-compatible'
35
+ },
36
+ deepseek: {
37
+ name: 'DeepSeek (DeepSeek V3/V3.1)',
38
+ baseUrl: 'https://api.deepseek.com/anthropic',
39
+ models: [
40
+ 'deepseek-chat'
41
+ ],
42
+ authTokenFormat: 'sk-...',
43
+ description: 'DeepSeek AI - Anthropic-compatible endpoint',
44
+ requiresToken: true,
45
+ compatibility: 'anthropic-compatible'
46
+ },
47
+ custom: {
48
+ name: 'Custom Anthropic-Compatible API',
49
+ baseUrl: 'https://your-api-server.com/v1/anthropic',
50
+ models: [
51
+ 'your-model-name'
52
+ ],
53
+ authTokenFormat: 'Bearer token or API key',
54
+ description: 'Custom server with Anthropic-compatible API',
55
+ requiresToken: true,
56
+ compatibility: 'anthropic-compatible',
57
+ note: 'Replace URL and model with your actual server details'
58
+ }
59
+ };
60
+
61
+ /**
62
+ * Get all available providers
63
+ */
64
+ function getAllProviders() {
65
+ return Object.keys(providers).map(key => ({
66
+ id: key,
67
+ ...providers[key]
68
+ }));
69
+ }
70
+
71
+ /**
72
+ * Get a specific provider by ID
73
+ */
74
+ function getProvider(providerId) {
75
+ return providers[providerId] || null;
76
+ }
77
+
78
+ /**
79
+ * Get suggested models for a provider
80
+ */
81
+ function getSuggestedModels(providerId) {
82
+ const provider = providers[providerId];
83
+ return provider ? provider.models : [];
84
+ }
85
+
86
+ /**
87
+ * Validate if a URL matches a known provider
88
+ */
89
+ function detectProvider(baseUrl) {
90
+ for (const [key, provider] of Object.entries(providers)) {
91
+ if (baseUrl.includes(provider.baseUrl.replace('https://', '').replace('http://', '').split('/')[0])) {
92
+ return key;
93
+ }
94
+ }
95
+ return 'custom';
96
+ }
97
+
98
+ module.exports = {
99
+ providers,
100
+ getAllProviders,
101
+ getProvider,
102
+ getSuggestedModels,
103
+ detectProvider
104
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Colors Module - ANSI color codes for Claude-style theming
3
+ */
4
+
5
+ const colors = {
6
+ reset: '\x1b[0m',
7
+ bright: '\x1b[1m',
8
+ dim: '\x1b[2m',
9
+
10
+ // Claude theme colors
11
+ orange: '\x1b[38;5;208m', // Claude brand orange
12
+ amber: '\x1b[38;5;214m', // Amber/yellow-orange
13
+
14
+ // Standard colors
15
+ white: '\x1b[37m',
16
+ gray: '\x1b[90m',
17
+ green: '\x1b[32m',
18
+ red: '\x1b[31m',
19
+ yellow: '\x1b[33m',
20
+ blue: '\x1b[34m',
21
+ cyan: '\x1b[36m',
22
+ black: '\x1b[30m',
23
+
24
+ // Background colors
25
+ bgOrange: '\x1b[48;5;208m', // Background orange
26
+ bgAmber: '\x1b[48;5;214m', // Background amber
27
+ bgRed: '\x1b[41m',
28
+ bgGreen: '\x1b[42m',
29
+ bgBlue: '\x1b[44m'
30
+ };
31
+
32
+ module.exports = colors;
@@ -0,0 +1,260 @@
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
+
11
+ /**
12
+ * Display simple interactive table for API selection
13
+ */
14
+ async function showApiSelectionTable(apis, title, actionType = 'select', activeIndex = -1, apiManager = null) {
15
+ if (apis.length === 0) {
16
+ console.clear();
17
+ console.log('');
18
+ console.log(colors.yellow + 'ℹ️ ' + i18n.tSync('messages.info.no_apis_info_title') + colors.reset);
19
+ console.log(colors.gray + ' ' + i18n.tSync('messages.info.apis_removed_or_none') + colors.reset);
20
+ console.log('');
21
+ console.log(colors.gray + i18n.tSync('messages.info.press_return_menu') + colors.reset);
22
+
23
+ await waitForKeyPress();
24
+ return null;
25
+ }
26
+
27
+ let selectedIndex = 0;
28
+ if (actionType === 'switch' && activeIndex >= 0 && activeIndex < apis.length) {
29
+ selectedIndex = activeIndex;
30
+ }
31
+
32
+ function displaySimpleTable() {
33
+ // Header info
34
+ console.log('');
35
+ console.log(colors.cyan + title + colors.reset);
36
+ console.log('');
37
+
38
+ // Show current active API for switch mode
39
+ if (actionType === 'switch' && activeIndex >= 0 && activeIndex < apis.length) {
40
+ const activeApi = apis[activeIndex];
41
+ console.log(colors.gray + i18n.tSync('ui.general.currently_active_api') + colors.reset);
42
+ console.log(colors.gray + ` Name: ${activeApi.name}` + colors.reset);
43
+ console.log(colors.gray + ` Provider: ${activeApi.provider}` + colors.reset);
44
+ console.log(colors.gray + ` Usage Count: ${activeApi.usageCount || 0}` + colors.reset);
45
+ console.log('');
46
+ }
47
+
48
+ // Table header with 3-column layout
49
+ console.log(colors.bright + colors.orange +
50
+ '┌────┬─────────────────────────┬────────────────────────────────────────────────────────────────────────┐' + colors.reset);
51
+ console.log(colors.bright + colors.orange +
52
+ '│ No.│ Name │ Detail │' + colors.reset);
53
+ console.log(colors.bright + colors.orange +
54
+ '├────┼─────────────────────────┼────────────────────────────────────────────────────────────────────────┤' + colors.reset);
55
+
56
+ // Testing with multi-row display loop
57
+ apis.forEach((api, index) => {
58
+ const num = (index + 1).toString().padStart(2, ' ');
59
+
60
+ // Check if this is the currently active API
61
+ const isActiveApi = activeIndex === index;
62
+ const activeMarker = isActiveApi ? '●' : ' ';
63
+
64
+ // Format name with active marker
65
+ const nameWithMarker = `${activeMarker} ${api.name}`;
66
+ const displayName = nameWithMarker.padEnd(23, ' ');
67
+
68
+ // Test decrypt and maskApiToken functions
69
+ const decryptedToken = decrypt(api.authToken);
70
+ const displayToken = decryptedToken.success ? maskApiToken(decryptedToken.value) : '***ERROR***';
71
+
72
+ // Create 6 detail lines (full version)
73
+ const details = [
74
+ `Provider: ${api.provider}`,
75
+ `URL: ${api.baseUrl}`,
76
+ `Model: ${api.model}`,
77
+ `Token: ${displayToken}`,
78
+ `Usage: ${api.usageCount || 0} times`,
79
+ `Last Used: ${api.lastUsed ? new Date(api.lastUsed).toLocaleString() : 'Never'}`
80
+ ];
81
+
82
+ // Pad each detail line to exactly 70 characters
83
+ const paddedDetails = details.map(detail => padStringToWidth(detail, 70));
84
+
85
+ // Color selection based on active state and selection
86
+ const nameColor = isActiveApi ? colors.green : (index === selectedIndex ? colors.white : colors.gray);
87
+ const detailColor = isActiveApi ? colors.green : (index === selectedIndex ? colors.white : colors.gray);
88
+ const bgColor = index === selectedIndex ? colors.bgAmber : '';
89
+ const textBg = index === selectedIndex ? colors.black : '';
90
+
91
+ // Display 6 rows for each API, with No. and Name centered on row 3 (index 2)
92
+ for (let i = 0; i < paddedDetails.length; i++) {
93
+ if (i === 2) {
94
+ // Middle row (3rd row) - show No. and Name for vertical centering
95
+ console.log(colors.orange + '│' + textBg + bgColor + nameColor +
96
+ ` ${num} ` + colors.reset + colors.orange + '│' + textBg + bgColor + nameColor +
97
+ ` ${displayName} ` + colors.reset + colors.orange + '│' + textBg + bgColor + detailColor +
98
+ ` ${paddedDetails[i]} ` + colors.reset + colors.orange + '│' + colors.reset);
99
+ } else {
100
+ // Other rows - empty No. and Name columns
101
+ console.log(colors.orange + '│' + textBg + bgColor + colors.gray +
102
+ ' ' + colors.reset + colors.orange + '│' + textBg + bgColor + colors.gray +
103
+ ' ' + colors.reset + colors.orange + '│' + textBg + bgColor + detailColor +
104
+ ' ' + paddedDetails[i] + ' ' + colors.reset + colors.orange + '│' + colors.reset);
105
+ }
106
+ }
107
+
108
+ // Add separator line after each API except the last one
109
+ if (index < apis.length - 1) {
110
+ console.log(colors.bright + colors.orange +
111
+ '├────┼─────────────────────────┼────────────────────────────────────────────────────────────────────────┤' + colors.reset);
112
+ }
113
+ });
114
+
115
+ console.log(colors.bright + colors.orange +
116
+ '└────┴─────────────────────────┴────────────────────────────────────────────────────────────────────────┘' + colors.reset);
117
+ console.log('');
118
+
119
+ if (actionType === 'switch' && activeIndex >= 0) {
120
+ console.log(colors.green + ' ● = ' + i18n.tSync('ui.general.currently_active_api') + colors.reset);
121
+ }
122
+
123
+ // Different action prompts for different functionality
124
+ const actionText = actionType === 'remove' ? 'remove' : (actionType === 'switch' ? 'switch' : 'select');
125
+ console.log(colors.amber + ' ' + i18n.tSync('navigation.use_arrows_esc', actionText) + colors.reset);
126
+ console.log('');
127
+ }
128
+
129
+ function handleKeyPress(key) {
130
+ switch (key) {
131
+ case '\u001b[A': // Up arrow
132
+ selectedIndex = (selectedIndex - 1 + apis.length) % apis.length;
133
+ console.clear(); // Force clear screen
134
+ displaySimpleTable();
135
+ break;
136
+
137
+ case '\u001b[B': // Down arrow
138
+ selectedIndex = (selectedIndex + 1) % apis.length;
139
+ console.clear(); // Force clear screen
140
+ displaySimpleTable();
141
+ break;
142
+
143
+ case '\r': // Enter
144
+ return apis[selectedIndex];
145
+
146
+ case '\u001b': // Escape
147
+ case 'q':
148
+ case 'Q':
149
+ return null;
150
+ }
151
+ return undefined;
152
+ }
153
+
154
+ return new Promise((resolve) => {
155
+ // Initial display
156
+ console.clear();
157
+ displaySimpleTable();
158
+
159
+ if (process.stdin.isTTY) {
160
+ process.stdin.removeAllListeners('data');
161
+ process.stdin.removeAllListeners('keypress');
162
+ process.stdin.setRawMode(true);
163
+ process.stdin.resume();
164
+ process.stdin.setEncoding('utf8');
165
+
166
+ const keyHandler = async (key) => {
167
+ const result = handleKeyPress(key);
168
+ if (result !== undefined) {
169
+ // Force complete cleanup to prevent navigation issues
170
+ if (process.stdin.isTTY) {
171
+ process.stdin.setRawMode(false);
172
+ }
173
+ process.stdin.removeAllListeners('data');
174
+ process.stdin.removeAllListeners('keypress');
175
+ process.stdin.pause();
176
+
177
+ // Handle switch mode - activate the selected API
178
+ if (result && actionType === 'switch' && apiManager) {
179
+ const selectedIndex = apis.findIndex(api => api.id === result.id);
180
+ const switchedApi = apiManager.setActiveApi(selectedIndex);
181
+
182
+ console.clear();
183
+ console.log('');
184
+ console.log(colors.bright + colors.green + `✓ ${i18n.tSync('messages.success.api_switched')}` + colors.reset);
185
+ console.log(colors.gray + ` ${i18n.tSync('api.actions.switch_success', switchedApi.name)}` + colors.reset);
186
+ console.log(colors.gray + ` ${i18n.tSync('api.details.provider')}: ${switchedApi.provider}` + colors.reset);
187
+ console.log(colors.gray + ` ${i18n.tSync('api.details.url')}: ${switchedApi.baseUrl}` + colors.reset);
188
+ console.log(colors.gray + ` ${i18n.tSync('api.details.model')}: ${switchedApi.model}` + colors.reset);
189
+ console.log('');
190
+
191
+ // Wait for user key press
192
+ console.log(colors.gray + i18n.tSync('messages.prompts.press_any_key') + colors.reset);
193
+ await waitForKeyPress();
194
+ } else {
195
+ console.clear();
196
+ console.log('');
197
+ console.log(colors.green + '✓ Selection completed: ' + (result ? result.name : 'Cancelled') + colors.reset);
198
+ console.log('');
199
+ }
200
+
201
+ resolve(result);
202
+ }
203
+ };
204
+
205
+ process.stdin.on('data', keyHandler);
206
+ } else {
207
+ resolve(null);
208
+ }
209
+ });
210
+ }
211
+
212
+ function waitForKeyPress() {
213
+ return new Promise((resolve) => {
214
+ const keyHandler = () => {
215
+ process.stdin.removeListener('data', keyHandler);
216
+ resolve();
217
+ };
218
+ process.stdin.once('data', keyHandler);
219
+ process.stdin.resume();
220
+ });
221
+ }
222
+
223
+ async function confirmDeletion(api) {
224
+ console.clear();
225
+ console.log('');
226
+ console.log(colors.red + colors.bright + '[!] ' + i18n.tSync('messages.prompts.confirm_deletion') + colors.reset);
227
+ console.log('');
228
+ console.log(colors.yellow + i18n.tSync('ui.general.confirm_delete_api') + colors.reset);
229
+ console.log('');
230
+ console.log(colors.gray + `Name: ${api.name}` + colors.reset);
231
+ console.log(colors.gray + `Provider: ${api.provider}` + colors.reset);
232
+ console.log(colors.gray + `Base URL: ${api.baseUrl}` + colors.reset);
233
+ console.log(colors.gray + `Model: ${api.model}` + colors.reset);
234
+ const decryptedToken = decrypt(api.authToken);
235
+ const displayToken = decryptedToken.success ? maskApiToken(decryptedToken.value) : '***ERROR***';
236
+ console.log(colors.gray + `Token: ${displayToken}` + colors.reset);
237
+ console.log('');
238
+ console.log(colors.red + i18n.tSync('ui.general.action_cannot_undone') + colors.reset);
239
+ console.log('');
240
+
241
+ const readline = require('readline');
242
+ const rl = readline.createInterface({
243
+ input: process.stdin,
244
+ output: process.stdout
245
+ });
246
+
247
+ return new Promise((resolve) => {
248
+ rl.question(colors.red + i18n.tSync('ui.general.confirm_deletion_prompt') + colors.reset, (answer) => {
249
+ rl.close();
250
+ const confirmed = answer.trim().toLowerCase() === 'y';
251
+ resolve(confirmed);
252
+ });
253
+ });
254
+ }
255
+
256
+ module.exports = {
257
+ showApiSelectionTable,
258
+ waitForKeyPress,
259
+ confirmDeletion
260
+ };