@twoer/ccx 0.1.2 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +33 -4
  2. package/dist/bin/ccx.js +137 -71
  3. package/package.json +2 -3
package/README.md CHANGED
@@ -7,7 +7,7 @@ $ ccx
7
7
 
8
8
  ┌ ⚡ ccx — Claude Code eXecutor
9
9
 
10
- │ 5 providers from cc-switch · v0.1.0
10
+ │ 5 providers from cc-switch · v0.2.0
11
11
  │ ccx add Add provider ccx edit Edit provider
12
12
  │ ccx list List providers ccx rm Remove provider
13
13
  │ ccx -n New window ccx help Show help
@@ -19,6 +19,24 @@ $ ccx
19
19
 
20
20
  ```
21
21
 
22
+ ## 为什么做 ccx
23
+
24
+ 日常使用 Claude Code 时,大部分时间用 Claude Official,但偶尔需要临时切换到其他 provider(如 GLM)。
25
+
26
+ 用 cc-switch 切换存在两个问题:
27
+
28
+ **1. 污染全局配置**
29
+
30
+ cc-switch 直接修改 `~/.claude/settings.json`,把 `ANTHROPIC_BASE_URL`、`ANTHROPIC_AUTH_TOKEN` 等写入全局配置。这意味着无法同时使用多个 provider——全局配置只有一份,切换后会影响所有正在运行的 Claude Code 会话。
31
+
32
+ ccx 通过临时文件注入:每次启动写入临时 `settings.json`,通过 `claude --settings <tmpfile>` 传入,退出后自动清理,全局配置始终保持不变。
33
+
34
+ **2. 打开新终端不可靠**
35
+
36
+ cc-switch 提供了"在新终端中打开"的功能,但实际使用中发现:如果已经在 Ghostty 中运行了 Claude Code,再次点击"打开终端"并不会新开一个 Ghostty 窗口,而是激活当前已有的窗口,无法实现多 provider 并行使用。
37
+
38
+ ccx 使用终端原生 API 创建新窗口,确保每次都打开独立终端。
39
+
22
40
  ## 安装
23
41
 
24
42
  ```bash
@@ -27,9 +45,9 @@ npm i -g @twoer/ccx
27
45
 
28
46
  ### 环境要求
29
47
 
30
- - macOS
31
48
  - Node.js >= 18
