easy-cc-api-switch 1.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/LICENSE +21 -0
- package/README.md +146 -0
- package/health.js +294 -0
- package/index.js +646 -0
- package/notify.js +516 -0
- package/package.json +39 -0
package/index.js
ADDED
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Claude配置切换工具
|
|
5
|
+
* 用于在不同的Claude API配置之间进行切换
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { program } = require('commander');
|
|
11
|
+
const chalk = require('chalk');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const readline = require('readline');
|
|
14
|
+
const inquirer = require('inquirer');
|
|
15
|
+
const { spawn } = require('child_process');
|
|
16
|
+
const notify = require('./notify');
|
|
17
|
+
const health = require('./health');
|
|
18
|
+
|
|
19
|
+
// 版本号
|
|
20
|
+
const VERSION = '1.7.0';
|
|
21
|
+
|
|
22
|
+
// 配置文件路径
|
|
23
|
+
const CONFIG_DIR = path.join(os.homedir(), '.claude');
|
|
24
|
+
const API_CONFIGS_FILE = path.join(CONFIG_DIR, 'apiConfigs.json');
|
|
25
|
+
const SETTINGS_FILE = path.join(CONFIG_DIR, 'settings.json');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 创建readline接口
|
|
29
|
+
* @returns {readline.Interface} readline接口
|
|
30
|
+
*/
|
|
31
|
+
function createReadlineInterface() {
|
|
32
|
+
return readline.createInterface({
|
|
33
|
+
input: process.stdin,
|
|
34
|
+
output: process.stdout
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 确保配置目录存在
|
|
40
|
+
*/
|
|
41
|
+
function ensureConfigDir() {
|
|
42
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
43
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
44
|
+
console.log(chalk.green(`创建配置目录: ${CONFIG_DIR}`));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 读取API配置文件
|
|
50
|
+
* @returns {Array} API配置数组
|
|
51
|
+
*/
|
|
52
|
+
function readApiConfigs() {
|
|
53
|
+
try {
|
|
54
|
+
if (!fs.existsSync(API_CONFIGS_FILE)) {
|
|
55
|
+
console.log(chalk.yellow(`警告: API配置文件不存在 (${API_CONFIGS_FILE})`));
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const data = fs.readFileSync(API_CONFIGS_FILE, 'utf8');
|
|
60
|
+
return JSON.parse(data);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(chalk.red(`读取API配置文件失败: ${error.message}`));
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 读取settings.json文件
|
|
69
|
+
* @returns {Object} 设置对象
|
|
70
|
+
*/
|
|
71
|
+
function readSettings() {
|
|
72
|
+
try {
|
|
73
|
+
if (!fs.existsSync(SETTINGS_FILE)) {
|
|
74
|
+
return { env: {} };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const data = fs.readFileSync(SETTINGS_FILE, 'utf8');
|
|
78
|
+
return JSON.parse(data);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error(chalk.red(`读取设置文件失败: ${error.message}`));
|
|
81
|
+
return { env: {} };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 保存settings.json文件
|
|
87
|
+
* @param {Object} settings 设置对象
|
|
88
|
+
*/
|
|
89
|
+
function saveSettings(settings) {
|
|
90
|
+
try {
|
|
91
|
+
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf8');
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error(chalk.red(`保存设置文件失败: ${error.message}`));
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 保存配置时保持现有的hooks和其他设置
|
|
100
|
+
* @param {Object} newConfig 新的配置对象
|
|
101
|
+
*/
|
|
102
|
+
function saveSettingsPreservingHooks(newConfig) {
|
|
103
|
+
try {
|
|
104
|
+
// 读取当前设置
|
|
105
|
+
const currentSettings = readSettings();
|
|
106
|
+
|
|
107
|
+
// 合并配置,只有当前设置中存在hooks时才保持
|
|
108
|
+
const mergedSettings = {
|
|
109
|
+
...newConfig
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// 如果当前设置中有hooks,则保持
|
|
113
|
+
if (currentSettings.hooks) {
|
|
114
|
+
mergedSettings.hooks = currentSettings.hooks;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 如果当前设置中有statusLine,则保持
|
|
118
|
+
if (currentSettings.statusLine) {
|
|
119
|
+
mergedSettings.statusLine = currentSettings.statusLine;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(mergedSettings, null, 2), 'utf8');
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error(chalk.red(`保存设置文件失败: ${error.message}`));
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 深度比较两个对象是否相等
|
|
131
|
+
* @param {Object} obj1
|
|
132
|
+
* @param {Object} obj2
|
|
133
|
+
* @returns {boolean}
|
|
134
|
+
*/
|
|
135
|
+
function deepEqual(obj1, obj2) {
|
|
136
|
+
if (obj1 === obj2) return true;
|
|
137
|
+
|
|
138
|
+
if (obj1 == null || obj2 == null) return false;
|
|
139
|
+
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return obj1 === obj2;
|
|
140
|
+
|
|
141
|
+
const keys1 = Object.keys(obj1);
|
|
142
|
+
const keys2 = Object.keys(obj2);
|
|
143
|
+
|
|
144
|
+
if (keys1.length !== keys2.length) return false;
|
|
145
|
+
|
|
146
|
+
for (let key of keys1) {
|
|
147
|
+
if (!keys2.includes(key)) return false;
|
|
148
|
+
if (!deepEqual(obj1[key], obj2[key])) return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 获取当前激活的API配置
|
|
156
|
+
* @returns {Object|null} 当前激活的配置对象或null(如果没有找到)
|
|
157
|
+
*/
|
|
158
|
+
function getCurrentConfig() {
|
|
159
|
+
const settings = readSettings();
|
|
160
|
+
|
|
161
|
+
// 如果settings为空,返回null
|
|
162
|
+
if (!settings || Object.keys(settings).length === 0) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 查找匹配的配置,只比较核心字段
|
|
167
|
+
const apiConfigs = readApiConfigs();
|
|
168
|
+
return apiConfigs.find(config => {
|
|
169
|
+
if (!config.config) return false;
|
|
170
|
+
|
|
171
|
+
// 主要比较 env 字段中的关键配置
|
|
172
|
+
const currentEnv = settings.env || {};
|
|
173
|
+
const configEnv = config.config.env || {};
|
|
174
|
+
|
|
175
|
+
return currentEnv.ANTHROPIC_BASE_URL === configEnv.ANTHROPIC_BASE_URL &&
|
|
176
|
+
currentEnv.ANTHROPIC_AUTH_TOKEN === configEnv.ANTHROPIC_AUTH_TOKEN;
|
|
177
|
+
}) || null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 列出所有可用的API配置并提示用户选择(同时支持交互式菜单和序号输入)
|
|
182
|
+
*/
|
|
183
|
+
function listAndSelectConfig() {
|
|
184
|
+
const apiConfigs = readApiConfigs();
|
|
185
|
+
|
|
186
|
+
if (apiConfigs.length === 0) {
|
|
187
|
+
console.log(chalk.yellow('没有找到可用的API配置'));
|
|
188
|
+
process.exit(0);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 获取当前激活的配置
|
|
192
|
+
const currentConfig = getCurrentConfig();
|
|
193
|
+
|
|
194
|
+
// 如果有当前激活的配置,显示它
|
|
195
|
+
if (currentConfig) {
|
|
196
|
+
console.log(chalk.green('当前激活的配置: ') + chalk.white(currentConfig.name));
|
|
197
|
+
console.log();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 找出最长的名称长度,用于对齐
|
|
201
|
+
const maxNameLength = apiConfigs.reduce((max, config) =>
|
|
202
|
+
Math.max(max, config.name.length), 0);
|
|
203
|
+
|
|
204
|
+
// 准备选项列表
|
|
205
|
+
const choices = apiConfigs.map((config, index) => {
|
|
206
|
+
// 如果是当前激活的配置,添加标记
|
|
207
|
+
const isActive = currentConfig && config.name === currentConfig.name;
|
|
208
|
+
|
|
209
|
+
// 格式化配置信息:[name] key url,name对齐,密钥不格式化
|
|
210
|
+
const paddedName = config.name.padEnd(maxNameLength, ' ');
|
|
211
|
+
const configInfo = `[${paddedName}] ${config.config.env.ANTHROPIC_AUTH_TOKEN} ${config.config.env.ANTHROPIC_BASE_URL}`;
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
name: `${index + 1}. ${configInfo}${isActive ? chalk.green(' (当前)') : ''}`,
|
|
215
|
+
value: index
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// 添加一个输入选项
|
|
220
|
+
choices.push(new inquirer.Separator());
|
|
221
|
+
choices.push({
|
|
222
|
+
name: '输入序号...',
|
|
223
|
+
value: 'input',
|
|
224
|
+
disabled: ' ' // 让输入序号选项不可选中
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// 使用inquirer创建交互式菜单
|
|
228
|
+
inquirer
|
|
229
|
+
.prompt([
|
|
230
|
+
{
|
|
231
|
+
type: 'list',
|
|
232
|
+
name: 'configIndex',
|
|
233
|
+
message: '请选择要切换的配置:',
|
|
234
|
+
choices: choices,
|
|
235
|
+
pageSize: choices.length, // 显示所有选项,确保"输入序号..."始终在底部
|
|
236
|
+
// 设置更宽的显示宽度以支持长配置信息
|
|
237
|
+
prefix: '',
|
|
238
|
+
suffix: '',
|
|
239
|
+
}
|
|
240
|
+
])
|
|
241
|
+
.then(answers => {
|
|
242
|
+
// 如果用户选择了"输入序号"选项
|
|
243
|
+
if (answers.configIndex === 'input') {
|
|
244
|
+
// 显示配置列表以供参考
|
|
245
|
+
console.log(chalk.cyan('\n可用的API配置:'));
|
|
246
|
+
apiConfigs.forEach((config, index) => {
|
|
247
|
+
const isActive = currentConfig && config.name === currentConfig.name;
|
|
248
|
+
const activeMarker = isActive ? chalk.green(' (当前)') : '';
|
|
249
|
+
const paddedName = config.name.padEnd(maxNameLength, ' ');
|
|
250
|
+
const configInfo = `[${paddedName}] ${config.config.env.ANTHROPIC_AUTH_TOKEN} ${config.config.env.ANTHROPIC_BASE_URL}`;
|
|
251
|
+
console.log(chalk.white(` ${index + 1}. ${configInfo}${activeMarker}`));
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const rl = createReadlineInterface();
|
|
255
|
+
|
|
256
|
+
rl.question(chalk.cyan('\n请输入配置序号 (1-' + apiConfigs.length + '): '), (indexAnswer) => {
|
|
257
|
+
const index = parseInt(indexAnswer, 10);
|
|
258
|
+
|
|
259
|
+
if (isNaN(index) || index < 1 || index > apiConfigs.length) {
|
|
260
|
+
console.error(chalk.red(`无效的序号: ${indexAnswer},有效范围: 1-${apiConfigs.length}`));
|
|
261
|
+
rl.close();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const selectedConfig = apiConfigs[index - 1];
|
|
266
|
+
|
|
267
|
+
// 如果选择的配置就是当前激活的配置,提示用户
|
|
268
|
+
if (currentConfig && selectedConfig.name === currentConfig.name) {
|
|
269
|
+
console.log(chalk.yellow(`\n配置 "${selectedConfig.name}" 已经是当前激活的配置`));
|
|
270
|
+
rl.close();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
processSelectedConfig(selectedConfig);
|
|
275
|
+
rl.close();
|
|
276
|
+
});
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// 用户通过交互式菜单选择了配置
|
|
281
|
+
const selectedIndex = answers.configIndex;
|
|
282
|
+
const selectedConfig = apiConfigs[selectedIndex];
|
|
283
|
+
|
|
284
|
+
// 如果选择的配置就是当前激活的配置,提示用户
|
|
285
|
+
if (currentConfig && selectedConfig.name === currentConfig.name) {
|
|
286
|
+
console.log(chalk.yellow(`\n配置 "${selectedConfig.name}" 已经是当前激活的配置`));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
processSelectedConfig(selectedConfig);
|
|
291
|
+
})
|
|
292
|
+
.catch(error => {
|
|
293
|
+
console.error(chalk.red(`发生错误: ${error.message}`));
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 处理用户选择的配置
|
|
299
|
+
* @param {Object} selectedConfig 选择的配置对象
|
|
300
|
+
*/
|
|
301
|
+
function processSelectedConfig(selectedConfig) {
|
|
302
|
+
console.log(chalk.cyan('\n当前选择的配置:'));
|
|
303
|
+
console.log(JSON.stringify(selectedConfig, null, 2));
|
|
304
|
+
|
|
305
|
+
inquirer
|
|
306
|
+
.prompt([
|
|
307
|
+
{
|
|
308
|
+
type: 'confirm',
|
|
309
|
+
name: 'confirm',
|
|
310
|
+
message: '确认切换到此配置?',
|
|
311
|
+
default: true // 修改默认值为true,按Enter键表示确认
|
|
312
|
+
}
|
|
313
|
+
])
|
|
314
|
+
.then(confirmAnswer => {
|
|
315
|
+
if (confirmAnswer.confirm) {
|
|
316
|
+
// 保存配置时保持现有的hooks设置
|
|
317
|
+
saveSettingsPreservingHooks(selectedConfig.config);
|
|
318
|
+
|
|
319
|
+
console.log(chalk.green(`\n成功切换到配置: ${selectedConfig.name}`));
|
|
320
|
+
|
|
321
|
+
// 显示当前配置信息
|
|
322
|
+
console.log(chalk.cyan('\n当前激活配置详情:'));
|
|
323
|
+
const { name, config } = selectedConfig;
|
|
324
|
+
console.log(chalk.white(`名称: ${name}`));
|
|
325
|
+
console.log(chalk.white(`API Key: ${config.env.ANTHROPIC_AUTH_TOKEN}`));
|
|
326
|
+
console.log(chalk.white(`Base URL: ${config.env.ANTHROPIC_BASE_URL}`));
|
|
327
|
+
console.log(chalk.white(`Model: ${config.model || 'default'}`));
|
|
328
|
+
|
|
329
|
+
// 询问是否要在当前目录运行 Claude
|
|
330
|
+
inquirer
|
|
331
|
+
.prompt([
|
|
332
|
+
{
|
|
333
|
+
type: 'confirm',
|
|
334
|
+
name: 'runClaude',
|
|
335
|
+
message: '是否要在当前目录运行 claude?',
|
|
336
|
+
default: true
|
|
337
|
+
}
|
|
338
|
+
])
|
|
339
|
+
.then(runAnswer => {
|
|
340
|
+
if (runAnswer.runClaude) {
|
|
341
|
+
console.log(chalk.green('\n正在启动 Claude...'));
|
|
342
|
+
|
|
343
|
+
// 启动 Claude
|
|
344
|
+
const claudeProcess = spawn('claude', [], {
|
|
345
|
+
stdio: 'inherit',
|
|
346
|
+
cwd: process.cwd()
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
claudeProcess.on('error', (error) => {
|
|
350
|
+
console.error(chalk.red(`启动 Claude 失败: ${error.message}`));
|
|
351
|
+
console.log(chalk.yellow('请确保 Claude CLI 已正确安装'));
|
|
352
|
+
});
|
|
353
|
+
} else {
|
|
354
|
+
console.log(chalk.yellow('您可以稍后手动运行 claude 命令'));
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
.catch(error => {
|
|
358
|
+
console.error(chalk.red(`发生错误: ${error.message}`));
|
|
359
|
+
});
|
|
360
|
+
} else {
|
|
361
|
+
console.log(chalk.yellow('\n操作已取消'));
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* 列出所有可用的API配置
|
|
368
|
+
*/
|
|
369
|
+
function listConfigs() {
|
|
370
|
+
const apiConfigs = readApiConfigs();
|
|
371
|
+
|
|
372
|
+
if (apiConfigs.length === 0) {
|
|
373
|
+
console.log(chalk.yellow('没有找到可用的API配置'));
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
console.log(chalk.cyan('可用的API配置:'));
|
|
378
|
+
apiConfigs.forEach((config, index) => {
|
|
379
|
+
console.log(chalk.white(` ${index + 1}. ${config.name}`));
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* 设置当前使用的API配置(使用交互式确认)
|
|
385
|
+
* @param {number} index 配置索引
|
|
386
|
+
*/
|
|
387
|
+
function setConfig(index) {
|
|
388
|
+
const apiConfigs = readApiConfigs();
|
|
389
|
+
|
|
390
|
+
if (apiConfigs.length === 0) {
|
|
391
|
+
console.log(chalk.yellow('没有找到可用的API配置'));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// 检查索引是否有效
|
|
396
|
+
if (index < 1 || index > apiConfigs.length) {
|
|
397
|
+
console.error(chalk.red(`无效的索引: ${index},有效范围: 1-${apiConfigs.length}`));
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const selectedConfig = apiConfigs[index - 1];
|
|
402
|
+
|
|
403
|
+
// 显示当前选择的配置
|
|
404
|
+
console.log(chalk.cyan('当前选择的配置:'));
|
|
405
|
+
console.log(JSON.stringify(selectedConfig, null, 2));
|
|
406
|
+
|
|
407
|
+
// 使用inquirer进行确认
|
|
408
|
+
inquirer
|
|
409
|
+
.prompt([
|
|
410
|
+
{
|
|
411
|
+
type: 'confirm',
|
|
412
|
+
name: 'confirm',
|
|
413
|
+
message: '确认切换到此配置?',
|
|
414
|
+
default: true // 修改默认值为true,按Enter键表示确认
|
|
415
|
+
}
|
|
416
|
+
])
|
|
417
|
+
.then(answers => {
|
|
418
|
+
if (answers.confirm) {
|
|
419
|
+
// 直接使用选择的配置替换整个settings.json
|
|
420
|
+
saveSettingsPreservingHooks(selectedConfig.config);
|
|
421
|
+
|
|
422
|
+
console.log(chalk.green(`\n成功切换到配置: ${selectedConfig.name}`));
|
|
423
|
+
} else {
|
|
424
|
+
console.log(chalk.yellow('\n操作已取消'));
|
|
425
|
+
}
|
|
426
|
+
})
|
|
427
|
+
.catch(error => {
|
|
428
|
+
console.error(chalk.red(`发生错误: ${error.message}`));
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* 获取API配置文件的示例内容
|
|
434
|
+
*/
|
|
435
|
+
function getApiConfigTemplate() {
|
|
436
|
+
return [
|
|
437
|
+
{
|
|
438
|
+
"name": "example-config",
|
|
439
|
+
"config": {
|
|
440
|
+
"env": {
|
|
441
|
+
"ANTHROPIC_AUTH_TOKEN": "sk-YOUR_API_KEY_HERE",
|
|
442
|
+
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
|
|
443
|
+
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
|
|
444
|
+
},
|
|
445
|
+
"permissions": {
|
|
446
|
+
"allow": [],
|
|
447
|
+
"deny": []
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
];
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* 获取设置文件的示例内容
|
|
456
|
+
*/
|
|
457
|
+
function getSettingsTemplate() {
|
|
458
|
+
return {
|
|
459
|
+
"env": {
|
|
460
|
+
"ANTHROPIC_AUTH_TOKEN": "sk-YOUR_API_KEY_HERE",
|
|
461
|
+
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
|
|
462
|
+
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
|
|
463
|
+
},
|
|
464
|
+
"permissions": {
|
|
465
|
+
"allow": [],
|
|
466
|
+
"deny": []
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* 打开指定的配置文件
|
|
473
|
+
* @param {string} filePath 文件路径
|
|
474
|
+
*/
|
|
475
|
+
function openConfigFile(filePath) {
|
|
476
|
+
const fullPath = path.resolve(filePath);
|
|
477
|
+
|
|
478
|
+
if (!fs.existsSync(fullPath)) {
|
|
479
|
+
// 确保配置目录存在
|
|
480
|
+
ensureConfigDir();
|
|
481
|
+
|
|
482
|
+
// 创建示例配置文件
|
|
483
|
+
let templateContent;
|
|
484
|
+
if (fullPath === API_CONFIGS_FILE) {
|
|
485
|
+
templateContent = JSON.stringify(getApiConfigTemplate(), null, 2);
|
|
486
|
+
console.log(chalk.green(`创建API配置文件: ${fullPath}`));
|
|
487
|
+
} else if (fullPath === SETTINGS_FILE) {
|
|
488
|
+
templateContent = JSON.stringify(getSettingsTemplate(), null, 2);
|
|
489
|
+
console.log(chalk.green(`创建设置配置文件: ${fullPath}`));
|
|
490
|
+
} else {
|
|
491
|
+
console.log(chalk.yellow(`配置文件不存在: ${fullPath}`));
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
fs.writeFileSync(fullPath, templateContent, 'utf8');
|
|
497
|
+
console.log(chalk.green(`已创建示例配置文件,请根据需要修改配置内容`));
|
|
498
|
+
} catch (error) {
|
|
499
|
+
console.error(chalk.red(`创建配置文件失败: ${error.message}`));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
console.log(chalk.cyan(`正在打开: ${fullPath}`));
|
|
505
|
+
|
|
506
|
+
// 使用spawn执行open命令
|
|
507
|
+
const child = spawn('open', [fullPath], {
|
|
508
|
+
stdio: 'inherit',
|
|
509
|
+
detached: true
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
child.on('error', (error) => {
|
|
513
|
+
console.error(chalk.red(`打开文件失败: ${error.message}`));
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
child.unref(); // 允许父进程独立于子进程退出
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* 显示版本信息
|
|
521
|
+
*/
|
|
522
|
+
function showVersion() {
|
|
523
|
+
console.log(`ccs 版本: ${VERSION}`);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* 切换到下一个API配置(循环切换)
|
|
528
|
+
*/
|
|
529
|
+
function switchToNextConfig() {
|
|
530
|
+
const apiConfigs = readApiConfigs();
|
|
531
|
+
|
|
532
|
+
if (apiConfigs.length === 0) {
|
|
533
|
+
console.log(chalk.yellow('没有找到可用的API配置'));
|
|
534
|
+
process.exit(0);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (apiConfigs.length === 1) {
|
|
538
|
+
console.log(chalk.yellow('只有一个配置,无需切换'));
|
|
539
|
+
console.log(chalk.white(`当前配置: ${apiConfigs[0].name}`));
|
|
540
|
+
process.exit(0);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// 获取当前激活的配置
|
|
544
|
+
const currentConfig = getCurrentConfig();
|
|
545
|
+
|
|
546
|
+
let nextIndex = 0;
|
|
547
|
+
let currentName = '无';
|
|
548
|
+
|
|
549
|
+
if (currentConfig) {
|
|
550
|
+
// 找到当前配置在列表中的索引
|
|
551
|
+
const currentIndex = apiConfigs.findIndex(config => config.name === currentConfig.name);
|
|
552
|
+
|
|
553
|
+
if (currentIndex !== -1) {
|
|
554
|
+
// 计算下一个索引(循环)
|
|
555
|
+
nextIndex = (currentIndex + 1) % apiConfigs.length;
|
|
556
|
+
currentName = currentConfig.name;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const nextConfig = apiConfigs[nextIndex];
|
|
561
|
+
|
|
562
|
+
// 直接切换,不需要确认
|
|
563
|
+
saveSettingsPreservingHooks(nextConfig.config);
|
|
564
|
+
|
|
565
|
+
console.log(chalk.green('✓ 配置切换成功'));
|
|
566
|
+
console.log(chalk.cyan(` 从: ${currentName}`));
|
|
567
|
+
console.log(chalk.cyan(` 到: ${nextConfig.name}`));
|
|
568
|
+
console.log();
|
|
569
|
+
console.log(chalk.white(`API Key: ${nextConfig.config.env.ANTHROPIC_AUTH_TOKEN}`));
|
|
570
|
+
console.log(chalk.white(`Base URL: ${nextConfig.config.env.ANTHROPIC_BASE_URL}`));
|
|
571
|
+
|
|
572
|
+
if (nextConfig.config.model) {
|
|
573
|
+
console.log(chalk.white(`Model: ${nextConfig.config.model}`));
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
// 设置命令行程序
|
|
579
|
+
program
|
|
580
|
+
.name('ccs')
|
|
581
|
+
.description('Claude配置切换工具')
|
|
582
|
+
.version(VERSION, '-v, --version', '显示版本信息');
|
|
583
|
+
|
|
584
|
+
program
|
|
585
|
+
.command('list')
|
|
586
|
+
.alias('ls')
|
|
587
|
+
.description('列出所有可用的API配置并提示选择')
|
|
588
|
+
.action(() => {
|
|
589
|
+
ensureConfigDir();
|
|
590
|
+
listAndSelectConfig();
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
program
|
|
594
|
+
.command('next')
|
|
595
|
+
.alias('n')
|
|
596
|
+
.description('切换到下一个API配置(循环切换)')
|
|
597
|
+
.action(() => {
|
|
598
|
+
ensureConfigDir();
|
|
599
|
+
switchToNextConfig();
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// 根据需求移除 set/use 命令
|
|
603
|
+
|
|
604
|
+
const openCommand = program
|
|
605
|
+
.command('o')
|
|
606
|
+
.description('打开Claude配置文件');
|
|
607
|
+
|
|
608
|
+
openCommand
|
|
609
|
+
.command('api')
|
|
610
|
+
.description('打开API配置文件 (apiConfigs.json)')
|
|
611
|
+
.action(() => {
|
|
612
|
+
openConfigFile(API_CONFIGS_FILE);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
openCommand
|
|
616
|
+
.command('setting')
|
|
617
|
+
.description('打开设置配置文件 (settings.json)')
|
|
618
|
+
.action(() => {
|
|
619
|
+
openConfigFile(SETTINGS_FILE);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// 注册notify相关命令
|
|
623
|
+
notify.registerNotifyCommands(program);
|
|
624
|
+
|
|
625
|
+
// 注册health相关命令
|
|
626
|
+
health.registerHealthCommands(program);
|
|
627
|
+
|
|
628
|
+
// 添加错误处理
|
|
629
|
+
program.on('command:*', (operands) => {
|
|
630
|
+
console.error(chalk.red(`错误: 未知命令 '${operands[0]}'`));
|
|
631
|
+
const availableCommands = program.commands.map(cmd => cmd.name());
|
|
632
|
+
console.log(chalk.cyan('\n可用命令:'));
|
|
633
|
+
availableCommands.forEach(cmd => {
|
|
634
|
+
console.log(` ${cmd}`);
|
|
635
|
+
});
|
|
636
|
+
console.log(chalk.cyan('\n使用 --help 查看更多信息'));
|
|
637
|
+
process.exit(1);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// 如果没有提供命令,显示帮助信息
|
|
641
|
+
if (!process.argv.slice(2).length) {
|
|
642
|
+
program.outputHelp();
|
|
643
|
+
process.exit(0); // 添加process.exit(0)确保程序在显示帮助信息后退出
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
program.parse(process.argv);
|