@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/claude-launcher CHANGED
@@ -1,679 +1,1120 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { spawn } = require('child_process');
3
+ /**
4
+ * Claude Launcher - Refactored modular version
5
+ * A launcher for Claude Code with third-party API support
6
+ */
7
+
8
+ /**
9
+ * Force complete stdin reset to prevent navigation issues
10
+ */
11
+ function forceStdinCleanup() {
12
+ try {
13
+ if (process.stdin.isTTY) {
14
+ process.stdin.setRawMode(false);
15
+ process.stdin.removeAllListeners('data');
16
+ process.stdin.pause();
17
+ }
18
+ } catch (error) {
19
+ // Ignore cleanup errors
20
+ }
21
+ }
22
+
23
+
24
+ const ApiManager = require('./lib/api-manager');
25
+ const Menu = require('./lib/ui/menu');
26
+ const colors = require('./lib/ui/colors');
27
+ const { checkForUpdates, forceCheckForUpdates } = require('./lib/utils/version-checker');
28
+ const {
29
+ waitForKey,
30
+ promptForThirdPartyApi,
31
+ confirmAction,
32
+ showSuccess,
33
+ showError,
34
+ showInfo
35
+ } = require('./lib/ui/prompts');
36
+ const {
37
+ launchClaudeDefault,
38
+ launchClaudeSkipPermissions,
39
+ launchClaudeWithApi
40
+ } = require('./lib/launcher');
41
+ const { getPasswordInput } = require('./lib/auth/password-input');
42
+ const { verifyExportPassword, setupNewPassword, changePassword: changePasswordModule } = require('./lib/auth/password-validator');
43
+ const { maskApiToken } = require('./lib/validators');
44
+ const { showApiSelectionTable, confirmDeletion } = require('./lib/ui/interactive-table');
45
+ const i18n = require('./lib/i18n');
4
46
  const fs = require('fs');
5
47
  const path = require('path');
