@tkpdx01/ccc 1.5.0 → 1.6.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/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
+ }