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