32
49
  - [Claude Code](https://docs.anthropic.com/en/docs/claude-code)
50
+ - 操作系统:macOS / Windows / Linux
33
51
 
34
52
  ## 使用
35
53
 
@@ -69,7 +87,10 @@ ccx 会自动检测可用的数据源:
69
87
 
70
88
  ### 2. JSON 文件(手动配置)
71
89
 
72
- 手动创建 `~/.config/ccx/providers.json`,或使用 `ccx add` 交互式创建:
90
+ 使用 `ccx add` 交互式创建,或手动创建配置文件:
91
+
92
+ - macOS / Linux:`~/.config/ccx/providers.json`
93
+ - Windows:`%APPDATA%/ccx/providers.json`
73
94
 
74
95
  ```json
75
96
  {
@@ -91,13 +112,21 @@ ccx 会自动检测可用的数据源:
91
112
 
92
113
  使用 `ccx --new` 时,首次运行会自动检测已安装的终端:
93
114
 
115
+ **macOS:**
94
116
  - Ghostty
95
117
  - iTerm2
96
118
  - Warp
97
119
  - kitty
98
120
  - Terminal.app
99
121
 
100
- 配置文件:`~/.config/ccx/config.json`
122
+ **Windows:**
123
+ - Windows Terminal
124
+ - PowerShell
125
+
126
+ **Linux:**
127
+ - gnome-terminal
128
+ - konsole
129
+ - xterm
101
130
 
102
131
  ## License
103
132
 
package/dist/bin/ccx.js CHANGED
@@ -10,7 +10,7 @@ import { intro as intro2, outro as outro2, select as select2, cancel as cancel2,
10
10
  import pc2 from "picocolors";
11
11
  import { writeFileSync as writeFileSync3, mkdtempSync, rmSync } from "fs";
12
12
  import { join as join5 } from "path";
13
- import { tmpdir } from "os";
13
+ import { tmpdir, platform as platform3 } from "os";
14
14
  import { spawn } from "child_process";
15
15
 
16
16
  // src/providers/cc-switch.ts
@@ -20,7 +20,7 @@ __export(cc_switch_exports, {
20
20
  list: () => list,
21
21
  source: () => source
22
22
  });
23
- import { existsSync } from "fs";
23
+ import { existsSync, readFileSync } from "fs";
24
24
  import { homedir } from "os";
25
25
  import { join } from "path";
26
26
  var DB_PATH = join(homedir(), ".cc-switch", "cc-switch.db");
@@ -28,16 +28,23 @@ function detect() {
28
28
  return existsSync(DB_PATH);
29
29
  }
30
30
  async function list() {
31
- const Database = (await import("better-sqlite3")).default;
32
- const db = new Database(DB_PATH, { readonly: true });
31
+ const initSqlJs = (await import("sql.js")).default;
32
+ const SQL = await initSqlJs();
33
+ const buf = readFileSync(DB_PATH);
34
+ const db = new SQL.Database(buf);
33
35
  try {
34
- const rows = db.prepare(`
36
+ const results = db.exec(`
35
37
  SELECT id, name, json_extract(settings_config, '$.env.ANTHROPIC_MODEL') as model,
36
38
  json_extract(settings_config, '$.env') as env
37
39
  FROM providers
38
40
  WHERE app_type = 'claude' AND settings_config LIKE '%"env"%'
39
41
  ORDER BY sort_index
40
- `).all();
42
+ `);
43
+ if (!results[0]) return [];
44
+ const columns = results[0].columns;
45
+ const rows = results[0].values.map(
46
+ (row) => Object.fromEntries(columns.map((col, i) => [col, row[i]]))
47
+ );
41
48
  return rows.map((row) => ({
42
49
  id: row.id,
43
50
  name: row.name,
@@ -58,19 +65,60 @@ __export(json_file_exports, {
58
65
  list: () => list2,
59
66
  source: () => source2
60
67
  });
61
- import { existsSync as existsSync2, readFileSync } from "fs";
62
- import { homedir as homedir2 } from "os";
68
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
69
+ import { join as join3 } from "path";
70
+
71
+ // src/config.ts
72
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
73
+ import { homedir as homedir2, platform } from "os";
63
74
  import { join as join2 } from "path";
64
- var PROVIDERS_FILE = join2(
65
- process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config"),
66
- "ccx",
67
- "providers.json"
68
- );
75
+ var getConfigBaseDir = () => {
76
+ if (platform() === "win32") {
77
+ return join2(process.env.APPDATA || join2(homedir2(), "AppData", "Roaming"), "ccx");
78
+ }
79
+ return join2(process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config"), "ccx");
80
+ };
81
+ var CONFIG_DIR = getConfigBaseDir();
82
+ var CONFIG_FILE = join2(CONFIG_DIR, "config.json");
83
+ function ensureDir() {
84
+ if (!existsSync2(CONFIG_DIR)) {
85
+ mkdirSync(CONFIG_DIR, { recursive: true });
86
+ }
87
+ }
88
+ function load() {
89
+ ensureDir();
90
+ if (!existsSync2(CONFIG_FILE)) return {};
91
+ try {
92
+ return JSON.parse(readFileSync2(CONFIG_FILE, "utf-8"));
93
+ } catch {
94
+ return {};
95
+ }
96
+ }
97
+ function save(config) {
98
+ ensureDir();
99
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
100
+ }
101
+ function get(key) {
102
+ return load()[key];
103
+ }
104
+ function set(key, value) {
105
+ const config = load();
106
+ config[key] = value;
107
+ save(config);
108
+ }
109
+ function reset() {
110
+ save({});
111
+ }
112
+ var configDir = CONFIG_DIR;
113
+ var configFile = CONFIG_FILE;
114
+
115
+ // src/providers/json-file.ts
116
+ var PROVIDERS_FILE = join3(getConfigBaseDir(), "providers.json");
69
117
  function detect2() {
70
- return existsSync2(PROVIDERS_FILE);
118
+ return existsSync3(PROVIDERS_FILE);
71
119
  }
72
120
  function list2() {
73
- const data = JSON.parse(readFileSync(PROVIDERS_FILE, "utf-8"));
121
+ const data = JSON.parse(readFileSync3(PROVIDERS_FILE, "utf-8"));
74
122
  const providers = data.providers || [];
75
123
  return providers.map((p, i) => ({
76
124
  id: `json:${i}`,
@@ -100,27 +148,30 @@ async function loadProviders() {
100
148
  }
101
149
 
102
150
  // src/terminals/index.ts
103
- import { existsSync as existsSync3 } from "fs";
151
+ import { existsSync as existsSync4 } from "fs";
104
152
  import { execSync } from "child_process";
105
- var terminals = [
153
+ import { platform as platform2 } from "os";
154
+ var isMac = platform2() === "darwin";
155
+ var isWin = platform2() === "win32";
156
+ var terminals = isMac ? [
106
157
  {
107
158
  name: "Ghostty",
108
- detect: () => existsSync3("/Applications/Ghostty.app"),
159
+ detect: () => existsSync4("/Applications/Ghostty.app"),
109
160
  open: (cmd) => execSync(`open -na Ghostty.app --args -e bash -c "${cmd}"`)
110
161
  },
111
162
  {
112
163
  name: "iTerm2",
113
- detect: () => existsSync3("/Applications/iTerm.app"),
164
+ detect: () => existsSync4("/Applications/iTerm.app"),
114
165
  open: (cmd) => execSync(`osascript -e 'tell application "iTerm" to create window with default profile command "bash -c \\"${cmd}\\""'`)
115
166
  },
116
167
  {
117
168
  name: "Warp",
118
- detect: () => existsSync3("/Applications/Warp.app"),
169
+ detect: () => existsSync4("/Applications/Warp.app"),
119
170
  open: (cmd) => execSync(`open -na Warp.app --args bash -c "${cmd}"`)
120
171
  },
121
172
  {
122
173
  name: "kitty",
123
- detect: () => existsSync3("/Applications/kitty.app"),
174
+ detect: () => existsSync4("/Applications/kitty.app"),
124
175
  open: (cmd) => execSync(`/Applications/kitty.app/Contents/MacOS/kitty bash -c "${cmd}" &`)
125
176
  },
126
177
  {
@@ -128,6 +179,61 @@ var terminals = [
128
179
  detect: () => true,
129
180
  open: (cmd) => execSync(`osascript -e 'tell application "Terminal" to do script "${cmd}"'`)
130
181
  }
182
+ ] : isWin ? [
183
+ {
184
+ name: "Windows Terminal",
185
+ detect: () => {
186
+ try {
187
+ execSync("where wt.exe", { stdio: "pipe" });
188
+ return true;
189
+ } catch {
190
+ return false;
191
+ }
192
+ },
193
+ open: (cmd) => execSync(`wt.exe bash -c "${cmd}"`, { shell: "cmd.exe" })
194
+ },
195
+ {
196
+ name: "PowerShell",
197
+ detect: () => true,
198
+ open: (cmd) => execSync(`start powershell -NoExit -Command "${cmd}"`, { shell: "cmd.exe" })
199
+ }
200
+ ] : [
201
+ {
202
+ name: "gnome-terminal",
203
+ detect: () => {
204
+ try {
205
+ execSync("which gnome-terminal", { stdio: "pipe" });
206
+ return true;
207
+ } catch {
208
+ return false;
209
+ }
210
+ },
211
+ open: (cmd) => execSync(`gnome-terminal -- bash -c "${cmd}"`)
212
+ },
213
+ {
214
+ name: "konsole",
215
+ detect: () => {
216
+ try {
217
+ execSync("which konsole", { stdio: "pipe" });
218
+ return true;
219
+ } catch {
220
+ return false;
221
+ }
222
+ },
223
+ open: (cmd) => execSync(`konsole -e bash -c "${cmd}"`)
224
+ },
225
+ {
226
+ name: "xterm",
227
+ detect: () => {
228
+ try {
229
+ execSync("which xterm", { stdio: "pipe" });
230
+ return true;
231
+ } catch {
232
+ return false;
233
+ }
234
+ },
235
+ open: (cmd) => execSync(`xterm -e bash -c "${cmd}" &`)
236
+ }
131
237
  ];
132
238
  function detectTerminals() {
133
239
  return terminals.filter((t) => t.detect());
@@ -136,55 +242,14 @@ function getTerminal(name) {
136
242
  return terminals.find((t) => t.name === name);
137
243
  }
138
244
 
139
- // src/config.ts
140
- import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
141
- import { homedir as homedir3 } from "os";
142
- import { join as join3 } from "path";
143
- var CONFIG_DIR = join3(process.env.XDG_CONFIG_HOME || join3(homedir3(), ".config"), "ccx");
144
- var CONFIG_FILE = join3(CONFIG_DIR, "config.json");
145
- function ensureDir() {
146
- if (!existsSync4(CONFIG_DIR)) {
147
- mkdirSync(CONFIG_DIR, { recursive: true });
148
- }
149
- }
150
- function load() {
151
- ensureDir();
152
- if (!existsSync4(CONFIG_FILE)) return {};
153
- try {
154
- return JSON.parse(readFileSync2(CONFIG_FILE, "utf-8"));
155
- } catch {
156
- return {};
157
- }
158
- }
159
- function save(config) {
160
- ensureDir();
161
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
162
- }
163
- function get(key) {
164
- return load()[key];
165
- }
166
- function set(key, value) {
167
- const config = load();
168
- config[key] = value;
169
- save(config);
170
- }
171
- function reset() {
172
- save({});
173
- }
174
-
175
245
  // src/commands.ts
176
246
  import { intro, outro, select, text, confirm, cancel, isCancel, log, note } from "@clack/prompts";
177
247
  import pc from "picocolors";
178
248
 
179
249
  // src/providers/manager.ts
180
- import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
181
- import { homedir as homedir4 } from "os";
250
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
182
251
  import { join as join4, dirname } from "path";
183
- var PROVIDERS_FILE2 = join4(
184
- process.env.XDG_CONFIG_HOME || join4(homedir4(), ".config"),
185
- "ccx",
186
- "providers.json"
187
- );
252
+ var PROVIDERS_FILE2 = join4(getConfigBaseDir(), "providers.json");
188
253
  function ensureFile() {
189
254
  const dir = dirname(PROVIDERS_FILE2);
190
255
  if (!existsSync5(dir)) mkdirSync2(dir, { recursive: true });
@@ -194,7 +259,7 @@ function ensureFile() {
194
259
  }
195
260
  function load2() {
196
261
  ensureFile();
197
- return JSON.parse(readFileSync3(PROVIDERS_FILE2, "utf-8"));
262
+ return JSON.parse(readFileSync4(PROVIDERS_FILE2, "utf-8"));
198
263
  }
199
264
  function save2(data) {
200
265
  ensureFile();
@@ -399,7 +464,7 @@ async function edit() {
399
464
  }
400
465
 
401
466
  // src/launcher.ts
402
- var VERSION = "0.1.1";
467
+ var VERSION = "0.2.0";
403
468
  var SUBCOMMANDS = ["list", "ls", "add", "rm", "remove", "edit", "help"];
404
469
  function parseArgs(argv) {
405
470
  const flags = { newWindow: false, help: false, version: false, reset: false, yolo: false };
@@ -470,10 +535,10 @@ function showHelp() {
470
535
  ${pc2.dim("$")} ccx rm ${pc2.dim("# Remove a provider")}
471
536
 
472
537
  ${pc2.bold("Providers:")}
473
- ${pc2.cyan("cc-switch")} ${pc2.dim("auto-detected from ~/.cc-switch/cc-switch.db")}
474
- ${pc2.cyan("JSON file")} ${pc2.dim("configure at ~/.config/ccx/providers.json")}
538
+ ${pc2.cyan("cc-switch")} ${pc2.dim("auto-detected from cc-switch.db")}
539
+ ${pc2.cyan("JSON file")} ${pc2.dim(`configure at ${configDir}/providers.json`)}
475
540
 
476
- ${pc2.bold("Config:")} ${pc2.dim("~/.config/ccx/config.json")}
541
+ ${pc2.bold("Config:")} ${pc2.dim(configFile)}
477
542
  `);