6
- const readline = require('readline');
7
- const crypto = require('crypto');
8
-
9
- // Get config file location - check multiple locations for global installation
10
- function getConfigPath() {
11
- const locations = [
12
- // Current working directory (highest priority)
13
- path.join(process.cwd(), '.claude-launcher.env'),
14
- // User home directory
15
- path.join(require('os').homedir(), '.claude-launcher.env'),
16
- // Script directory (for local installation)
17
- path.join(__dirname, '.claude-launcher.env')
18
- ];
19
-
20
- // Return the first existing config file, or the home directory path as default
21
- for (const location of locations) {
22
- if (fs.existsSync(location)) {
23
- return location;
24
- }
48
+ const os = require('os');
49
+ const { exec } = require('child_process');
50
+
51
+ // Initialize components
52
+ const apiManager = new ApiManager();
53
+
54
+ // Global menu objects to prevent screen flickering during navigation
55
+ let globalMainMenu = null;
56
+ let globalConfirmMenu = null;
57
+ let globalApiManagementMenu = null;
58
+
59
+ /**
60
+ * Initialize global menu objects to prevent recreation and screen flickering
61
+ */
62
+ function initializeGlobalMenus() {
63
+ if (!globalMainMenu) {
64
+ globalMainMenu = new Menu();
65
+ }
66
+ if (!globalConfirmMenu) {
67
+ globalConfirmMenu = new Menu();
68
+ }
69
+ if (!globalApiManagementMenu) {
70
+ globalApiManagementMenu = new Menu();
25
71
  }
26
-
27
- // Default to home directory if no config exists
28
- return path.join(require('os').homedir(), '.claude-launcher.env');
29
72
  }
30
73
 
31
- const CONFIG_FILE = getConfigPath();
32
-
33
- // ANSI color codes for Claude-style theming
34
- const colors = {
35
- reset: '\x1b[0m',
36
- bright: '\x1b[1m',
37
- orange: '\x1b[38;5;208m', // Claude brand orange
38
- amber: '\x1b[38;5;214m', // Amber/yellow-orange
39
- white: '\x1b[37m',
40
- gray: '\x1b[90m',
41
- green: '\x1b[32m',
42
- red: '\x1b[31m',
43
- yellow: '\x1b[33m',
44
- black: '\x1b[30m',
45
- bgOrange: '\x1b[48;5;208m', // Background orange
46
- bgAmber: '\x1b[48;5;214m' // Background amber
47
- };
48
-
49
- // Generate encryption key from machine-specific data
50
- function getEncryptionKey() {
51
- const os = require('os');
52
- // Use a combination of hostname and user info to create a machine-specific key
53
- const machineId = os.hostname() + os.userInfo().username + os.platform();
54
- // Derive a 32-byte key using PBKDF2
55
- return crypto.pbkdf2Sync(machineId, 'claude-launcher-salt', 10000, 32, 'sha256');
74
+ /**
75
+ * Get export directory path and create if not exists
76
+ * @returns {string} - Export directory path
77
+ */
78
+ function getExportDirectory() {
79
+ const userHome = os.homedir();
80
+ const exportDir = path.join(userHome, 'claude-launcher');
81
+
82
+ // Create directory if it doesn't exist
83
+ if (!fs.existsSync(exportDir)) {
84
+ fs.mkdirSync(exportDir, { recursive: true });
85
+ }
86
+
87
+ return exportDir;
56
88
  }
57
89
 
58
- // Encrypt API key using AES-256-CBC
59
- function encryptApiKey(plaintext) {
60
- try {
61
- const key = getEncryptionKey();
62
- const iv = crypto.randomBytes(16); // Generate random IV
63
- const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
64
-
65
- let encrypted = cipher.update(plaintext, 'utf8', 'hex');
66
- encrypted += cipher.final('hex');
67
-
68
- // Combine IV and encrypted data
69
- const result = iv.toString('hex') + ':' + encrypted;
70
-
71
- return {
72
- success: true,
73
- value: result,
74
- error: null
75
- };
76
- } catch (error) {
77
- return {
78
- success: false,
79
- value: null,
80
- error: error.message
81
- };
90
+ /**
91
+ * Generate timestamp-based filename for export
92
+ * @returns {string} - Filename with timestamp
93
+ */
94
+ function generateExportFilename() {
95
+ const now = new Date();
96
+ const timestamp = now.getFullYear().toString() +
97
+ (now.getMonth() + 1).toString().padStart(2, '0') +
98
+ now.getDate().toString().padStart(2, '0') +
99
+ now.getHours().toString().padStart(2, '0') +
100
+ now.getMinutes().toString().padStart(2, '0') +
101
+ now.getSeconds().toString().padStart(2, '0');
102
+
103
+ return `claude-launcher-export-${timestamp}.json`;
104
+ }
105
+
106
+ /**
107
+ * Open file with system default application
108
+ * @param {string} filePath - Path to the file to open
109
+ */
110
+ function openFileWithDefault(filePath) {
111
+ const platform = process.platform;
112
+ let command;
113
+
114
+ if (platform === 'win32') {
115
+ command = `start "" "${filePath}"`;
116
+ } else if (platform === 'darwin') {
117
+ command = `open "${filePath}"`;
118
+ } else {
119
+ command = `xdg-open "${filePath}"`;
82
120
  }
121
+
122
+ exec(command, (error) => {
123
+ if (error) {
124
+ console.log(colors.yellow + `Could not open file automatically: ${error.message}` + colors.reset);
125
+ }
126
+ });
83
127
  }
84
128
 
85
- // Decrypt API key using AES-256-CBC
86
- function decryptApiKey(encryptedData) {
129
+ /**
130
+ * Validate import file path and JSON format
131
+ * @param {string} filePath - Path to the JSON file
132
+ * @returns {Object} - Validation result with success status and data/error
133
+ */
134
+ function validateImportFile(filePath) {
135
+ const result = {
136
+ valid: false,
137
+ data: null,
138
+ error: null
139
+ };
140
+
87
141
  try {
88
- const key = getEncryptionKey();
89
-
90
- // Split IV and encrypted data
91
- const parts = encryptedData.split(':');
92
- if (parts.length !== 2) {
93
- throw new Error('Invalid encrypted data format');
142
+ // Check if file path is provided
143
+ if (!filePath || filePath.trim() === '') {
144
+ result.error = 'File path cannot be empty';
145
+ return result;
146
+ }
147
+
148
+ // Normalize and resolve path
149
+ const normalizedPath = path.resolve(filePath.trim());
150
+
151
+ // Check if file exists
152
+ if (!fs.existsSync(normalizedPath)) {
153
+ result.error = i18n.tSync('errors.file.file_not_found', normalizedPath);
154
+ return result;
155
+ }
156
+
157
+ // Check if it's a file (not directory)
158
+ const stats = fs.statSync(normalizedPath);
159
+ if (!stats.isFile()) {
160
+ result.error = `Path is not a file: ${normalizedPath}`;
161
+ return result;
162
+ }
163
+
164
+ // Check file extension
165
+ if (path.extname(normalizedPath).toLowerCase() !== '.json') {
166
+ result.error = `File must have .json extension: ${normalizedPath}`;
167
+ return result;
168
+ }
169
+
170
+ // Read and parse JSON file
171
+ const fileContent = fs.readFileSync(normalizedPath, 'utf8');
172
+
173
+ // Validate JSON format
174
+ let jsonData;
175
+ try {
176
+ jsonData = JSON.parse(fileContent);
177
+ } catch (parseError) {
178
+ result.error = `Invalid JSON format: ${parseError.message}`;
179
+ return result;
180
+ }
181
+
182
+ // Basic structure validation for claude-launcher config
183
+ if (!jsonData || typeof jsonData !== 'object') {
184
+ result.error = 'JSON file must contain a valid configuration object';
185
+ return result;
186
+ }
187
+
188
+ // Check for required fields (basic validation)
189
+ if (!jsonData.hasOwnProperty('apis') || !Array.isArray(jsonData.apis)) {
190
+ result.error = i18n.tSync('errors.file.invalid_format', 'JSON file must contain an "apis" array');
191
+ return result;
94
192
  }
95
-
96
- const iv = Buffer.from(parts[0], 'hex');
97
- const encrypted = parts[1];
98
-
99
- const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
100
-
101
- let decrypted = decipher.update(encrypted, 'hex', 'utf8');
102
- decrypted += decipher.final('utf8');
103
-
104
- return {
105
- success: true,
106
- value: decrypted,
107
- error: null
108
- };
193
+
194
+ result.valid = true;
195
+ result.data = fileContent; // Return raw JSON string for compatibility
196
+ return result;
197
+
109
198
  } catch (error) {
110
- return {
111
- success: false,
112
- value: null,
113
- error: error.message
114
- };
199
+ result.error = `File validation error: ${error.message}`;
200
+ return result;
115
201
  }
116
202
  }
117
203
 
118
- // Validate API key format
119
- function validateApiKey(apiKey) {
120
- if (!apiKey || apiKey.trim() === '') {
121
- return {
122
- valid: false,
123
- error: 'API key is empty or missing'
124
- };
125
- }
126
-
127
- if (!apiKey.startsWith('sk-')) {
128
- return {
129
- valid: false,
130
- error: 'API key must start with "sk-"'
131
- };
132
- }
133
-
134
- if (apiKey.length < 20) {
135
- return {
136
- valid: false,
137
- error: 'API key appears to be too short'
138
- };
204
+ // Main menu options - will be populated dynamically with i18n
205
+ let menuOptions = [];
206
+
207
+ /**
208
+ * Add new third-party API
209
+ */
210
+ async function addNewThirdPartyApi() {
211
+ try {
212
+ const apiData = await promptForThirdPartyApi();
213
+
214
+ // Check if this is the first API
215
+ const isFirstApi = apiManager.getApis().length === 0;
216
+ const hasExportPassword = apiManager.hasExportPassword();
217
+
218
+ const newApi = apiManager.addApi(
219
+ apiData.baseUrl,
220
+ apiData.authToken,
221
+ apiData.model,
222
+ apiData.name,
223
+ apiData.provider
224
+ );
225
+
226
+ showSuccess(await i18n.t('messages.success.api_added'), [
227
+ `Name: ${newApi.name}`,
228
+ `${await i18n.t('api.details.provider')}: ${newApi.provider}`,
229
+ `${await i18n.t('api.details.url')}: ${newApi.baseUrl}`,
230
+ `${await i18n.t('api.details.model')}: ${newApi.model}`
231
+ ]);
232
+
233
+ if (isFirstApi) {
234
+ showInfo(await i18n.t('messages.info.first_time_usage'));
235
+ }
236
+
237
+ } catch (error) {
238
+ // Force cleanup stdin state to prevent navigation issues
239
+ forceStdinCleanup();
240
+
241
+ // Check if user cancelled the operation
242
+ const cancelledMessage = await i18n.t('errors.general.cancelled_by_user');
243
+ if (error.message === cancelledMessage) {
244
+ // User cancelled - show neutral message instead of error
245
+ console.log(colors.yellow + await i18n.t('messages.info.operation_cancelled') + colors.reset);
246
+ } else {
247
+ // Actual error occurred
248
+ showError(await i18n.t('errors.api.failed_add', error.message));
249
+ }
139
250
  }
140
-
141
- return {
142
- valid: true,
143
- error: null,
144
- value: apiKey
145
- };
251
+
252
+ await waitForKey(await i18n.t('messages.prompts.press_any_key'));
146
253
  }
147
254
 
148
- // Load configuration from .env file
149
- function loadConfig() {
150
- const config = {};
151
-
255
+ /**
256
+ * Remove third-party API
257
+ */
258
+ async function removeThirdPartyApi() {
152
259
  try {
153
- if (fs.existsSync(CONFIG_FILE)) {
154
- const envContent = fs.readFileSync(CONFIG_FILE, 'utf8');
155
- const lines = envContent.split('\n');
156
-
157
- lines.forEach(line => {
158
- const match = line.match(/^([^=]+)=(.*)$/);
159
- if (match) {
160
- const key = match[1];
161
- let value = match[2];
162
-
163
- // Store raw value for KIMI_API_KEY (validation will be done later)
164
- // Don't decrypt here to allow proper error handling
165
-
166
- config[key] = value;
167
- }
168
- });
169
- } else {
170
- console.log(colors.yellow + 'Warning: .claude-launcher.env file not found!' + colors.reset);
171
- console.log(colors.gray + `Searched locations:` + colors.reset);
172
- console.log(colors.gray + ` - ${path.join(process.cwd(), '.claude-launcher.env')} (current directory)` + colors.reset);
173
- console.log(colors.gray + ` - ${path.join(require('os').homedir(), '.claude-launcher.env')} (home directory)` + colors.reset);
174
- console.log(colors.gray + `Creating default config at: ${CONFIG_FILE}` + colors.reset);
175
-
176
- // Try to read template file, fallback to hardcoded defaults
177
- let defaultConfig = `KIMI_API_KEY=Your_Double_Base64_Encoded_Api_Key
178
- KIMI_BASE_URL=https://api.moonshot.cn/anthropic/`;
179
-
180
- const templatePath = path.join(__dirname, 'claude-launcher-template.env');
260
+ // Get API list
261
+ const apis = apiManager.getApis();
262
+ const activeApi = apiManager.getActiveApi();
263
+ const activeIndex = activeApi ? apis.findIndex(api => api.id === activeApi.id) : -1;
264
+
265
+ // Show API selection table (handles no APIs case internally)
266
+ const selectedApi = await showApiSelectionTable(
267
+ apis,
268
+ i18n.tSync('ui.general.select_api_remove'),
269
+ 'remove',
270
+ activeIndex
271
+ );
272
+
273
+ if (!selectedApi) {
274
+ return showMenu();
275
+ }
276
+
277
+ // Show confirmation dialog
278
+ const confirmed = await confirmDeletion(selectedApi);
279
+
280
+ if (confirmed) {
181
281
  try {
182
- if (fs.existsSync(templatePath)) {
183
- defaultConfig = fs.readFileSync(templatePath, 'utf8');
184
- console.log(colors.gray + `Using template from: ${templatePath}` + colors.reset);
282
+ // Find the index of the selected API in the current list
283
+ const selectedIndex = apis.findIndex(api => api.id === selectedApi.id);
284
+ apiManager.removeApi(selectedIndex);
285
+
286
+ console.clear();
287
+ showSuccess(await i18n.t('messages.success.api_removed'), [
288
+ `${await i18n.t('api.actions.removed_info', selectedApi.name)}`,
289
+ `${await i18n.t('api.details.provider')}: ${selectedApi.provider}`
290
+ ]);
291
+
292
+ // Show success message and return to main menu
293
+ const remainingApis = apiManager.getApis();
294
+ if (remainingApis.length === 0) {
295
+ showInfo(await i18n.t('messages.info.all_apis_removed'));
185
296
  }
186
- } catch (templateError) {
187
- console.log(colors.yellow + 'Template file not found, using defaults' + colors.reset);
188
- }
189
-
190
- try {
191
- fs.writeFileSync(CONFIG_FILE, defaultConfig);
192
- console.log(colors.green + 'Default configuration file created successfully!' + colors.reset);
193
- console.log(colors.gray + 'Please edit the file to add your actual API credentials.' + colors.reset);
194
- } catch (error) {
195
- console.log(colors.red + 'Failed to create config file: ' + error.message + colors.reset);
297
+ await waitForKey(await i18n.t('messages.prompts.press_any_key'));
298
+ return showMenu();
299
+
300
+ } catch (removeError) {
301
+ showError(await i18n.t('errors.api.failed_remove', removeError.message));
302
+ await waitForKey(await i18n.t('messages.prompts.press_any_key'));
303
+ return showMenu();
196
304
  }
197
-
198
- config.KIMI_API_KEY = 'Your_Double_Base64_Encoded_Api_Key';
199
- config.KIMI_BASE_URL = 'https://api.moonshot.cn/anthropic/';
305
+ } else {
306
+ showInfo(await i18n.t('messages.info.removal_cancelled'));
307
+ await waitForKey(await i18n.t('messages.prompts.press_any_key'));
308
+ return showMenu();
200
309
  }
310
+
201
311
  } catch (error) {
202
- console.log(colors.red + 'Error loading configuration: ' + error.message + colors.reset);
203
- config.KIMI_API_KEY = 'Your_Double_Base64_Encoded_Api_Key';
204
- config.KIMI_BASE_URL = 'https://api.moonshot.cn/anthropic/';
312
+ forceStdinCleanup();
313
+ showError('Failed to remove API', [error.message]);
314
+ await waitForKey(i18n.tSync('messages.prompts.press_any_key'));
205
315
  }
206
-
207
- return config;
208
316
  }
209
317
 
210
- // Launch Claude Code function with clean environment handoff
211
- function launchClaude(command, envVars = {}, disableAuthTokens = false) {
212
- console.log('');
213
- console.log(colors.yellow + 'Starting Claude Code...' + colors.reset);
214
- console.log(colors.gray + `Command: ${command}` + colors.reset);
215
-
216
- if (Object.keys(envVars).length > 0) {
217
- console.log(colors.gray + 'Environment variables:' + colors.reset);
218
- for (const [key, value] of Object.entries(envVars)) {
219
- if (key === 'ANTHROPIC_API_KEY') {
220
- console.log(colors.gray + ` ${key}=***` + colors.reset);
221
- } else {
222
- console.log(colors.gray + ` ${key}=${value}` + colors.reset);
223
- }
224
- }
225
- }
226
-
227
- console.log('');
228
- console.log(colors.green + '[+] Claude will run in current terminal.' + colors.reset);
229
- console.log(colors.gray + ' Launcher will exit to transfer control to Claude.' + colors.reset);
230
- console.log('');
231
-
232
- // Prepare clean environment
233
- const env = { ...process.env, ...envVars };
234
-
235
- // Disable conflicting auth tokens when using Kimi API
236
- if (disableAuthTokens) {
237
- delete env.ANTHROPIC_AUTH_TOKEN;
238
- delete env.CLAUDE_CODE_OAUTH_TOKEN;
239
- console.log(colors.gray + ' Disabled: ANTHROPIC_AUTH_TOKEN, CLAUDE_CODE_OAUTH_TOKEN' + colors.reset);
240
- }
241
-
242
- // Parse command and arguments
243
- const args = command.split(' ');
244
- const cmd = args.shift();
245
-
318
+ /**
319
+ * Switch active third-party API
320
+ */
321
+ async function switchThirdPartyApi() {
246
322
  try {
247
- // Clean up terminal state before launching Claude
248
- if (process.stdin.isTTY) {
249
- process.stdin.setRawMode(false);
250
- process.stdin.pause();
251
- }
252
-
253
- // Remove all event listeners to avoid conflicts
254
- process.stdin.removeAllListeners('data');
255
- process.stdin.removeAllListeners('keypress');
256
- process.removeAllListeners('SIGINT');
257
- process.removeAllListeners('SIGTERM');
258
-
259
- // Launch Claude in current terminal, let it inherit everything
260
- const child = spawn(cmd, args, {
261
- stdio: 'inherit', // Claude takes over current terminal I/O
262
- env: env,
263
- cwd: process.cwd(),
264
- shell: true // Use shell to find the command
265
- });
266
-
267
- // Don't exit immediately, wait for Claude to exit then exit launcher
268
- child.on('close', (code) => {
269
- process.exit(code || 0);
270
- });
271
-
272
- child.on('error', (error) => {
273
- console.log(colors.red + 'Error running Claude: ' + error.message + colors.reset);
274
- process.exit(1);
275
- });
276
-
323
+ const apis = apiManager.getApis();
324
+ const activeApi = apiManager.getActiveApi();
325
+ const activeIndex = activeApi ? apis.findIndex(api => api.id === activeApi.id) : -1;
326
+
327
+ // 现在表格函数内部处理整个切换流程
328
+ const selectedApi = await showApiSelectionTable(
329
+ apis,
330
+ i18n.tSync('api.actions.select_to_switch'),
331
+ 'switch',
332
+ activeIndex,
333
+ apiManager // 传递 apiManager 让表格函数处理切换逻辑
334
+ );
335
+
336
+ // 表格函数已经处理了所有显示和切换逻辑,直接返回主菜单
337
+ return showMenu();
338
+
277
339
  } catch (error) {
278
- console.log(colors.red + 'Error launching Claude Code: ' + error.message + colors.reset);
279
- console.log(colors.gray + 'Press any key to return to menu...' + colors.reset);
280
- process.stdin.setRawMode(true);
281
- process.stdin.resume();
282
- process.stdin.once('data', () => {
283
- process.stdin.setRawMode(false);
284
- showMenu();
285
- });
340
+ forceStdinCleanup();
341
+ showError(await i18n.t('errors.api.failed_switch', error.message));
342
+ await waitForKey(await i18n.t('messages.prompts.press_any_key'));
343
+ return showMenu();
286
344
  }
287
345
  }
288
346
 
289
- // Simple input using readline (supports paste naturally)
290
- function simpleInput(prompt) {
291
- return new Promise((resolve) => {
292
- const rl = readline.createInterface({
293
- input: process.stdin,
294
- output: process.stdout
295
- });
296
-
297
- rl.question(prompt, (answer) => {
298
- rl.close();
299
- resolve(answer.trim());
347
+ /**
348
+ * View API statistics
349
+ */
350
+ async function viewStatistics() {
351
+ console.clear();
352
+ console.log('');
353
+ console.log(colors.bright + colors.orange + '📊 ' + await i18n.t('statistics.title') + colors.reset);
354
+ console.log('');
355
+
356
+ const stats = apiManager.getStatistics();
357
+ const apis = apiManager.getApis();
358
+
359
+ console.log(colors.cyan + ' ' + i18n.tSync('ui.general.summary') + colors.reset);
360
+ console.log(colors.gray + ` ${await i18n.t('statistics.total_apis', stats.totalApis)}` + colors.reset);
361
+ console.log(colors.gray + ` ${await i18n.t('statistics.active_api', stats.activeApiName)}` + colors.reset);
362
+ console.log(colors.gray + ` ${await i18n.t('statistics.most_used', stats.mostUsedApi)}` + colors.reset);
363
+ console.log(colors.gray + ` ${await i18n.t('statistics.total_usage', stats.totalUsage)}` + colors.reset);
364
+ console.log('');
365
+
366
+ if (apis.length > 0) {
367
+ console.log(colors.cyan + ' ' + i18n.tSync('ui.general.configured_apis') + colors.reset);
368
+
369
+ // Pre-fetch translations to avoid await in forEach
370
+ const currentlyActiveText = await i18n.t('api.details.currently_active');
371
+ const providerText = await i18n.t('api.details.provider');
372
+ const usageText = await i18n.t('api.details.usage');
373
+ const timesSuffixText = await i18n.t('api.details.times_suffix');
374
+ const createdAtText = await i18n.t('api.details.created_at');
375
+
376
+ apis.forEach((api, index) => {
377
+ const isActive = apiManager.getActiveApi()?.id === api.id;
378
+ const activeText = isActive ? ` (${currentlyActiveText})` : '';
379
+ console.log(colors.gray + ` ${index + 1}. ${api.name}${activeText}` + colors.reset);
380
+ console.log(colors.dim + ` ${providerText}: ${api.provider}` + colors.reset);
381
+ console.log(colors.dim + ` ${usageText}: ${api.usageCount || 0} ${timesSuffixText}` + colors.reset);
382
+ console.log(colors.dim + ` ${createdAtText}: ${api.createdAt}` + colors.reset);
300
383
  });
301
- });
384
+ }
385
+
386
+ console.log('');
387
+ await waitForKey(await i18n.t('messages.prompts.press_any_key'));
302
388
  }
303
389
 
304
- // Prompt user to input API key with simple readline
305
- async function promptForApiKey() {
306
- try {
390
+
391
+ /**
392
+ * Handle first time password setup
393
+ */
394
+ async function handleFirstTimePasswordSetup() {
395
+ while (true) {
396
+ // Clear screen and show header
307
397
  console.clear();
308
398
  console.log('');
309
- console.log(colors.bright + colors.orange + '[*] Kimi API Key Setup' + colors.reset);
399
+ console.log(colors.bright + colors.yellow + '🔐 ' + i18n.tSync('password.setup.first_time_title') + colors.reset);
310
400
  console.log('');
311
- console.log(colors.yellow + '[!] This message appears because you have not set up a Kimi API key,' + colors.reset);
312
- console.log(colors.yellow + ' or the API key decrypted from your local config file is invalid.' + colors.reset);
401
+
402
+ // Show information
403
+ console.log(colors.cyan + i18n.tSync('password.setup.why_needed') + colors.reset);
404
+ const whyNeededItems = i18n.tSync('password.setup.why_needed_items');
405
+ if (Array.isArray(whyNeededItems)) {
406
+ whyNeededItems.forEach(item => {
407
+ console.log(colors.gray + '• ' + item + colors.reset);
408
+ });
409
+ }
313
410
  console.log('');
314
- console.log(colors.yellow + '[?] Why do you need to enter your API key?' + colors.reset);
315
- console.log(colors.gray + ' • Kimi K2 API provides Claude-compatible interface' + colors.reset);
316
- console.log(colors.gray + ' • Your API key enables access to Kimi\'s AI services' + colors.reset);
317
- console.log(colors.bright + colors.green + ' • The key will be encrypted and stored locally' + colors.reset);
318
- console.log(colors.bright + colors.green + ' Only accessible on this machine' + colors.reset);
411
+ console.log(colors.cyan + '🔒 ' + i18n.tSync('password.setup.new_security_title') + colors.reset);
412
+ const securityItems = i18n.tSync('password.setup.security_items');
413
+ if (Array.isArray(securityItems)) {
414
+ securityItems.forEach(item => {
415
+ console.log(colors.gray + '• ' + item + colors.reset);
416
+ });
417
+ }
319
418
  console.log('');
320
- console.log(colors.yellow + '[!] Security:' + colors.reset);
321
- console.log(colors.gray + ' API key is encrypted using AES-256-CBC' + colors.reset);
322
- console.log(colors.gray + ' Encryption key derived from machine-specific data' + colors.reset);
323
- console.log(colors.gray + ' • Key cannot be decrypted on other machines' + colors.reset);
419
+ console.log(colors.yellow + i18n.tSync('password.setup.options_title') + colors.reset);
420
+ console.log(colors.gray + '• ' + i18n.tSync('password.setup.option_set') + colors.reset);
421
+ console.log(colors.gray + '• ' + i18n.tSync('password.setup.option_skip') + colors.reset);
324
422
  console.log('');
325
-
326
- // Show input box (fixed spacing)
327
- console.log(colors.orange + '┌─────────────────────────────────────────────────────────┐' + colors.reset);
328
- console.log(colors.orange + '│' + colors.reset + ' ' + colors.bright + 'Enter your Kimi API key' + ' ' + colors.reset + colors.orange + '│' + colors.reset);
329
- console.log(colors.orange + '├─────────────────────────────────────────────────────────┤' + colors.reset);
330
- console.log(colors.orange + '│' + colors.reset + ' ' + colors.gray + 'Format: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' + ' ' + colors.reset + colors.orange + '│' + colors.reset);
331
- console.log(colors.orange + '│' + colors.reset + ' ' + colors.gray + 'You can copy and paste your API key here' + ' ' + colors.reset + colors.orange + '│' + colors.reset);
332
- console.log(colors.orange + '│' + colors.reset + ' ' + colors.bright + colors.yellow + 'After entering, press ENTER to continue' + ' ' + colors.reset + colors.orange + '│' + colors.reset);
333
- console.log(colors.orange + '│' + colors.reset + ' ' + colors.gray + 'Type "exit" or "quit" to return to menu' + ' ' + colors.reset + colors.orange + '│' + colors.reset);
334
- console.log(colors.orange + '└─────────────────────────────────────────────────────────┘' + colors.reset);
423
+ console.log(colors.red + i18n.tSync('password.setup.warning_skip') + colors.reset);
335
424
  console.log('');
336
-
337
- // Clean up any existing listeners once before the input loop
338
- process.stdin.removeAllListeners('data');
339
- process.stdin.removeAllListeners('keypress');
340
-
341
- // Input loop - allow retry on invalid input
342
- while (true) {
343
- const apiKey = await simpleInput(colors.green + '[>] API Key (press ENTER after input): ' + colors.reset);
344
-
345
- // Check for exit commands
346
- if (apiKey.toLowerCase() === 'exit' || apiKey.toLowerCase() === 'quit') {
347
- console.log('');
348
- console.log(colors.yellow + '[!] Setup cancelled by user.' + colors.reset);
349
- throw new Error('User cancelled setup');
350
- }
351
-
352
- // Validate the entered API key
353
- const validation = validateApiKey(apiKey);
354
-
355
- if (!validation.valid) {
356
- console.log(colors.red + '[X] Invalid API key: ' + validation.error + colors.reset);
357
- console.log(colors.yellow + '[!] Please try again with a valid API key.' + colors.reset);
358
- console.log('');
359
- continue; // Ask again
360
- }
361
-
362
- // Encrypt and save the API key
363
- const encrypted = encryptApiKey(apiKey);
364
-
365
- if (!encrypted.success) {
366
- console.log(colors.red + '[X] Failed to encrypt API key: ' + encrypted.error + colors.reset);
367
- console.log(colors.yellow + '[!] Please try again.' + colors.reset);
368
- console.log('');
369
- continue; // Ask again
425
+
426
+ console.log(colors.gray + '按任意键继续...' + colors.reset);
427
+
428
+ // Wait for user to read the information
429
+ await new Promise((resolve) => {
430
+ if (process.stdin.isTTY) {
431
+ process.stdin.setRawMode(true);
432
+ process.stdin.resume();
433
+ process.stdin.once('data', () => {
434
+ try {
435
+ process.stdin.setRawMode(false);
436
+ process.stdin.pause();
437
+ } catch (error) {
438
+ // Ignore cleanup errors
439
+ }
440
+ resolve();
441
+ });
442
+ } else {
443
+ resolve();
370
444
  }
371
-
372
- // Update configuration file
373
- updateConfigFile('KIMI_API_KEY', encrypted.value);
374
-
375
- console.log('');
376
- console.log(colors.green + '[+] API key encrypted and saved successfully!' + colors.reset);
377
- console.log(colors.gray + ' Configuration saved to: ' + CONFIG_FILE + colors.reset);
378
- console.log('');
379
-
380
- return apiKey;
445
+ });
446
+
447
+ // Now show the menu for selection
448
+ const menuOptions = [
449
+ i18n.tSync('password.setup.menu_set_password'),
450
+ i18n.tSync('password.setup.menu_skip_setup'),
451
+ i18n.tSync('menu.api_management.back')
452
+ ];
453
+
454
+ // Ensure global menus are initialized
455
+ initializeGlobalMenus();
456
+ globalConfirmMenu.setOptions(menuOptions);
457
+
458
+ const choice = await globalConfirmMenu.navigate();
459
+
460
+ switch (choice) {
461
+ case 0: // Set password
462
+ const passwordResult = await promptForPasswordSetup();
463
+ if (passwordResult) {
464
+ return true; // Password set successfully
465
+ }
466
+ // If password setup failed, continue loop to show menu again
467
+ break;
468
+
469
+ case 1: // Skip setup
470
+ const skipResult = await confirmSkipPassword();
471
+ if (skipResult === true) {
472
+ return true; // Skip confirmed
473
+ } else if (skipResult === 'reconsider') {
474
+ continue; // Return to main setup menu
475
+ }
476
+ // If skip was canceled, continue loop
477
+ break;
478
+
479
+ case 2: // Back to main menu
480
+ case -1: // ESC pressed
481
+ default:
482
+ return false; // Exit setup
381
483
  }
382
-
383
- } catch (error) {
384
- console.log(colors.red + '[X] Setup failed: ' + error.message + colors.reset);
385
- process.exit(1);
386
484
  }
387
485
  }
388
486
 
389
- // Update configuration file with new value
390
- function updateConfigFile(key, value) {
391
- try {
392
- let configContent = '';
393
- let keyExists = false;
394
-
395
- // Read existing config if it exists
396
- if (fs.existsSync(CONFIG_FILE)) {
397
- configContent = fs.readFileSync(CONFIG_FILE, 'utf8');
398
- const lines = configContent.split('\n');
399
-
400
- // Update existing key or add new one
401
- for (let i = 0; i < lines.length; i++) {
402
- const match = lines[i].match(/^([^=]+)=(.*)$/);
403
- if (match && match[1] === key) {
404
- lines[i] = `${key}=${value}`;
405
- keyExists = true;
406
- break;
407
- }
408
- }
409
-
410
- if (keyExists) {
411
- configContent = lines.join('\n');
412
- } else {
413
- // Add new key at the end
414
- configContent += `\n${key}=${value}`;
415
- }
416
- } else {
417
- // Create new config file
418
- configContent = `# Claude Launcher Configuration\n# Generated automatically\n\n${key}=${value}\nKIMI_BASE_URL=https://api.moonshot.cn/anthropic/\n`;
419
- }
420
-
421
- fs.writeFileSync(CONFIG_FILE, configContent);
422
- } catch (error) {
423
- console.log(colors.red + 'Error updating config file: ' + error.message + colors.reset);
424
- throw error;
487
+ /**
488
+ * Prompt user to set password
489
+ */
490
+ async function promptForPasswordSetup() {
491
+ const result = await setupNewPassword(apiManager, true);
492
+ if (result) {
493
+ await waitForKey(i18n.tSync('messages.prompts.press_any_key'));
425
494
  }
495
+ return result;
426
496
  }
427
497
 
428
- // Get Kimi configuration with validation and setup
429
- async function getKimiConfig() {
430
- try {
431
- const config = loadConfig();
432
- const rawApiKey = config.KIMI_API_KEY;
433
- const baseUrl = config.KIMI_BASE_URL || 'https://api.moonshot.cn/anthropic/';
434
-
435
- console.log('');
436
- console.log(colors.bright + colors.orange + '[*] Validating Kimi API Configuration...' + colors.reset);
437
- console.log('');
438
-
439
- // Check if API key is configured and valid
440
- if (!rawApiKey || rawApiKey === 'Your_Double_Base64_Encoded_Api_Key' || rawApiKey === 'your_kimi_api_key_here') {
441
- console.log(colors.yellow + '[!] No API key configured. Starting first-time setup...' + colors.reset);
442
- console.log('');
443
-
444
- const apiKey = await promptForApiKey();
445
- return {
446
- ANTHROPIC_BASE_URL: baseUrl,
447
- ANTHROPIC_API_KEY: apiKey
448
- };
449
- }
450
-
451
- // Try to decrypt existing API key
452
- const decrypted = decryptApiKey(rawApiKey);
453
-
454
- if (!decrypted.success) {
455
- console.log(colors.red + '[X] Failed to decrypt stored API key: ' + decrypted.error + colors.reset);
456
- console.log(colors.yellow + '[!] This might happen if the key was encrypted on a different machine.' + colors.reset);
457
- console.log(colors.yellow + ' Please re-enter your API key...' + colors.reset);
458
- console.log('');
459
-
460
- const apiKey = await promptForApiKey();
461
- return {
462
- ANTHROPIC_BASE_URL: baseUrl,
463
- ANTHROPIC_API_KEY: apiKey
464
- };
465
- }
466
-
467
- // Validate decrypted API key
468
- const validation = validateApiKey(decrypted.value);
469
-
470
- if (!validation.valid) {
471
- console.log(colors.red + '[X] Stored API key is invalid: ' + validation.error + colors.reset);
472
- console.log('');
473
-
474
- const apiKey = await promptForApiKey();
475
- return {
476
- ANTHROPIC_BASE_URL: baseUrl,
477
- ANTHROPIC_API_KEY: apiKey
478
- };
498
+ /**
499
+ * Confirm skip password setup
500
+ */
501
+ async function confirmSkipPassword() {
502
+ console.log('');
503
+ console.log(colors.bright + colors.red + '⚠️ ' + i18n.tSync('errors.password.confirm_skip_title') + colors.reset);
504
+ console.log('');
505
+ console.log(colors.gray + i18n.tSync('ui.general.after_skipping_password_setup') + colors.reset);
506
+ console.log(colors.gray + '• ' + i18n.tSync('ui.general.password_skip_consequences')[0] + colors.reset);
507
+ console.log(colors.gray + '' + i18n.tSync('ui.general.password_skip_consequences')[1] + colors.reset);
508
+ console.log(colors.gray + '• ' + i18n.tSync('ui.general.password_skip_consequences')[2] + colors.reset);
509
+ console.log('');
510
+
511
+ // Ensure global menus are initialized
512
+ initializeGlobalMenus();
513
+
514
+ globalConfirmMenu.setOptions([
515
+ i18n.tSync('ui.general.confirm_skip_option'),
516
+ i18n.tSync('ui.general.reconsider_option')
517
+ ]);
518
+
519
+ const choice = await globalConfirmMenu.navigate();
520
+
521
+ if (choice === 0) {
522
+ try {
523
+ apiManager.skipPasswordSetup();
524
+ console.log(colors.yellow + i18n.tSync('errors.password.setup_skipped') + colors.reset);
525
+ await waitForKey(i18n.tSync('messages.prompts.press_any_key'));
526
+ return true;
527
+ } catch (error) {
528
+ forceStdinCleanup();
529
+ console.log(colors.red + `Operation failed: ${error.message}` + colors.reset);
530
+ await waitForKey(i18n.tSync('messages.prompts.press_any_key'));
531
+ return false;
479
532
  }
480
-
481
- // Success case
482
- console.log(colors.green + '[+] API Key validation successful!' + colors.reset);
483
- console.log(colors.gray + ` Decrypted key starts with: ${validation.value.substring(0, 8)}...` + colors.reset);
484
- console.log(colors.gray + ` Base URL: ${baseUrl}` + colors.reset);
485
- console.log('');
486
-
487
- return {
488
- ANTHROPIC_BASE_URL: baseUrl,
489
- ANTHROPIC_API_KEY: validation.value
490
- };
491
-
492
- } catch (error) {
493
- console.log(colors.red + '[X] Configuration error: ' + error.message + colors.reset);
494
- throw error;
495
533
  }
534
+
535
+ return 'reconsider'; // Return to password setup main menu
496
536
  }
497
537
 
498
- // Display Claude Code style header
499
- function displayHeader() {
538
+ /**
539
+ * Show API Management Menu
540
+ */
541
+ async function showApiManagementMenu() {
542
+ // Force cleanup stdin state before showing API management menu
543
+ forceStdinCleanup();
544
+
500
545
  console.clear();
501
546
  console.log('');
502
-
503
- // Claude-style orange/amber border with Unicode box drawing characters
504
- const border = colors.orange;
505
- const title = colors.white + colors.bright;
506
-
507
- console.log(border + ' ┌────────────────────────────────────────┐' + colors.reset);
508
- console.log(border + ' │' + title + ' Claude Code Launcher ' + border + '│' + colors.reset);
509
- console.log(border + ' └────────────────────────────────────────┘' + colors.reset);
510
- console.log('');
511
- console.log(colors.gray + ' Use ↑↓ arrow keys to navigate, Enter to select' + colors.reset);
547
+ console.log(colors.bright + colors.orange + '📋 ' + await i18n.t('menu.api_management.title') + colors.reset);
512
548
  console.log('');
513
- }
514
549
 
515
- // Menu options
516
- const menuOptions = [
517
- 'Launch Claude Code',
518
- 'Launch Claude Code (Skip Permissions)',
519
- 'Launch Claude Code with Kimi K2 API',
520
- 'Launch Claude Code with Kimi K2 API (Skip Permissions)',
521
- 'Exit'
522
- ];
523
-
524
- let selectedIndex = 0;
525
-
526
- // Display menu with current selection
527
- function displayMenu() {
528
- displayHeader();
529
-
530
- menuOptions.forEach((option, index) => {
531
- if (index === selectedIndex) {
532
- // Selected item with Claude-style highlighting
533
- console.log(colors.orange + ' → ' + colors.black + colors.bgAmber + option + colors.reset);
534
- } else {
535
- // Normal item
536
- console.log(colors.gray + ' ' + option + colors.reset);
550
+ // Check if this is first time usage and prompt for password setup
551
+ if (apiManager.isFirstTimeUsage()) {
552
+ const passwordChoice = await handleFirstTimePasswordSetup();
553
+ if (!passwordChoice) {
554
+ // User chose to skip or canceled, return to main menu
555
+ return showMenu();
537
556
  }
538
- });
539
-
540
- console.log('');
541
- }
557
+ }
542
558
 
543
- // Handle key press
544
- function handleKeyPress(key) {
545
- switch (key) {
546
- case '\u001b[A': // Up arrow
547
- selectedIndex = (selectedIndex - 1 + menuOptions.length) % menuOptions.length;
548
- displayMenu();
549
- break;
550
-
551
- case '\u001b[B': // Down arrow
552
- selectedIndex = (selectedIndex + 1) % menuOptions.length;
553
- displayMenu();
554
- break;
555
-
556
- case '\r': // Enter
557
- executeSelection();
558
- break;
559
-
560
- case '\u001b': // Escape
561
- case 'q':
562
- case 'Q':
563
- console.log('');
564
- console.log(colors.green + 'Goodbye!' + colors.reset);
565
- process.exit(0);
566
- break;
559
+ // Build menu options based on password setup status
560
+ const menuOptions = [
561
+ await i18n.t('menu.api_management.add_new'),
562
+ await i18n.t('menu.api_management.remove'),
563
+ await i18n.t('menu.api_management.switch'),
564
+ await i18n.t('menu.api_management.statistics')
565
+ ];
566
+
567
+ // Add import/export options only if password is set
568
+ if (apiManager.canUseImportExport()) {
569
+ menuOptions.push(await i18n.t('menu.api_management.export'));
570
+ menuOptions.push(await i18n.t('menu.api_management.import'));
571
+ menuOptions.push(await i18n.t('menu.api_management.change_password'));
567
572
  }
573
+
574
+ menuOptions.push(await i18n.t('menu.api_management.back'));
575
+
576
+ // Ensure global menus are initialized
577
+ initializeGlobalMenus();
578
+
579
+ globalApiManagementMenu.setOptions(menuOptions);
580
+
581
+ const choice = await globalApiManagementMenu.navigate();
582
+
583
+ // Handle menu choices based on current menu options
584
+ if (choice === 0) { // Add New API
585
+ await addNewThirdPartyApi();
586
+ return showMenu();
587
+ } else if (choice === 1) { // Remove API
588
+ await removeThirdPartyApi();
589
+ return showMenu();
590
+ } else if (choice === 2) { // Switch Active API
591
+ await switchThirdPartyApi();
592
+ return showMenu();
593
+ } else if (choice === 3) { // View API Statistics
594
+ await viewStatistics();
595
+ return showMenu();
596
+ } else if (apiManager.canUseImportExport()) {
597
+ // If import/export is available, handle those options
598
+ if (choice === 4) { // Export Configuration
599
+ await exportConfiguration();
600
+ return showMenu();
601
+ } else if (choice === 5) { // Import Configuration
602
+ await importConfiguration();
603
+ return showMenu();
604
+ } else if (choice === 6) { // Change Password
605
+ await changePassword();
606
+ return showMenu();
607
+ } else if (choice === 7) { // Back to Main Menu
608
+ return showMenu();
609
+ }
610
+ } else {
611
+ // If import/export is not available, only Back to Main Menu
612
+ if (choice === 4) { // Back to Main Menu
613
+ return showMenu();
614
+ }
615
+ }
616
+
617
+ // Default fallback to main menu
618
+ return showMenu();
568
619
  }
569
620
 
570
- // Handle Kimi API launches with async configuration
571
- async function handleKimiLaunch(command) {
621
+ /**
622
+ * Handle third-party API launch
623
+ */
624
+ async function handleThirdPartyApiLaunch(skipPermissions = false) {
572
625
  try {
573
- // Clean up existing listeners before API key input
574
- if (process.stdin.isTTY) {
575
- process.stdin.setRawMode(false);
576
- process.stdin.removeAllListeners('data');
577
- process.stdin.pause();
578
- }
579
-
580
- const kimiConfig = await getKimiConfig();
581
- if (kimiConfig) {
582
- launchClaude(command, kimiConfig, true); // true to disable auth tokens
626
+ const activeApi = apiManager.getActiveApi();
627
+
628
+ if (!activeApi) {
629
+ console.clear();
630
+ showInfo(i18n.tSync('launch.no_active_api'), [
631
+ i18n.tSync('launch.no_active_api_desc'),
632
+ i18n.tSync('launch.add_configure_first')
633
+ ]);
634
+
635
+ await waitForKey(i18n.tSync('launch.press_key_return'));
636
+ return showMenu();
583
637
  }
638
+
639
+ // Increment usage count for the active API since we're actually using it
640
+ apiManager.incrementActiveApiUsage();
641
+
642
+ launchClaudeWithApi(activeApi, skipPermissions);
643
+
584
644
  } catch (error) {
585
- console.log('');
586
- console.log(colors.red + '[X] Failed to configure Kimi API: ' + error.message + colors.reset);
587
- console.log(colors.gray + 'Returning to menu...' + colors.reset);
588
- console.log('');
589
-
590
- // Simple delay before returning to menu
645
+ showError('Failed to launch with third-party API', [error.message]);
646
+
591
647
  setTimeout(() => {
592
648
  showMenu();
593
649
  }, 2000);
594
650
  }
595
651
  }
596
652
 
597
- // Execute selected menu item
598
- function executeSelection() {
653
+ /**
654
+ * Execute selected menu item
655
+ */
656
+ async function executeSelection(selectedIndex) {
599
657
  switch (selectedIndex) {
600
658
  case 0: // Launch Claude Code
601
- launchClaude('claude');
659
+ launchClaudeDefault();
602
660
  break;
603
-
661
+
604
662
  case 1: // Launch Claude Code (Skip Permissions)
605
- launchClaude('claude --dangerously-skip-permissions');
663
+ launchClaudeSkipPermissions();
606
664
  break;
607
-
608
- case 2: // Launch Claude Code with Kimi K2 API
609
- handleKimiLaunch('claude');
665
+
666
+ case 2: // Launch Claude Code with 3rd-party API
667
+ await handleThirdPartyApiLaunch(false);
610
668
  break;
611
-
612
- case 3: // Launch Claude Code with Kimi K2 API (Skip Permissions)
613
- handleKimiLaunch('claude --dangerously-skip-permissions');
669
+
670
+ case 3: // Launch Claude Code with 3rd-party API (Skip Permissions)
671
+ await handleThirdPartyApiLaunch(true);
614
672
  break;
615
-
616
- case 4: // Exit
673
+
674
+ case 4: // 3rd-party API Management
675
+ return await showApiManagementMenu();
676
+
677
+ case 5: // Language Settings
678
+ return await showLanguageSettings();
679
+
680
+ case 6: // Version Update Check
681
+ return await showVersionUpdateCheck();
682
+
683
+ case 7: // Exit
617
684
  console.log('');
618
- console.log(colors.green + 'Goodbye!' + colors.reset);
685
+ console.log(colors.green + '👋 ' + await i18n.t('menu.main.exit') + '!' + colors.reset);
619
686
  process.exit(0);
620
687
  break;
688
+
689
+ default:
690
+ showMenu();
691
+ break;
621
692
  }
622
693
  }
623
694
 
624
- // Initialize menu
625
- function showMenu() {
626
- displayMenu();
627
-
628
- // Check if we're in a TTY environment
629
- if (process.stdin.isTTY) {
630
- // Set up raw mode for capturing arrow keys
631
- process.stdin.setRawMode(true);
632
- process.stdin.resume();
633
- process.stdin.setEncoding('utf8');
634
-
635
- process.stdin.on('data', (key) => {
636
- handleKeyPress(key);
637
- });
695
+ /**
696
+ * Show main menu
697
+ */
698
+ async function showMenu() {
699
+ // Force cleanup stdin state before showing main menu
700
+ forceStdinCleanup();
701
+
702
+ // Ensure global menus are initialized
703
+ initializeGlobalMenus();
704
+
705
+ // ========================================
706
+ // Version check configuration (hardcoded, modify as needed)
707
+ // ========================================
708
+ const ALWAYS_SHOW_VERSION = false; // Version display toggle: true=always show, false=only show updates
709
+ const VERSION_CHECK_INTERVAL_HOURS = 12; // Version check interval (hours): modify this to adjust check frequency
710
+
711
+ // Check for updates and prepare version info string
712
+ let versionInfo = null;
713
+ try {
714
+ const updateInfo = await checkForUpdates(false, VERSION_CHECK_INTERVAL_HOURS);
715
+
716
+ // Use hardcoded switch instead of config file settings
717
+ const shouldShowVersion = ALWAYS_SHOW_VERSION || updateInfo.available;
718
+
719
+ if (shouldShowVersion) {
720
+ if (updateInfo.available) {
721
+ // Display update notification (Claude Code style)
722
+ versionInfo = colors.yellow + ' ⚠️ ' + i18n.tSync('version.update_available', updateInfo.latestVersion, updateInfo.currentVersion) + colors.reset + '\n' +
723
+ colors.yellow + ' ' + i18n.tSync('version.install_command') + colors.reset;
724
+ } else if (ALWAYS_SHOW_VERSION) {
725
+ // Display current version info (always show mode)
726
+ versionInfo = colors.cyan + ' ℹ️ ' + i18n.tSync('version.current_version_info', updateInfo.currentVersion, updateInfo.latestVersion) + colors.reset + '\n' +
727
+ colors.yellow + ' ' + i18n.tSync('version.install_command') + colors.reset;
728
+ }
729
+ }
730
+ } catch (error) {
731
+ // Silently ignore update check errors
732
+ }
733
+
734
+ // Populate menu options dynamically with i18n translations
735
+ menuOptions = [
736
+ await i18n.t('menu.main.launch_default'),
737
+ await i18n.t('menu.main.launch_skip'),
738
+ await i18n.t('menu.main.launch_api'),
739
+ await i18n.t('menu.main.launch_api_skip'),
740
+ await i18n.t('menu.main.api_management'),
741
+ await i18n.t('menu.main.language_settings'),
742
+ await i18n.t('menu.main.version_check'),
743
+ await i18n.t('menu.main.exit')
744
+ ];
745
+
746
+ globalMainMenu.setOptions(menuOptions);
747
+ const selection = await globalMainMenu.navigate(false, versionInfo); // Pass version info to display between banner and nav
748
+
749
+ if (selection === -1) {
750
+ console.log('');
751
+ console.log(colors.green + '👋 ' + await i18n.t('menu.main.exit') + '!' + colors.reset);
752
+ process.exit(0);
638
753
  } else {
639
- // Fallback for non-TTY environments - use readline
640
- const rl = readline.createInterface({
641
- input: process.stdin,
642
- output: process.stdout
643
- });
644
-
645
- console.log(colors.yellow + ' Arrow keys not available. Enter selection number (1-5): ' + colors.reset);
646
-
647
- rl.on('line', (input) => {
648
- const choice = parseInt(input.trim());
649
- if (choice >= 1 && choice <= menuOptions.length) {
650
- selectedIndex = choice - 1;
651
- rl.close();
652
- executeSelection();
653
- } else if (input.toLowerCase() === 'q' || input.toLowerCase() === 'exit') {
654
- rl.close();
754
+ await executeSelection(selection);
755
+ }
756
+ }
757
+
758
+
759
+
760
+
761
+
762
+ /**
763
+ * Export configuration with password encryption
764
+ */
765
+ async function exportConfiguration() {
766
+ console.clear();
767
+ console.log('');
768
+ console.log(colors.bright + colors.orange + '💾 ' + await i18n.t('import_export.export.title') + colors.reset);
769
+ console.log('');
770
+
771
+ // Add export function description
772
+ console.log(colors.cyan + '📄 ' + i18n.tSync('import_export.export.description_title') + colors.reset);
773
+ console.log(colors.gray + ' • ' + i18n.tSync('import_export.export.description_items')[0] + colors.reset);
774
+ console.log(colors.gray + ' • ' + i18n.tSync('import_export.export.description_items')[1] + colors.reset);
775
+ console.log(colors.gray + ' • ' + i18n.tSync('import_export.export.description_items')[2] + colors.reset);
776
+ console.log(colors.gray + ' • ' + i18n.tSync('import_export.export.description_items')[3] + colors.reset);
777
+ console.log('');
778
+
779
+ // Verify password before export
780
+ const verified = await verifyExportPassword(apiManager, 'export');
781
+ if (!verified) {
782
+ console.log('');
783
+ await waitForKey(i18n.tSync('messages.prompts.press_any_key_menu'));
784
+ return;
785
+ }
786
+
787
+ try {
788
+ // Get export data
789
+ const exportData = apiManager.exportConfigAuthenticated();
790
+
791
+ // Prepare file path
792
+ const exportDir = getExportDirectory();
793
+ const filename = generateExportFilename();
794
+ const filePath = path.join(exportDir, filename);
795
+
796
+ // Write JSON file
797
+ fs.writeFileSync(filePath, exportData, 'utf8');
798
+
799
+ console.log(colors.green + '✓ ' + i18n.tSync('import_export.export.success_title') + colors.reset);
800
+ console.log('');
801
+ console.log(colors.cyan + '📁 ' + i18n.tSync('import_export.export.details_title') + colors.reset);
802
+ console.log(colors.gray + ` • ` + i18n.tSync('import_export.export.details_file_saved', filePath) + colors.reset);
803
+ console.log(colors.gray + ` • ` + i18n.tSync('import_export.export.details_export_dir', exportDir) + colors.reset);
804
+ console.log(colors.gray + ` • ` + i18n.tSync('import_export.export.details_filename', filename) + colors.reset);
805
+ console.log('');
806
+
807
+ // Open file with default application
808
+ console.log(colors.yellow + '🔍 ' + i18n.tSync('import_export.export.opening_file') + colors.reset);
809
+ openFileWithDefault(filePath);
810
+
811
+ console.log('');
812
+ console.log(colors.cyan + '💡 ' + i18n.tSync('import_export.export.tips_title') + colors.reset);
813
+ console.log(colors.gray + ' • ' + i18n.tSync('import_export.export.tips_items')[0] + colors.reset);
814
+ console.log(colors.gray + ' • ' + i18n.tSync('import_export.export.tips_items')[1] + colors.reset);
815
+
816
+ } catch (error) {
817
+ forceStdinCleanup();
818
+ console.log(colors.red + `❌ Export failed: ${error.message}` + colors.reset);
819
+ }
820
+
821
+ console.log('');
822
+ await waitForKey(i18n.tSync('messages.prompts.press_any_key_menu'));
823
+ }
824
+
825
+ /**
826
+ * Import configuration from plaintext JSON
827
+ */
828
+ async function importConfiguration() {
829
+ console.clear();
830
+ console.log('');
831
+ console.log(colors.bright + colors.orange + '📥 ' + await i18n.t('import_export.import.title') + colors.reset);
832
+ console.log('');
833
+
834
+ // Add import function description
835
+ console.log(colors.cyan + '📄 ' + i18n.tSync('ui.general.import_function_description') + colors.reset);
836
+ console.log(colors.gray + ' • ' + i18n.tSync('import_export.export.description_items')[0] + colors.reset);
837
+ console.log(colors.gray + ' • ' + i18n.tSync('ui.general.import_description_items')[0] + colors.reset);
838
+ console.log(colors.gray + ' • ' + i18n.tSync('ui.general.import_description_items')[1] + colors.reset);
839
+ console.log(colors.gray + ' • ' + i18n.tSync('ui.general.import_description_items')[2] + colors.reset);
840
+ console.log('');
841
+
842
+ // Verify password identity
843
+ const passwordVerified = await verifyExportPassword(apiManager, 'import');
844
+ if (!passwordVerified) {
845
+ await waitForKey(i18n.tSync('messages.prompts.press_any_key_menu'));
846
+ return;
847
+ }
848
+
849
+ console.log('');
850
+ console.log(colors.cyan + '📁 ' + i18n.tSync('ui.general.file_input_required') + colors.reset);
851
+ console.log(colors.gray + ' • ' + i18n.tSync('ui.general.file_input_items')[0] + colors.reset);
852
+ console.log(colors.gray + ' • ' + i18n.tSync('ui.general.file_input_items')[1] + colors.reset);
853
+ console.log(colors.gray + ' • ' + i18n.tSync('ui.general.file_input_items')[2] + colors.reset);
854
+ console.log('');
855
+
856
+ const { simpleInput } = require('./lib/ui/prompts');
857
+
858
+ let attempts = 0;
859
+ const maxAttempts = 3;
860
+
861
+ while (attempts < maxAttempts) {
862
+ attempts++;
863
+
864
+ // Get file path from user
865
+ const filePrompt = i18n.tSync('ui.general.enter_json_file_path_attempt', attempts, maxAttempts);
866
+ const filePath = await simpleInput(colors.green + filePrompt + colors.reset);
867
+
868
+ if (!filePath) {
869
+ console.log(colors.red + i18n.tSync('ui.general.file_path_empty') + colors.reset);
870
+ if (attempts < maxAttempts) {
655
871
  console.log('');
656
- console.log(colors.green + 'Goodbye!' + colors.reset);
657
- process.exit(0);
872
+ continue;
658
873
  } else {
659
- console.log(colors.red + ' Invalid selection. Please enter 1-5.' + colors.reset);
874
+ console.log(colors.red + i18n.tSync('ui.general.max_attempts_import_cancelled') + colors.reset);
875
+ await waitForKey(i18n.tSync('messages.prompts.press_any_key_menu'));
876
+ return;
660
877
  }
661
- });
878
+ }
879
+
880
+ // Validate file
881
+ console.log('');
882
+ console.log(colors.yellow + i18n.tSync('ui.general.validating_file') + colors.reset);
883
+ const validation = validateImportFile(filePath);
884
+
885
+ if (!validation.valid) {
886
+ console.log(colors.red + '❌ ' + i18n.tSync('ui.general.file_validation_failed', validation.error) + colors.reset);
887
+ if (attempts < maxAttempts) {
888
+ console.log(colors.yellow + i18n.tSync('ui.general.check_file_path_json') + colors.reset);
889
+ console.log('');
890
+ continue;
891
+ } else {
892
+ console.log(colors.red + i18n.tSync('ui.general.max_attempts_import_cancelled') + colors.reset);
893
+ await waitForKey(i18n.tSync('messages.prompts.press_any_key_menu'));
894
+ return;
895
+ }
896
+ }
897
+
898
+ // File is valid, proceed with import
899
+ console.log(colors.green + i18n.tSync('ui.general.file_validation_successful') + colors.reset);
900
+ console.log('');
901
+
902
+ try {
903
+ // Import the validated configuration data
904
+ const result = apiManager.importConfigAuthenticated(validation.data);
905
+
906
+ console.log(colors.green + i18n.tSync('ui.general.import_successful') + colors.reset);
907
+ console.log('');
908
+ console.log(colors.cyan + i18n.tSync('ui.general.import_statistics') + colors.reset);
909
+ const importItems = i18n.tSync('ui.general.import_stats_items');
910
+ console.log(colors.gray + ` • ` + importItems[0].replace('{0}', result.imported) + colors.reset);
911
+ console.log(colors.gray + ` • ` + importItems[1].replace('{1}', result.skipped) + colors.reset);
912
+ console.log(colors.gray + ` • ` + importItems[2] + colors.reset);
913
+ console.log(colors.gray + ` • ` + importItems[3].replace('{0}', path.resolve(filePath)) + colors.reset);
914
+
915
+ break; // Success, exit the loop
916
+
917
+ } catch (error) {
918
+ forceStdinCleanup();
919
+ console.log(colors.red + `❌ Import failed: ${error.message}` + colors.reset);
920
+ if (attempts < maxAttempts) {
921
+ console.log(colors.yellow + i18n.tSync('ui.general.import_tips')[0] + colors.reset);
922
+ console.log('');
923
+ continue;
924
+ } else {
925
+ console.log(colors.red + i18n.tSync('ui.general.max_attempts_import_failed') + colors.reset);
926
+ }
927
+ }
662
928
  }
929
+
930
+ console.log('');
931
+ await waitForKey(i18n.tSync('messages.prompts.press_any_key_menu'));
663
932
  }
664
933
 
665
- // Handle process termination
666
- process.on('SIGINT', () => {
934
+ /**
935
+ * Change password
936
+ */
937
+ async function changePassword() {
938
+ console.clear();
667
939
  console.log('');
668
- console.log(colors.green + 'Goodbye!' + colors.reset);
669
- process.exit(0);
670
- });
940
+ console.log(colors.bright + colors.orange + '🔑 ' + i18n.tSync('errors.password.change_password_title') + colors.reset);
941
+ console.log('');
942
+
943
+ // Use unified password change module
944
+ const success = await changePasswordModule(apiManager);
945
+
946
+ if (success) {
947
+ console.log('');
948
+ }
949
+
950
+ await waitForKey(i18n.tSync('messages.prompts.press_any_key_menu'));
951
+ }
952
+
953
+ /**
954
+ * Show language settings menu
955
+ */
956
+ async function showLanguageSettings() {
957
+ try {
958
+ const supportedLanguages = i18n.getSupportedLanguages();
959
+ const currentLanguage = i18n.getCurrentLanguage();
960
+ const currentLanguageName = i18n.getCurrentLanguageName();
961
+
962
+ console.clear();
963
+ console.log('');
964
+ console.log(colors.bright + colors.orange + '🌍 ' + await i18n.t('menu.language.title') + colors.reset);
965
+ console.log('');
966
+
967
+ // Show current language
968
+ console.log(colors.cyan + await i18n.t('menu.language.current', currentLanguageName) + colors.reset);
969
+ console.log('');
970
+ console.log(colors.yellow + await i18n.t('menu.language.select_prompt') + colors.reset);
971
+ console.log('');
972
+
973
+ // Create menu options
974
+ const languageOptions = [];
975
+ const languageCodes = Object.keys(supportedLanguages);
976
+
977
+ languageCodes.forEach(langCode => {
978
+ const isActive = langCode === currentLanguage;
979
+ const displayName = supportedLanguages[langCode];
980
+ languageOptions.push(isActive ? `● ${displayName}` : ` ${displayName}`);
981
+ });
982
+
983
+ languageOptions.push(await i18n.t('menu.language.back'));
984
+
985
+ // Ensure global menus are initialized
986
+ initializeGlobalMenus();
987
+
988
+ globalApiManagementMenu.setOptions(languageOptions);
989
+ const choice = await globalApiManagementMenu.navigate();
671
990
 
991
+ if (choice === -1 || choice === languageOptions.length - 1) {
992
+ // Back to main menu
993
+ return showMenu();
994
+ }
995
+
996
+ // Check if user selected current language
997
+ const selectedLangCode = languageCodes[choice];
998
+ if (selectedLangCode === currentLanguage) {
999
+ // Already current language, return to main menu
1000
+ return showMenu();
1001
+ }
1002
+
1003
+ // Switch language
1004
+ console.clear();
1005
+ console.log('');
1006
+ console.log(colors.yellow + await i18n.t('status.switching_language') + colors.reset);
1007
+
1008
+ try {
1009
+ await i18n.setLanguage(selectedLangCode);
1010
+
1011
+ console.clear();
1012
+ const newLanguageName = i18n.getCurrentLanguageName();
1013
+ showSuccess(await i18n.t('messages.success.language_changed'), [
1014
+ await i18n.t('menu.language.changed_success', newLanguageName)
1015
+ ]);
1016
+
1017
+ await waitForKey(await i18n.t('messages.prompts.press_any_key'));
1018
+
1019
+ // Return to main menu after successful language change
1020
+ return showMenu();
1021
+
1022
+ } catch (error) {
1023
+ showError(await i18n.t('errors.general.operation_failed', error.message));
1024
+ await waitForKey(await i18n.t('messages.prompts.press_any_key'));
1025
+ return showMenu();
1026
+ }
1027
+
1028
+ } catch (error) {
1029
+ showError('Failed to show language settings', [error.message]);
1030
+ await waitForKey(i18n.tSync('messages.prompts.press_any_key_menu'));
1031
+ return showMenu();
1032
+ }
1033
+ }
1034
+
1035
+ /**
1036
+ * Show version update check screen
1037
+ */
1038
+ async function showVersionUpdateCheck() {
1039
+ try {
1040
+ console.clear();
1041
+ console.log('');
1042
+ console.log(colors.bright + colors.orange + '🔄 ' + await i18n.t('version_check.title') + colors.reset);
1043
+ console.log('');
1044
+
1045
+ console.log(colors.cyan + await i18n.t('version_check.checking') + colors.reset);
1046
+ console.log(colors.gray + await i18n.t('version_check.please_wait') + colors.reset);
1047
+ console.log('');
1048
+
1049
+ // Show progress indicator
1050
+ const progressInterval = setInterval(() => {
1051
+ process.stdout.write('.');
1052
+ }, 500);
1053
+
1054
+ try {
1055
+ // Force check with 15 second timeout
1056
+ const result = await forceCheckForUpdates(15000);
1057
+
1058
+ // Stop progress indicator
1059
+ clearInterval(progressInterval);
1060
+ console.log('\n');
1061
+
1062
+ if (result.error) {
1063
+ // Handle errors (timeout, network, etc.)
1064
+ console.log(colors.red + '❌ ' + await i18n.t('version_check.error') + colors.reset);
1065
+ console.log(colors.red + ' ' + result.error + colors.reset);
1066
+ console.log('');
1067
+ console.log(colors.gray + await i18n.t('version_check.error_tips') + colors.reset);
1068
+ } else if (result.available) {
1069
+ // Update available
1070
+ console.log(colors.yellow + '🎉 ' + await i18n.t('version_check.update_available') + colors.reset);
1071
+ console.log('');
1072
+ console.log(colors.cyan + ' ' + await i18n.t('version_check.current_version', result.currentVersion) + colors.reset);
1073
+ console.log(colors.green + ' ' + await i18n.t('version_check.latest_version', result.latestVersion) + colors.reset);
1074
+ console.log('');
1075
+ console.log(colors.yellow + '💡 ' + await i18n.t('version_check.update_command') + colors.reset);
1076
+ console.log(colors.yellow + ' npm update -g @kikkimo/claude-launcher' + colors.reset);
1077
+ } else {
1078
+ // Already up to date
1079
+ console.log(colors.green + '✅ ' + await i18n.t('version_check.up_to_date') + colors.reset);
1080
+ console.log('');
1081
+ console.log(colors.cyan + ' ' + await i18n.t('version_check.current_version', result.currentVersion) + colors.reset);
1082
+ console.log(colors.cyan + ' ' + await i18n.t('version_check.latest_version', result.latestVersion) + colors.reset);
1083
+ }
1084
+
1085
+ } catch (error) {
1086
+ // Stop progress indicator
1087
+ clearInterval(progressInterval);
1088
+ console.log('\n');
1089
+
1090
+ console.log(colors.red + '❌ ' + await i18n.t('version_check.unexpected_error') + colors.reset);
1091
+ console.log(colors.red + ' ' + error.message + colors.reset);
1092
+ }
1093
+
1094
+ console.log('');
1095
+ await waitForKey(await i18n.t('messages.prompts.press_any_key'));
1096
+ return showMenu();
1097
+
1098
+ } catch (error) {
1099
+ console.log(colors.red + '❌ Failed to check version: ' + error.message + colors.reset);
1100
+ await waitForKey(i18n.tSync('messages.prompts.press_any_key_menu'));
1101
+ return showMenu();
1102
+ }
1103
+ }
1104
+
1105
+ // End of API Management functions
1106
+
1107
+ /**
1108
+ * Graceful shutdown handlers
1109
+ */
672
1110
  process.on('SIGTERM', () => {
673
1111
  console.log('');
674
- console.log(colors.green + 'Goodbye!' + colors.reset);
1112
+ console.log(colors.green + i18n.tSync('ui.general.goodbye') + colors.reset);
675
1113
  process.exit(0);
676
1114
  });
677
1115
 
1116
+ // Initialize global menus and start the application
1117
+ initializeGlobalMenus();
1118
+
678
1119
  // Start the application
679
- showMenu();
1120
+ showMenu();