@twoer/ccx 0.1.1 → 0.1.3
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 +18 -0
- package/dist/bin/ccx.js +608 -0
- package/package.json +12 -5
- package/bin/ccx.mjs +0 -5
- package/src/commands.mjs +0 -228
- package/src/config.mjs +0 -44
- package/src/launcher.mjs +0 -218
- package/src/providers/cc-switch.mjs +0 -35
- package/src/providers/index.mjs +0 -20
- package/src/providers/json-file.mjs +0 -28
- package/src/providers/manager.mjs +0 -52
- package/src/terminals/index.mjs +0 -38
package/README.md
CHANGED
|
@@ -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 通过临时文件注入:每次启动写入 `/tmp/ccx-xxx/settings.json`,通过 `claude --settings <tmpfile>` 传入,退出后自动清理,全局配置始终保持不变。
|
|
33
|
+
|
|
34
|
+
**2. 打开新终端不可靠**
|
|
35
|
+
|
|
36
|
+
cc-switch 提供了"在新终端中打开"的功能,但实际使用中发现:如果已经在 Ghostty 中运行了 Claude Code,再次点击"打开终端"并不会新开一个 Ghostty 窗口,而是激活当前已有的窗口,无法实现多 provider 并行使用。
|
|
37
|
+
|
|
38
|
+
ccx 使用终端原生 API 直接 `open -na` 或 `osascript` 创建新窗口,确保每次都打开独立终端。
|
|
39
|
+
|
|
22
40
|
## 安装
|
|
23
41
|
|
|
24
42
|
```bash
|
package/dist/bin/ccx.js
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __export = (target, all) => {
|
|
4
|
+
for (var name in all)
|
|
5
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// src/launcher.ts
|
|
9
|
+
import { intro as intro2, outro as outro2, select as select2, cancel as cancel2, isCancel as isCancel2, log as log2 } from "@clack/prompts";
|
|
10
|
+
import pc2 from "picocolors";
|
|
11
|
+
import { writeFileSync as writeFileSync3, mkdtempSync, rmSync } from "fs";
|
|
12
|
+
import { join as join5 } from "path";
|
|
13
|
+
import { tmpdir } from "os";
|
|
14
|
+
import { spawn } from "child_process";
|
|
15
|
+
|
|
16
|
+
// src/providers/cc-switch.ts
|
|
17
|
+
var cc_switch_exports = {};
|
|
18
|
+
__export(cc_switch_exports, {
|
|
19
|
+
detect: () => detect,
|
|
20
|
+
list: () => list,
|
|
21
|
+
source: () => source
|
|
22
|
+
});
|
|
23
|
+
import { existsSync } from "fs";
|
|
24
|
+
import { homedir } from "os";
|
|
25
|
+
import { join } from "path";
|
|
26
|
+
var DB_PATH = join(homedir(), ".cc-switch", "cc-switch.db");
|
|
27
|
+
function detect() {
|
|
28
|
+
return existsSync(DB_PATH);
|
|
29
|
+
}
|
|
30
|
+
async function list() {
|
|
31
|
+
const Database = (await import("better-sqlite3")).default;
|
|
32
|
+
const db = new Database(DB_PATH, { readonly: true });
|
|
33
|
+
try {
|
|
34
|
+
const rows = db.prepare(`
|
|
35
|
+
SELECT id, name, json_extract(settings_config, '$.env.ANTHROPIC_MODEL') as model,
|
|
36
|
+
json_extract(settings_config, '$.env') as env
|
|
37
|
+
FROM providers
|
|
38
|
+
WHERE app_type = 'claude' AND settings_config LIKE '%"env"%'
|
|
39
|
+
ORDER BY sort_index
|
|
40
|
+
`).all();
|
|
41
|
+
return rows.map((row) => ({
|
|
42
|
+
id: row.id,
|
|
43
|
+
name: row.name,
|
|
44
|
+
model: row.model || "unknown",
|
|
45
|
+
env: JSON.parse(row.env || "{}")
|
|
46
|
+
}));
|
|
47
|
+
} finally {
|
|
48
|
+
db.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
var source = "cc-switch";
|
|
52
|
+
|
|
53
|
+
// src/providers/json-file.ts
|
|
54
|
+
var json_file_exports = {};
|
|
55
|
+
__export(json_file_exports, {
|
|
56
|
+
detect: () => detect2,
|
|
57
|
+
filePath: () => filePath,
|
|
58
|
+
list: () => list2,
|
|
59
|
+
source: () => source2
|
|
60
|
+
});
|
|
61
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
62
|
+
import { homedir as homedir2 } from "os";
|
|
63
|
+
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
|
+
);
|
|
69
|
+
function detect2() {
|
|
70
|
+
return existsSync2(PROVIDERS_FILE);
|
|
71
|
+
}
|
|
72
|
+
function list2() {
|
|
73
|
+
const data = JSON.parse(readFileSync(PROVIDERS_FILE, "utf-8"));
|
|
74
|
+
const providers = data.providers || [];
|
|
75
|
+
return providers.map((p, i) => ({
|
|
76
|
+
id: `json:${i}`,
|
|
77
|
+
name: p.name,
|
|
78
|
+
model: p.model || p.env?.ANTHROPIC_MODEL || "unknown",
|
|
79
|
+
env: p.env || {}
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
var source2 = "json";
|
|
83
|
+
var filePath = PROVIDERS_FILE;
|
|
84
|
+
|
|
85
|
+
// src/providers/index.ts
|
|
86
|
+
var sources = [cc_switch_exports, json_file_exports];
|
|
87
|
+
async function loadProviders() {
|
|
88
|
+
for (const source3 of sources) {
|
|
89
|
+
if (source3.detect()) {
|
|
90
|
+
try {
|
|
91
|
+
const providers = await source3.list();
|
|
92
|
+
if (providers.length > 0) {
|
|
93
|
+
return { providers, source: source3.source };
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { providers: [], source: null };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/terminals/index.ts
|
|
103
|
+
import { existsSync as existsSync3 } from "fs";
|
|
104
|
+
import { execSync } from "child_process";
|
|
105
|
+
var terminals = [
|
|
106
|
+
{
|
|
107
|
+
name: "Ghostty",
|
|
108
|
+
detect: () => existsSync3("/Applications/Ghostty.app"),
|
|
109
|
+
open: (cmd) => execSync(`open -na Ghostty.app --args -e bash -c "${cmd}"`)
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "iTerm2",
|
|
113
|
+
detect: () => existsSync3("/Applications/iTerm.app"),
|
|
114
|
+
open: (cmd) => execSync(`osascript -e 'tell application "iTerm" to create window with default profile command "bash -c \\"${cmd}\\""'`)
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "Warp",
|
|
118
|
+
detect: () => existsSync3("/Applications/Warp.app"),
|
|
119
|
+
open: (cmd) => execSync(`open -na Warp.app --args bash -c "${cmd}"`)
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "kitty",
|
|
123
|
+
detect: () => existsSync3("/Applications/kitty.app"),
|
|
124
|
+
open: (cmd) => execSync(`/Applications/kitty.app/Contents/MacOS/kitty bash -c "${cmd}" &`)
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "Terminal",
|
|
128
|
+
detect: () => true,
|
|
129
|
+
open: (cmd) => execSync(`osascript -e 'tell application "Terminal" to do script "${cmd}"'`)
|
|
130
|
+
}
|
|
131
|
+
];
|
|
132
|
+
function detectTerminals() {
|
|
133
|
+
return terminals.filter((t) => t.detect());
|
|
134
|
+
}
|
|
135
|
+
function getTerminal(name) {
|
|
136
|
+
return terminals.find((t) => t.name === name);
|
|
137
|
+
}
|
|
138
|
+
|
|
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
|
+
// src/commands.ts
|
|
176
|
+
import { intro, outro, select, text, confirm, cancel, isCancel, log, note } from "@clack/prompts";
|
|
177
|
+
import pc from "picocolors";
|
|
178
|
+
|
|
179
|
+
// 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";
|
|
182
|
+
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
|
+
);
|
|
188
|
+
function ensureFile() {
|
|
189
|
+
const dir = dirname(PROVIDERS_FILE2);
|
|
190
|
+
if (!existsSync5(dir)) mkdirSync2(dir, { recursive: true });
|
|
191
|
+
if (!existsSync5(PROVIDERS_FILE2)) {
|
|
192
|
+
writeFileSync2(PROVIDERS_FILE2, JSON.stringify({ providers: [] }, null, 2) + "\n");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function load2() {
|
|
196
|
+
ensureFile();
|
|
197
|
+
return JSON.parse(readFileSync3(PROVIDERS_FILE2, "utf-8"));
|
|
198
|
+
}
|
|
199
|
+
function save2(data) {
|
|
200
|
+
ensureFile();
|
|
201
|
+
writeFileSync2(PROVIDERS_FILE2, JSON.stringify(data, null, 2) + "\n");
|
|
202
|
+
}
|
|
203
|
+
function getAll() {
|
|
204
|
+
return load2().providers || [];
|
|
205
|
+
}
|
|
206
|
+
function add(provider) {
|
|
207
|
+
const data = load2();
|
|
208
|
+
data.providers = data.providers || [];
|
|
209
|
+
data.providers.push(provider);
|
|
210
|
+
save2(data);
|
|
211
|
+
}
|
|
212
|
+
function remove(index) {
|
|
213
|
+
const data = load2();
|
|
214
|
+
data.providers.splice(index, 1);
|
|
215
|
+
save2(data);
|
|
216
|
+
}
|
|
217
|
+
function update(index, provider) {
|
|
218
|
+
const data = load2();
|
|
219
|
+
data.providers[index] = provider;
|
|
220
|
+
save2(data);
|
|
221
|
+
}
|
|
222
|
+
var filePath2 = PROVIDERS_FILE2;
|
|
223
|
+
|
|
224
|
+
// src/commands.ts
|
|
225
|
+
function guard(result) {
|
|
226
|
+
if (isCancel(result)) {
|
|
227
|
+
cancel("Cancelled");
|
|
228
|
+
process.exit(0);
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
async function list3() {
|
|
233
|
+
intro(pc.cyan(pc.bold("\u26A1 ccx list")));
|
|
234
|
+
const { providers, source: source3 } = await loadProviders();
|
|
235
|
+
if (providers.length === 0) {
|
|
236
|
+
log.warn("No providers found");
|
|
237
|
+
outro(pc.dim("Run `ccx add` to add one"));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
log.message(pc.dim(`${providers.length} providers from ${source3}`));
|
|
241
|
+
const lines = providers.map((p, i) => {
|
|
242
|
+
const idx = pc.dim(`${String(i + 1).padStart(2)}.`);
|
|
243
|
+
const name = pc.bold(p.name);
|
|
244
|
+
const model = pc.dim(p.model);
|
|
245
|
+
const url = pc.dim(p.env?.ANTHROPIC_BASE_URL || "");
|
|
246
|
+
return `${idx} ${name} ${model}
|
|
247
|
+
${url}`;
|
|
248
|
+
});
|
|
249
|
+
note(lines.join("\n"), "Providers");
|
|
250
|
+
outro(pc.dim(`Config: ${filePath2}`));
|
|
251
|
+
}
|
|
252
|
+
async function add2() {
|
|
253
|
+
intro(pc.cyan(pc.bold("\u26A1 ccx add")));
|
|
254
|
+
const name = guard(await text({
|
|
255
|
+
message: "Provider name",
|
|
256
|
+
placeholder: "e.g. Zhipu GLM-5.1",
|
|
257
|
+
validate: (v) => v.trim() ? void 0 : "Name is required"
|
|
258
|
+
}));
|
|
259
|
+
const baseUrl = guard(await text({
|
|
260
|
+
message: "API base URL",
|
|
261
|
+
placeholder: "e.g. https://open.bigmodel.cn/api/anthropic",
|
|
262
|
+
validate: (v) => v.trim() ? void 0 : "URL is required"
|
|
263
|
+
}));
|
|
264
|
+
const authToken = guard(await text({
|
|
265
|
+
message: "API key / Auth token",
|
|
266
|
+
placeholder: "sk-xxx or your-api-key",
|
|
267
|
+
validate: (v) => v.trim() ? void 0 : "Token is required"
|
|
268
|
+
}));
|
|
269
|
+
const model = guard(await text({
|
|
270
|
+
message: "Model name",
|
|
271
|
+
placeholder: "e.g. glm-5.1, claude-sonnet-4-20250514",
|
|
272
|
+
validate: (v) => v.trim() ? void 0 : "Model is required"
|
|
273
|
+
}));
|
|
274
|
+
const fillAll = guard(await confirm({
|
|
275
|
+
message: "Set this model for all roles (Sonnet/Opus/Haiku)?",
|
|
276
|
+
initialValue: true
|
|
277
|
+
}));
|
|
278
|
+
const env = {
|
|
279
|
+
ANTHROPIC_BASE_URL: baseUrl.trim(),
|
|
280
|
+
ANTHROPIC_AUTH_TOKEN: authToken.trim(),
|
|
281
|
+
ANTHROPIC_MODEL: model.trim()
|
|
282
|
+
};
|
|
283
|
+
if (fillAll) {
|
|
284
|
+
env.ANTHROPIC_DEFAULT_SONNET_MODEL = model.trim();
|
|
285
|
+
env.ANTHROPIC_DEFAULT_OPUS_MODEL = model.trim();
|
|
286
|
+
env.ANTHROPIC_DEFAULT_HAIKU_MODEL = model.trim();
|
|
287
|
+
env.ANTHROPIC_REASONING_MODEL = model.trim();
|
|
288
|
+
}
|
|
289
|
+
const provider = {
|
|
290
|
+
name: name.trim(),
|
|
291
|
+
model: model.trim(),
|
|
292
|
+
env
|
|
293
|
+
};
|
|
294
|
+
note(
|
|
295
|
+
[
|
|
296
|
+
`${pc.bold("Name:")} ${provider.name}`,
|
|
297
|
+
`${pc.bold("Model:")} ${provider.model}`,
|
|
298
|
+
`${pc.bold("URL:")} ${env.ANTHROPIC_BASE_URL}`,
|
|
299
|
+
`${pc.bold("Key:")} ${env.ANTHROPIC_AUTH_TOKEN.slice(0, 8)}${"*".repeat(8)}`
|
|
300
|
+
].join("\n"),
|
|
301
|
+
"Review"
|
|
302
|
+
);
|
|
303
|
+
const ok = guard(await confirm({ message: "Add this provider?" }));
|
|
304
|
+
if (!ok) {
|
|
305
|
+
cancel("Cancelled");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
add(provider);
|
|
309
|
+
outro(pc.green("\u2714") + ` Added ${pc.bold(provider.name)}`);
|
|
310
|
+
}
|
|
311
|
+
async function rm() {
|
|
312
|
+
intro(pc.cyan(pc.bold("\u26A1 ccx rm")));
|
|
313
|
+
const providers = getAll();
|
|
314
|
+
if (providers.length === 0) {
|
|
315
|
+
log.warn("No providers in JSON config");
|
|
316
|
+
outro(pc.dim("Nothing to remove"));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const result = guard(await select({
|
|
320
|
+
message: "Remove which provider?",
|
|
321
|
+
options: providers.map((p, i) => ({
|
|
322
|
+
value: i,
|
|
323
|
+
label: p.name,
|
|
324
|
+
hint: pc.dim(p.model || p.env?.ANTHROPIC_MODEL || "")
|
|
325
|
+
}))
|
|
326
|
+
}));
|
|
327
|
+
const target = providers[result];
|
|
328
|
+
const ok = guard(await confirm({
|
|
329
|
+
message: `Remove ${pc.bold(target.name)}?`,
|
|
330
|
+
initialValue: false
|
|
331
|
+
}));
|
|
332
|
+
if (!ok) {
|
|
333
|
+
cancel("Cancelled");
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
remove(result);
|
|
337
|
+
outro(pc.green("\u2714") + ` Removed ${pc.bold(target.name)}`);
|
|
338
|
+
}
|
|
339
|
+
async function edit() {
|
|
340
|
+
intro(pc.cyan(pc.bold("\u26A1 ccx edit")));
|
|
341
|
+
const providers = getAll();
|
|
342
|
+
if (providers.length === 0) {
|
|
343
|
+
log.warn("No providers in JSON config");
|
|
344
|
+
outro(pc.dim("Run `ccx add` to add one"));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const index = guard(await select({
|
|
348
|
+
message: "Edit which provider?",
|
|
349
|
+
options: providers.map((p, i) => ({
|
|
350
|
+
value: i,
|
|
351
|
+
label: p.name,
|
|
352
|
+
hint: pc.dim(p.model || p.env?.ANTHROPIC_MODEL || "")
|
|
353
|
+
}))
|
|
354
|
+
}));
|
|
355
|
+
const current = providers[index];
|
|
356
|
+
const env = current.env || {};
|
|
357
|
+
const name = guard(await text({
|
|
358
|
+
message: "Provider name",
|
|
359
|
+
initialValue: current.name,
|
|
360
|
+
validate: (v) => v.trim() ? void 0 : "Name is required"
|
|
361
|
+
}));
|
|
362
|
+
const baseUrl = guard(await text({
|
|
363
|
+
message: "API base URL",
|
|
364
|
+
initialValue: env.ANTHROPIC_BASE_URL || "",
|
|
365
|
+
validate: (v) => v.trim() ? void 0 : "URL is required"
|
|
366
|
+
}));
|
|
367
|
+
const authToken = guard(await text({
|
|
368
|
+
message: "API key / Auth token",
|
|
369
|
+
initialValue: env.ANTHROPIC_AUTH_TOKEN || "",
|
|
370
|
+
validate: (v) => v.trim() ? void 0 : "Token is required"
|
|
371
|
+
}));
|
|
372
|
+
const model = guard(await text({
|
|
373
|
+
message: "Model name",
|
|
374
|
+
initialValue: env.ANTHROPIC_MODEL || current.model || "",
|
|
375
|
+
validate: (v) => v.trim() ? void 0 : "Model is required"
|
|
376
|
+
}));
|
|
377
|
+
const fillAll = guard(await confirm({
|
|
378
|
+
message: "Set this model for all roles (Sonnet/Opus/Haiku)?",
|
|
379
|
+
initialValue: true
|
|
380
|
+
}));
|
|
381
|
+
const newEnv = {
|
|
382
|
+
ANTHROPIC_BASE_URL: baseUrl.trim(),
|
|
383
|
+
ANTHROPIC_AUTH_TOKEN: authToken.trim(),
|
|
384
|
+
ANTHROPIC_MODEL: model.trim()
|
|
385
|
+
};
|
|
386
|
+
if (fillAll) {
|
|
387
|
+
newEnv.ANTHROPIC_DEFAULT_SONNET_MODEL = model.trim();
|
|
388
|
+
newEnv.ANTHROPIC_DEFAULT_OPUS_MODEL = model.trim();
|
|
389
|
+
newEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL = model.trim();
|
|
390
|
+
newEnv.ANTHROPIC_REASONING_MODEL = model.trim();
|
|
391
|
+
}
|
|
392
|
+
const updated = {
|
|
393
|
+
name: name.trim(),
|
|
394
|
+
model: model.trim(),
|
|
395
|
+
env: newEnv
|
|
396
|
+
};
|
|
397
|
+
update(index, updated);
|
|
398
|
+
outro(pc.green("\u2714") + ` Updated ${pc.bold(updated.name)}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/launcher.ts
|
|
402
|
+
var VERSION = "0.1.3";
|
|
403
|
+
var SUBCOMMANDS = ["list", "ls", "add", "rm", "remove", "edit", "help"];
|
|
404
|
+
function parseArgs(argv) {
|
|
405
|
+
const flags = { newWindow: false, help: false, version: false, reset: false, yolo: false };
|
|
406
|
+
let command = null;
|
|
407
|
+
let query = "";
|
|
408
|
+
for (const arg of argv) {
|
|
409
|
+
switch (arg) {
|
|
410
|
+
case "--new":
|
|
411
|
+
case "-n":
|
|
412
|
+
flags.newWindow = true;
|
|
413
|
+
break;
|
|
414
|
+
case "--yes":
|
|
415
|
+
case "-y":
|
|
416
|
+
flags.yolo = true;
|
|
417
|
+
break;
|
|
418
|
+
case "--help":
|
|
419
|
+
case "-h":
|
|
420
|
+
flags.help = true;
|
|
421
|
+
break;
|
|
422
|
+
case "--version":
|
|
423
|
+
case "-v":
|
|
424
|
+
flags.version = true;
|
|
425
|
+
break;
|
|
426
|
+
case "--reset":
|
|
427
|
+
flags.reset = true;
|
|
428
|
+
break;
|
|
429
|
+
default:
|
|
430
|
+
if (arg.startsWith("-")) {
|
|
431
|
+
console.error(`Unknown option: ${arg}`);
|
|
432
|
+
process.exit(1);
|
|
433
|
+
}
|
|
434
|
+
if (!command && SUBCOMMANDS.includes(arg)) {
|
|
435
|
+
command = arg;
|
|
436
|
+
} else {
|
|
437
|
+
query = arg;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return { flags, command, query };
|
|
442
|
+
}
|
|
443
|
+
function showHelp() {
|
|
444
|
+
console.log(`
|
|
445
|
+
${pc2.cyan(pc2.bold("\u26A1 ccx"))} ${pc2.dim(`v${VERSION}`)}
|
|
446
|
+
${pc2.dim("Claude Code launcher")}
|
|
447
|
+
|
|
448
|
+
${pc2.bold("Usage:")} ccx [command] [options] [provider-name]
|
|
449
|
+
|
|
450
|
+
${pc2.bold("Commands:")}
|
|
451
|
+
${pc2.cyan("list")}, ${pc2.cyan("ls")} List all providers
|
|
452
|
+
${pc2.cyan("add")} Add a new provider
|
|
453
|
+
${pc2.cyan("edit")} Edit an existing provider
|
|
454
|
+
${pc2.cyan("rm")} Remove a provider
|
|
455
|
+
|
|
456
|
+
${pc2.bold("Options:")}
|
|
457
|
+
${pc2.cyan("-n")}, ${pc2.cyan("--new")} Open in a new terminal window
|
|
458
|
+
${pc2.cyan("-y")}, ${pc2.cyan("--yes")} Skip permissions (dangerously)
|
|
459
|
+
${pc2.cyan("-h")}, ${pc2.cyan("--help")} Show this help
|
|
460
|
+
${pc2.cyan("-v")}, ${pc2.cyan("--version")} Show version
|
|
461
|
+
${pc2.cyan("--reset")} Reset all configuration
|
|
462
|
+
|
|
463
|
+
${pc2.bold("Examples:")}
|
|
464
|
+
${pc2.dim("$")} ccx ${pc2.dim("# Interactive select, current terminal")}
|
|
465
|
+
${pc2.dim("$")} ccx glm ${pc2.dim("# Fuzzy match provider name")}
|
|
466
|
+
${pc2.dim("$")} ccx --new ${pc2.dim("# Interactive select, new window")}
|
|
467
|
+
${pc2.dim("$")} ccx add ${pc2.dim("# Add a new provider")}
|
|
468
|
+
${pc2.dim("$")} ccx list ${pc2.dim("# List all providers")}
|
|
469
|
+
${pc2.dim("$")} ccx edit ${pc2.dim("# Edit a provider")}
|
|
470
|
+
${pc2.dim("$")} ccx rm ${pc2.dim("# Remove a provider")}
|
|
471
|
+
|
|
472
|
+
${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")}
|
|
475
|
+
|
|
476
|
+
${pc2.bold("Config:")} ${pc2.dim("~/.config/ccx/config.json")}
|
|
477
|
+
`);
|
|
478
|
+
}
|
|
479
|
+
function writeTempSettings(env) {
|
|
480
|
+
const dir = mkdtempSync(join5(tmpdir(), "ccx-"));
|
|
481
|
+
const file = join5(dir, "settings.json");
|
|
482
|
+
writeFileSync3(file, JSON.stringify({ env }, null, 2));
|
|
483
|
+
return file;
|
|
484
|
+
}
|
|
485
|
+
async function selectTerminal() {
|
|
486
|
+
const saved = get("terminal");
|
|
487
|
+
if (saved) {
|
|
488
|
+
const t = getTerminal(saved);
|
|
489
|
+
if (t) return t;
|
|
490
|
+
}
|
|
491
|
+
const available = detectTerminals();
|
|
492
|
+
if (available.length === 1) {
|
|
493
|
+
set("terminal", available[0].name);
|
|
494
|
+
return available[0];
|
|
495
|
+
}
|
|
496
|
+
const result = await select2({
|
|
497
|
+
message: "Select default terminal for new windows",
|
|
498
|
+
options: available.map((t) => ({ value: t.name, label: t.name }))
|
|
499
|
+
});
|
|
500
|
+
if (isCancel2(result)) {
|
|
501
|
+
cancel2("Cancelled");
|
|
502
|
+
process.exit(0);
|
|
503
|
+
}
|
|
504
|
+
set("terminal", result);
|
|
505
|
+
return getTerminal(result);
|
|
506
|
+
}
|
|
507
|
+
async function run(argv) {
|
|
508
|
+
const { flags, command, query } = parseArgs(argv);
|
|
509
|
+
if (flags.version) {
|
|
510
|
+
console.log(`ccx ${VERSION}`);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (flags.help) {
|
|
514
|
+
showHelp();
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (flags.reset) {
|
|
518
|
+
reset();
|
|
519
|
+
log2.success("Config reset");
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (command) {
|
|
523
|
+
switch (command) {
|
|
524
|
+
case "help":
|
|
525
|
+
showHelp();
|
|
526
|
+
return;
|
|
527
|
+
case "list":
|
|
528
|
+
case "ls":
|
|
529
|
+
return list3();
|
|
530
|
+
case "add":
|
|
531
|
+
return add2();
|
|
532
|
+
case "rm":
|
|
533
|
+
case "remove":
|
|
534
|
+
return rm();
|
|
535
|
+
case "edit":
|
|
536
|
+
return edit();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
intro2(`${pc2.cyan(pc2.bold("\u26A1 ccx"))} ${pc2.dim("\u2014 Claude Code eXecutor")}`);
|
|
540
|
+
const { providers, source: source3 } = await loadProviders();
|
|
541
|
+
log2.message(
|
|
542
|
+
pc2.dim(`${providers.length} providers from ${source3 || "none"} \xB7 v${VERSION}
|
|
543
|
+
`) + pc2.dim(` ccx ${pc2.cyan("add")} Add provider ccx ${pc2.cyan("edit")} Edit provider
|
|
544
|
+
`) + pc2.dim(` ccx ${pc2.cyan("list")} List providers ccx ${pc2.cyan("rm")} Remove provider
|
|
545
|
+
`) + pc2.dim(` ccx ${pc2.cyan("-n")} New window ccx ${pc2.cyan("help")} Show help`)
|
|
546
|
+
);
|
|
547
|
+
if (providers.length === 0) {
|
|
548
|
+
log2.error("No providers found");
|
|
549
|
+
log2.message("");
|
|
550
|
+
log2.message(` ${pc2.cyan("1.")} Run ${pc2.bold("ccx add")} to add a provider`);
|
|
551
|
+
log2.message(` ${pc2.cyan("2.")} Or install ${pc2.bold("cc-switch")} for auto-detection`);
|
|
552
|
+
log2.message("");
|
|
553
|
+
cancel2("Setup a provider first");
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
let selected;
|
|
557
|
+
if (query) {
|
|
558
|
+
const lowerQuery = query.toLowerCase();
|
|
559
|
+
selected = providers.find(
|
|
560
|
+
(p) => p.name.toLowerCase().includes(lowerQuery) || p.model.toLowerCase().includes(lowerQuery)
|
|
561
|
+
);
|
|
562
|
+
if (!selected) {
|
|
563
|
+
log2.warn(`No match for "${query}"`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (!selected) {
|
|
567
|
+
const result = await select2({
|
|
568
|
+
message: "Select provider",
|
|
569
|
+
options: providers.map((p) => ({
|
|
570
|
+
value: p.id,
|
|
571
|
+
label: p.name,
|
|
572
|
+
hint: pc2.dim(p.model)
|
|
573
|
+
}))
|
|
574
|
+
});
|
|
575
|
+
if (isCancel2(result)) {
|
|
576
|
+
cancel2("Cancelled");
|
|
577
|
+
process.exit(0);
|
|
578
|
+
}
|
|
579
|
+
selected = providers.find((p) => p.id === result);
|
|
580
|
+
}
|
|
581
|
+
const settingsFile = writeTempSettings(selected.env);
|
|
582
|
+
const yoloFlag = flags.yolo ? " --dangerously-skip-permissions" : "";
|
|
583
|
+
if (flags.newWindow) {
|
|
584
|
+
const terminal = await selectTerminal();
|
|
585
|
+
const cwd = process.cwd();
|
|
586
|
+
const cmd = `cd '${cwd}'; echo '=== Claude Code [${selected.name}] ==='; echo; claude --settings '${settingsFile}'${yoloFlag}; rm -f '${settingsFile}'; exec bash`;
|
|
587
|
+
terminal.open(cmd);
|
|
588
|
+
outro2(`${pc2.green("\u26A1")} ${selected.name} ${pc2.dim(`(${selected.model})`)} \u2192 ${pc2.dim(terminal.name)}`);
|
|
589
|
+
} else {
|
|
590
|
+
outro2(`${pc2.green("\u26A1")} ${selected.name} ${pc2.dim(`(${selected.model})`)}`);
|
|
591
|
+
const claudeArgs = ["--settings", settingsFile];
|
|
592
|
+
if (flags.yolo) claudeArgs.push("--dangerously-skip-permissions");
|
|
593
|
+
const child = spawn("claude", claudeArgs, {
|
|
594
|
+
stdio: "inherit",
|
|
595
|
+
env: { ...process.env }
|
|
596
|
+
});
|
|
597
|
+
child.on("exit", (code) => {
|
|
598
|
+
try {
|
|
599
|
+
rmSync(settingsFile, { force: true });
|
|
600
|
+
} catch {
|
|
601
|
+
}
|
|
602
|
+
process.exit(code ?? 0);
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/bin.ts
|
|
608
|
+
run(process.argv.slice(2));
|
package/package.json
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twoer/ccx",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Claude Code launcher - switch providers and models with ease",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"ccx": "./bin/ccx.
|
|
7
|
+
"ccx": "./dist/bin/ccx.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"
|
|
11
|
-
"src"
|
|
10
|
+
"dist"
|
|
12
11
|
],
|
|
13
12
|
"scripts": {
|
|
14
|
-
"
|
|
13
|
+
"build": "tsup",
|
|
14
|
+
"dev": "tsup --watch",
|
|
15
|
+
"start": "node dist/bin/ccx.js"
|
|
15
16
|
},
|
|
16
17
|
"keywords": [
|
|
17
18
|
"claude",
|
|
@@ -25,5 +26,11 @@
|
|
|
25
26
|
"@clack/prompts": "^0.10.0",
|
|
26
27
|
"better-sqlite3": "^11.8.0",
|
|
27
28
|
"picocolors": "^1.1.1"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
32
|
+
"@types/node": "^25.5.0",
|
|
33
|
+
"tsup": "^8.5.1",
|
|
34
|
+
"typescript": "^6.0.2"
|
|
28
35
|
}
|
|
29
36
|
}
|
package/bin/ccx.mjs
DELETED
package/src/commands.mjs
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import { intro, outro, select, text, confirm, cancel, isCancel, log, note } from '@clack/prompts'
|
|
2
|
-
import pc from 'picocolors'
|
|
3
|
-
import * as manager from './providers/manager.mjs'
|
|
4
|
-
import { loadProviders } from './providers/index.mjs'
|
|
5
|
-
|
|
6
|
-
function guard(result) {
|
|
7
|
-
if (isCancel(result)) {
|
|
8
|
-
cancel('Cancelled')
|
|
9
|
-
process.exit(0)
|
|
10
|
-
}
|
|
11
|
-
return result
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// ── ccx list ────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
export async function list() {
|
|
17
|
-
intro(pc.cyan(pc.bold('⚡ ccx list')))
|
|
18
|
-
|
|
19
|
-
const { providers, source } = await loadProviders()
|
|
20
|
-
|
|
21
|
-
if (providers.length === 0) {
|
|
22
|
-
log.warn('No providers found')
|
|
23
|
-
outro(pc.dim('Run `ccx add` to add one'))
|
|
24
|
-
return
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
log.message(pc.dim(`${providers.length} providers from ${source}`))
|
|
28
|
-
|
|
29
|
-
const lines = providers.map((p, i) => {
|
|
30
|
-
const idx = pc.dim(`${String(i + 1).padStart(2)}.`)
|
|
31
|
-
const name = pc.bold(p.name)
|
|
32
|
-
const model = pc.dim(p.model)
|
|
33
|
-
const url = pc.dim(p.env?.ANTHROPIC_BASE_URL || '')
|
|
34
|
-
return `${idx} ${name} ${model}\n ${url}`
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
note(lines.join('\n'), 'Providers')
|
|
38
|
-
outro(pc.dim(`Config: ${manager.filePath}`))
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ── ccx add ─────────────────────────────────────────────
|
|
42
|
-
|
|
43
|
-
export async function add() {
|
|
44
|
-
intro(pc.cyan(pc.bold('⚡ ccx add')))
|
|
45
|
-
|
|
46
|
-
const name = guard(await text({
|
|
47
|
-
message: 'Provider name',
|
|
48
|
-
placeholder: 'e.g. Zhipu GLM-5.1',
|
|
49
|
-
validate: (v) => v.trim() ? undefined : 'Name is required',
|
|
50
|
-
}))
|
|
51
|
-
|
|
52
|
-
const baseUrl = guard(await text({
|
|
53
|
-
message: 'API base URL',
|
|
54
|
-
placeholder: 'e.g. https://open.bigmodel.cn/api/anthropic',
|
|
55
|
-
validate: (v) => v.trim() ? undefined : 'URL is required',
|
|
56
|
-
}))
|
|
57
|
-
|
|
58
|
-
const authToken = guard(await text({
|
|
59
|
-
message: 'API key / Auth token',
|
|
60
|
-
placeholder: 'sk-xxx or your-api-key',
|
|
61
|
-
validate: (v) => v.trim() ? undefined : 'Token is required',
|
|
62
|
-
}))
|
|
63
|
-
|
|
64
|
-
const model = guard(await text({
|
|
65
|
-
message: 'Model name',
|
|
66
|
-
placeholder: 'e.g. glm-5.1, claude-sonnet-4-20250514',
|
|
67
|
-
validate: (v) => v.trim() ? undefined : 'Model is required',
|
|
68
|
-
}))
|
|
69
|
-
|
|
70
|
-
const fillAll = guard(await confirm({
|
|
71
|
-
message: 'Set this model for all roles (Sonnet/Opus/Haiku)?',
|
|
72
|
-
initialValue: true,
|
|
73
|
-
}))
|
|
74
|
-
|
|
75
|
-
const env = {
|
|
76
|
-
ANTHROPIC_BASE_URL: baseUrl.trim(),
|
|
77
|
-
ANTHROPIC_AUTH_TOKEN: authToken.trim(),
|
|
78
|
-
ANTHROPIC_MODEL: model.trim(),
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (fillAll) {
|
|
82
|
-
env.ANTHROPIC_DEFAULT_SONNET_MODEL = model.trim()
|
|
83
|
-
env.ANTHROPIC_DEFAULT_OPUS_MODEL = model.trim()
|
|
84
|
-
env.ANTHROPIC_DEFAULT_HAIKU_MODEL = model.trim()
|
|
85
|
-
env.ANTHROPIC_REASONING_MODEL = model.trim()
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const provider = {
|
|
89
|
-
name: name.trim(),
|
|
90
|
-
model: model.trim(),
|
|
91
|
-
env,
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
note(
|
|
95
|
-
[
|
|
96
|
-
`${pc.bold('Name:')} ${provider.name}`,
|
|
97
|
-
`${pc.bold('Model:')} ${provider.model}`,
|
|
98
|
-
`${pc.bold('URL:')} ${env.ANTHROPIC_BASE_URL}`,
|
|
99
|
-
`${pc.bold('Key:')} ${env.ANTHROPIC_AUTH_TOKEN.slice(0, 8)}${'*'.repeat(8)}`,
|
|
100
|
-
].join('\n'),
|
|
101
|
-
'Review',
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
const ok = guard(await confirm({ message: 'Add this provider?' }))
|
|
105
|
-
|
|
106
|
-
if (!ok) {
|
|
107
|
-
cancel('Cancelled')
|
|
108
|
-
return
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
manager.add(provider)
|
|
112
|
-
outro(pc.green('✔') + ` Added ${pc.bold(provider.name)}`)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// ── ccx rm ──────────────────────────────────────────────
|
|
116
|
-
|
|
117
|
-
export async function rm() {
|
|
118
|
-
intro(pc.cyan(pc.bold('⚡ ccx rm')))
|
|
119
|
-
|
|
120
|
-
const providers = manager.getAll()
|
|
121
|
-
|
|
122
|
-
if (providers.length === 0) {
|
|
123
|
-
log.warn('No providers in JSON config')
|
|
124
|
-
outro(pc.dim('Nothing to remove'))
|
|
125
|
-
return
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const result = guard(await select({
|
|
129
|
-
message: 'Remove which provider?',
|
|
130
|
-
options: providers.map((p, i) => ({
|
|
131
|
-
value: i,
|
|
132
|
-
label: p.name,
|
|
133
|
-
hint: pc.dim(p.model || p.env?.ANTHROPIC_MODEL || ''),
|
|
134
|
-
})),
|
|
135
|
-
}))
|
|
136
|
-
|
|
137
|
-
const target = providers[result]
|
|
138
|
-
|
|
139
|
-
const ok = guard(await confirm({
|
|
140
|
-
message: `Remove ${pc.bold(target.name)}?`,
|
|
141
|
-
initialValue: false,
|
|
142
|
-
}))
|
|
143
|
-
|
|
144
|
-
if (!ok) {
|
|
145
|
-
cancel('Cancelled')
|
|
146
|
-
return
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
manager.remove(result)
|
|
150
|
-
outro(pc.green('✔') + ` Removed ${pc.bold(target.name)}`)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ── ccx edit ────────────────────────────────────────────
|
|
154
|
-
|
|
155
|
-
export async function edit() {
|
|
156
|
-
intro(pc.cyan(pc.bold('⚡ ccx edit')))
|
|
157
|
-
|
|
158
|
-
const providers = manager.getAll()
|
|
159
|
-
|
|
160
|
-
if (providers.length === 0) {
|
|
161
|
-
log.warn('No providers in JSON config')
|
|
162
|
-
outro(pc.dim('Run `ccx add` to add one'))
|
|
163
|
-
return
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const index = guard(await select({
|
|
167
|
-
message: 'Edit which provider?',
|
|
168
|
-
options: providers.map((p, i) => ({
|
|
169
|
-
value: i,
|
|
170
|
-
label: p.name,
|
|
171
|
-
hint: pc.dim(p.model || p.env?.ANTHROPIC_MODEL || ''),
|
|
172
|
-
})),
|
|
173
|
-
}))
|
|
174
|
-
|
|
175
|
-
const current = providers[index]
|
|
176
|
-
const env = current.env || {}
|
|
177
|
-
|
|
178
|
-
const name = guard(await text({
|
|
179
|
-
message: 'Provider name',
|
|
180
|
-
initialValue: current.name,
|
|
181
|
-
validate: (v) => v.trim() ? undefined : 'Name is required',
|
|
182
|
-
}))
|
|
183
|
-
|
|
184
|
-
const baseUrl = guard(await text({
|
|
185
|
-
message: 'API base URL',
|
|
186
|
-
initialValue: env.ANTHROPIC_BASE_URL || '',
|
|
187
|
-
validate: (v) => v.trim() ? undefined : 'URL is required',
|
|
188
|
-
}))
|
|
189
|
-
|
|
190
|
-
const authToken = guard(await text({
|
|
191
|
-
message: 'API key / Auth token',
|
|
192
|
-
initialValue: env.ANTHROPIC_AUTH_TOKEN || '',
|
|
193
|
-
validate: (v) => v.trim() ? undefined : 'Token is required',
|
|
194
|
-
}))
|
|
195
|
-
|
|
196
|
-
const model = guard(await text({
|
|
197
|
-
message: 'Model name',
|
|
198
|
-
initialValue: env.ANTHROPIC_MODEL || current.model || '',
|
|
199
|
-
validate: (v) => v.trim() ? undefined : 'Model is required',
|
|
200
|
-
}))
|
|
201
|
-
|
|
202
|
-
const fillAll = guard(await confirm({
|
|
203
|
-
message: 'Set this model for all roles (Sonnet/Opus/Haiku)?',
|
|
204
|
-
initialValue: true,
|
|
205
|
-
}))
|
|
206
|
-
|
|
207
|
-
const newEnv = {
|
|
208
|
-
ANTHROPIC_BASE_URL: baseUrl.trim(),
|
|
209
|
-
ANTHROPIC_AUTH_TOKEN: authToken.trim(),
|
|
210
|
-
ANTHROPIC_MODEL: model.trim(),
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (fillAll) {
|
|
214
|
-
newEnv.ANTHROPIC_DEFAULT_SONNET_MODEL = model.trim()
|
|
215
|
-
newEnv.ANTHROPIC_DEFAULT_OPUS_MODEL = model.trim()
|
|
216
|
-
newEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL = model.trim()
|
|
217
|
-
newEnv.ANTHROPIC_REASONING_MODEL = model.trim()
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const updated = {
|
|
221
|
-
name: name.trim(),
|
|
222
|
-
model: model.trim(),
|
|
223
|
-
env: newEnv,
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
manager.update(index, updated)
|
|
227
|
-
outro(pc.green('✔') + ` Updated ${pc.bold(updated.name)}`)
|
|
228
|
-
}
|
package/src/config.mjs
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
-
import { homedir } from 'node:os'
|
|
3
|
-
import { join } from 'node:path'
|
|
4
|
-
|
|
5
|
-
const CONFIG_DIR = join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'ccx')
|
|
6
|
-
const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
|
|
7
|
-
|
|
8
|
-
function ensureDir() {
|
|
9
|
-
if (!existsSync(CONFIG_DIR)) {
|
|
10
|
-
mkdirSync(CONFIG_DIR, { recursive: true })
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function load() {
|
|
15
|
-
ensureDir()
|
|
16
|
-
if (!existsSync(CONFIG_FILE)) return {}
|
|
17
|
-
try {
|
|
18
|
-
return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'))
|
|
19
|
-
} catch {
|
|
20
|
-
return {}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function save(config) {
|
|
25
|
-
ensureDir()
|
|
26
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n')
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function get(key) {
|
|
30
|
-
return load()[key]
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function set(key, value) {
|
|
34
|
-
const config = load()
|
|
35
|
-
config[key] = value
|
|
36
|
-
save(config)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function reset() {
|
|
40
|
-
save({})
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export const configDir = CONFIG_DIR
|
|
44
|
-
export const configFile = CONFIG_FILE
|
package/src/launcher.mjs
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import { intro, outro, select, cancel, isCancel, log } from '@clack/prompts'
|
|
2
|
-
import pc from 'picocolors'
|
|
3
|
-
import { writeFileSync, mkdtempSync, rmSync } from 'node:fs'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { tmpdir } from 'node:os'
|
|
6
|
-
import { spawn } from 'node:child_process'
|
|
7
|
-
import { loadProviders } from './providers/index.mjs'
|
|
8
|
-
import { detectTerminals, getTerminal } from './terminals/index.mjs'
|
|
9
|
-
import * as config from './config.mjs'
|
|
10
|
-
import * as commands from './commands.mjs'
|
|
11
|
-
|
|
12
|
-
const VERSION = '0.1.0'
|
|
13
|
-
|
|
14
|
-
const SUBCOMMANDS = ['list', 'ls', 'add', 'rm', 'remove', 'edit', 'help']
|
|
15
|
-
|
|
16
|
-
function parseArgs(argv) {
|
|
17
|
-
const flags = { newWindow: false, help: false, version: false, reset: false }
|
|
18
|
-
let command = null
|
|
19
|
-
let query = ''
|
|
20
|
-
|
|
21
|
-
for (const arg of argv) {
|
|
22
|
-
switch (arg) {
|
|
23
|
-
case '--new': case '-n': flags.newWindow = true; break
|
|
24
|
-
case '--help': case '-h': flags.help = true; break
|
|
25
|
-
case '--version': case '-v': flags.version = true; break
|
|
26
|
-
case '--reset': flags.reset = true; break
|
|
27
|
-
default:
|
|
28
|
-
if (arg.startsWith('-')) {
|
|
29
|
-
console.error(`Unknown option: ${arg}`)
|
|
30
|
-
process.exit(1)
|
|
31
|
-
}
|
|
32
|
-
if (!command && SUBCOMMANDS.includes(arg)) {
|
|
33
|
-
command = arg
|
|
34
|
-
} else {
|
|
35
|
-
query = arg
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return { flags, command, query }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function showHelp() {
|
|
44
|
-
console.log(`
|
|
45
|
-
${pc.cyan(pc.bold('⚡ ccx'))} ${pc.dim(`v${VERSION}`)}
|
|
46
|
-
${pc.dim('Claude Code launcher')}
|
|
47
|
-
|
|
48
|
-
${pc.bold('Usage:')} ccx [command] [options] [provider-name]
|
|
49
|
-
|
|
50
|
-
${pc.bold('Commands:')}
|
|
51
|
-
${pc.cyan('list')}, ${pc.cyan('ls')} List all providers
|
|
52
|
-
${pc.cyan('add')} Add a new provider
|
|
53
|
-
${pc.cyan('edit')} Edit an existing provider
|
|
54
|
-
${pc.cyan('rm')} Remove a provider
|
|
55
|
-
|
|
56
|
-
${pc.bold('Options:')}
|
|
57
|
-
${pc.cyan('-n')}, ${pc.cyan('--new')} Open in a new terminal window
|
|
58
|
-
${pc.cyan('-h')}, ${pc.cyan('--help')} Show this help
|
|
59
|
-
${pc.cyan('-v')}, ${pc.cyan('--version')} Show version
|
|
60
|
-
${pc.cyan('--reset')} Reset all configuration
|
|
61
|
-
|
|
62
|
-
${pc.bold('Examples:')}
|
|
63
|
-
${pc.dim('$')} ccx ${pc.dim('# Interactive select, current terminal')}
|
|
64
|
-
${pc.dim('$')} ccx glm ${pc.dim('# Fuzzy match provider name')}
|
|
65
|
-
${pc.dim('$')} ccx --new ${pc.dim('# Interactive select, new window')}
|
|
66
|
-
${pc.dim('$')} ccx add ${pc.dim('# Add a new provider')}
|
|
67
|
-
${pc.dim('$')} ccx list ${pc.dim('# List all providers')}
|
|
68
|
-
${pc.dim('$')} ccx edit ${pc.dim('# Edit a provider')}
|
|
69
|
-
${pc.dim('$')} ccx rm ${pc.dim('# Remove a provider')}
|
|
70
|
-
|
|
71
|
-
${pc.bold('Providers:')}
|
|
72
|
-
${pc.cyan('cc-switch')} ${pc.dim('auto-detected from ~/.cc-switch/cc-switch.db')}
|
|
73
|
-
${pc.cyan('JSON file')} ${pc.dim('configure at ~/.config/ccx/providers.json')}
|
|
74
|
-
|
|
75
|
-
${pc.bold('Config:')} ${pc.dim('~/.config/ccx/config.json')}
|
|
76
|
-
`)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function writeTempSettings(env) {
|
|
80
|
-
const dir = mkdtempSync(join(tmpdir(), 'ccx-'))
|
|
81
|
-
const file = join(dir, 'settings.json')
|
|
82
|
-
writeFileSync(file, JSON.stringify({ env }, null, 2))
|
|
83
|
-
return file
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function selectTerminal() {
|
|
87
|
-
const saved = config.get('terminal')
|
|
88
|
-
if (saved) {
|
|
89
|
-
const t = getTerminal(saved)
|
|
90
|
-
if (t) return t
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const available = detectTerminals()
|
|
94
|
-
if (available.length === 1) {
|
|
95
|
-
config.set('terminal', available[0].name)
|
|
96
|
-
return available[0]
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const result = await select({
|
|
100
|
-
message: 'Select default terminal for new windows',
|
|
101
|
-
options: available.map(t => ({ value: t.name, label: t.name })),
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
if (isCancel(result)) {
|
|
105
|
-
cancel('Cancelled')
|
|
106
|
-
process.exit(0)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
config.set('terminal', result)
|
|
110
|
-
return getTerminal(result)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export async function run(argv) {
|
|
114
|
-
const { flags, command, query } = parseArgs(argv)
|
|
115
|
-
|
|
116
|
-
if (flags.version) {
|
|
117
|
-
console.log(`ccx ${VERSION}`)
|
|
118
|
-
return
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (flags.help) {
|
|
122
|
-
showHelp()
|
|
123
|
-
return
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (flags.reset) {
|
|
127
|
-
config.reset()
|
|
128
|
-
log.success('Config reset')
|
|
129
|
-
return
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Handle subcommands
|
|
133
|
-
if (command) {
|
|
134
|
-
switch (command) {
|
|
135
|
-
case 'help': showHelp(); return
|
|
136
|
-
case 'list': case 'ls': return commands.list()
|
|
137
|
-
case 'add': return commands.add()
|
|
138
|
-
case 'rm': case 'remove': return commands.rm()
|
|
139
|
-
case 'edit': return commands.edit()
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Default: launch claude
|
|
144
|
-
intro(`${pc.cyan(pc.bold('⚡ ccx'))} ${pc.dim('— Claude Code eXecutor')}`)
|
|
145
|
-
const { providers, source } = await loadProviders()
|
|
146
|
-
log.message(
|
|
147
|
-
pc.dim(`${providers.length} providers from ${source || 'none'} · v${VERSION}\n`) +
|
|
148
|
-
pc.dim(` ccx ${pc.cyan('add')} Add provider ccx ${pc.cyan('edit')} Edit provider\n`) +
|
|
149
|
-
pc.dim(` ccx ${pc.cyan('list')} List providers ccx ${pc.cyan('rm')} Remove provider\n`) +
|
|
150
|
-
pc.dim(` ccx ${pc.cyan('-n')} New window ccx ${pc.cyan('help')} Show help`),
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
if (providers.length === 0) {
|
|
154
|
-
log.error('No providers found')
|
|
155
|
-
log.message('')
|
|
156
|
-
log.message(` ${pc.cyan('1.')} Run ${pc.bold('ccx add')} to add a provider`)
|
|
157
|
-
log.message(` ${pc.cyan('2.')} Or install ${pc.bold('cc-switch')} for auto-detection`)
|
|
158
|
-
log.message('')
|
|
159
|
-
cancel('Setup a provider first')
|
|
160
|
-
return
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Select provider
|
|
164
|
-
let selected
|
|
165
|
-
|
|
166
|
-
if (query) {
|
|
167
|
-
const lowerQuery = query.toLowerCase()
|
|
168
|
-
selected = providers.find(p =>
|
|
169
|
-
p.name.toLowerCase().includes(lowerQuery) ||
|
|
170
|
-
p.model.toLowerCase().includes(lowerQuery),
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
if (!selected) {
|
|
174
|
-
log.warn(`No match for "${query}"`)
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (!selected) {
|
|
179
|
-
const result = await select({
|
|
180
|
-
message: 'Select provider',
|
|
181
|
-
options: providers.map(p => ({
|
|
182
|
-
value: p.id,
|
|
183
|
-
label: p.name,
|
|
184
|
-
hint: pc.dim(p.model),
|
|
185
|
-
})),
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
if (isCancel(result)) {
|
|
189
|
-
cancel('Cancelled')
|
|
190
|
-
process.exit(0)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
selected = providers.find(p => p.id === result)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Write temp settings
|
|
197
|
-
const settingsFile = writeTempSettings(selected.env)
|
|
198
|
-
|
|
199
|
-
if (flags.newWindow) {
|
|
200
|
-
const terminal = await selectTerminal()
|
|
201
|
-
const cwd = process.cwd()
|
|
202
|
-
const cmd = `cd '${cwd}'; echo '=== Claude Code [${selected.name}] ==='; echo; claude --settings '${settingsFile}'; rm -f '${settingsFile}'; exec bash`
|
|
203
|
-
terminal.open(cmd)
|
|
204
|
-
outro(`${pc.green('⚡')} ${selected.name} ${pc.dim(`(${selected.model})`)} → ${pc.dim(terminal.name)}`)
|
|
205
|
-
} else {
|
|
206
|
-
outro(`${pc.green('⚡')} ${selected.name} ${pc.dim(`(${selected.model})`)}`)
|
|
207
|
-
|
|
208
|
-
const child = spawn('claude', ['--settings', settingsFile], {
|
|
209
|
-
stdio: 'inherit',
|
|
210
|
-
env: { ...process.env },
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
child.on('exit', (code) => {
|
|
214
|
-
try { rmSync(settingsFile, { force: true }) } catch {}
|
|
215
|
-
process.exit(code ?? 0)
|
|
216
|
-
})
|
|
217
|
-
}
|
|
218
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
2
|
-
import { homedir } from 'node:os'
|
|
3
|
-
import { join } from 'node:path'
|
|
4
|
-
|
|
5
|
-
const DB_PATH = join(homedir(), '.cc-switch', 'cc-switch.db')
|
|
6
|
-
|
|
7
|
-
export function detect() {
|
|
8
|
-
return existsSync(DB_PATH)
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export async function list() {
|
|
12
|
-
const Database = (await import('better-sqlite3')).default
|
|
13
|
-
const db = new Database(DB_PATH, { readonly: true })
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
const rows = db.prepare(`
|
|
17
|
-
SELECT id, name, json_extract(settings_config, '$.env.ANTHROPIC_MODEL') as model,
|
|
18
|
-
json_extract(settings_config, '$.env') as env
|
|
19
|
-
FROM providers
|
|
20
|
-
WHERE app_type = 'claude' AND settings_config LIKE '%"env"%'
|
|
21
|
-
ORDER BY sort_index
|
|
22
|
-
`).all()
|
|
23
|
-
|
|
24
|
-
return rows.map(row => ({
|
|
25
|
-
id: row.id,
|
|
26
|
-
name: row.name,
|
|
27
|
-
model: row.model || 'unknown',
|
|
28
|
-
env: JSON.parse(row.env || '{}'),
|
|
29
|
-
}))
|
|
30
|
-
} finally {
|
|
31
|
-
db.close()
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export const source = 'cc-switch'
|
package/src/providers/index.mjs
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import * as ccSwitch from './cc-switch.mjs'
|
|
2
|
-
import * as jsonFile from './json-file.mjs'
|
|
3
|
-
|
|
4
|
-
const sources = [ccSwitch, jsonFile]
|
|
5
|
-
|
|
6
|
-
export async function loadProviders() {
|
|
7
|
-
for (const source of sources) {
|
|
8
|
-
if (source.detect()) {
|
|
9
|
-
try {
|
|
10
|
-
const providers = await source.list()
|
|
11
|
-
if (providers.length > 0) {
|
|
12
|
-
return { providers, source: source.source }
|
|
13
|
-
}
|
|
14
|
-
} catch {
|
|
15
|
-
// Try next source
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return { providers: [], source: null }
|
|
20
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
-
import { homedir } from 'node:os'
|
|
3
|
-
import { join } from 'node:path'
|
|
4
|
-
|
|
5
|
-
const PROVIDERS_FILE = join(
|
|
6
|
-
process.env.XDG_CONFIG_HOME || join(homedir(), '.config'),
|
|
7
|
-
'ccx',
|
|
8
|
-
'providers.json',
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
export function detect() {
|
|
12
|
-
return existsSync(PROVIDERS_FILE)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function list() {
|
|
16
|
-
const data = JSON.parse(readFileSync(PROVIDERS_FILE, 'utf-8'))
|
|
17
|
-
const providers = data.providers || []
|
|
18
|
-
|
|
19
|
-
return providers.map((p, i) => ({
|
|
20
|
-
id: `json:${i}`,
|
|
21
|
-
name: p.name,
|
|
22
|
-
model: p.model || p.env?.ANTHROPIC_MODEL || 'unknown',
|
|
23
|
-
env: p.env || {},
|
|
24
|
-
}))
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const source = 'json'
|
|
28
|
-
export const filePath = PROVIDERS_FILE
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
2
|
-
import { homedir } from 'node:os'
|
|
3
|
-
import { join, dirname } from 'node:path'
|
|
4
|
-
|
|
5
|
-
const PROVIDERS_FILE = join(
|
|
6
|
-
process.env.XDG_CONFIG_HOME || join(homedir(), '.config'),
|
|
7
|
-
'ccx',
|
|
8
|
-
'providers.json',
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
function ensureFile() {
|
|
12
|
-
const dir = dirname(PROVIDERS_FILE)
|
|
13
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
14
|
-
if (!existsSync(PROVIDERS_FILE)) {
|
|
15
|
-
writeFileSync(PROVIDERS_FILE, JSON.stringify({ providers: [] }, null, 2) + '\n')
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function load() {
|
|
20
|
-
ensureFile()
|
|
21
|
-
return JSON.parse(readFileSync(PROVIDERS_FILE, 'utf-8'))
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function save(data) {
|
|
25
|
-
ensureFile()
|
|
26
|
-
writeFileSync(PROVIDERS_FILE, JSON.stringify(data, null, 2) + '\n')
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function getAll() {
|
|
30
|
-
return load().providers || []
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function add(provider) {
|
|
34
|
-
const data = load()
|
|
35
|
-
data.providers = data.providers || []
|
|
36
|
-
data.providers.push(provider)
|
|
37
|
-
save(data)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function remove(index) {
|
|
41
|
-
const data = load()
|
|
42
|
-
data.providers.splice(index, 1)
|
|
43
|
-
save(data)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function update(index, provider) {
|
|
47
|
-
const data = load()
|
|
48
|
-
data.providers[index] = provider
|
|
49
|
-
save(data)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export const filePath = PROVIDERS_FILE
|
package/src/terminals/index.mjs
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
2
|
-
import { execSync } from 'node:child_process'
|
|
3
|
-
|
|
4
|
-
const terminals = [
|
|
5
|
-
{
|
|
6
|
-
name: 'Ghostty',
|
|
7
|
-
detect: () => existsSync('/Applications/Ghostty.app'),
|
|
8
|
-
open: (cmd) => execSync(`open -na Ghostty.app --args -e bash -c "${cmd}"`),
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
name: 'iTerm2',
|
|
12
|
-
detect: () => existsSync('/Applications/iTerm.app'),
|
|
13
|
-
open: (cmd) => execSync(`osascript -e 'tell application "iTerm" to create window with default profile command "bash -c \\"${cmd}\\""'`),
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
name: 'Warp',
|
|
17
|
-
detect: () => existsSync('/Applications/Warp.app'),
|
|
18
|
-
open: (cmd) => execSync(`open -na Warp.app --args bash -c "${cmd}"`),
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
name: 'kitty',
|
|
22
|
-
detect: () => existsSync('/Applications/kitty.app'),
|
|
23
|
-
open: (cmd) => execSync(`/Applications/kitty.app/Contents/MacOS/kitty bash -c "${cmd}" &`),
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
name: 'Terminal',
|
|
27
|
-
detect: () => true,
|
|
28
|
-
open: (cmd) => execSync(`osascript -e 'tell application "Terminal" to do script "${cmd}"'`),
|
|
29
|
-
},
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
export function detectTerminals() {
|
|
33
|
-
return terminals.filter(t => t.detect())
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function getTerminal(name) {
|
|
37
|
-
return terminals.find(t => t.name === name)
|
|
38
|
-
}
|