@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/launcher.js
ADDED
|
@@ -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
|
+
};
|
package/lib/ui/colors.js
ADDED
|
@@ -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
|
+
};
|