@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.
- package/README.md +33 -4
- package/dist/bin/ccx.js +137 -71
- 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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
32
|
-
const
|
|
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
|
|
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
|
-
`)
|
|
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
|
|
62
|
-
import {
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
118
|
+
return existsSync3(PROVIDERS_FILE);
|
|
71
119
|
}
|
|
72
120
|
function list2() {
|
|
73
|
-
const data = JSON.parse(
|
|
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
|
|
151
|
+
import { existsSync as existsSync4 } from "fs";
|
|
104
152
|
import { execSync } from "child_process";
|
|
105
|
-
|
|
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: () =>
|
|
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: () =>
|
|
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: () =>
|
|
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: () =>
|
|
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
|
|
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(
|
|
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.
|
|
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
|
|
474
|
-
${pc2.cyan("JSON file")} ${pc2.dim(
|
|
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(
|
|
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.
|
|
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
|
-
"
|
|
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"
|