@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.
@@ -1,11 +1,15 @@
1
1
  import chalk from 'chalk';
2
2
  import inquirer from 'inquirer';
3
- import {
4
- getProfiles,
5
- getDefaultProfile,
6
- profileExists,
7
- getProfilePath,
8
- readProfile
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 profiles = getProfiles();
21
+ const allProfiles = getAllProfiles();
18
22
 
19
- if (profiles.length === 0) {
23
+ if (allProfiles.length === 0) {
20
24
  console.log(chalk.yellow('没有可用的 profiles'));
21
25
  process.exit(0);
22
26
  }
23
27
 
24
- // 如果没有指定 profile,交互选择
28
+ let profileInfo;
29
+
25
30
  if (!profile) {
26
31
  const defaultProfile = getDefaultProfile();
27
- const { selectedProfile } = await inquirer.prompt([
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: 'selectedProfile',
40
+ name: 'selected',
31
41
  message: '选择要查看的配置:',
32
- choices: profiles,
33
- default: defaultProfile
42
+ choices,
43
+ default: allProfiles.findIndex(p => p.name === defaultProfile)
34
44
  }
35
45
  ]);
36
- profile = selectedProfile;
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
- if (!profileExists(profile)) {
40
- console.log(chalk.red(`Profile "${profile}" 不存在`));
41
- process.exit(1);
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
- const profilePath = getProfilePath(profile);
45
- const settings = readProfile(profile);
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
- console.log(chalk.cyan.bold(`\n Profile: ${profile}`) + (isDefault ? chalk.green(' (默认)') : ''));
49
- console.log(chalk.gray(` 路径: ${profilePath}\n`));
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
- Object.entries(settings).forEach(([key, value]) => {
53
- const formattedValue = formatValue(key, value);
54
- if ((key === 'apiKey' || key === 'ANTHROPIC_AUTH_TOKEN') && 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)}`);
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
-
@@ -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
- resolveProfile
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('同步 ~/.claude/settings.json 到 profile(保留 API 凭证)')
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
- const profiles = getProfiles();
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
- // 同步所有 profiles
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: `确定要同步所有 ${profiles.length} 个 profiles 吗?`,
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 profiles) {
52
- const result = syncProfileWithTemplate(p);
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
- console.log(chalk.green(` ✓ ${p}`));
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}/${profiles.length} 个 profiles`));
76
+ console.log(chalk.green(`\n✓ 已同步 ${successCount}/${allProfiles.length} 个 profiles`));
62
77
  return;
63
78
  }
64
79
 
65
- // 同步单个 profile
80
+ // 同步单个
81
+ let profileInfo;
82
+
66
83
  if (!profile) {
67
- const { selectedProfile } = await inquirer.prompt([
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: 'selectedProfile',
92
+ name: 'selected',
71
93
  message: '选择要同步的配置:',
72
- choices: profiles
94
+ choices
73
95
  }
74
96
  ]);
75
- profile = selectedProfile;
97
+ profileInfo = selected;
76
98
  } else {
77
- const resolved = resolveProfile(profile);
78
- if (!resolved) {
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
- const result = syncProfileWithTemplate(profile);
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 "${profile}" 已同步(保留了 API 凭证)`));
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);
@@ -1,19 +1,20 @@
1
1
  import chalk from 'chalk';
2
- import { profileExists, setDefaultProfile } from '../profiles.js';
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
- if (!profileExists(profile)) {
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
- setDefaultProfile(profile);
16
- console.log(chalk.green(`✓ 默认 profile 已设置为 "${profile}"`));
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
- getProfiles,
6
- getDefaultProfile,
7
- profileExists,
8
- getProfilePath
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 profiles = getProfiles();
92
+ const allProfiles = getAllProfiles();
43
93
 
44
- if (profiles.length === 0) {
94
+ if (allProfiles.length === 0) {
45
95
  console.log(chalk.yellow('没有可用的 profiles'));
46
- console.log(chalk.gray('使用 "ccc import" 导入配置'));
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 = profiles.map((p, index) => ({
53
- name: p === defaultProfile ? `${index + 1}. ${p} ${chalk.green('(默认)')}` : `${index + 1}. ${p}`,
54
- value: p
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
- launchClaude(profile, dangerouslySkipPermissions);
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
+ }