@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/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 +12 -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/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
|
+
}
|