@tkpdx01/ccc 1.6.5 → 1.6.6

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 CHANGED
@@ -40,6 +40,7 @@ ccc delete [profile] # Delete profile
40
40
  ccc sync [profile] # Sync from template, preserve credentials
41
41
  ccc sync --all # Sync all profiles
42
42
  ccc apply [profile] # Write profile config to ~/.claude or ~/.codex
43
+ ccc resettodefault # Restore pre-apply ~/.codex and clean OPENAI env exports
43
44
  ```
44
45
 
45
46
  ### WebDAV Cloud Sync
@@ -69,7 +70,8 @@ Each profile is a directory containing `auth.json` + `config.toml`. Launched via
69
70
  CODEX_HOME=~/.ccc/codex-profiles/<name>/ codex
70
71
  ```
71
72
 
72
- No global environment variables are modified — everything is process-scoped.
73
+ `ccc <profile>` 启动仍是进程级环境变量,不污染全局。
74
+ `ccc apply`(Codex)会同步 `OPENAI_BASE_URL` / `OPENAI_API_KEY` 到 shell rc,并可用 `ccc resettodefault` 回滚。
73
75
 
74
76
  ### Storage
75
77
 
@@ -89,9 +91,9 @@ No global environment variables are modified — everything is process-scoped.
89
91
  - **Dual CLI support** — Claude Code + OpenAI Codex in one tool
90
92
  - **Unified index** — All profiles sorted together, launch by number
91
93
  - **Apply command** — Push a profile's config to `~/.claude` or `~/.codex`
94
+ - **Reset to default** — Restore pre-apply Codex config and shell env exports
92
95
  - **Template sync** — Update from main settings, keep credentials
93
96
  - **Cloud sync** — E2E encrypted WebDAV sync across devices
94
- - **Zero env pollution** — API keys stored in config files, not shell env
95
97
 
96
98
  ## Security
97
99
 
package/index.js CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  deleteCommand,
18
18
  syncCommand,
19
19
  applyCommand,
20
+ resetToDefaultCommand,
20
21
  webdavCommand,
21
22
  helpCommand
22
23
  } from './src/commands/index.js';
@@ -27,7 +28,7 @@ const program = new Command();
27
28
  program
28
29
  .name('ccc')
29
30
  .description('Claude Code / Codex Settings Launcher - 管理多个 Claude Code 和 Codex 配置文件')
30
- .version('1.6.0');
31
+ .version('1.6.6');
31
32
 
32
33
  // 注册所有命令
33
34
  listCommand(program);
@@ -38,6 +39,7 @@ editCommand(program);
38
39
  deleteCommand(program);
39
40
  syncCommand(program);
40
41
  applyCommand(program);
42
+ resetToDefaultCommand(program);
41
43
  webdavCommand(program);
42
44
  helpCommand(program);
43
45
 
@@ -51,7 +53,7 @@ program
51
53
 
