@tkpdx01/ccc 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,365 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import inquirer from 'inquirer';
5
+ import readline from 'readline';
6
+ import Table from 'cli-table3';
7
+ import {
8
+ ensureDirs,
9
+ getProfiles,
10
+ getProfilePath,
11
+ setDefaultProfile,
12
+ profileExists,
13
+ getClaudeSettingsTemplate
14
+ } from '../profiles.js';
15
+ import { parseCCSwitchSQL, parseAllApiHubJSON, detectFileFormat } from '../parsers.js';
16
+ import { extractFromText, getDomainName, sanitizeProfileName, convertToClaudeSettings } from '../utils.js';
17
+
18
+ // 生成唯一的 profile 名称(如果重复则加后缀 token2, token3...)
19
+ function getUniqueProfileName(baseName, usedNames) {
20
+ if (!usedNames.has(baseName)) {
21
+ usedNames.add(baseName);
22
+ return baseName;
23
+ }
24
+
25
+ let suffix = 2;
26
+ let newName = `${baseName}-token${suffix}`;
27
+ while (usedNames.has(newName)) {
28
+ suffix++;
29
+ newName = `${baseName}-token${suffix}`;
30
+ }
31
+ usedNames.add(newName);
32
+ return newName;
33
+ }
34
+
35
+ // 交互式导入命令
36
+ export function importCommand(program) {
37
+ // ccc import <file> - 从文件导入(自动识别格式)
38
+ program
39
+ .command('import <file>')
40
+ .aliases(['if'])
41
+ .description('从文件导入配置(自动识别 CC-Switch SQL 或 All API Hub JSON)')
42
+ .action(async (file) => {
43
+ // 检查文件是否存在
44
+ const filePath = path.resolve(file);
45
+ if (!fs.existsSync(filePath)) {
46
+ console.log(chalk.red(`文件不存在: ${filePath}`));
47
+ process.exit(1);
48
+ }
49
+
50
+ console.log(chalk.cyan(`读取文件: ${filePath}\n`));
51
+ const content = fs.readFileSync(filePath, 'utf-8');
52
+
53
+ // 自动检测格式
54
+ const format = detectFileFormat(content);
55
+
56
+ if (!format) {
57
+ console.log(chalk.red('无法识别文件格式'));
58
+ console.log(chalk.gray('支持的格式:'));
59
+ console.log(chalk.gray(' - CC-Switch SQL 导出文件 (.sql)'));
60
+ console.log(chalk.gray(' - All API Hub JSON 导出文件 (.json)'));
61
+ process.exit(1);
62
+ }
63
+
64
+ // 获取模板
65
+ const template = getClaudeSettingsTemplate();
66
+
67
+ let providers = [];
68
+ let formatName = '';
69
+
70
+ if (format === 'ccswitch') {
71
+ formatName = 'CC-Switch SQL';
72
+ providers = parseCCSwitchSQL(content);
73
+ } else if (format === 'allapihub') {
74
+ formatName = 'All API Hub JSON';
75
+ providers = parseAllApiHubJSON(content);
76
+ }
77
+
78
+ if (providers.length === 0) {
79
+ console.log(chalk.yellow('未找到有效的配置'));
80
+ process.exit(0);
81
+ }
82
+
83
+ console.log(chalk.green(`✓ 识别到 ${formatName} 格式`));
84
+ console.log(chalk.green(`✓ 找到 ${providers.length} 个配置\n`));
85
+
86
+ // 显示模板状态
87
+ if (template) {
88
+ console.log(chalk.gray('将使用 ~/.claude/settings.json 作为模板合并设置\n'));
89
+ }
90
+
91
+ // 显示找到的配置
92
+ const table = new Table({
93
+ head: [chalk.cyan('#'), chalk.cyan('Profile 名称'), chalk.cyan('API URL'), chalk.cyan('备注')],
94
+ style: { head: [], border: [] }
95
+ });
96
+
97
+ // 用于跟踪已使用的名称(预览阶段)
98
+ const previewUsedNames = new Set();
99
+
100
+ providers.forEach((p, i) => {
101
+ const url = p.settingsConfig?.env?.ANTHROPIC_BASE_URL || p.websiteUrl || '(未设置)';
102
+ // 使用 API URL 生成 profile 名称,重复时加后缀
103
+ const baseName = sanitizeProfileName(getDomainName(url) || p.name);
104
+ const profileName = getUniqueProfileName(baseName, previewUsedNames);
105
+ let note = '';
106
+
107
+ if (format === 'ccswitch') {
108
+ note = p.settingsConfig?.model || '(默认模型)';
109
+ } else if (format === 'allapihub') {
110
+ note = p.meta?.health === 'healthy' ? chalk.green('健康') :
111
+ p.meta?.health === 'warning' ? chalk.yellow('警告') :
112
+ p.meta?.health === 'error' ? chalk.red('错误') : chalk.gray('未知');
113
+ }
114
+
115
+ table.push([i + 1, profileName, url, note]);
116
+ });
117
+
118
+ console.log(table.toString());
119
+ console.log();
120
+
121
+ // All API Hub 特殊警告
122
+ if (format === 'allapihub') {
123
+ console.log(chalk.yellow('⚠ 注意: All API Hub 的 access_token 格式可能需要手动调整'));
124
+ console.log(chalk.gray(' 导入后可使用 "ccc edit <profile>" 修改 API Key\n'));
125
+ }
126
+
127
+ // 确认导入
128
+ const { confirmImport } = await inquirer.prompt([
129
+ {
130
+ type: 'confirm',
131
+ name: 'confirmImport',
132
+ message: `确定要导入这 ${providers.length} 个配置吗?`,
133
+ default: true
134
+ }
135
+ ]);
136
+
137
+ if (!confirmImport) {
138
+ console.log(chalk.yellow('已取消'));
139
+ process.exit(0);
140
+ }
141
+
142
+ // 选择要导入的配置
143
+ // 重新计算名称用于选择列表
144
+ const selectionUsedNames = new Set();
145
+ const { selection } = await inquirer.prompt([
146
+ {
147
+ type: 'checkbox',
148
+ name: 'selection',
149
+ message: '选择要导入的配置 (空格选择,回车确认):',
150
+ choices: providers.map((p, i) => {
151
+ const url = p.settingsConfig?.env?.ANTHROPIC_BASE_URL || p.websiteUrl || '';
152
+ // 使用 API URL 生成 profile 名称,重复时加后缀
153
+ const baseName = sanitizeProfileName(getDomainName(url) || p.name);
154
+ const profileName = getUniqueProfileName(baseName, selectionUsedNames);
155
+ return {
156
+ name: `${profileName} (${url})`,
157
+ value: i,
158
+ checked: format === 'allapihub' ? p.meta?.health === 'healthy' : true
159
+ };
160
+ })
161
+ }
162
+ ]);
163
+
164
+ const selectedProviders = selection.map(i => providers[i]);
165
+
166
+ if (selectedProviders.length === 0) {
167
+ console.log(chalk.yellow('未选择任何配置'));
168
+ process.exit(0);
169
+ }
170
+
171
+ // 导入选中的配置
172
+ ensureDirs();
173
+ let imported = 0;
174
+ let skipped = 0;
175
+
176
+ // 用于跟踪导入时已使用的名称(包括已存在的 profiles)
177
+ const importUsedNames = new Set(getProfiles());
178
+
179
+ for (const provider of selectedProviders) {
180
+ const url = provider.settingsConfig?.env?.ANTHROPIC_BASE_URL || provider.websiteUrl || '';
181
+ // 使用 API URL 生成 profile 名称,重复时加后缀
182
+ const baseName = sanitizeProfileName(getDomainName(url) || provider.name);
183
+ const profileName = getUniqueProfileName(baseName, importUsedNames);
184
+ const profilePath = getProfilePath(profileName);
185
+
186
+ // 检查是否已存在
187
+ if (fs.existsSync(profilePath)) {
188
+ const { overwrite } = await inquirer.prompt([
189
+ {
190
+ type: 'confirm',
191
+ name: 'overwrite',
192
+ message: `配置 "${profileName}" 已存在,是否覆盖?`,
193
+ default: false
194
+ }
195
+ ]);
196
+
197
+ if (!overwrite) {
198
+ skipped++;
199
+ continue;
200
+ }
201
+ }
202
+
203
+ // 转换并保存配置
204
+ const settings = convertToClaudeSettings(provider, template);
205
+ fs.writeFileSync(profilePath, JSON.stringify(settings, null, 2));
206
+ console.log(chalk.green(`✓ ${profileName}`));
207
+ imported++;
208
+ }
209
+
210
+ console.log(chalk.green(`\n✓ 导入完成: ${imported} 个成功` + (skipped > 0 ? `, ${skipped} 个跳过` : '')));
211
+
212
+ // 如果是第一次导入,设置默认
213
+ const profiles = getProfiles();
214
+ if (profiles.length === imported && imported > 0) {
215
+ const { setDefault } = await inquirer.prompt([
216
+ {
217
+ type: 'confirm',
218
+ name: 'setDefault',
219
+ message: '是否设置第一个配置为默认?',
220
+ default: true
221
+ }
222
+ ]);
223
+
224
+ if (setDefault) {
225
+ setDefaultProfile(profiles[0]);
226
+ console.log(chalk.green(`✓ 已设置 "${profiles[0]}" 为默认配置`));
227
+ }
228
+ }
229
+ });
230
+ }
231
+
232
+ // 交互式粘贴导入(原 importProfile 功能,可选添加)
233
+ export async function interactiveImport() {
234
+ console.log(chalk.cyan('请粘贴包含 API URL 和 SK Token 的文本,然后按两次回车确认:'));
235
+ console.log(chalk.gray('(支持自动识别 URL 和 sk-xxx 格式的 token)'));
236
+ console.log();
237
+
238
+ const rl = readline.createInterface({
239
+ input: process.stdin,
240
+ output: process.stdout
241
+ });
242
+
243
+ let inputText = '';
244
+ let emptyLineCount = 0;
245
+
246
+ const text = await new Promise((resolve) => {
247
+ rl.on('line', (line) => {
248
+ if (line === '') {
249
+ emptyLineCount++;
250
+ if (emptyLineCount >= 2) {
251
+ rl.close();
252
+ resolve(inputText);
253
+ }
254
+ } else {
255
+ emptyLineCount = 0;
256
+ inputText += line + '\n';
257
+ }
258
+ });
259
+ });
260
+
261
+ const { urls, tokens } = extractFromText(text);
262
+
263
+ if (urls.length === 0 && tokens.length === 0) {
264
+ console.log(chalk.red('未找到有效的 URL 或 Token'));
265
+ process.exit(1);
266
+ }
267
+
268
+ console.log();
269
+ console.log(chalk.green('识别到的内容:'));
270
+
271
+ if (urls.length > 0) {
272
+ console.log(chalk.cyan('URLs:'));
273
+ urls.forEach(u => console.log(` - ${u}`));
274
+ }
275
+
276
+ if (tokens.length > 0) {
277
+ console.log(chalk.cyan('Tokens:'));
278
+ tokens.forEach(t => console.log(` - ${t.substring(0, 10)}...`));
279
+ }
280
+
281
+ // 使用第一个 URL 的域名作为默认名称
282
+ let defaultName = 'custom';
283
+ if (urls.length > 0) {
284
+ const domainName = getDomainName(urls[0]);
285
+ if (domainName) {
286
+ defaultName = domainName;
287
+ }
288
+ }
289
+
290
+ const { profileName, apiUrl, apiKey } = await inquirer.prompt([
291
+ {
292
+ type: 'input',
293
+ name: 'profileName',
294
+ message: 'Profile 名称:',
295
+ default: defaultName
296
+ },
297
+ {
298
+ type: 'list',
299
+ name: 'apiUrl',
300
+ message: '选择 API URL:',
301
+ choices: urls.length > 0 ? urls : ['https://api.anthropic.com'],
302
+ when: urls.length > 0
303
+ },
304
+ {
305
+ type: 'input',
306
+ name: 'apiUrl',
307
+ message: 'API URL:',
308
+ default: 'https://api.anthropic.com',
309
+ when: urls.length === 0
310
+ },
311
+ {
312
+ type: 'list',
313
+ name: 'apiKey',
314
+ message: '选择 API Key:',
315
+ choices: tokens.map(t => ({ name: `${t.substring(0, 15)}...`, value: t })),
316
+ when: tokens.length > 1
317
+ },
318
+ {
319
+ type: 'input',
320
+ name: 'apiKey',
321
+ message: 'API Key:',
322
+ default: tokens[0] || '',
323
+ when: tokens.length <= 1
324
+ }
325
+ ]);
326
+
327
+ const finalApiUrl = apiUrl || 'https://api.anthropic.com';
328
+ const finalApiKey = apiKey || tokens[0] || '';
329
+
330
+ // 创建 settings.json
331
+ const settings = {
332
+ apiUrl: finalApiUrl,
333
+ apiKey: finalApiKey
334
+ };
335
+
336
+ ensureDirs();
337
+ const profilePath = getProfilePath(profileName);
338
+
339
+ if (fs.existsSync(profilePath)) {
340
+ const { overwrite } = await inquirer.prompt([
341
+ {
342
+ type: 'confirm',
343
+ name: 'overwrite',
344
+ message: `Profile "${profileName}" 已存在,是否覆盖?`,
345
+ default: false
346
+ }
347
+ ]);
348
+
349
+ if (!overwrite) {
350
+ console.log(chalk.yellow('已取消'));
351
+ process.exit(0);
352
+ }
353
+ }
354
+
355
+ fs.writeFileSync(profilePath, JSON.stringify(settings, null, 2));
356
+ console.log(chalk.green(`\n✓ Profile "${profileName}" 已保存到 ${profilePath}`));
357
+
358
+ // 如果是第一个 profile,设为默认
359
+ const profiles = getProfiles();
360
+ if (profiles.length === 1) {
361
+ setDefaultProfile(profileName);
362
+ console.log(chalk.green(`✓ 已设为默认 profile`));
363
+ }
364
+ }
365
+
@@ -0,0 +1,10 @@
1
+ export { listCommand } from './list.js';
2
+ export { useCommand } from './use.js';
3
+ export { showCommand } from './show.js';
4
+ export { importCommand } from './import.js';
5
+ export { newCommand } from './new.js';
6
+ export { syncCommand } from './sync.js';
7
+ export { editCommand } from './edit.js';
8
+ export { deleteCommand } from './delete.js';
9
+ export { helpCommand, showHelp } from './help.js';
10
+
@@ -0,0 +1,53 @@
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+ import { getProfiles, getDefaultProfile, getProfilePath, readProfile } from '../profiles.js';
4
+
5
+ export function listCommand(program) {
6
+ program
7
+ .command('list')
8
+ .alias('ls')
9
+ .description('列出所有 profiles')
10
+ .action(() => {
11
+ const profiles = getProfiles();
12
+ const defaultProfile = getDefaultProfile();
13
+
14
+ if (profiles.length === 0) {
15
+ console.log(chalk.yellow('没有可用的 profiles'));
16
+ console.log(chalk.gray('使用 "ccc import" 导入配置'));
17
+ return;
18
+ }
19
+
20
+ const table = new Table({
21
+ head: [chalk.cyan('#'), chalk.cyan('Profile'), chalk.cyan('API URL')],
22
+ style: { head: [], border: [] },
23
+ chars: {
24
+ 'top': '─', 'top-mid': '┬', 'top-left': '┌', 'top-right': '┐',
25
+ 'bottom': '─', 'bottom-mid': '┴', 'bottom-left': '└', 'bottom-right': '┘',
26
+ 'left': '│', 'left-mid': '├', 'mid': '─', 'mid-mid': '┼',
27
+ 'right': '│', 'right-mid': '┤', 'middle': '│'
28
+ }
29
+ });
30
+
31
+ profiles.forEach((p, index) => {
32
+ const isDefault = p === defaultProfile;
33
+ const settings = readProfile(p);
34
+ let apiUrl = chalk.gray('(未设置)');
35
+
36
+ if (settings) {
37
+ // 兼容两种格式:apiUrl 或 env.ANTHROPIC_BASE_URL
38
+ apiUrl = settings.apiUrl || settings.env?.ANTHROPIC_BASE_URL || chalk.gray('(未设置)');
39
+ } else {
40
+ apiUrl = chalk.red('(读取失败)');
41
+ }
42
+
43
+ const num = isDefault ? chalk.green(`${index + 1}`) : chalk.gray(`${index + 1}`);
44
+ const name = isDefault ? chalk.green(`${p} *`) : p;
45
+ table.push([num, name, apiUrl]);
46
+ });
47
+
48
+ console.log();
49
+ console.log(table.toString());
50
+ console.log(chalk.gray(`\n 共 ${profiles.length} 个配置,* 表示默认,可用序号或名称启动\n`));
51
+ });
52
+ }
53
+
@@ -0,0 +1,143 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import {
4
+ ensureDirs,
5
+ getProfiles,
6
+ profileExists,
7
+ saveProfile,
8
+ setDefaultProfile,
9
+ getClaudeSettingsTemplate
10
+ } from '../profiles.js';
11
+ import { launchClaude } from '../launch.js';
12
+
13
+ export function newCommand(program) {
14
+ program
15
+ .command('new [name]')
16
+ .description('基于 ~/.claude/settings.json 模板创建新配置')
17
+ .action(async (name) => {
18
+ const template = getClaudeSettingsTemplate();
19
+
20
+ // 显示模板状态
21
+ if (template) {
22
+ console.log(chalk.green('✓ 检测到模板文件: ~/.claude/settings.json'));
23
+ console.log(chalk.gray(' 将基于此模板创建新配置\n'));
24
+ } else {
25
+ console.log(chalk.yellow('! 未找到模板文件: ~/.claude/settings.json'));
26
+ console.log(chalk.gray(' 将创建空白配置\n'));
27
+ }
28
+
29
+ // 如果没有提供名称,询问
30
+ if (!name) {
31
+ const { profileName } = await inquirer.prompt([
32
+ {
33
+ type: 'input',
34
+ name: 'profileName',
35
+ message: '配置名称:',
36
+ validate: (input) => input.trim() ? true : '请输入配置名称'
37
+ }
38
+ ]);
39
+ name = profileName;
40
+ }
41
+
42
+ // 检查是否已存在
43
+ if (profileExists(name)) {
44
+ const { overwrite } = await inquirer.prompt([
45
+ {
46
+ type: 'confirm',
47
+ name: 'overwrite',
48
+ message: `配置 "${name}" 已存在,是否覆盖?`,
49
+ default: false
50
+ }
51
+ ]);
52
+ if (!overwrite) {
53
+ console.log(chalk.yellow('已取消'));
54
+ process.exit(0);
55
+ }
56
+ }
57
+
58
+ // 基于模板创建,但需要用户填写 API 信息
59
+ const baseSettings = template || {};
60
+
61
+ // 显示模板中已有的设置(如果有)
62
+ if (template) {
63
+ console.log(chalk.cyan('模板中的现有设置:'));
64
+ if (template.apiUrl) console.log(chalk.gray(` API URL: ${template.apiUrl}`));
65
+ if (template.apiKey) console.log(chalk.gray(` API Key: ${template.apiKey.substring(0, 10)}...`));
66
+ const otherKeys = Object.keys(template).filter(k => !['apiUrl', 'apiKey'].includes(k));
67
+ if (otherKeys.length > 0) {
68
+ console.log(chalk.gray(` 其他设置: ${otherKeys.join(', ')}`));
69
+ }
70
+ console.log();
71
+ }
72
+
73
+ const { apiUrl, apiKey, finalName } = await inquirer.prompt([
74
+ {
75
+ type: 'input',
76
+ name: 'apiUrl',
77
+ message: 'API URL:',
78
+ default: baseSettings.apiUrl || 'https://api.anthropic.com'
79
+ },
80
+ {
81
+ type: 'input',
82
+ name: 'apiKey',
83
+ message: 'API Key:',
84
+ default: baseSettings.apiKey || ''
85
+ },
86
+ {
87
+ type: 'input',
88
+ name: 'finalName',
89
+ message: 'Profile 名称:',
90
+ default: name
91
+ }
92
+ ]);
93
+
94
+ // 如果名称改变了,检查新名称是否存在
95
+ if (finalName !== name && profileExists(finalName)) {
96
+ const { overwriteNew } = await inquirer.prompt([
97
+ {
98
+ type: 'confirm',
99
+ name: 'overwriteNew',
100
+ message: `配置 "${finalName}" 已存在,是否覆盖?`,
101
+ default: false
102
+ }
103
+ ]);
104
+ if (!overwriteNew) {
105
+ console.log(chalk.yellow('已取消'));
106
+ process.exit(0);
107
+ }
108
+ }
109
+
110
+ // 合并设置:保留模板中的其他设置,覆盖 API 相关设置
111
+ const newSettings = {
112
+ ...baseSettings,
113
+ apiUrl,
114
+ apiKey
115
+ };
116
+
117
+ ensureDirs();
118
+ saveProfile(finalName, newSettings);
119
+ console.log(chalk.green(`\n✓ 配置 "${finalName}" 已创建`));
120
+
121
+ // 如果是第一个 profile,设为默认
122
+ const profiles = getProfiles();
123
+ if (profiles.length === 1) {
124
+ setDefaultProfile(finalName);
125
+ console.log(chalk.green(`✓ 已设为默认配置`));
126
+ }
127
+
128
+ // 询问是否立即使用
129
+ const { useNow } = await inquirer.prompt([
130
+ {
131
+ type: 'confirm',
132
+ name: 'useNow',
133
+ message: '是否立即启动 Claude?',
134
+ default: false
135
+ }
136
+ ]);
137
+
138
+ if (useNow) {
139
+ launchClaude(finalName);
140
+ }
141
+ });
142
+ }
143
+
@@ -0,0 +1,68 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import {
4
+ getProfiles,
5
+ getDefaultProfile,
6
+ profileExists,
7
+ getProfilePath,
8
+ readProfile
9
+ } from '../profiles.js';
10
+ import { formatValue } from '../utils.js';
11
+
12
+ export function showCommand(program) {
13
+ program
14
+ .command('show [profile]')
15
+ .description('显示 profile 的完整配置')
16
+ .action(async (profile) => {
17
+ const profiles = getProfiles();
18
+
19
+ if (profiles.length === 0) {
20
+ console.log(chalk.yellow('没有可用的 profiles'));
21
+ process.exit(0);
22
+ }
23
+
24
+ // 如果没有指定 profile,交互选择
25
+ if (!profile) {
26
+ const defaultProfile = getDefaultProfile();
27
+ const { selectedProfile } = await inquirer.prompt([
28
+ {
29
+ type: 'list',
30
+ name: 'selectedProfile',
31
+ message: '选择要查看的配置:',
32
+ choices: profiles,
33
+ default: defaultProfile
34
+ }
35
+ ]);
36
+ profile = selectedProfile;
37
+ }
38
+
39
+ if (!profileExists(profile)) {
40
+ console.log(chalk.red(`Profile "${profile}" 不存在`));
41
+ process.exit(1);
42
+ }
43
+
44
+ const profilePath = getProfilePath(profile);
45
+ const settings = readProfile(profile);
46
+ const isDefault = getDefaultProfile() === profile;
47
+
48
+ console.log(chalk.cyan.bold(`\n Profile: ${profile}`) + (isDefault ? chalk.green(' (默认)') : ''));
49
+ console.log(chalk.gray(` 路径: ${profilePath}\n`));
50
+
51
+ // 格式化显示配置
52
+ Object.entries(settings).forEach(([key, value]) => {
53
+ const formattedValue = formatValue(key, value);
54
+ if (key === 'apiKey' && value) {
55
+ console.log(` ${chalk.cyan(key)}: ${chalk.yellow(formattedValue)}`);
56
+ } else if (typeof value === 'boolean') {
57
+ console.log(` ${chalk.cyan(key)}: ${value ? chalk.green(formattedValue) : chalk.red(formattedValue)}`);
58
+ } else if (typeof value === 'object') {
59
+ console.log(` ${chalk.cyan(key)}: ${chalk.gray(formattedValue)}`);
60
+ } else {
61
+ console.log(` ${chalk.cyan(key)}: ${chalk.white(formattedValue)}`);
62
+ }
63
+ });
64
+
65
+ console.log();
66
+ });
67
+ }
68
+