@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,168 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import {
4
+ getProfiles,
5
+ profileExists,
6
+ getProfilePath,
7
+ readProfile,
8
+ saveProfile,
9
+ getClaudeSettingsTemplate
10
+ } from '../profiles.js';
11
+
12
+ export function syncCommand(program) {
13
+ program
14
+ .command('sync [profile]')
15
+ .description('从 ~/.claude/settings.json 同步设置(保留 API 配置)')
16
+ .option('-a, --all', '同步所有配置')
17
+ .action(async (profile, options) => {
18
+ const template = getClaudeSettingsTemplate();
19
+
20
+ if (!template) {
21
+ console.log(chalk.red('✗ 未找到模板文件: ~/.claude/settings.json'));
22
+ console.log(chalk.gray(' 请确保该文件存在'));
23
+ process.exit(1);
24
+ }
25
+
26
+ const profiles = getProfiles();
27
+ if (profiles.length === 0) {
28
+ console.log(chalk.yellow('没有可用的配置'));
29
+ process.exit(0);
30
+ }
31
+
32
+ // 需要保留的字段(每个 profile 独立的设置)
33
+ const preserveKeys = ['apiUrl', 'apiKey', 'includeCoAuthoredBy', 'model'];
34
+
35
+ // 同步单个配置的函数
36
+ const syncProfile = (name) => {
37
+ const currentSettings = readProfile(name);
38
+
39
+ // 保留指定字段
40
+ const preserved = {};
41
+ preserveKeys.forEach(key => {
42
+ if (currentSettings[key] !== undefined) {
43
+ preserved[key] = currentSettings[key];
44
+ }
45
+ });
46
+
47
+ // 确保 includeCoAuthoredBy 为 false
48
+ if (preserved.includeCoAuthoredBy === undefined) {
49
+ preserved.includeCoAuthoredBy = false;
50
+ }
51
+
52
+ // 合并:模板 + 保留的字段
53
+ const newSettings = {
54
+ ...template,
55
+ ...preserved
56
+ };
57
+
58
+ saveProfile(name, newSettings);
59
+ return { name, preserved };
60
+ };
61
+
62
+ if (options.all) {
63
+ // 同步所有配置
64
+ console.log(chalk.cyan(`同步所有配置 (${profiles.length} 个)...\n`));
65
+
66
+ const { confirm } = await inquirer.prompt([
67
+ {
68
+ type: 'confirm',
69
+ name: 'confirm',
70
+ message: `确定要同步所有 ${profiles.length} 个配置吗?`,
71
+ default: false
72
+ }
73
+ ]);
74
+
75
+ if (!confirm) {
76
+ console.log(chalk.yellow('已取消'));
77
+ process.exit(0);
78
+ }
79
+
80
+ profiles.forEach(name => {
81
+ const result = syncProfile(name);
82
+ console.log(chalk.green(`✓ ${name}`) + chalk.gray(` (保留: ${Object.keys(result.preserved).join(', ')})`));
83
+ });
84
+
85
+ console.log(chalk.green(`\n✓ 已同步 ${profiles.length} 个配置`));
86
+ } else {
87
+ // 同步单个配置
88
+ if (!profile) {
89
+ const { selectedProfile } = await inquirer.prompt([
90
+ {
91
+ type: 'list',
92
+ name: 'selectedProfile',
93
+ message: '选择要同步的配置:',
94
+ choices: [...profiles, new inquirer.Separator(), { name: '同步全部', value: '__all__' }]
95
+ }
96
+ ]);
97
+
98
+ if (selectedProfile === '__all__') {
99
+ // 递归调用同步全部
100
+ const { confirm } = await inquirer.prompt([
101
+ {
102
+ type: 'confirm',
103
+ name: 'confirm',
104
+ message: `确定要同步所有 ${profiles.length} 个配置吗?`,
105
+ default: false
106
+ }
107
+ ]);
108
+
109
+ if (!confirm) {
110
+ console.log(chalk.yellow('已取消'));
111
+ process.exit(0);
112
+ }
113
+
114
+ profiles.forEach(name => {
115
+ const result = syncProfile(name);
116
+ console.log(chalk.green(`✓ ${name}`) + chalk.gray(` (保留: ${Object.keys(result.preserved).join(', ')})`));
117
+ });
118
+
119
+ console.log(chalk.green(`\n✓ 已同步 ${profiles.length} 个配置`));
120
+ return;
121
+ }
122
+
123
+ profile = selectedProfile;
124
+ }
125
+
126
+ if (!profileExists(profile)) {
127
+ console.log(chalk.red(`配置 "${profile}" 不存在`));
128
+ process.exit(1);
129
+ }
130
+
131
+ // 显示将要进行的更改
132
+ const currentSettings = readProfile(profile);
133
+
134
+ console.log(chalk.cyan(`\n同步配置: ${profile}`));
135
+ console.log(chalk.gray('将保留以下字段:'));
136
+ preserveKeys.forEach(key => {
137
+ const value = currentSettings[key];
138
+ if (value !== undefined) {
139
+ const display = key === 'apiKey' ? value.substring(0, 10) + '...' : value;
140
+ console.log(chalk.gray(` ${key}: ${display}`));
141
+ }
142
+ });
143
+
144
+ const templateKeys = Object.keys(template).filter(k => !preserveKeys.includes(k));
145
+ console.log(chalk.gray('\n将从模板同步:'));
146
+ console.log(chalk.gray(` ${templateKeys.join(', ') || '(无)'}`));
147
+ console.log();
148
+
149
+ const { confirm } = await inquirer.prompt([
150
+ {
151
+ type: 'confirm',
152
+ name: 'confirm',
153
+ message: '确认同步?',
154
+ default: true
155
+ }
156
+ ]);
157
+
158
+ if (!confirm) {
159
+ console.log(chalk.yellow('已取消'));
160
+ process.exit(0);
161
+ }
162
+
163
+ syncProfile(profile);
164
+ console.log(chalk.green(`\n✓ 配置 "${profile}" 已同步`));
165
+ }
166
+ });
167
+ }
168
+
@@ -0,0 +1,19 @@
1
+ import chalk from 'chalk';
2
+ import { profileExists, setDefaultProfile } from '../profiles.js';
3
+
4
+ export function useCommand(program) {
5
+ program
6
+ .command('use <profile>')
7
+ .description('设置默认 profile')
8
+ .action((profile) => {
9
+ if (!profileExists(profile)) {
10
+ console.log(chalk.red(`Profile "${profile}" 不存在`));
11
+ console.log(chalk.yellow(`使用 "ccc list" 查看可用的 profiles`));
12
+ process.exit(1);
13
+ }
14
+
15
+ setDefaultProfile(profile);
16
+ console.log(chalk.green(`✓ 默认 profile 已设置为 "${profile}"`));
17
+ });
18
+ }
19
+
package/src/config.js ADDED
@@ -0,0 +1,9 @@
1
+ import path from 'path';
2
+ import os from 'os';
3
+
4
+ // 配置文件存储目录
5
+ export const CONFIG_DIR = path.join(os.homedir(), '.ccc');
6
+ export const PROFILES_DIR = path.join(CONFIG_DIR, 'profiles');
7
+ export const DEFAULT_FILE = path.join(CONFIG_DIR, 'default');
8
+ export const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
9
+
package/src/launch.js ADDED
@@ -0,0 +1,69 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import { spawn } from 'child_process';
4
+ import {
5
+ getProfiles,
6
+ getDefaultProfile,
7
+ profileExists,
8
+ getProfilePath
9
+ } from './profiles.js';
10
+
11
+ // 启动 claude
12
+ export function launchClaude(profileName, dangerouslySkipPermissions = false) {
13
+ const profilePath = getProfilePath(profileName);
14
+
15
+ if (!profileExists(profileName)) {
16
+ console.log(chalk.red(`Profile "${profileName}" 不存在`));
17
+ console.log(chalk.yellow(`使用 "ccc list" 查看可用的 profiles`));
18
+ process.exit(1);
19
+ }
20
+
21
+ const args = ['--settings', profilePath];
22
+ if (dangerouslySkipPermissions) {
23
+ args.push('--dangerously-skip-permissions');
24
+ }
25
+
26
+ console.log(chalk.green(`启动 Claude Code,使用配置: ${profileName}`));
27
+ console.log(chalk.gray(`命令: claude ${args.join(' ')}`));
28
+
29
+ const child = spawn('claude', args, {
30
+ stdio: 'inherit',
31
+ shell: true
32
+ });
33
+
34
+ child.on('error', (err) => {
35
+ console.log(chalk.red(`启动失败: ${err.message}`));
36
+ process.exit(1);
37
+ });
38
+ }
39
+
40
+ // 交互式选择 profile
41
+ export async function selectProfile(dangerouslySkipPermissions = false) {
42
+ const profiles = getProfiles();
43
+
44
+ if (profiles.length === 0) {
45
+ console.log(chalk.yellow('没有可用的 profiles'));
46
+ console.log(chalk.gray('使用 "ccc import" 导入配置'));
47
+ process.exit(0);
48
+ }
49
+
50
+ const defaultProfile = getDefaultProfile();
51
+
52
+ const choices = profiles.map((p, index) => ({
53
+ name: p === defaultProfile ? `${index + 1}. ${p} ${chalk.green('(默认)')}` : `${index + 1}. ${p}`,
54
+ value: p
55
+ }));
56
+
57
+ const { profile } = await inquirer.prompt([
58
+ {
59
+ type: 'list',
60
+ name: 'profile',
61
+ message: '选择要使用的配置:',
62
+ choices,
63
+ default: defaultProfile
64
+ }
65
+ ]);
66
+
67
+ launchClaude(profile, dangerouslySkipPermissions);
68
+ }
69
+
package/src/parsers.js ADDED
@@ -0,0 +1,154 @@
1
+ // 解析 CC-Switch SQL 导出文件
2
+ export function parseCCSwitchSQL(content) {
3
+ const providers = [];
4
+ // 匹配 INSERT INTO "providers" 语句
5
+ const insertRegex = /INSERT INTO "providers" \([^)]+\) VALUES \(([^;]+)\);/g;
6
+ let match;
7
+
8
+ while ((match = insertRegex.exec(content)) !== null) {
9
+ try {
10
+ const valuesStr = match[1];
11
+ // 解析 VALUES 中的各个字段
12
+ // 格式: 'id', 'app_type', 'name', 'settings_config', 'website_url', ...
13
+ const values = [];
14
+ let current = '';
15
+ let inQuote = false;
16
+ let quoteChar = '';
17
+ let depth = 0;
18
+
19
+ for (let i = 0; i < valuesStr.length; i++) {
20
+ const char = valuesStr[i];
21
+
22
+ if (!inQuote && (char === "'" || char === '"')) {
23
+ inQuote = true;
24
+ quoteChar = char;
25
+ current += char;
26
+ } else if (inQuote && char === quoteChar && valuesStr[i-1] !== '\\') {
27
+ // 检查是否是转义的引号 ''
28
+ if (valuesStr[i+1] === quoteChar) {
29
+ current += char;
30
+ i++; // 跳过下一个引号
31
+ current += valuesStr[i];
32
+ } else {
33
+ inQuote = false;
34
+ quoteChar = '';
35
+ current += char;
36
+ }
37
+ } else if (!inQuote && char === ',' && depth === 0) {
38
+ values.push(current.trim());
39
+ current = '';
40
+ } else {
41
+ current += char;
42
+ }
43
+ }
44
+ if (current.trim()) {
45
+ values.push(current.trim());
46
+ }
47
+
48
+ // 清理值(去除引号)
49
+ const cleanValue = (v) => {
50
+ if (!v || v === 'NULL') return null;
51
+ if ((v.startsWith("'") && v.endsWith("'")) || (v.startsWith('"') && v.endsWith('"'))) {
52
+ return v.slice(1, -1).replace(/''/g, "'");
53
+ }
54
+ return v;
55
+ };
56
+
57
+ const id = cleanValue(values[0]);
58
+ const appType = cleanValue(values[1]);
59
+ const name = cleanValue(values[2]);
60
+ const settingsConfigStr = cleanValue(values[3]);
61
+ const websiteUrl = cleanValue(values[4]);
62
+
63
+ // 只处理 claude 类型
64
+ if (appType === 'claude' && settingsConfigStr) {
65
+ try {
66
+ const settingsConfig = JSON.parse(settingsConfigStr);
67
+ providers.push({
68
+ id,
69
+ name,
70
+ websiteUrl,
71
+ settingsConfig
72
+ });
73
+ } catch (e) {
74
+ // JSON 解析失败,跳过
75
+ }
76
+ }
77
+ } catch (e) {
78
+ // 解析失败,跳过
79
+ }
80
+ }
81
+
82
+ return providers;
83
+ }
84
+
85
+ // 解析 All API Hub JSON 导出文件
86
+ export function parseAllApiHubJSON(content) {
87
+ try {
88
+ const data = JSON.parse(content);
89
+ const accounts = data.accounts?.accounts || [];
90
+
91
+ return accounts.map(account => {
92
+ // 从 site_url 提取 base URL
93
+ let baseUrl = account.site_url;
94
+ if (!baseUrl.startsWith('http')) {
95
+ baseUrl = 'https://' + baseUrl;
96
+ }
97
+
98
+ // access_token 需要解码(Base64)然后作为 API key
99
+ let apiKey = '';
100
+ if (account.account_info?.access_token) {
101
+ // All API Hub 的 access_token 是加密的,我们使用原始值
102
+ // 实际上需要生成 sk- 格式的 token
103
+ // 这里我们用 site_url + username 来生成一个标识
104
+ apiKey = `sk-${account.account_info.access_token.replace(/[^a-zA-Z0-9]/g, '')}`;
105
+ }
106
+
107
+ return {
108
+ id: account.id,
109
+ name: account.site_name,
110
+ websiteUrl: baseUrl,
111
+ settingsConfig: {
112
+ env: {
113
+ ANTHROPIC_AUTH_TOKEN: apiKey,
114
+ ANTHROPIC_BASE_URL: baseUrl
115
+ }
116
+ },
117
+ // 额外的元数据
118
+ meta: {
119
+ siteType: account.site_type,
120
+ health: account.health?.status,
121
+ quota: account.account_info?.quota,
122
+ username: account.account_info?.username
123
+ }
124
+ };
125
+ });
126
+ } catch (e) {
127
+ return [];
128
+ }
129
+ }
130
+
131
+ // 检测文件格式
132
+ export function detectFileFormat(content) {
133
+ // 检测 CC-Switch SQL 格式
134
+ if (content.includes('INSERT INTO "providers"') && content.includes('app_type')) {
135
+ return 'ccswitch';
136
+ }
137
+
138
+ // 检测 All API Hub JSON 格式
139
+ try {
140
+ const data = JSON.parse(content);
141
+ if (data.accounts?.accounts && Array.isArray(data.accounts.accounts)) {
142
+ // 检查是否有 All API Hub 特有的字段
143
+ const firstAccount = data.accounts.accounts[0];
144
+ if (firstAccount && (firstAccount.site_name || firstAccount.site_url || firstAccount.account_info)) {
145
+ return 'allapihub';
146
+ }
147
+ }
148
+ } catch {
149
+ // 不是有效的 JSON
150
+ }
151
+
152
+ return null;
153
+ }
154
+
@@ -0,0 +1,123 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { CONFIG_DIR, PROFILES_DIR, DEFAULT_FILE, CLAUDE_SETTINGS_PATH } from './config.js';
4
+
5
+ // 确保目录存在
6
+ export function ensureDirs() {
7
+ if (!fs.existsSync(CONFIG_DIR)) {
8
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
9
+ }
10
+ if (!fs.existsSync(PROFILES_DIR)) {
11
+ fs.mkdirSync(PROFILES_DIR, { recursive: true });
12
+ }
13
+ }
14
+
15
+ // 获取所有 profiles(按 a-z 排序)
16
+ export function getProfiles() {
17
+ ensureDirs();
18
+ const files = fs.readdirSync(PROFILES_DIR);
19
+ return files
20
+ .filter(f => f.endsWith('.json'))
21
+ .map(f => f.replace('.json', ''))
22
+ .sort((a, b) => a.localeCompare(b, 'zh-CN', { sensitivity: 'base' }));
23
+ }
24
+
25
+ // 获取带序号的 profiles 映射 { 序号: profileName }
26
+ export function getProfilesWithIndex() {
27
+ const profiles = getProfiles();
28
+ const map = {};
29
+ profiles.forEach((p, i) => {
30
+ map[i + 1] = p;
31
+ });
32
+ return { profiles, map };
33
+ }
34
+
35
+ // 根据序号或名称解析 profile
36
+ export function resolveProfile(input) {
37
+ const { profiles, map } = getProfilesWithIndex();
38
+
39
+ // 尝试作为数字序号
40
+ const num = parseInt(input, 10);
41
+ if (!isNaN(num) && map[num]) {
42
+ return map[num];
43
+ }
44
+
45
+ // 作为名称
46
+ if (profiles.includes(input)) {
47
+ return input;
48
+ }
49
+
50
+ return null;
51
+ }
52
+
53
+ // 获取默认 profile
54
+ export function getDefaultProfile() {
55
+ if (fs.existsSync(DEFAULT_FILE)) {
56
+ return fs.readFileSync(DEFAULT_FILE, 'utf-8').trim();
57
+ }
58
+ return null;
59
+ }
60
+
61
+ // 设置默认 profile
62
+ export function setDefaultProfile(name) {
63
+ ensureDirs();
64
+ fs.writeFileSync(DEFAULT_FILE, name);
65
+ }
66
+
67
+ // 获取 profile 路径
68
+ export function getProfilePath(name) {
69
+ return path.join(PROFILES_DIR, `${name}.json`);
70
+ }
71
+
72
+ // 检查 profile 是否存在
73
+ export function profileExists(name) {
74
+ return fs.existsSync(getProfilePath(name));
75
+ }
76
+
77
+ // 获取 Claude 默认 settings 模板
78
+ export function getClaudeSettingsTemplate() {
79
+ if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
80
+ try {
81
+ return JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf-8'));
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+
89
+ // 读取 profile 配置
90
+ export function readProfile(name) {
91
+ const profilePath = getProfilePath(name);
92
+ if (!fs.existsSync(profilePath)) {
93
+ return null;
94
+ }
95
+ try {
96
+ return JSON.parse(fs.readFileSync(profilePath, 'utf-8'));
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ // 保存 profile 配置
103
+ export function saveProfile(name, settings) {
104
+ ensureDirs();
105
+ const profilePath = getProfilePath(name);
106
+ fs.writeFileSync(profilePath, JSON.stringify(settings, null, 2));
107
+ }
108
+
109
+ // 删除 profile
110
+ export function deleteProfile(name) {
111
+ const profilePath = getProfilePath(name);
112
+ if (fs.existsSync(profilePath)) {
113
+ fs.unlinkSync(profilePath);
114
+ }
115
+ }
116
+
117
+ // 清除默认 profile 设置
118
+ export function clearDefaultProfile() {
119
+ if (fs.existsSync(DEFAULT_FILE)) {
120
+ fs.unlinkSync(DEFAULT_FILE);
121
+ }
122
+ }
123
+
package/src/utils.js ADDED
@@ -0,0 +1,82 @@
1
+ // 从文本中提取 URL 和 token
2
+ export function extractFromText(text) {
3
+ // 提取 URL
4
+ const urlRegex = /https?:\/\/[^\s"'<>]+/gi;
5
+ const urls = text.match(urlRegex) || [];
6
+
7
+ // 提取 sk token
8
+ const tokenRegex = /sk-[a-zA-Z0-9_-]+/g;
9
+ const tokens = text.match(tokenRegex) || [];
10
+
11
+ return { urls, tokens };
12
+ }
13
+
14
+ // 从 URL 获取域名作为名称(去掉协议和www子域名)
15
+ export function getDomainName(url) {
16
+ try {
17
+ const urlObj = new URL(url);
18
+ // 获取完整域名
19
+ let hostname = urlObj.hostname;
20
+ // 移除 www. 前缀
21
+ hostname = hostname.replace(/^www\./, '');
22
+ return hostname;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ // 生成安全的 profile 名称
29
+ export function sanitizeProfileName(name) {
30
+ return name
31
+ .replace(/[<>:"/\\|?*]/g, '_') // 替换 Windows 非法字符
32
+ .replace(/\s+/g, '_') // 替换空格
33
+ .replace(/_+/g, '_') // 合并多个下划线
34
+ .replace(/^_|_$/g, '') // 去除首尾下划线
35
+ .substring(0, 50); // 限制长度
36
+ }
37
+
38
+ // 将导入的配置转换为 Claude Code settings 格式
39
+ export function convertToClaudeSettings(provider, template) {
40
+ const baseSettings = template || {};
41
+ const config = provider.settingsConfig || {};
42
+
43
+ // 从 env 中提取 API 信息
44
+ const env = config.env || {};
45
+ const apiKey = env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_API_KEY || config.apiKey || '';
46
+ const apiUrl = env.ANTHROPIC_BASE_URL || config.apiUrl || provider.websiteUrl || '';
47
+
48
+ // 提取 model 设置
49
+ const model = config.model || '';
50
+
51
+ // 只保留模板设置,替换 env 中的 API 信息
52
+ const settings = {
53
+ ...baseSettings,
54
+ env: {
55
+ ...baseSettings.env,
56
+ ANTHROPIC_AUTH_TOKEN: apiKey,
57
+ ANTHROPIC_BASE_URL: apiUrl
58
+ }
59
+ };
60
+
61
+ // 如果有 model 设置,添加到配置中
62
+ if (model) {
63
+ settings.model = model;
64
+ }
65
+
66
+ return settings;
67
+ }
68
+
69
+ // 格式化显示配置值
70
+ export function formatValue(key, value) {
71
+ if (key === 'apiKey' && value) {
72
+ return value.substring(0, 15) + '...';
73
+ }
74
+ if (typeof value === 'boolean') {
75
+ return value ? 'true' : 'false';
76
+ }
77
+ if (typeof value === 'object') {
78
+ return JSON.stringify(value, null, 2).split('\n').join('\n ');
79
+ }
80
+ return String(value);
81
+ }
82
+