52
54
  if (profile) {
53
55
  // 检查是否是子命令
54
- if (['list', 'ls', 'use', 'show', 'new', 'edit', 'delete', 'rm', 'sync', 'apply', 'webdav', 'help'].includes(profile)) {
56
+ if (['list', 'ls', 'use', 'show', 'new', 'edit', 'delete', 'rm', 'sync', 'apply', 'resettodefault', 'webdav', 'help'].includes(profile)) {
55
57
  return; // 让子命令处理
56
58
  }
57
59
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tkpdx01/ccc",
3
- "version": "1.6.5",
3
+ "version": "1.6.6",
4
4
  "description": "Claude Code / Codex Settings Launcher - Manage multiple Claude Code and Codex profiles",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -69,8 +69,21 @@ export function applyCommand(program) {
69
69
  result = applyClaudeProfile(profileInfo.name);
70
70
  }
71
71
 
72
- if (result) {
72
+ const success = profileInfo.type === 'codex'
73
+ ? Boolean(result && (result.success ?? result))
74
+ : Boolean(result);
75
+
76
+ if (success) {
73
77
  console.log(chalk.green(`\n✓ ${typeLabel} 配置 "${profileInfo.name}" 已应用到 ${targetDir}`));
78
+
79
+ if (profileInfo.type === 'codex' && result?.envSync?.filePath) {
80
+ const home = process.env.HOME || '';
81
+ const rcPathDisplay = home && result.envSync.filePath.startsWith(home)
82
+ ? `~${result.envSync.filePath.slice(home.length)}`
83
+ : result.envSync.filePath;
84
+ console.log(chalk.gray(` 已同步 OPENAI_BASE_URL / OPENAI_API_KEY 到 ${rcPathDisplay}`));
85
+ console.log(chalk.gray(` 当前终端可执行: source ${rcPathDisplay}`));
86
+ }
74
87
  } else {
75
88
  console.log(chalk.red(`\n✗ 应用失败`));
76
89
  process.exit(1);
@@ -19,6 +19,7 @@ export function showHelp() {
19
19
  console.log(chalk.gray(' ccc sync [profile] ') + '从模板同步配置(保留 API 凭证)');
20
20
  console.log(chalk.gray(' ccc sync --all ') + '同步所有配置');
21
21
  console.log(chalk.gray(' ccc apply [profile] ') + '将配置应用到默认目录(~/.claude 或 ~/.codex)');
22
+ console.log(chalk.gray(' ccc resettodefault ') + '恢复 apply 前的 ~/.codex 配置并移除 OPENAI 环境变量');
22
23
  console.log(chalk.gray(' ccc edit [profile] ') + '编辑配置');
23
24
  console.log(chalk.gray(' ccc delete, rm [name] ') + '删除配置');
24
25
  console.log(chalk.gray(' ccc help ') + '显示此帮助信息');
@@ -6,6 +6,6 @@ export { editCommand } from './edit.js';
6
6
  export { deleteCommand } from './delete.js';
7
7
  export { syncCommand } from './sync.js';
8
8
  export { applyCommand } from './apply.js';
9
+ export { resetToDefaultCommand } from './resettodefault.js';
9
10
  export { webdavCommand } from './webdav.js';
10
11
  export { helpCommand, showHelp } from './help.js';
11
-
@@ -25,6 +25,7 @@ const RESERVED_PROFILE_NAMES = [
25
25
  'rm',
26
26
  'sync',
27
27
  'apply',
28
+ 'resettodefault',
28
29
  'webdav',
29
30
  'help'
30
31
  ];
@@ -0,0 +1,42 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import { resetCodexDefaultProfile } from '../profiles.js';
4
+
5
+ export function resetToDefaultCommand(program) {
6
+ program
7
+ .command('resettodefault')
8
+ .description('恢复 apply 前的 ~/.codex 配置,并移除 OPENAI 相关环境变量')
9
+ .action(async () => {
10
+ const { confirm } = await inquirer.prompt([
11
+ {
12
+ type: 'confirm',
13
+ name: 'confirm',
14
+ message: '恢复 ~/.codex 到 apply 前状态,并清理 OPENAI 环境变量?',
15
+ default: false
16
+ }
17
+ ]);
18
+
19
+ if (!confirm) {
20
+ console.log(chalk.yellow('已取消'));
21
+ process.exit(0);
22
+ }
23
+
24
+ const result = resetCodexDefaultProfile();
25
+ if (!result.success) {
26
+ if (result.reason === 'no_backup') {
27
+ console.log(chalk.yellow('未找到可恢复的备份(请先执行一次 ccc apply <codex-profile>)'));
28
+ process.exit(0);
29
+ }
30
+ console.log(chalk.red('恢复失败:备份状态文件损坏'));
31
+ process.exit(1);
32
+ }
33
+
34
+ const home = process.env.HOME || '';
35
+ const rcPathDisplay = home && result.shellRcPath.startsWith(home)
36
+ ? `~${result.shellRcPath.slice(home.length)}`
37
+ : result.shellRcPath;
38
+
39
+ console.log(chalk.green('✓ 已恢复 ~/.codex 原始配置'));
40
+ console.log(chalk.green(`✓ 已清理/还原 ${rcPathDisplay} 中的 OPENAI 环境变量`));
41
+ });
42
+ }
package/src/profiles.js CHANGED
@@ -338,6 +338,10 @@ export function clearDefaultProfile() {
338
338
 
339
339
  const OPENAI_DEFAULT_BASE_URL = 'https://api.openai.com/v1';
340
340
  const CCC_OPENAI_COMPAT_PROVIDER = 'ccc_openai';
341
+ const CODEX_RESET_DIR = path.join(CODEX_HOME_PATH, '.ccc-reset-default');
342
+ const CODEX_RESET_AUTH_BACKUP = path.join(CODEX_RESET_DIR, 'auth.json.original');
343
+ const CODEX_RESET_CONFIG_BACKUP = path.join(CODEX_RESET_DIR, 'config.toml.original');
344
+ const CODEX_RESET_META_PATH = path.join(CODEX_RESET_DIR, 'meta.json');
341
345
 
342
346
  function normalizeBaseUrl(baseUrl) {
343
347
  return (baseUrl || '').trim().replace(/\/+$/, '');
@@ -362,6 +366,132 @@ function upsertTomlKey(block, key, valueLiteral) {
362
366
  return `${block.trimEnd()}\n${key} = ${valueLiteral}\n`;
363
367
  }
364
368
 
369
+ function escapeRegExp(value) {
370
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
371
+ }
372
+
373
+ function escapeShellSingleQuote(value) {
374
+ return String(value ?? '').replace(/'/g, `'\"'\"'`);
375
+ }
376
+
377
+ function getPreferredShellRcPath() {
378
+ const shellPath = (process.env.SHELL || '').toLowerCase();
379
+ if (shellPath.includes('zsh')) {
380
+ return path.join(os.homedir(), '.zshrc');
381
+ }
382
+ if (shellPath.includes('bash')) {
383
+ return path.join(os.homedir(), '.bashrc');
384
+ }
385
+ return path.join(os.homedir(), '.profile');
386
+ }
387
+
388
+ function stripShellExport(content, key) {
389
+ const pattern = new RegExp(`^\\s*export\\s+${escapeRegExp(key)}=.*(?:\\r?\\n)?`, 'gm');
390
+ return content.replace(pattern, '');
391
+ }
392
+
393
+ function extractShellExportLine(content, key) {
394
+ const pattern = new RegExp(`^\\s*export\\s+${escapeRegExp(key)}=.*$`, 'm');
395
+ const match = content.match(pattern);
396
+ return match ? match[0] : '';
397
+ }
398
+
399
+ function upsertShellExport(content, key, rawValue) {
400
+ if (rawValue === undefined || rawValue === null || rawValue === '') {
401
+ return { content, changed: false };
402
+ }
403
+
404
+ const line = `export ${key}='${escapeShellSingleQuote(rawValue)}'`;
405
+ let nextContent = stripShellExport(content, key);
406
+ if (nextContent && !nextContent.endsWith('\n')) {
407
+ nextContent += '\n';
408
+ }
409
+ nextContent += `${line}\n`;
410
+ return { content: nextContent, changed: nextContent !== content };
411
+ }
412
+
413
+ function upsertShellExportLine(content, key, line) {
414
+ let nextContent = stripShellExport(content, key);
415
+ if (!line) {
416
+ return { content: nextContent, changed: nextContent !== content };
417
+ }
418
+ if (nextContent && !nextContent.endsWith('\n')) {
419
+ nextContent += '\n';
420
+ }
421
+ nextContent += `${line}\n`;
422
+ return { content: nextContent, changed: nextContent !== content };
423
+ }
424
+
425
+ function ensureCodexResetBackup(shellRcPath) {
426
+ if (fs.existsSync(CODEX_RESET_META_PATH)) {
427
+ return;
428
+ }
429
+
430
+ if (!fs.existsSync(CODEX_RESET_DIR)) {
431
+ fs.mkdirSync(CODEX_RESET_DIR, { recursive: true });
432
+ }
433
+
434
+ const authPath = path.join(CODEX_HOME_PATH, 'auth.json');
435
+ const configPath = path.join(CODEX_HOME_PATH, 'config.toml');
436
+ const authExisted = fs.existsSync(authPath);
437
+ const configExisted = fs.existsSync(configPath);
438
+
439
+ if (authExisted) {
440
+ fs.copyFileSync(authPath, CODEX_RESET_AUTH_BACKUP);
441
+ }
442
+ if (configExisted) {
443
+ fs.copyFileSync(configPath, CODEX_RESET_CONFIG_BACKUP);
444
+ }
445
+
446
+ const rcContent = fs.existsSync(shellRcPath) ? fs.readFileSync(shellRcPath, 'utf-8') : '';
447
+ const meta = {
448
+ version: 1,
449
+ createdAt: new Date().toISOString(),
450
+ authExisted,
451
+ configExisted,
452
+ shellRcPath,
453
+ originalExports: {
454
+ OPENAI_BASE_URL: extractShellExportLine(rcContent, 'OPENAI_BASE_URL'),
455
+ OPENAI_API_KEY: extractShellExportLine(rcContent, 'OPENAI_API_KEY')
456
+ }
457
+ };
458
+
459
+ fs.writeFileSync(CODEX_RESET_META_PATH, JSON.stringify(meta, null, 2) + '\n');
460
+ }
461
+
462
+ function syncCodexEnvToShell(baseUrl, apiKey, shellRcPath) {
463
+ const rcPath = shellRcPath || getPreferredShellRcPath();
464
+ const current = fs.existsSync(rcPath) ? fs.readFileSync(rcPath, 'utf-8') : '';
465
+
466
+ let next = current;
467
+ let changed = false;
468
+
469
+ const baseUrlResult = upsertShellExport(next, 'OPENAI_BASE_URL', baseUrl);
470
+ next = baseUrlResult.content;
471
+ changed = changed || baseUrlResult.changed;
472
+
473
+ const apiKeyResult = upsertShellExport(next, 'OPENAI_API_KEY', apiKey);
474
+ next = apiKeyResult.content;
475
+ changed = changed || apiKeyResult.changed;
476
+
477
+ if (changed) {
478
+ fs.writeFileSync(rcPath, next);
479
+ }
480
+
481
+ return { filePath: rcPath, changed };
482
+ }
483
+
484
+ function normalizeCodexAuthForApply(auth) {
485
+ if (!auth || typeof auth !== 'object' || Array.isArray(auth)) {
486
+ return auth;
487
+ }
488
+ const apiKey = typeof auth.OPENAI_API_KEY === 'string' ? auth.OPENAI_API_KEY : '';
489
+ if (apiKey) {
490
+ return { auth_mode: 'apikey', OPENAI_API_KEY: apiKey };
491
+ }
492
+ return auth;
493
+ }
494
+
365
495
  function ensureCodexOpenAICompatConfig(configToml, baseUrl) {
366
496
  if (!isCustomOpenAIBaseUrl(baseUrl)) {
367
497
  return configToml;
@@ -571,6 +701,13 @@ export function applyCodexProfile(name) {
571
701
  const profile = readCodexProfile(name);
572
702
  if (!profile) return false;
573
703
 
704
+ const shellRcPath = getPreferredShellRcPath();
705
+ const baseUrl = extractBaseUrlFromConfigToml(profile.configToml);
706
+ const apiKey = profile.auth?.OPENAI_API_KEY || '';
707
+
708
+ // 首次 apply 时备份 ~/.codex 与 shell env 现场,供 resettodefault 回滚
709
+ ensureCodexResetBackup(shellRcPath);
710
+
574
711
  if (!fs.existsSync(CODEX_HOME_PATH)) {
575
712
  fs.mkdirSync(CODEX_HOME_PATH, { recursive: true });
576
713
  }
@@ -578,17 +715,75 @@ export function applyCodexProfile(name) {
578
715
  // 写入 auth.json
579
716
  fs.writeFileSync(
580
717
  path.join(CODEX_HOME_PATH, 'auth.json'),
581
- JSON.stringify(profile.auth, null, 2) + '\n'
718
+ JSON.stringify(normalizeCodexAuthForApply(profile.auth), null, 2) + '\n'
582
719
  );
583
720
 
584
721
  // 写入 config.toml(如果有内容)
585
722
  if (profile.configToml && profile.configToml.trim()) {
586
- const baseUrl = extractBaseUrlFromConfigToml(profile.configToml);
587
723
  const compatConfig = ensureCodexOpenAICompatConfig(profile.configToml, baseUrl);
588
724
  fs.writeFileSync(path.join(CODEX_HOME_PATH, 'config.toml'), compatConfig);
589
725
  }
590
726
 
591
- return true;
727
+ const envSync = syncCodexEnvToShell(baseUrl, apiKey, shellRcPath);
728
+
729
+ return { success: true, envSync };
730
+ }
731
+
732
+ // 恢复 apply 前的 ~/.codex 配置,并移除/还原相关 OPENAI 环境变量
733
+ export function resetCodexDefaultProfile() {
734
+ if (!fs.existsSync(CODEX_RESET_META_PATH)) {
735
+ return { success: false, reason: 'no_backup' };
736
+ }
737
+
738
+ let meta;
739
+ try {
740
+ meta = JSON.parse(fs.readFileSync(CODEX_RESET_META_PATH, 'utf-8'));
741
+ } catch {
742
+ return { success: false, reason: 'invalid_backup' };
743
+ }
744
+
745
+ if (!fs.existsSync(CODEX_HOME_PATH)) {
746
+ fs.mkdirSync(CODEX_HOME_PATH, { recursive: true });
747
+ }
748
+
749
+ const authPath = path.join(CODEX_HOME_PATH, 'auth.json');
750
+ const configPath = path.join(CODEX_HOME_PATH, 'config.toml');
751
+
752
+ if (meta.authExisted && fs.existsSync(CODEX_RESET_AUTH_BACKUP)) {
753
+ fs.copyFileSync(CODEX_RESET_AUTH_BACKUP, authPath);
754
+ } else if (fs.existsSync(authPath)) {
755
+ fs.unlinkSync(authPath);
756
+ }
757
+
758
+ if (meta.configExisted && fs.existsSync(CODEX_RESET_CONFIG_BACKUP)) {
759
+ fs.copyFileSync(CODEX_RESET_CONFIG_BACKUP, configPath);
760
+ } else if (fs.existsSync(configPath)) {
761
+ fs.unlinkSync(configPath);
762
+ }
763
+
764
+ const shellRcPath = meta.shellRcPath || getPreferredShellRcPath();
765
+ const rcExists = fs.existsSync(shellRcPath);
766
+ const rcBefore = rcExists ? fs.readFileSync(shellRcPath, 'utf-8') : '';
767
+ let rcAfter = rcBefore;
768
+
769
+ const originalBaseUrlLine = meta.originalExports?.OPENAI_BASE_URL || '';
770
+ const originalApiKeyLine = meta.originalExports?.OPENAI_API_KEY || '';
771
+
772
+ rcAfter = upsertShellExportLine(rcAfter, 'OPENAI_BASE_URL', originalBaseUrlLine).content;
773
+ rcAfter = upsertShellExportLine(rcAfter, 'OPENAI_API_KEY', originalApiKeyLine).content;
774
+
775
+ const envChanged = rcAfter !== rcBefore;
776
+ if (envChanged || (!rcExists && (originalBaseUrlLine || originalApiKeyLine))) {
777
+ fs.writeFileSync(shellRcPath, rcAfter);
778
+ }
779
+
780
+ fs.rmSync(CODEX_RESET_DIR, { recursive: true, force: true });
781
+
782
+ return {
783
+ success: true,
784
+ shellRcPath,
785
+ envChanged
786
+ };
592
787
  }
593
788
 
594
789
  // ============================================================