@twoer/ccx 0.1.1 → 0.1.2

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.
@@ -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.1";
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.1",
3
+ "version": "0.1.2",
4
4
  "description": "Claude Code launcher - switch providers and models with ease",
5
5
  "type": "module",
6
6
  "bin": {
7
- "ccx": "./bin/ccx.mjs"
7
+ "ccx": "./dist/bin/ccx.js"
8
8
  },
9
9
  "files": [
10
- "bin",
11
- "src"
10
+ "dist"
12
11
  ],
13
12
  "scripts": {
14
- "start": "node bin/ccx.mjs"
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
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { run } from '../src/launcher.mjs'
4
-
5
- run(process.argv.slice(2))
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'
@@ -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
@@ -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
- }