@tkpdx01/ccc 1.5.0 → 1.6.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.
- package/README.md +65 -87
- package/cover.png +0 -0
- package/index.js +23 -16
- package/package.json +4 -2
- package/src/commands/apply.js +79 -0
- package/src/commands/delete.js +35 -23
- package/src/commands/edit.js +139 -80
- package/src/commands/help.js +19 -11
- package/src/commands/index.js +1 -0
- package/src/commands/list.js +20 -13
- package/src/commands/new.js +146 -55
- package/src/commands/show.js +66 -36
- package/src/commands/sync.js +69 -29
- package/src/commands/use.js +6 -5
- package/src/config.js +2 -0
- package/src/launch.js +68 -15
- package/src/profiles.js +222 -1
package/src/commands/show.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
getDefaultProfile,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
import {
|
|
4
|
+
getAllProfiles,
|
|
5
|
+
getDefaultProfile,
|
|
6
|
+
anyProfileExists,
|
|
7
|
+
resolveAnyProfile,
|
|
8
|
+
getProfilePath,
|
|
9
|
+
getCodexProfileDir,
|
|
10
|
+
readProfile,
|
|
11
|
+
readCodexProfile,
|
|
12
|
+
getCodexProfileCredentials
|
|
9
13
|
} from '../profiles.js';
|
|
10
14
|
import { formatValue } from '../utils.js';
|
|
11
15
|
|
|
@@ -14,55 +18,81 @@ export function showCommand(program) {
|
|
|
14
18
|
.command('show [profile]')
|
|
15
19
|
.description('显示 profile 的完整配置')
|
|
16
20
|
.action(async (profile) => {
|
|
17
|
-
const
|
|
21
|
+
const allProfiles = getAllProfiles();
|
|
18
22
|
|
|
19
|
-
if (
|
|
23
|
+
if (allProfiles.length === 0) {
|
|
20
24
|
console.log(chalk.yellow('没有可用的 profiles'));
|
|
21
25
|
process.exit(0);
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
let profileInfo;
|
|
29
|
+
|
|
25
30
|
if (!profile) {
|
|
26
31
|
const defaultProfile = getDefaultProfile();
|
|
27
|
-
const
|
|
32
|
+
const choices = allProfiles.map(p => {
|
|
33
|
+
const typeTag = p.type === 'codex' ? chalk.blue('[Codex]') : chalk.magenta('[Claude]');
|
|
34
|
+
return { name: `${typeTag} ${p.name}`, value: p };
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const { selected } = await inquirer.prompt([
|
|
28
38
|
{
|
|
29
39
|
type: 'list',
|
|
30
|
-
name: '
|
|
40
|
+
name: 'selected',
|
|
31
41
|
message: '选择要查看的配置:',
|
|
32
|
-
choices
|
|
33
|
-
default: defaultProfile
|
|
42
|
+
choices,
|
|
43
|
+
default: allProfiles.findIndex(p => p.name === defaultProfile)
|
|
34
44
|
}
|
|
35
45
|
]);
|
|
36
|
-
|
|
46
|
+
profileInfo = selected;
|
|
47
|
+
} else {
|
|
48
|
+
profileInfo = resolveAnyProfile(profile);
|
|
49
|
+
if (!profileInfo) {
|
|
50
|
+
console.log(chalk.red(`Profile "${profile}" 不存在`));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
37
53
|
}
|
|
38
54
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
const isDefault = getDefaultProfile() === profileInfo.name;
|
|
56
|
+
|
|
57
|
+
if (profileInfo.type === 'codex') {
|
|
58
|
+
const dir = getCodexProfileDir(profileInfo.name);
|
|
59
|
+
const codexProfile = readCodexProfile(profileInfo.name);
|
|
60
|
+
const { apiKey, baseUrl, model } = getCodexProfileCredentials(profileInfo.name);
|
|
43
61
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const isDefault = getDefaultProfile() === profile;
|
|
62
|
+
console.log(chalk.cyan.bold(`\n Profile: ${profileInfo.name}`) + ` ${chalk.blue('[Codex]')}` + (isDefault ? chalk.green(' (默认)') : ''));
|
|
63
|
+
console.log(chalk.gray(` 路径: ${dir}\n`));
|
|
47
64
|
|
|
48
|
-
|
|
49
|
-
|
|
65
|
+
console.log(` ${chalk.cyan('OPENAI_API_KEY')}: ${chalk.yellow(apiKey ? apiKey.substring(0, 15) + '...' : '未设置')}`);
|
|
66
|
+
console.log(` ${chalk.cyan('Base URL')}: ${chalk.white(baseUrl)}`);
|
|
67
|
+
console.log(` ${chalk.cyan('Model')}: ${chalk.white(model || '(默认)')}`);
|
|
50
68
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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)}`);
|
|
69
|
+
if (codexProfile?.configToml) {
|
|
70
|
+
console.log(`\n ${chalk.cyan('config.toml')}:`);
|
|
71
|
+
codexProfile.configToml.split('\n').forEach(line => {
|
|
72
|
+
console.log(` ${chalk.gray(line)}`);
|
|
73
|
+
});
|
|
62
74
|
}
|
|
63
|
-
}
|
|
75
|
+
} else {
|
|
76
|
+
const profilePath = getProfilePath(profileInfo.name);
|
|
77
|
+
const settings = readProfile(profileInfo.name);
|
|
78
|
+
|
|
79
|
+
console.log(chalk.cyan.bold(`\n Profile: ${profileInfo.name}`) + ` ${chalk.magenta('[Claude]')}` + (isDefault ? chalk.green(' (默认)') : ''));
|
|
80
|
+
console.log(chalk.gray(` 路径: ${profilePath}\n`));
|
|
81
|
+
|
|
82
|
+
Object.entries(settings).forEach(([key, value]) => {
|
|
83
|
+
const formattedValue = formatValue(key, value);
|
|
84
|
+
if ((key === 'apiKey' || key === 'ANTHROPIC_AUTH_TOKEN') && value) {
|
|
85
|
+
console.log(` ${chalk.cyan(key)}: ${chalk.yellow(formattedValue)}`);
|
|
86
|
+
} else if (typeof value === 'boolean') {
|
|
87
|
+
console.log(` ${chalk.cyan(key)}: ${value ? chalk.green(formattedValue) : chalk.red(formattedValue)}`);
|
|
88
|
+
} else if (typeof value === 'object') {
|
|
89
|
+
console.log(` ${chalk.cyan(key)}: ${chalk.gray(formattedValue)}`);
|
|
90
|
+
} else {
|
|
91
|
+
console.log(` ${chalk.cyan(key)}: ${chalk.white(formattedValue)}`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
64
95
|
|
|
65
96
|
console.log();
|
|
66
97
|
});
|
|
67
98
|
}
|
|
68
|
-
|
package/src/commands/sync.js
CHANGED
|
@@ -1,41 +1,39 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import {
|
|
4
|
+
getAllProfiles,
|
|
4
5
|
getProfiles,
|
|
6
|
+
getCodexProfiles,
|
|
5
7
|
syncProfileWithTemplate,
|
|
8
|
+
syncCodexProfileWithTemplate,
|
|
6
9
|
getClaudeSettingsTemplate,
|
|
7
|
-
|
|
10
|
+
resolveAnyProfile
|
|
8
11
|
} from '../profiles.js';
|
|
12
|
+
import { CODEX_HOME_PATH } from '../config.js';
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
9
15
|
|
|
10
16
|
export function syncCommand(program) {
|
|
11
17
|
program
|
|
12
18
|
.command('sync [profile]')
|
|
13
|
-
.description('
|
|
19
|
+
.description('同步模板配置到 profile(保留 API 凭证)')
|
|
14
20
|
.option('-a, --all', '同步所有 profiles')
|
|
15
21
|
.action(async (profile, options) => {
|
|
16
|
-
|
|
17
|
-
const template = getClaudeSettingsTemplate();
|
|
18
|
-
if (!template) {
|
|
19
|
-
console.log(chalk.red('未找到 ~/.claude/settings.json'));
|
|
20
|
-
console.log(chalk.gray('请确保 Claude Code 已正确安装'));
|
|
21
|
-
process.exit(1);
|
|
22
|
-
}
|
|
22
|
+
const allProfiles = getAllProfiles();
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (profiles.length === 0) {
|
|
24
|
+
if (allProfiles.length === 0) {
|
|
27
25
|
console.log(chalk.yellow('没有可用的 profiles'));
|
|
28
26
|
console.log(chalk.gray('使用 "ccc new" 创建配置'));
|
|
29
27
|
process.exit(0);
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
// 同步所有
|
|
30
|
+
// 同步所有
|
|
33
31
|
if (options.all) {
|
|
34
32
|
const { confirm } = await inquirer.prompt([
|
|
35
33
|
{
|
|
36
34
|
type: 'confirm',
|
|
37
35
|
name: 'confirm',
|
|
38
|
-
message: `确定要同步所有 ${
|
|
36
|
+
message: `确定要同步所有 ${allProfiles.length} 个 profiles 吗?`,
|
|
39
37
|
default: false
|
|
40
38
|
}
|
|
41
39
|
]);
|
|
@@ -48,43 +46,85 @@ export function syncCommand(program) {
|
|
|
48
46
|
console.log(chalk.cyan('\n开始同步所有 profiles...\n'));
|
|
49
47
|
|
|
50
48
|
let successCount = 0;
|
|
51
|
-
for (const p of
|
|
52
|
-
|
|
49
|
+
for (const p of allProfiles) {
|
|
50
|
+
let result;
|
|
51
|
+
if (p.type === 'codex') {
|
|
52
|
+
const codexConfigPath = path.join(CODEX_HOME_PATH, 'config.toml');
|
|
53
|
+
if (!fs.existsSync(codexConfigPath)) {
|
|
54
|
+
console.log(chalk.yellow(` ⚠ ${chalk.blue('[Codex]')} ${p.name} (无 ~/.codex/config.toml 模板,跳过)`));
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
result = syncCodexProfileWithTemplate(p.name);
|
|
58
|
+
} else {
|
|
59
|
+
const template = getClaudeSettingsTemplate();
|
|
60
|
+
if (!template) {
|
|
61
|
+
console.log(chalk.yellow(` ⚠ ${chalk.magenta('[Claude]')} ${p.name} (无 ~/.claude/settings.json 模板,跳过)`));
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
result = syncProfileWithTemplate(p.name);
|
|
65
|
+
}
|
|
66
|
+
|
|
53
67
|
if (result) {
|
|
54
|
-
|
|
68
|
+
const typeTag = p.type === 'codex' ? chalk.blue('[Codex]') : chalk.magenta('[Claude]');
|
|
69
|
+
console.log(chalk.green(` ✓ ${typeTag} ${p.name}`));
|
|
55
70
|
successCount++;
|
|
56
71
|
} else {
|
|
57
|
-
console.log(chalk.red(` ✗ ${p} (同步失败)`));
|
|
72
|
+
console.log(chalk.red(` ✗ ${p.name} (同步失败)`));
|
|
58
73
|
}
|
|
59
74
|
}
|
|
60
75
|
|
|
61
|
-
console.log(chalk.green(`\n✓ 已同步 ${successCount}/${
|
|
76
|
+
console.log(chalk.green(`\n✓ 已同步 ${successCount}/${allProfiles.length} 个 profiles`));
|
|
62
77
|
return;
|
|
63
78
|
}
|
|
64
79
|
|
|
65
|
-
// 同步单个
|
|
80
|
+
// 同步单个
|
|
81
|
+
let profileInfo;
|
|
82
|
+
|
|
66
83
|
if (!profile) {
|
|
67
|
-
const
|
|
84
|
+
const choices = allProfiles.map(p => {
|
|
85
|
+
const typeTag = p.type === 'codex' ? chalk.blue('[Codex]') : chalk.magenta('[Claude]');
|
|
86
|
+
return { name: `${typeTag} ${p.name}`, value: p };
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const { selected } = await inquirer.prompt([
|
|
68
90
|
{
|
|
69
91
|
type: 'list',
|
|
70
|
-
name: '
|
|
92
|
+
name: 'selected',
|
|
71
93
|
message: '选择要同步的配置:',
|
|
72
|
-
choices
|
|
94
|
+
choices
|
|
73
95
|
}
|
|
74
96
|
]);
|
|
75
|
-
|
|
97
|
+
profileInfo = selected;
|
|
76
98
|
} else {
|
|
77
|
-
|
|
78
|
-
if (!
|
|
99
|
+
profileInfo = resolveAnyProfile(profile);
|
|
100
|
+
if (!profileInfo) {
|
|
79
101
|
console.log(chalk.red(`Profile "${profile}" 不存在`));
|
|
80
102
|
process.exit(1);
|
|
81
103
|
}
|
|
82
|
-
profile = resolved;
|
|
83
104
|
}
|
|
84
105
|
|
|
85
|
-
|
|
106
|
+
let result;
|
|
107
|
+
if (profileInfo.type === 'codex') {
|
|
108
|
+
const codexConfigPath = path.join(CODEX_HOME_PATH, 'config.toml');
|
|
109
|
+
if (!fs.existsSync(codexConfigPath)) {
|
|
110
|
+
console.log(chalk.red('未找到 ~/.codex/config.toml'));
|
|
111
|
+
console.log(chalk.gray('请确保 Codex CLI 已正确配置'));
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
result = syncCodexProfileWithTemplate(profileInfo.name);
|
|
115
|
+
} else {
|
|
116
|
+
const template = getClaudeSettingsTemplate();
|
|
117
|
+
if (!template) {
|
|
118
|
+
console.log(chalk.red('未找到 ~/.claude/settings.json'));
|
|
119
|
+
console.log(chalk.gray('请确保 Claude Code 已正确安装'));
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
result = syncProfileWithTemplate(profileInfo.name);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const typeLabel = profileInfo.type === 'codex' ? 'Codex' : 'Claude';
|
|
86
126
|
if (result) {
|
|
87
|
-
console.log(chalk.green(`\n✓ Profile "${
|
|
127
|
+
console.log(chalk.green(`\n✓ ${typeLabel} Profile "${profileInfo.name}" 已同步(保留了 API 凭证)`));
|
|
88
128
|
} else {
|
|
89
129
|
console.log(chalk.red(`\n✗ 同步失败`));
|
|
90
130
|
process.exit(1);
|
package/src/commands/use.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import {
|
|
2
|
+
import { anyProfileExists, resolveAnyProfile, setDefaultProfile } from '../profiles.js';
|
|
3
3
|
|
|
4
4
|
export function useCommand(program) {
|
|
5
5
|
program
|
|
6
6
|
.command('use <profile>')
|
|
7
7
|
.description('设置默认 profile')
|
|
8
8
|
.action((profile) => {
|
|
9
|
-
|
|
9
|
+
const resolved = resolveAnyProfile(profile);
|
|
10
|
+
if (!resolved) {
|
|
10
11
|
console.log(chalk.red(`Profile "${profile}" 不存在`));
|
|
11
12
|
console.log(chalk.yellow(`使用 "ccc list" 查看可用的 profiles`));
|
|
12
13
|
process.exit(1);
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
const typeTag = resolved.type === 'codex' ? chalk.blue('[Codex]') : chalk.magenta('[Claude]');
|
|
17
|
+
setDefaultProfile(resolved.name);
|
|
18
|
+
console.log(chalk.green(`✓ 默认 profile 已设置为 ${typeTag} "${resolved.name}"`));
|
|
17
19
|
});
|
|
18
20
|
}
|
|
19
|
-
|
package/src/config.js
CHANGED
|
@@ -4,6 +4,8 @@ import os from 'os';
|
|
|
4
4
|
// 配置文件存储目录
|
|
5
5
|
export const CONFIG_DIR = path.join(os.homedir(), '.ccc');
|
|
6
6
|
export const PROFILES_DIR = path.join(CONFIG_DIR, 'profiles');
|
|
7
|
+
export const CODEX_PROFILES_DIR = path.join(CONFIG_DIR, 'codex-profiles');
|
|
7
8
|
export const DEFAULT_FILE = path.join(CONFIG_DIR, 'default');
|
|
8
9
|
export const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
|
|
10
|
+
export const CODEX_HOME_PATH = path.join(os.homedir(), '.codex');
|
|
9
11
|
|
package/src/launch.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
getDefaultProfile,
|
|
7
|
-
profileExists,
|
|
8
|
-
|
|
4
|
+
import {
|
|
5
|
+
getAllProfiles,
|
|
6
|
+
getDefaultProfile,
|
|
7
|
+
profileExists,
|
|
8
|
+
codexProfileExists,
|
|
9
|
+
getProfilePath,
|
|
10
|
+
getCodexProfileDir,
|
|
11
|
+
getCodexProfileCredentials
|
|
9
12
|
} from './profiles.js';
|
|
10
13
|
|
|
11
14
|
// 启动 claude
|
|
@@ -37,22 +40,73 @@ export function launchClaude(profileName, dangerouslySkipPermissions = false) {
|
|
|
37
40
|
});
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
// 启动 codex
|
|
44
|
+
export function launchCodex(profileName, dangerouslySkipPermissions = false) {
|
|
45
|
+
const codexHome = getCodexProfileDir(profileName);
|
|
46
|
+
|
|
47
|
+
if (!codexProfileExists(profileName)) {
|
|
48
|
+
console.log(chalk.red(`Profile "${profileName}" 不存在`));
|
|
49
|
+
console.log(chalk.yellow(`使用 "ccc list" 查看可用的 profiles`));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { baseUrl } = getCodexProfileCredentials(profileName);
|
|
54
|
+
|
|
55
|
+
const args = [];
|
|
56
|
+
if (dangerouslySkipPermissions) {
|
|
57
|
+
args.push('--full-auto');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 构建进程环境变量
|
|
61
|
+
const env = { ...process.env, CODEX_HOME: codexHome };
|
|
62
|
+
if (baseUrl && baseUrl !== 'https://api.openai.com/v1') {
|
|
63
|
+
env.OPENAI_BASE_URL = baseUrl;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(chalk.green(`启动 Codex,使用配置: ${profileName}`));
|
|
67
|
+
console.log(chalk.gray(`CODEX_HOME=${codexHome} codex ${args.join(' ')}`));
|
|
68
|
+
|
|
69
|
+
const child = spawn('codex', args, {
|
|
70
|
+
stdio: 'inherit',
|
|
71
|
+
shell: true,
|
|
72
|
+
env
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
child.on('error', (err) => {
|
|
76
|
+
console.log(chalk.red(`启动失败: ${err.message}`));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 根据 profile 类型自动选择启动方式
|
|
82
|
+
export function launchProfile(profileName, type, dangerouslySkipPermissions = false) {
|
|
83
|
+
if (type === 'codex') {
|
|
84
|
+
launchCodex(profileName, dangerouslySkipPermissions);
|
|
85
|
+
} else {
|
|
86
|
+
launchClaude(profileName, dangerouslySkipPermissions);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
40
90
|
// 交互式选择 profile
|
|
41
91
|
export async function selectProfile(dangerouslySkipPermissions = false) {
|
|
42
|
-
const
|
|
92
|
+
const allProfiles = getAllProfiles();
|
|
43
93
|
|
|
44
|
-
if (
|
|
94
|
+
if (allProfiles.length === 0) {
|
|
45
95
|
console.log(chalk.yellow('没有可用的 profiles'));
|
|
46
|
-
console.log(chalk.gray('使用 "ccc
|
|
96
|
+
console.log(chalk.gray('使用 "ccc new" 创建配置'));
|
|
47
97
|
process.exit(0);
|
|
48
98
|
}
|
|
49
99
|
|
|
50
100
|
const defaultProfile = getDefaultProfile();
|
|
51
101
|
|
|
52
|
-
const choices =
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
102
|
+
const choices = allProfiles.map((p, index) => {
|
|
103
|
+
const typeTag = p.type === 'codex' ? chalk.blue('[Codex]') : chalk.magenta('[Claude]');
|
|
104
|
+
const isDefault = p.name === defaultProfile;
|
|
105
|
+
const label = isDefault
|
|
106
|
+
? `${index + 1}. ${typeTag} ${p.name} ${chalk.green('(默认)')}`
|
|
107
|
+
: `${index + 1}. ${typeTag} ${p.name}`;
|
|
108
|
+
return { name: label, value: p };
|
|
109
|
+
});
|
|
56
110
|
|
|
57
111
|
const { profile } = await inquirer.prompt([
|
|
58
112
|
{
|
|
@@ -60,10 +114,9 @@ export async function selectProfile(dangerouslySkipPermissions = false) {
|
|
|
60
114
|
name: 'profile',
|
|
61
115
|
message: '选择要使用的配置:',
|
|
62
116
|
choices,
|
|
63
|
-
default: defaultProfile
|
|
117
|
+
default: allProfiles.findIndex(p => p.name === defaultProfile)
|
|
64
118
|
}
|
|
65
119
|
]);
|
|
66
120
|
|
|
67
|
-
|
|
121
|
+
launchProfile(profile.name, profile.type, dangerouslySkipPermissions);
|
|
68
122
|
}
|
|
69
|
-
|
package/src/profiles.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
-
import { CONFIG_DIR, PROFILES_DIR, DEFAULT_FILE, CLAUDE_SETTINGS_PATH } from './config.js';
|
|
4
|
+
import { CONFIG_DIR, PROFILES_DIR, CODEX_PROFILES_DIR, DEFAULT_FILE, CLAUDE_SETTINGS_PATH, CODEX_HOME_PATH } from './config.js';
|
|
5
5
|
|
|
6
6
|
function stringifyClaudeSettings(settings) {
|
|
7
7
|
// Claude Code 默认 settings.json 使用 2 空格缩进,并以换行结尾(便于 diff/兼容各平台编辑器)
|
|
@@ -16,6 +16,9 @@ export function ensureDirs() {
|
|
|
16
16
|
if (!fs.existsSync(PROFILES_DIR)) {
|
|
17
17
|
fs.mkdirSync(PROFILES_DIR, { recursive: true });
|
|
18
18
|
}
|
|
19
|
+
if (!fs.existsSync(CODEX_PROFILES_DIR)) {
|
|
20
|
+
fs.mkdirSync(CODEX_PROFILES_DIR, { recursive: true });
|
|
21
|
+
}
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
// 获取所有 profiles(按 a-z 排序)
|
|
@@ -328,3 +331,221 @@ export function clearDefaultProfile() {
|
|
|
328
331
|
fs.unlinkSync(DEFAULT_FILE);
|
|
329
332
|
}
|
|
330
333
|
}
|
|
334
|
+
|
|
335
|
+
// ============================================================
|
|
336
|
+
// Codex Profile 管理
|
|
337
|
+
// ============================================================
|
|
338
|
+
|
|
339
|
+
// 获取 Codex profile 目录路径
|
|
340
|
+
export function getCodexProfileDir(name) {
|
|
341
|
+
return path.join(CODEX_PROFILES_DIR, name);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// 检查 Codex profile 是否存在
|
|
345
|
+
export function codexProfileExists(name) {
|
|
346
|
+
const dir = getCodexProfileDir(name);
|
|
347
|
+
return fs.existsSync(path.join(dir, 'auth.json'));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// 获取所有 Codex profiles(按 a-z 排序)
|
|
351
|
+
export function getCodexProfiles() {
|
|
352
|
+
ensureDirs();
|
|
353
|
+
if (!fs.existsSync(CODEX_PROFILES_DIR)) return [];
|
|
354
|
+
return fs.readdirSync(CODEX_PROFILES_DIR, { withFileTypes: true })
|
|
355
|
+
.filter(d => d.isDirectory() && fs.existsSync(path.join(CODEX_PROFILES_DIR, d.name, 'auth.json')))
|
|
356
|
+
.map(d => d.name)
|
|
357
|
+
.sort((a, b) => a.localeCompare(b, 'zh-CN', { sensitivity: 'base' }));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 读取 Codex profile
|
|
361
|
+
export function readCodexProfile(name) {
|
|
362
|
+
const dir = getCodexProfileDir(name);
|
|
363
|
+
const authPath = path.join(dir, 'auth.json');
|
|
364
|
+
const configPath = path.join(dir, 'config.toml');
|
|
365
|
+
|
|
366
|
+
if (!fs.existsSync(authPath)) return null;
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const auth = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
|
|
370
|
+
const configToml = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf-8') : '';
|
|
371
|
+
return { auth, configToml };
|
|
372
|
+
} catch {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// 保存 Codex profile
|
|
378
|
+
export function saveCodexProfile(name, auth, configToml) {
|
|
379
|
+
ensureDirs();
|
|
380
|
+
const dir = getCodexProfileDir(name);
|
|
381
|
+
if (!fs.existsSync(dir)) {
|
|
382
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
383
|
+
}
|
|
384
|
+
fs.writeFileSync(path.join(dir, 'auth.json'), JSON.stringify(auth, null, 2) + '\n');
|
|
385
|
+
fs.writeFileSync(path.join(dir, 'config.toml'), configToml);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// 生成 Codex config.toml 内容
|
|
389
|
+
export function generateCodexConfigToml(baseUrl, model) {
|
|
390
|
+
let lines = ['# Codex profile managed by ccc'];
|
|
391
|
+
|
|
392
|
+
if (model) {
|
|
393
|
+
lines.push(`model = "${model}"`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (baseUrl && baseUrl !== 'https://api.openai.com/v1') {
|
|
397
|
+
lines.push('');
|
|
398
|
+
lines.push('[model_providers.openai]');
|
|
399
|
+
lines.push(`name = "OpenAI"`);
|
|
400
|
+
lines.push(`base_url = "${baseUrl}"`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
lines.push('');
|
|
404
|
+
return lines.join('\n');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// 创建 Codex profile
|
|
408
|
+
export function createCodexProfile(name, apiKey, baseUrl, model) {
|
|
409
|
+
const auth = {
|
|
410
|
+
auth_mode: 'apikey',
|
|
411
|
+
OPENAI_API_KEY: apiKey
|
|
412
|
+
};
|
|
413
|
+
const configToml = generateCodexConfigToml(baseUrl, model);
|
|
414
|
+
saveCodexProfile(name, auth, configToml);
|
|
415
|
+
return { auth, configToml };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// 获取 Codex profile 的凭证
|
|
419
|
+
export function getCodexProfileCredentials(name) {
|
|
420
|
+
const profile = readCodexProfile(name);
|
|
421
|
+
if (!profile) return { apiKey: '', baseUrl: '', model: '' };
|
|
422
|
+
|
|
423
|
+
const apiKey = profile.auth?.OPENAI_API_KEY || '';
|
|
424
|
+
|
|
425
|
+
// 从 config.toml 解析 base_url 和 model
|
|
426
|
+
let baseUrl = '';
|
|
427
|
+
let model = '';
|
|
428
|
+
if (profile.configToml) {
|
|
429
|
+
const baseUrlMatch = profile.configToml.match(/base_url\s*=\s*"([^"]+)"/);
|
|
430
|
+
if (baseUrlMatch) baseUrl = baseUrlMatch[1];
|
|
431
|
+
const modelMatch = profile.configToml.match(/^model\s*=\s*"([^"]+)"/m);
|
|
432
|
+
if (modelMatch) model = modelMatch[1];
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return { apiKey, baseUrl: baseUrl || 'https://api.openai.com/v1', model: model || '' };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// 删除 Codex profile
|
|
439
|
+
export function deleteCodexProfile(name) {
|
|
440
|
+
const dir = getCodexProfileDir(name);
|
|
441
|
+
if (fs.existsSync(dir)) {
|
|
442
|
+
fs.rmSync(dir, { recursive: true });
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// 同步 Codex profile(从 ~/.codex/ 模板同步,保留 API 凭证)
|
|
447
|
+
export function syncCodexProfileWithTemplate(name) {
|
|
448
|
+
const templateConfigPath = path.join(CODEX_HOME_PATH, 'config.toml');
|
|
449
|
+
if (!fs.existsSync(templateConfigPath)) return null;
|
|
450
|
+
|
|
451
|
+
const current = readCodexProfile(name);
|
|
452
|
+
if (!current) return null;
|
|
453
|
+
|
|
454
|
+
// 读取模板 config.toml
|
|
455
|
+
let templateConfig = fs.readFileSync(templateConfigPath, 'utf-8');
|
|
456
|
+
|
|
457
|
+
// 保留当前 profile 的 base_url 和 model
|
|
458
|
+
const { baseUrl, model } = getCodexProfileCredentials(name);
|
|
459
|
+
|
|
460
|
+
// 在模板基础上覆盖 base_url 和 model
|
|
461
|
+
// 如果当前 profile 有自定义 base_url,追加到模板
|
|
462
|
+
if (baseUrl && baseUrl !== 'https://api.openai.com/v1') {
|
|
463
|
+
// 检查模板是否已有 [model_providers.openai] 节
|
|
464
|
+
if (templateConfig.includes('[model_providers.openai]')) {
|
|
465
|
+
templateConfig = templateConfig.replace(
|
|
466
|
+
/(\[model_providers\.openai\][^\[]*?)base_url\s*=\s*"[^"]*"/,
|
|
467
|
+
`$1base_url = "${baseUrl}"`
|
|
468
|
+
);
|
|
469
|
+
} else {
|
|
470
|
+
templateConfig += `\n[model_providers.openai]\nbase_url = "${baseUrl}"\n`;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (model) {
|
|
475
|
+
if (templateConfig.match(/^model\s*=/m)) {
|
|
476
|
+
templateConfig = templateConfig.replace(/^model\s*=\s*"[^"]*"/m, `model = "${model}"`);
|
|
477
|
+
} else {
|
|
478
|
+
templateConfig = `model = "${model}"\n` + templateConfig;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
saveCodexProfile(name, current.auth, templateConfig);
|
|
483
|
+
return { auth: current.auth, configToml: templateConfig };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// 应用 Claude profile 到 ~/.claude/settings.json
|
|
487
|
+
export function applyClaudeProfile(name) {
|
|
488
|
+
const profile = readProfile(name);
|
|
489
|
+
if (!profile) return false;
|
|
490
|
+
fs.writeFileSync(CLAUDE_SETTINGS_PATH, stringifyClaudeSettings(profile));
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// 应用 Codex profile 到 ~/.codex/
|
|
495
|
+
export function applyCodexProfile(name) {
|
|
496
|
+
const profile = readCodexProfile(name);
|
|
497
|
+
if (!profile) return false;
|
|
498
|
+
|
|
499
|
+
if (!fs.existsSync(CODEX_HOME_PATH)) {
|
|
500
|
+
fs.mkdirSync(CODEX_HOME_PATH, { recursive: true });
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// 写入 auth.json
|
|
504
|
+
fs.writeFileSync(
|
|
505
|
+
path.join(CODEX_HOME_PATH, 'auth.json'),
|
|
506
|
+
JSON.stringify(profile.auth, null, 2) + '\n'
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
// 写入 config.toml(如果有内容)
|
|
510
|
+
if (profile.configToml && profile.configToml.trim()) {
|
|
511
|
+
fs.writeFileSync(path.join(CODEX_HOME_PATH, 'config.toml'), profile.configToml);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ============================================================
|
|
518
|
+
// 统一 Profile 管理(Claude + Codex 混合)
|
|
519
|
+
// ============================================================
|
|
520
|
+
|
|
521
|
+
// 获取所有 profiles(混合 Claude 和 Codex),按名称排序
|
|
522
|
+
export function getAllProfiles() {
|
|
523
|
+
const claudeProfiles = getProfiles().map(name => ({ name, type: 'claude' }));
|
|
524
|
+
const codexProfiles = getCodexProfiles().map(name => ({ name, type: 'codex' }));
|
|
525
|
+
return [...claudeProfiles, ...codexProfiles]
|
|
526
|
+
.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN', { sensitivity: 'base' }));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// 根据序号或名称解析 profile(统一)
|
|
530
|
+
export function resolveAnyProfile(input) {
|
|
531
|
+
const all = getAllProfiles();
|
|
532
|
+
const map = {};
|
|
533
|
+
all.forEach((p, i) => { map[i + 1] = p; });
|
|
534
|
+
|
|
535
|
+
// 尝试作为数字序号
|
|
536
|
+
const num = parseInt(input, 10);
|
|
537
|
+
if (!isNaN(num) && map[num]) {
|
|
538
|
+
return map[num];
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// 作为名称查找
|
|
542
|
+
const found = all.find(p => p.name === input);
|
|
543
|
+
return found || null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// 检查任意类型 profile 是否存在
|
|
547
|
+
export function anyProfileExists(name) {
|
|
548
|
+
if (profileExists(name)) return { exists: true, type: 'claude' };
|
|
549
|
+
if (codexProfileExists(name)) return { exists: true, type: 'codex' };
|
|
550
|
+
return { exists: false, type: null };
|
|
551
|
+
}
|