478
543
  }
479
544
  function writeTempSettings(env) {
@@ -583,7 +648,7 @@ async function run(argv) {
583
648
  if (flags.newWindow) {
584
649
  const terminal = await selectTerminal();
585
650
  const cwd = process.cwd();
586
- const cmd = `cd '${cwd}'; echo '=== Claude Code [${selected.name}] ==='; echo; claude --settings '${settingsFile}'${yoloFlag}; rm -f '${settingsFile}'; exec bash`;
651
+ const cmd = platform3() === "win32" ? `cd /d "${cwd}" & echo === Claude Code [${selected.name}] === & echo. & claude --settings "${settingsFile}"${yoloFlag} & del /f "${settingsFile}"` : `cd '${cwd}'; echo '=== Claude Code [${selected.name}] ==='; echo; claude --settings '${settingsFile}'${yoloFlag}; rm -f '${settingsFile}'; exec bash`;
587
652
  terminal.open(cmd);
588
653
  outro2(`${pc2.green("\u26A1")} ${selected.name} ${pc2.dim(`(${selected.model})`)} \u2192 ${pc2.dim(terminal.name)}`);
589
654
  } else {
@@ -592,7 +657,8 @@ async function run(argv) {
592
657
  if (flags.yolo) claudeArgs.push("--dangerously-skip-permissions");
593
658
  const child = spawn("claude", claudeArgs, {
594
659
  stdio: "inherit",
595
- env: { ...process.env }
660
+ env: { ...process.env },
661
+ shell: platform3() === "win32"
596
662
  });
597
663
  child.on("exit", (code) => {
598
664
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twoer/ccx",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Claude Code launcher - switch providers and models with ease",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,11 +24,10 @@
24
24
  "license": "MIT",
25
25
  "dependencies": {
26
26
  "@clack/prompts": "^0.10.0",
27
- "better-sqlite3": "^11.8.0",
27
+ "sql.js": "^1.12.0",
28
28
  "picocolors": "^1.1.1"
29
29
  },
30
30
  "devDependencies": {
31
- "@types/better-sqlite3": "^7.6.13",
32
31
  "@types/node": "^25.5.0",
33
32
  "tsup": "^8.5.1",
34
33
  "typescript": "^6.0.2"