cc-cast 1.3.5

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/src/index.ts ADDED
@@ -0,0 +1,1149 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "commander";
4
+ import chalk from "chalk";
5
+ import { readRc, writeRc, getStore, isCcSwitchGuiRunning } from "./utils.js";
6
+ import { ccSwitchExists } from "./store/cc-switch.js";
7
+ import { readClaudeSettings, applyProfile } from "./claude.js";
8
+ import { createInterface } from "readline";
9
+ import { spawnSync } from "child_process";
10
+ import { writeFileSync, readFileSync, unlinkSync, existsSync } from "fs";
11
+ import { tmpdir, homedir } from "os";
12
+ import { join, dirname } from "path";
13
+ import { fileURLToPath } from "url";
14
+ import { t, setLocale, getLocale } from "./i18n/index.js";
15
+ import Enquirer from "enquirer";
16
+ const Select = (Enquirer as any).Select;
17
+
18
+ function createSelect(options: any) {
19
+ const prompt = new Select(options);
20
+ prompt.prefix = async () => "";
21
+ prompt.separator = async () => "";
22
+ prompt.cancel = async function (err: any) {
23
+ this.state.cancelled = true;
24
+ this.state.submitted = true;
25
+ this.clear(this.state.size);
26
+ this.stdout.write("\u001b[?25h");
27
+ if (typeof this.stop === "function") this.stop();
28
+ this.emit("cancel", err);
29
+ };
30
+ (prompt as any).choiceMessage = function (choice: any, i: number) {
31
+ const hasColor = (s: string) => /\x1b\[\d+m/.test(String(s));
32
+ let message = this.resolve(choice.message, this.state, choice, i);
33
+ if (choice.role === "heading" && !hasColor(message)) {
34
+ message = this.styles.strong(message);
35
+ }
36
+ if (this.index === i && !hasColor(message)) {
37
+ message = this.styles.primary(message);
38
+ }
39
+ return this.resolve(message, this.state, choice, i);
40
+ };
41
+ return prompt;
42
+ }
43
+
44
+ const __dirname = dirname(fileURLToPath(import.meta.url));
45
+ const packageJsonPath = join(__dirname, "..", "package.json");
46
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
47
+
48
+ const program = new Command();
49
+
50
+ program
51
+ .name("cc-cast")
52
+ .description(t("program.description"))
53
+ .version(packageJson.version);
54
+
55
+ // Helper: prompt user for input, optionally pre-filling the input field
56
+ function ask(question: string, prefill?: string): Promise<string> {
57
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
58
+ return new Promise((resolve) => {
59
+ rl.question(question, (answer) => {
60
+ rl.close();
61
+ resolve(answer.trim());
62
+ });
63
+ if (prefill) {
64
+ (rl as any).line = prefill;
65
+ (rl as any).cursor = prefill.length;
66
+ (rl as any)._refreshLine();
67
+ }
68
+ });
69
+ }
70
+
71
+ // Helper: ensure store ready
72
+ function ensureStore() {
73
+ return getStore();
74
+ }
75
+
76
+ // Helper: print current active configuration
77
+ function printCurrent(): void {
78
+ const store = ensureStore();
79
+ const currentName = store.getCurrent();
80
+
81
+ if (!currentName) {
82
+ console.log(chalk.yellow(t("current.none")));
83
+ console.log(chalk.gray(`\n${t("current.settings_header")}`));
84
+ const settings = readClaudeSettings();
85
+ const env = (settings.env || {}) as Record<string, string>;
86
+ console.log(formatEnv(env));
87
+ return;
88
+ }
89
+
90
+ const profile = store.get(currentName);
91
+ if (!profile) {
92
+ console.log(chalk.yellow(t("current.not_exist", { name: currentName })));
93
+ return;
94
+ }
95
+
96
+ console.log(`\n${t("current.header", { name: chalk.green.bold(profile.name) })}\n`);
97
+ const env = (profile.settingsConfig.env || {}) as Record<string, string>;
98
+ console.log(formatEnv(env));
99
+ if (profile.settingsConfig.model) {
100
+ console.log(` ${chalk.gray("model")}: ${profile.settingsConfig.model}`);
101
+ }
102
+ console.log();
103
+ }
104
+
105
+ // Helper: format env for display
106
+ function formatEnv(env: Record<string, string>): string {
107
+ const lines: string[] = [];
108
+ const order = [
109
+ "ANTHROPIC_BASE_URL",
110
+ "ANTHROPIC_MODEL",
111
+ "ANTHROPIC_DEFAULT_OPUS_MODEL",
112
+ "ANTHROPIC_DEFAULT_SONNET_MODEL",
113
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL",
114
+ ];
115
+ for (const key of order) {
116
+ if (key in env) {
117
+ lines.push(` ${chalk.gray(key)}: ${env[key]}`);
118
+ }
119
+ }
120
+ // Show remaining keys (skip token for security)
121
+ for (const [key, val] of Object.entries(env)) {
122
+ if (!order.includes(key) && key !== "ANTHROPIC_AUTH_TOKEN") {
123
+ lines.push(` ${chalk.gray(key)}: ${val}`);
124
+ }
125
+ }
126
+ if ("ANTHROPIC_AUTH_TOKEN" in env) {
127
+ const token = env["ANTHROPIC_AUTH_TOKEN"];
128
+ const masked = token.slice(0, 8) + "..." + token.slice(-4);
129
+ lines.push(` ${chalk.gray("ANTHROPIC_AUTH_TOKEN")}: ${masked}`);
130
+ }
131
+ return lines.join("\n");
132
+ }
133
+
134
+ // Helper: Levenshtein distance
135
+ function levenshtein(a: string, b: string): number {
136
+ const la = a.length, lb = b.length;
137
+ const dp: number[][] = Array.from({ length: la + 1 }, (_, i) =>
138
+ Array.from({ length: lb + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
139
+ );
140
+ for (let i = 1; i <= la; i++) {
141
+ for (let j = 1; j <= lb; j++) {
142
+ dp[i][j] = a[i - 1] === b[j - 1]
143
+ ? dp[i - 1][j - 1]
144
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
145
+ }
146
+ }
147
+ return dp[la][lb];
148
+ }
149
+
150
+ // Helper: find suggestions for a mistyped name
151
+ function findSuggestions(input: string, names: string[]): string[] {
152
+ const lower = input.toLowerCase();
153
+
154
+ // 1. exact case-insensitive match
155
+ const exact = names.find((n) => n.toLowerCase() === lower);
156
+ if (exact) return [exact];
157
+
158
+ // 2. substring match (input is part of name, or name is part of input)
159
+ const substring = names.filter(
160
+ (n) => n.toLowerCase().includes(lower) || lower.includes(n.toLowerCase())
161
+ );
162
+ if (substring.length > 0) return substring;
163
+
164
+ // 3. Levenshtein distance <= 3
165
+ const fuzzy = names
166
+ .map((n) => ({ name: n, dist: levenshtein(lower, n.toLowerCase()) }))
167
+ .filter((x) => x.dist <= 3)
168
+ .sort((a, b) => a.dist - b.dist)
169
+ .map((x) => x.name);
170
+
171
+ return fuzzy;
172
+ }
173
+
174
+ // Helper: get alias target if exists
175
+ function getAliasTarget(input: string): string | undefined {
176
+ const rc = readRc();
177
+ return rc?.aliases?.[input];
178
+ }
179
+
180
+ // Helper: resolve name with alias conflict handling, returns profile or null
181
+ async function resolveProfile(store: ReturnType<typeof ensureStore>, input: string) {
182
+ const aliasTarget = getAliasTarget(input);
183
+ const directProfile = store.get(input);
184
+
185
+ // Both alias and config name exist → ask
186
+ if (aliasTarget && directProfile && aliasTarget !== input) {
187
+ console.log(chalk.yellow(t("alias.conflict", { name: input, target: aliasTarget })));
188
+ console.log(` ${chalk.cyan("1)")} ${t("alias.conflict_alias", { target: aliasTarget })}`);
189
+ console.log(` ${chalk.cyan("2)")} ${t("alias.conflict_config", { name: input })}`);
190
+ const choice = await ask(t("alias.choose_conflict"));
191
+ if (choice === "1") {
192
+ const profile = store.get(aliasTarget);
193
+ if (!profile) {
194
+ console.log(chalk.red(t("error.alias_target_missing", { alias: input, target: aliasTarget })));
195
+ return null;
196
+ }
197
+ return profile;
198
+ }
199
+ return directProfile;
200
+ }
201
+
202
+ // Alias exists → resolve
203
+ if (aliasTarget) {
204
+ const profile = store.get(aliasTarget);
205
+ if (profile) return profile;
206
+ console.log(chalk.red(t("error.alias_target_missing", { alias: input, target: aliasTarget })));
207
+ return null;
208
+ }
209
+
210
+ // Direct match
211
+ if (directProfile) return directProfile;
212
+
213
+ // Fuzzy matching
214
+ const allNames = store.list().map((p) => p.name);
215
+ const suggestions = findSuggestions(input, allNames);
216
+
217
+ console.log(chalk.red(t("error.not_found", { name: input })));
218
+ if (suggestions.length === 1) {
219
+ console.log(chalk.yellow(t("suggest.did_you_mean", { name: chalk.bold(suggestions[0]) })));
220
+ } else if (suggestions.length > 1) {
221
+ console.log(chalk.yellow(t("suggest.did_you_mean_header")));
222
+ for (const s of suggestions) {
223
+ console.log(` - ${chalk.bold(s)}`);
224
+ }
225
+ } else {
226
+ console.log(chalk.gray(t("suggest.use_list")));
227
+ }
228
+ return null;
229
+ }
230
+
231
+ // cc-castinit
232
+ program
233
+ .command("init")
234
+ .description(t("init.description"))
235
+ .action(async () => {
236
+ const rc = readRc();
237
+ writeRc({ aliases: rc?.aliases, locale: rc?.locale });
238
+ console.log(chalk.green(t("init.done")));
239
+
240
+ if (ccSwitchExists()) {
241
+ console.log(chalk.green(t("init.cc_switch_mode")));
242
+
243
+ // If standalone config.json has profiles, offer to migrate them into cc-switch DB
244
+ const { StandaloneStore } = await import("./store/standalone.js");
245
+ const standaloneStore = new StandaloneStore();
246
+ const standaloneProfiles = standaloneStore.list();
247
+
248
+ if (standaloneProfiles.length > 0) {
249
+ const migrate = await ask(t("init.cc_switch_migrate"));
250
+ if (migrate.toLowerCase() !== "n") {
251
+ const { CcSwitchStore } = await import("./store/cc-switch.js");
252
+ const ccStore = new CcSwitchStore();
253
+ const standaloneCurrent = standaloneStore.getCurrent();
254
+ for (const profile of standaloneProfiles) {
255
+ ccStore.save(profile.name, profile.settingsConfig);
256
+ }
257
+ if (standaloneCurrent) {
258
+ ccStore.setCurrent(standaloneCurrent);
259
+ }
260
+ console.log(chalk.green(t("init.cc_switch_migrate_done", { count: String(standaloneProfiles.length) })));
261
+ ccStore.close();
262
+ }
263
+ }
264
+ }
265
+ });
266
+
267
+ // cc-castsync
268
+ program
269
+ .command("sync")
270
+ .description(t("sync.description"))
271
+ .action(async () => {
272
+ if (!ccSwitchExists()) {
273
+ console.log(chalk.red(t("sync.no_cc_switch")));
274
+ return;
275
+ }
276
+ const { CcSwitchStore } = await import("./store/cc-switch.js");
277
+ const { StandaloneStore } = await import("./store/standalone.js");
278
+ const ccStore = new CcSwitchStore();
279
+ const standaloneStore = new StandaloneStore();
280
+
281
+ const profiles = ccStore.list();
282
+ if (profiles.length === 0) {
283
+ console.log(chalk.yellow(t("sync.empty")));
284
+ ccStore.close();
285
+ return;
286
+ }
287
+
288
+ for (const profile of profiles) {
289
+ standaloneStore.save(profile.name, profile.settingsConfig);
290
+ }
291
+
292
+ const current = ccStore.getCurrent();
293
+ if (current) {
294
+ standaloneStore.setCurrent(current);
295
+ }
296
+
297
+ console.log(chalk.green(t("sync.done", { count: String(profiles.length) })));
298
+ if (current) {
299
+ console.log(chalk.gray(t("sync.current", { name: current })));
300
+ } else {
301
+ console.log(chalk.gray(t("sync.no_current")));
302
+ }
303
+
304
+ ccStore.close();
305
+ });
306
+
307
+ // cc-castclear
308
+ program
309
+ .command("clear")
310
+ .description(t("clear.description"))
311
+ .action(async () => {
312
+ const confirm = await ask(t("clear.confirm"));
313
+ if (confirm.toLowerCase() !== "y") {
314
+ console.log(chalk.gray(t("clear.cancelled")));
315
+ return;
316
+ }
317
+
318
+ const rcPath = join(homedir(), ".cc-cast", "rc.json");
319
+ const configPath = join(homedir(), ".cc-cast", "config.json");
320
+
321
+ if (existsSync(configPath)) {
322
+ unlinkSync(configPath);
323
+ console.log(chalk.green(t("clear.removed", { path: configPath })));
324
+ }
325
+ if (existsSync(rcPath)) {
326
+ unlinkSync(rcPath);
327
+ console.log(chalk.green(t("clear.removed", { path: rcPath })));
328
+ }
329
+
330
+ console.log(chalk.green(t("clear.done")));
331
+ });
332
+
333
+ // cc-castimport
334
+ program
335
+ .command("import [file]")
336
+ .description(t("import.description"))
337
+ .action(async (file?: string) => {
338
+ const store = ensureStore();
339
+
340
+ let jsonContent: string;
341
+ if (file) {
342
+ // Read from file
343
+ if (!existsSync(file)) {
344
+ console.log(chalk.red(t("import.file_not_found", { file })));
345
+ return;
346
+ }
347
+ jsonContent = readFileSync(file, "utf-8");
348
+ } else {
349
+ // Read from stdin
350
+ console.log(chalk.gray(t("import.paste_hint")));
351
+ const chunks: Buffer[] = [];
352
+ process.stdin.setEncoding("utf-8");
353
+ for await (const chunk of process.stdin) {
354
+ chunks.push(Buffer.from(chunk));
355
+ }
356
+ jsonContent = Buffer.concat(chunks).toString("utf-8");
357
+ }
358
+
359
+ let configs: Record<string, Record<string, unknown>>;
360
+ try {
361
+ configs = JSON.parse(jsonContent);
362
+ } catch {
363
+ console.log(chalk.red(t("import.json_parse_error")));
364
+ return;
365
+ }
366
+
367
+ if (typeof configs !== "object" || configs === null || Object.keys(configs).length === 0) {
368
+ console.log(chalk.red(t("import.invalid_format")));
369
+ return;
370
+ }
371
+
372
+ let count = 0;
373
+ for (const [name, settingsConfig] of Object.entries(configs)) {
374
+ store.save(name, settingsConfig);
375
+ count++;
376
+ }
377
+
378
+ console.log(chalk.green(t("import.done", { count: String(count) })));
379
+ });
380
+
381
+ // cc-castlist
382
+ program
383
+ .command("list")
384
+ .alias("ls")
385
+ .description(t("list.description"))
386
+ .action(async () => {
387
+ const store = ensureStore();
388
+ const profiles = store.list();
389
+ const current = store.getCurrent();
390
+
391
+ if (profiles.length === 0) {
392
+ console.log(chalk.yellow(t("list.empty")));
393
+ return;
394
+ }
395
+
396
+ // Helper: apply selected profile
397
+ const switchTo = (name: string) => {
398
+ if (name === current) return;
399
+ const profile = store.get(name)!;
400
+ store.setCurrent(profile.name);
401
+ console.log(chalk.green(t("use.done", { name: chalk.bold(profile.name) })));
402
+ if (isCcSwitchGuiRunning()) {
403
+ console.log(chalk.yellow(t("use.cc_switch_running")));
404
+ } else {
405
+ applyProfile(profile.name, profile.settingsConfig);
406
+ const env = (profile.settingsConfig.env || {}) as Record<string, string>;
407
+ const model = env["ANTHROPIC_MODEL"] || t("common.model_default");
408
+ console.log(` ${t("common.model")}: ${chalk.cyan(model)}`);
409
+ console.log(chalk.gray(` ${t("use.restart")}`));
410
+ }
411
+ };
412
+
413
+ const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
414
+
415
+ if (isInteractive) {
416
+ const options = profiles.map((p) => {
417
+ const isCurrent = p.name === current;
418
+ const env = (p.settingsConfig.env || {}) as Record<string, string>;
419
+ const model = env["ANTHROPIC_MODEL"] || t("common.model_default");
420
+ const baseUrl = env["ANTHROPIC_BASE_URL"] || "default";
421
+ const tag = isCurrent ? ` ${t("list.current_marker")}` : "";
422
+ return {
423
+ label: `${p.name}${tag}`,
424
+ hint: `${t("common.model")}: ${model} ${t("common.source")}: ${baseUrl}`,
425
+ value: p.name,
426
+ };
427
+ });
428
+
429
+ const initial = profiles.findIndex((p) => p.name === current);
430
+ const prompt = createSelect({
431
+ message: "",
432
+ choices: options.map((o) => ({ name: o.value, message: o.label })),
433
+ initial: initial >= 0 ? initial : 0,
434
+ pointer: "●",
435
+ styles: { em: (k: any) => k, strong: (k: any) => k },
436
+ });
437
+ try {
438
+ const value = await prompt.run() as string;
439
+ switchTo(value);
440
+ } catch {
441
+ console.log(chalk.gray(t("common.cancelled")));
442
+ return;
443
+ }
444
+ } else {
445
+ // Fallback: numbered list + type to select
446
+ console.log(chalk.bold(`\n${t("list.header")}\n`));
447
+ profiles.forEach((p, i) => {
448
+ const isCurrent = p.name === current;
449
+ const marker = isCurrent ? chalk.green("● ") : " ";
450
+ const name = isCurrent ? chalk.green.bold(p.name) : p.name;
451
+ const env = (p.settingsConfig.env || {}) as Record<string, string>;
452
+ const model = env["ANTHROPIC_MODEL"] || t("common.model_default");
453
+ const baseUrl = env["ANTHROPIC_BASE_URL"] || "default";
454
+ console.log(`${marker}${chalk.gray(`${i + 1}.`)} ${name}`);
455
+ console.log(` ${t("common.model")}: ${chalk.cyan(model)} ${t("common.source")}: ${chalk.gray(baseUrl)}`);
456
+ });
457
+ console.log();
458
+ const input = await ask(t("list.choose_number"));
459
+ if (!input) return;
460
+ const idx = parseInt(input, 10) - 1;
461
+ if (isNaN(idx) || idx < 0 || idx >= profiles.length) {
462
+ console.log(chalk.red(t("error.invalid_choice")));
463
+ return;
464
+ }
465
+ switchTo(profiles[idx].name);
466
+ }
467
+ });
468
+
469
+ // cc-castcurrent
470
+ program
471
+ .command("current")
472
+ .description(t("current.description"))
473
+ .action(() => {
474
+ printCurrent();
475
+ });
476
+
477
+ // cc-castuse [name]
478
+ program
479
+ .command("use [name]")
480
+ .description(t("use.description"))
481
+ .action(async (name?: string) => {
482
+ const store = ensureStore();
483
+
484
+ if (!name) {
485
+ // No argument: behave like `ls`
486
+ await program.commands.find((c) => c.name() === "list")!.parseAsync([]);
487
+ return;
488
+ }
489
+
490
+ const profile = await resolveProfile(store, name);
491
+ if (!profile) return;
492
+
493
+ store.setCurrent(profile.name);
494
+ console.log(chalk.green(t("use.done", { name: chalk.bold(profile.name) })));
495
+ if (isCcSwitchGuiRunning()) {
496
+ console.log(chalk.yellow(t("use.cc_switch_running")));
497
+ } else {
498
+ applyProfile(profile.name, profile.settingsConfig);
499
+ const env = (profile.settingsConfig.env || {}) as Record<string, string>;
500
+ const model = env["ANTHROPIC_MODEL"] || t("common.model_default");
501
+ console.log(` ${t("common.model")}: ${chalk.cyan(model)}`);
502
+ console.log(chalk.gray(` ${t("use.restart")}`));
503
+ }
504
+ });
505
+
506
+ // cc-castsave <name>
507
+ program
508
+ .command("save <name>")
509
+ .description(t("save.description"))
510
+ .action((name: string) => {
511
+ const store = ensureStore();
512
+ const existing = store.get(name);
513
+
514
+ if (existing) {
515
+ console.log(chalk.yellow(t("save.overwrite", { name })));
516
+ }
517
+
518
+ const settings = readClaudeSettings();
519
+ const settingsConfig: Record<string, unknown> = {};
520
+ if (settings.env) settingsConfig.env = settings.env;
521
+ if (settings.model) settingsConfig.model = settings.model;
522
+ if (settings.hooks) settingsConfig.hooks = settings.hooks;
523
+ if (settings.statusLine) settingsConfig.statusLine = settings.statusLine;
524
+
525
+ store.save(name, settingsConfig);
526
+ store.setCurrent(name);
527
+
528
+ console.log(chalk.green(t("save.done", { name })));
529
+ });
530
+
531
+ // Helper: open editor with content, return parsed JSON or null
532
+ function openEditor(name: string, content: Record<string, unknown>): Record<string, unknown> | null {
533
+ const tmpFile = join(tmpdir(), `cc-cast-${name}-${Date.now()}.json`);
534
+ writeFileSync(tmpFile, JSON.stringify(content, null, 2));
535
+
536
+ const editor = process.env.EDITOR || "vi";
537
+ const result = spawnSync(editor, [tmpFile], { stdio: "inherit" });
538
+
539
+ let parsed: Record<string, unknown> | null = null;
540
+ if (result.status === 0) {
541
+ try {
542
+ parsed = JSON.parse(readFileSync(tmpFile, "utf-8"));
543
+ } catch {
544
+ console.log(chalk.red(t("add.json_parse_error")));
545
+ }
546
+ }
547
+
548
+ try { unlinkSync(tmpFile); } catch { /* ignore */ }
549
+ return parsed;
550
+ }
551
+
552
+ // Helper: save and optionally switch after add
553
+ async function saveAndSwitch(store: ReturnType<typeof ensureStore>, name: string, settingsConfig: Record<string, unknown>) {
554
+ store.save(name, settingsConfig);
555
+ console.log(chalk.green(t("add.done", { name })));
556
+
557
+ const switchChoice = await ask(t("add.switch_confirm"));
558
+ if (switchChoice.toLowerCase() !== "n") {
559
+ store.setCurrent(name);
560
+ console.log(chalk.green(t("use.done", { name: chalk.bold(name) })));
561
+ if (isCcSwitchGuiRunning()) {
562
+ console.log(chalk.yellow(t("use.cc_switch_running")));
563
+ } else {
564
+ applyProfile(name, settingsConfig);
565
+ console.log(chalk.gray(` ${t("use.restart")}`));
566
+ }
567
+ }
568
+ }
569
+
570
+ const BUILTIN_BASE_URLS: Record<string, string> = {
571
+ kimi: "https://api.moonshot.cn/v1",
572
+ "kimi-coding": "https://api.kimi.com/coding/",
573
+ openrouter: "https://openrouter.ai/api/v1",
574
+ deepseek: "https://api.deepseek.com",
575
+ zenmux: "https://zenmux.ai/api/anthropic",
576
+ fusecode: "https://www.fusecode.cc",
577
+ };
578
+
579
+ function getKnownBaseUrl(name: string): string | undefined {
580
+ return BUILTIN_BASE_URLS[name.toLowerCase()];
581
+ }
582
+
583
+ // cc-castadd
584
+ program
585
+ .command("add")
586
+ .alias("new")
587
+ .description(t("add.description"))
588
+ .action(async () => {
589
+ const store = ensureStore();
590
+
591
+ // 1. Ask name first
592
+ const name = await ask(t("add.prompt_name"));
593
+ if (!name) {
594
+ console.log(chalk.red(t("add.name_required")));
595
+ return;
596
+ }
597
+
598
+ // Check if exists
599
+ const existing = store.get(name);
600
+ if (existing) {
601
+ const overwrite = await ask(t("add.already_exists", { name }));
602
+ if (overwrite.toLowerCase() !== "y") {
603
+ console.log(chalk.gray(t("add.cancelled")));
604
+ return;
605
+ }
606
+ }
607
+
608
+ // 2. Choose mode
609
+ console.log(`\n${chalk.bold(t("add.mode_select"))}\n`);
610
+ console.log(` ${chalk.cyan("1)")} ${t("add.mode_interactive")}`);
611
+ console.log(` ${chalk.cyan("2)")} ${t("add.mode_json")}\n`);
612
+ const mode = await ask(t("add.mode_choose"));
613
+
614
+ if (mode === "2") {
615
+ // JSON mode: open editor with template
616
+ const template: Record<string, unknown> = {
617
+ env: {
618
+ ANTHROPIC_BASE_URL: getKnownBaseUrl(name) ?? "",
619
+ ANTHROPIC_AUTH_TOKEN: "",
620
+ ANTHROPIC_MODEL: "",
621
+ ANTHROPIC_DEFAULT_OPUS_MODEL: "",
622
+ ANTHROPIC_DEFAULT_SONNET_MODEL: "",
623
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: "",
624
+ },
625
+ };
626
+
627
+ console.log(chalk.gray(t("add.json_template_hint")));
628
+ const edited = openEditor(name, template);
629
+ if (!edited) return;
630
+
631
+ await saveAndSwitch(store, name, edited);
632
+ return;
633
+ }
634
+
635
+ // Interactive mode with step-based back support
636
+ const defaultBaseUrl = getKnownBaseUrl(name);
637
+ interface Step { key: string; prompt: string; required: boolean; defaultValue?: string; }
638
+ const steps: Step[] = [
639
+ { key: "ANTHROPIC_BASE_URL", prompt: t("add.prompt_base_url"), required: true, defaultValue: defaultBaseUrl },
640
+ { key: "ANTHROPIC_AUTH_TOKEN", prompt: t("add.prompt_auth_token"), required: true },
641
+ { key: "ANTHROPIC_MODEL", prompt: t("add.prompt_model"), required: false },
642
+ { key: "ANTHROPIC_DEFAULT_OPUS_MODEL", prompt: t("add.prompt_default_opus"), required: false },
643
+ { key: "ANTHROPIC_DEFAULT_SONNET_MODEL", prompt: t("add.prompt_default_sonnet"), required: false },
644
+ { key: "ANTHROPIC_DEFAULT_HAIKU_MODEL", prompt: t("add.prompt_default_haiku"), required: false },
645
+ ];
646
+
647
+ console.log(chalk.gray(t("add.back_hint")));
648
+ const values: Record<string, string> = {};
649
+ let i = 0;
650
+ while (i < steps.length) {
651
+ const step = steps[i];
652
+ const promptText = step.defaultValue
653
+ ? `${step.prompt}(${chalk.gray(step.defaultValue)}): `
654
+ : step.prompt;
655
+ const input = await ask(promptText, step.defaultValue);
656
+
657
+ if (input === "<") {
658
+ if (i > 0) i--;
659
+ continue;
660
+ }
661
+
662
+ const value = input || step.defaultValue || "";
663
+
664
+ if (step.required && !value) {
665
+ console.log(chalk.red(t("add.field_required", { field: step.key })));
666
+ continue;
667
+ }
668
+
669
+ if (value) values[step.key] = value;
670
+ else delete values[step.key];
671
+ i++;
672
+ }
673
+
674
+ // Build config
675
+ const env: Record<string, string> = {};
676
+ for (const [k, v] of Object.entries(values)) {
677
+ env[k] = v;
678
+ }
679
+
680
+ let settingsConfig: Record<string, unknown> = { env };
681
+
682
+ // Preview + optional edit
683
+ console.log(`\n${chalk.bold(t("add.preview_header"))}\n`);
684
+ console.log(JSON.stringify(settingsConfig, null, 2));
685
+ console.log();
686
+
687
+ const editChoice = await ask(t("add.edit_confirm"));
688
+ if (editChoice.toLowerCase() === "y") {
689
+ const edited = openEditor(name, settingsConfig);
690
+ if (edited) settingsConfig = edited;
691
+ }
692
+
693
+ await saveAndSwitch(store, name, settingsConfig);
694
+ });
695
+
696
+ // cc-castshow [name]
697
+ program
698
+ .command("show [name]")
699
+ .description(t("show.description"))
700
+ .action(async (name?: string) => {
701
+ const store = ensureStore();
702
+
703
+ if (!name) {
704
+ // Show all configurations
705
+ const profiles = store.list();
706
+ if (profiles.length === 0) {
707
+ console.log(chalk.yellow(t("list.empty")));
708
+ return;
709
+ }
710
+
711
+ console.log(chalk.bold(`\n${t("show.all_header")}\n`));
712
+ const allConfigs: Record<string, Record<string, unknown>> = {};
713
+ for (const profile of profiles) {
714
+ allConfigs[profile.name] = profile.settingsConfig;
715
+ }
716
+ console.log(JSON.stringify(allConfigs, null, 2));
717
+ return;
718
+ }
719
+
720
+ const profile = await resolveProfile(store, name);
721
+ if (!profile) return;
722
+
723
+ console.log(`\n${chalk.bold(profile.name)}\n`);
724
+ const env = (profile.settingsConfig.env || {}) as Record<string, string>;
725
+ console.log(formatEnv(env));
726
+ if (profile.settingsConfig.model) {
727
+ console.log(` ${chalk.gray("model")}: ${profile.settingsConfig.model}`);
728
+ }
729
+ console.log();
730
+ });
731
+
732
+ // cc-castmodify [name]
733
+ program
734
+ .command("modify [name]")
735
+ .alias("edit")
736
+ .description(t("modify.description"))
737
+ .action(async (name?: string) => {
738
+ const store = ensureStore();
739
+ const profiles = store.list();
740
+ const current = store.getCurrent();
741
+
742
+ // 1. Select profile
743
+ if (!name) {
744
+ if (profiles.length === 0) {
745
+ console.log(chalk.yellow(t("list.empty")));
746
+ return;
747
+ }
748
+
749
+ const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
750
+
751
+ if (isInteractive) {
752
+ const options = profiles.map((p) => {
753
+ const isCurrent = p.name === current;
754
+ const env = (p.settingsConfig.env || {}) as Record<string, string>;
755
+ const model = env["ANTHROPIC_MODEL"] || t("common.model_default");
756
+ const tag = isCurrent ? ` ${t("list.current_marker")}` : "";
757
+ return {
758
+ label: `${p.name}${tag}`,
759
+ hint: `${t("common.model")}: ${model}`,
760
+ value: p.name,
761
+ };
762
+ });
763
+
764
+ const prompt = createSelect({
765
+ message: "",
766
+ choices: options.map((o) => ({ name: o.value, message: o.label, hint: o.hint })),
767
+ pointer: "●",
768
+ styles: { em: (k: any) => k, strong: (k: any) => k },
769
+ });
770
+ try {
771
+ name = await prompt.run() as string;
772
+ } catch {
773
+ console.log(chalk.gray(t("common.cancelled")));
774
+ return;
775
+ }
776
+ } else {
777
+ console.log(chalk.bold(`\n${t("list.header")}\n`));
778
+ profiles.forEach((p, i) => {
779
+ const isCurrent = p.name === current;
780
+ const marker = isCurrent ? chalk.green("● ") : " ";
781
+ const label = isCurrent ? chalk.green.bold(p.name) : p.name;
782
+ const env = (p.settingsConfig.env || {}) as Record<string, string>;
783
+ const model = env["ANTHROPIC_MODEL"] || t("common.model_default");
784
+ console.log(`${marker}${chalk.gray(`${i + 1}.`)} ${label}`);
785
+ console.log(` ${t("common.model")}: ${chalk.cyan(model)}`);
786
+ });
787
+ console.log();
788
+ const input = await ask(t("list.choose_number"));
789
+ if (!input) return;
790
+ const idx = parseInt(input, 10) - 1;
791
+ if (isNaN(idx) || idx < 0 || idx >= profiles.length) {
792
+ console.log(chalk.red(t("error.invalid_choice")));
793
+ return;
794
+ }
795
+ name = profiles[idx].name;
796
+ }
797
+ }
798
+
799
+ const profile = await resolveProfile(store, name);
800
+ if (!profile) return;
801
+
802
+ const currentEnv = (profile.settingsConfig.env || {}) as Record<string, string>;
803
+
804
+ // 2. Choose mode
805
+ console.log(`\n${chalk.bold(t("add.mode_select"))}\n`);
806
+ console.log(` ${chalk.cyan("1)")} ${t("add.mode_interactive")}`);
807
+ console.log(` ${chalk.cyan("2)")} ${t("add.mode_json")}\n`);
808
+ const mode = await ask(t("add.mode_choose"));
809
+
810
+ let settingsConfig: Record<string, unknown>;
811
+
812
+ if (mode === "2") {
813
+ // JSON mode
814
+ const edited = openEditor(profile.name, profile.settingsConfig);
815
+ if (!edited) return;
816
+ settingsConfig = edited;
817
+ } else {
818
+ // Step-by-step mode with current values as defaults
819
+ interface Step { key: string; prompt: string; required: boolean; }
820
+ const steps: Step[] = [
821
+ { key: "ANTHROPIC_BASE_URL", prompt: "ANTHROPIC_BASE_URL", required: true },
822
+ { key: "ANTHROPIC_AUTH_TOKEN", prompt: "ANTHROPIC_AUTH_TOKEN", required: true },
823
+ { key: "ANTHROPIC_MODEL", prompt: "ANTHROPIC_MODEL", required: false },
824
+ { key: "ANTHROPIC_DEFAULT_OPUS_MODEL", prompt: "ANTHROPIC_DEFAULT_OPUS_MODEL", required: false },
825
+ { key: "ANTHROPIC_DEFAULT_SONNET_MODEL", prompt: "ANTHROPIC_DEFAULT_SONNET_MODEL", required: false },
826
+ { key: "ANTHROPIC_DEFAULT_HAIKU_MODEL", prompt: "ANTHROPIC_DEFAULT_HAIKU_MODEL", required: false },
827
+ ];
828
+
829
+ console.log(chalk.gray(t("add.back_hint")));
830
+ const values: Record<string, string> = { ...currentEnv };
831
+ let i = 0;
832
+ while (i < steps.length) {
833
+ const step = steps[i];
834
+ const cur = currentEnv[step.key]
835
+ || (step.key === "ANTHROPIC_BASE_URL" ? (getKnownBaseUrl(profile.name) ?? "") : "");
836
+ const hint = cur ? `(${chalk.gray(cur)})` : "";
837
+ const input = await ask(`${step.prompt}${hint}: `, cur || undefined);
838
+
839
+ if (input === "<") {
840
+ if (i > 0) i--;
841
+ continue;
842
+ }
843
+
844
+ if (input) {
845
+ values[step.key] = input;
846
+ } else if (step.required && !cur) {
847
+ console.log(chalk.red(t("add.field_required", { field: step.key })));
848
+ continue;
849
+ }
850
+ // empty input + has current value → keep current (already in values)
851
+ i++;
852
+ }
853
+
854
+ const env: Record<string, string> = {};
855
+ for (const [k, v] of Object.entries(values)) {
856
+ if (v) env[k] = v;
857
+ }
858
+ settingsConfig = { ...profile.settingsConfig, env };
859
+ }
860
+
861
+ // 3. Preview
862
+ console.log(`\n${chalk.bold(t("add.preview_header"))}\n`);
863
+ console.log(JSON.stringify(settingsConfig, null, 2));
864
+ console.log();
865
+
866
+ // 4. Optional editor (only for step mode)
867
+ if (mode !== "2") {
868
+ const editChoice = await ask(t("add.edit_confirm"));
869
+ if (editChoice.toLowerCase() === "y") {
870
+ const edited = openEditor(profile.name, settingsConfig);
871
+ if (edited) settingsConfig = edited;
872
+ }
873
+ }
874
+
875
+ // 5. Save
876
+ store.save(profile.name, settingsConfig);
877
+ console.log(chalk.green(t("modify.done", { name: profile.name })));
878
+
879
+ // 6. Switch if not current
880
+ if (profile.name !== current) {
881
+ const switchChoice = await ask(t("add.switch_confirm"));
882
+ if (switchChoice.toLowerCase() !== "n") {
883
+ store.setCurrent(profile.name);
884
+ console.log(chalk.green(t("use.done", { name: chalk.bold(profile.name) })));
885
+ if (isCcSwitchGuiRunning()) {
886
+ console.log(chalk.yellow(t("use.cc_switch_running")));
887
+ } else {
888
+ applyProfile(profile.name, settingsConfig);
889
+ console.log(chalk.gray(` ${t("use.restart")}`));
890
+ }
891
+ }
892
+ } else {
893
+ applyProfile(profile.name, settingsConfig);
894
+ console.log(chalk.gray(` ${t("use.restart")}`));
895
+ }
896
+ });
897
+
898
+ // cc-castremove [name]
899
+ program
900
+ .command("remove [name]")
901
+ .alias("rm")
902
+ .description(t("remove.description"))
903
+ .action(async (name?: string) => {
904
+ const store = ensureStore();
905
+ const profiles = store.list();
906
+ const current = store.getCurrent();
907
+
908
+ if (!name) {
909
+ if (profiles.length === 0) {
910
+ console.log(chalk.yellow(t("list.empty")));
911
+ return;
912
+ }
913
+
914
+ const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
915
+
916
+ if (isInteractive) {
917
+ const options = profiles.map((p) => {
918
+ const isCurrent = p.name === current;
919
+ const env = (p.settingsConfig.env || {}) as Record<string, string>;
920
+ const model = env["ANTHROPIC_MODEL"] || t("common.model_default");
921
+ const tag = isCurrent ? ` ${t("list.current_marker")}` : "";
922
+ return {
923
+ label: `${p.name}${tag}`,
924
+ hint: `${t("common.model")}: ${model}`,
925
+ value: p.name,
926
+ };
927
+ });
928
+
929
+ const prompt = createSelect({
930
+ message: "",
931
+ choices: options.map((o) => ({ name: o.value, message: o.label, hint: o.hint })),
932
+ pointer: "●",
933
+ styles: { em: (k: any) => k, strong: (k: any) => k },
934
+ });
935
+ try {
936
+ name = await prompt.run() as string;
937
+ } catch {
938
+ console.log(chalk.gray(t("common.cancelled")));
939
+ return;
940
+ }
941
+ } else {
942
+ console.log(chalk.bold(`\n${t("list.header")}\n`));
943
+ profiles.forEach((p, i) => {
944
+ const isCurrent = p.name === current;
945
+ const marker = isCurrent ? chalk.green("● ") : " ";
946
+ const label = isCurrent ? chalk.green.bold(p.name) : p.name;
947
+ const env = (p.settingsConfig.env || {}) as Record<string, string>;
948
+ const model = env["ANTHROPIC_MODEL"] || t("common.model_default");
949
+ console.log(`${marker}${chalk.gray(`${i + 1}.`)} ${label}`);
950
+ console.log(` ${t("common.model")}: ${chalk.cyan(model)}`);
951
+ });
952
+ console.log();
953
+ const input = await ask(t("list.choose_number"));
954
+ if (!input) return;
955
+ const idx = parseInt(input, 10) - 1;
956
+ if (isNaN(idx) || idx < 0 || idx >= profiles.length) {
957
+ console.log(chalk.red(t("error.invalid_choice")));
958
+ return;
959
+ }
960
+ name = profiles[idx].name;
961
+ }
962
+ }
963
+
964
+ // Check if name is an alias
965
+ const aliasTarget = getAliasTarget(name);
966
+ if (aliasTarget) {
967
+ console.log(chalk.yellow(t("alias.is_alias", { name, target: aliasTarget })));
968
+ console.log(`\n${t("alias.rm_which")}\n`);
969
+ console.log(` ${chalk.cyan("1)")} ${t("alias.rm_alias", { name })}`);
970
+ console.log(` ${chalk.cyan("2)")} ${t("alias.rm_config", { target: aliasTarget })}`);
971
+ const choice = await ask(t("alias.rm_choose"));
972
+ if (choice === "1") {
973
+ const rc = readRc()!;
974
+ delete rc.aliases![name];
975
+ writeRc(rc);
976
+ console.log(chalk.green(t("alias.rm_done", { short: name })));
977
+ return;
978
+ }
979
+ // choice === "2" → delete the config
980
+ name = aliasTarget;
981
+ }
982
+
983
+ const profile = await resolveProfile(store, name);
984
+ if (!profile) return;
985
+
986
+ const confirm = await ask(t("remove.confirm", { name: profile.name }));
987
+ if (confirm.toLowerCase() !== "y") return;
988
+
989
+ store.remove(profile.name);
990
+ console.log(chalk.green(t("remove.done", { name: profile.name })));
991
+ });
992
+
993
+ // cc-castalias
994
+ const aliasCmd = program
995
+ .command("alias")
996
+ .description(t("alias.description"));
997
+
998
+ aliasCmd
999
+ .command("set <short> <name>")
1000
+ .description(t("alias.set_description"))
1001
+ .action((short: string, name: string) => {
1002
+ const store = ensureStore();
1003
+ if (!store.get(name)) {
1004
+ const allNames = store.list().map((p) => p.name);
1005
+ const suggestions = findSuggestions(name, allNames);
1006
+ console.log(chalk.red(t("error.not_found", { name })));
1007
+ if (suggestions.length > 0) {
1008
+ console.log(chalk.yellow(t("suggest.did_you_mean", { name: suggestions.join(", ") })));
1009
+ }
1010
+ return;
1011
+ }
1012
+
1013
+ const rc = readRc()!;
1014
+ rc.aliases = rc.aliases || {};
1015
+ rc.aliases[short] = name;
1016
+ writeRc(rc);
1017
+ console.log(chalk.green(t("alias.set_done", { short: chalk.bold(short), name })));
1018
+ });
1019
+
1020
+ aliasCmd
1021
+ .command("rm <short>")
1022
+ .description(t("alias.rm_description"))
1023
+ .action((short: string) => {
1024
+ const rc = readRc();
1025
+ if (!rc?.aliases?.[short]) {
1026
+ console.log(chalk.red(t("alias.rm_not_found", { short })));
1027
+ return;
1028
+ }
1029
+ delete rc.aliases![short];
1030
+ writeRc(rc);
1031
+ console.log(chalk.green(t("alias.rm_done", { short })));
1032
+ });
1033
+
1034
+ aliasCmd
1035
+ .command("list")
1036
+ .alias("ls")
1037
+ .description(t("alias.list_description"))
1038
+ .action(() => {
1039
+ const rc = readRc();
1040
+ const aliases = rc?.aliases || {};
1041
+ const entries = Object.entries(aliases);
1042
+
1043
+ if (entries.length === 0) {
1044
+ console.log(chalk.yellow(t("alias.list_empty")));
1045
+ return;
1046
+ }
1047
+
1048
+ console.log(chalk.bold(`\n${t("alias.list_header")}\n`));
1049
+ for (const [short, name] of entries) {
1050
+ console.log(` ${chalk.cyan.bold(short)} → ${name}`);
1051
+ }
1052
+ console.log();
1053
+ });
1054
+
1055
+ // Default: cc-cast alias (no subcommand) → show list
1056
+ aliasCmd.action(() => {
1057
+ aliasCmd.commands.find((c) => c.name() === "list")!.parseAsync([]);
1058
+ });
1059
+
1060
+ // cc-castlocale
1061
+ const localeCmd = program
1062
+ .command("locale")
1063
+ .description(t("locale.description"));
1064
+
1065
+ localeCmd
1066
+ .command("set <lang>")
1067
+ .description(t("locale.set_description"))
1068
+ .action((lang: string) => {
1069
+ if (lang !== "zh" && lang !== "en") {
1070
+ console.log(chalk.red(t("locale.set_invalid", { locale: lang })));
1071
+ return;
1072
+ }
1073
+ switchLocale(lang);
1074
+ });
1075
+
1076
+ const SUPPORTED_LOCALES: Array<{ code: string; label: string }> = [
1077
+ { code: "zh", label: "中文" },
1078
+ { code: "en", label: "English" },
1079
+ ];
1080
+
1081
+ const switchLocale = (code: string) => {
1082
+ const rc = readRc();
1083
+ rc.locale = code as "zh" | "en";
1084
+ writeRc(rc);
1085
+ setLocale(code as "zh" | "en");
1086
+ console.log(chalk.green(t("locale.set_done", { locale: code })));
1087
+ };
1088
+
1089
+ localeCmd
1090
+ .command("list")
1091
+ .alias("ls")
1092
+ .description(t("locale.list_description"))
1093
+ .action(async () => {
1094
+ const rc = readRc();
1095
+ const current = rc?.locale || getLocale();
1096
+ const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
1097
+
1098
+ if (isInteractive) {
1099
+ const options = SUPPORTED_LOCALES.map(({ code, label }) => {
1100
+ const isCurrent = code === current;
1101
+ const tag = isCurrent ? ` ${t("locale.list_current_marker")}` : "";
1102
+ return { label: `${code} - ${label}${tag}`, value: code };
1103
+ });
1104
+
1105
+ const initialIdx = options.findIndex((o) => o.value === current);
1106
+ const prompt = createSelect({
1107
+ message: "",
1108
+ choices: options.map((o) => ({ name: o.value, message: o.label })),
1109
+ initial: initialIdx >= 0 ? initialIdx : 0,
1110
+ pointer: "●",
1111
+ styles: { em: (k: any) => k, strong: (k: any) => k },
1112
+ });
1113
+ try {
1114
+ const value = await prompt.run() as string;
1115
+ if (value === current) return;
1116
+ switchLocale(value);
1117
+ } catch {
1118
+ console.log(chalk.gray(t("common.cancelled")));
1119
+ return;
1120
+ }
1121
+ } else {
1122
+ console.log(chalk.bold(`\n${t("locale.list_header")}\n`));
1123
+ SUPPORTED_LOCALES.forEach(({ code, label }, i) => {
1124
+ const isCurrent = code === current;
1125
+ const marker = isCurrent ? chalk.green("● ") : " ";
1126
+ const name = isCurrent ? chalk.green.bold(`${code} - ${label}`) : `${code} - ${label}`;
1127
+ const tag = isCurrent ? chalk.gray(` ${t("locale.list_current_marker")}`) : "";
1128
+ console.log(`${marker}${chalk.gray(`${i + 1}.`)} ${name}${tag}`);
1129
+ });
1130
+ console.log();
1131
+ const input = await ask(t("locale.choose_number"));
1132
+ if (!input) return;
1133
+ const idx = parseInt(input, 10) - 1;
1134
+ if (isNaN(idx) || idx < 0 || idx >= SUPPORTED_LOCALES.length) {
1135
+ console.log(chalk.red(t("error.invalid_choice")));
1136
+ return;
1137
+ }
1138
+ const selected = SUPPORTED_LOCALES[idx].code;
1139
+ if (selected === current) return;
1140
+ switchLocale(selected);
1141
+ }
1142
+ });
1143
+
1144
+ // Default: cc-cast locale (no subcommand) → show list
1145
+ localeCmd.action(() => {
1146
+ localeCmd.commands.find((c) => c.name() === "list")!.parseAsync([]);
1147
+ });
1148
+
1149
+ program.parse();