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/LICENSE +21 -0
- package/README.md +202 -0
- package/README.zh-CN.md +202 -0
- package/dist/claude.d.ts +3 -0
- package/dist/claude.js +27 -0
- package/dist/i18n/en.d.ts +3 -0
- package/dist/i18n/en.js +139 -0
- package/dist/i18n/index.d.ts +6 -0
- package/dist/i18n/index.js +36 -0
- package/dist/i18n/zh.d.ts +120 -0
- package/dist/i18n/zh.js +139 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1060 -0
- package/dist/store/cc-switch.d.ts +13 -0
- package/dist/store/cc-switch.js +114 -0
- package/dist/store/interface.d.ts +1 -0
- package/dist/store/interface.js +1 -0
- package/dist/store/standalone.d.ts +9 -0
- package/dist/store/standalone.js +65 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.js +42 -0
- package/package.json +37 -0
- package/src/claude.ts +32 -0
- package/src/i18n/en.ts +161 -0
- package/src/i18n/index.ts +44 -0
- package/src/i18n/zh.ts +160 -0
- package/src/index.ts +1149 -0
- package/src/store/cc-switch.ts +145 -0
- package/src/store/interface.ts +1 -0
- package/src/store/standalone.ts +80 -0
- package/src/types.ts +19 -0
- package/src/utils.ts +47 -0
- package/tsconfig.json +14 -0
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();
|