@daylenjeez/ccm-switch 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +195 -0
- package/README.zh-CN.md +195 -0
- package/dist/claude.d.ts +3 -0
- package/dist/claude.js +25 -0
- package/dist/i18n/en.d.ts +3 -0
- package/dist/i18n/en.js +124 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/index.js +35 -0
- package/dist/i18n/zh.d.ts +107 -0
- package/dist/i18n/zh.js +124 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +872 -0
- package/dist/store/cc-switch.d.ts +13 -0
- package/dist/store/cc-switch.js +91 -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 +18 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +34 -0
- package/package.json +32 -0
- package/src/claude.ts +30 -0
- package/src/i18n/en.ts +144 -0
- package/src/i18n/index.ts +44 -0
- package/src/i18n/zh.ts +143 -0
- package/src/index.ts +949 -0
- package/src/store/cc-switch.ts +114 -0
- package/src/store/interface.ts +1 -0
- package/src/store/standalone.ts +79 -0
- package/src/types.ts +20 -0
- package/src/utils.ts +37 -0
- package/tsconfig.json +14 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { readRc, writeRc, getStore } 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 } from "fs";
|
|
10
|
+
import { tmpdir } from "os";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { t, setLocale } from "./i18n/index.js";
|
|
13
|
+
import * as clack from "@clack/prompts";
|
|
14
|
+
const program = new Command();
|
|
15
|
+
program
|
|
16
|
+
.name("ccm")
|
|
17
|
+
.description(t("program.description"))
|
|
18
|
+
.version("1.0.0");
|
|
19
|
+
// Helper: prompt user for input
|
|
20
|
+
function ask(question) {
|
|
21
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
rl.question(question, (answer) => {
|
|
24
|
+
rl.close();
|
|
25
|
+
resolve(answer.trim());
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
// Helper: ensure initialized
|
|
30
|
+
function ensureStore() {
|
|
31
|
+
const store = getStore();
|
|
32
|
+
if (!store) {
|
|
33
|
+
console.log(chalk.yellow(t("common.not_init")));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
return store;
|
|
37
|
+
}
|
|
38
|
+
// Helper: format env for display
|
|
39
|
+
function formatEnv(env) {
|
|
40
|
+
const lines = [];
|
|
41
|
+
const order = [
|
|
42
|
+
"ANTHROPIC_BASE_URL",
|
|
43
|
+
"ANTHROPIC_MODEL",
|
|
44
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
45
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
46
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
47
|
+
];
|
|
48
|
+
for (const key of order) {
|
|
49
|
+
if (key in env) {
|
|
50
|
+
lines.push(` ${chalk.gray(key)}: ${env[key]}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Show remaining keys (skip token for security)
|
|
54
|
+
for (const [key, val] of Object.entries(env)) {
|
|
55
|
+
if (!order.includes(key) && key !== "ANTHROPIC_AUTH_TOKEN") {
|
|
56
|
+
lines.push(` ${chalk.gray(key)}: ${val}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if ("ANTHROPIC_AUTH_TOKEN" in env) {
|
|
60
|
+
const token = env["ANTHROPIC_AUTH_TOKEN"];
|
|
61
|
+
const masked = token.slice(0, 8) + "..." + token.slice(-4);
|
|
62
|
+
lines.push(` ${chalk.gray("ANTHROPIC_AUTH_TOKEN")}: ${masked}`);
|
|
63
|
+
}
|
|
64
|
+
return lines.join("\n");
|
|
65
|
+
}
|
|
66
|
+
// Helper: Levenshtein distance
|
|
67
|
+
function levenshtein(a, b) {
|
|
68
|
+
const la = a.length, lb = b.length;
|
|
69
|
+
const dp = Array.from({ length: la + 1 }, (_, i) => Array.from({ length: lb + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
|
|
70
|
+
for (let i = 1; i <= la; i++) {
|
|
71
|
+
for (let j = 1; j <= lb; j++) {
|
|
72
|
+
dp[i][j] = a[i - 1] === b[j - 1]
|
|
73
|
+
? dp[i - 1][j - 1]
|
|
74
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return dp[la][lb];
|
|
78
|
+
}
|
|
79
|
+
// Helper: find suggestions for a mistyped name
|
|
80
|
+
function findSuggestions(input, names) {
|
|
81
|
+
const lower = input.toLowerCase();
|
|
82
|
+
// 1. exact case-insensitive match
|
|
83
|
+
const exact = names.find((n) => n.toLowerCase() === lower);
|
|
84
|
+
if (exact)
|
|
85
|
+
return [exact];
|
|
86
|
+
// 2. substring match (input is part of name, or name is part of input)
|
|
87
|
+
const substring = names.filter((n) => n.toLowerCase().includes(lower) || lower.includes(n.toLowerCase()));
|
|
88
|
+
if (substring.length > 0)
|
|
89
|
+
return substring;
|
|
90
|
+
// 3. Levenshtein distance <= 3
|
|
91
|
+
const fuzzy = names
|
|
92
|
+
.map((n) => ({ name: n, dist: levenshtein(lower, n.toLowerCase()) }))
|
|
93
|
+
.filter((x) => x.dist <= 3)
|
|
94
|
+
.sort((a, b) => a.dist - b.dist)
|
|
95
|
+
.map((x) => x.name);
|
|
96
|
+
return fuzzy;
|
|
97
|
+
}
|
|
98
|
+
// Helper: get alias target if exists
|
|
99
|
+
function getAliasTarget(input) {
|
|
100
|
+
const rc = readRc();
|
|
101
|
+
return rc?.aliases?.[input];
|
|
102
|
+
}
|
|
103
|
+
// Helper: resolve name with alias conflict handling, returns profile or null
|
|
104
|
+
async function resolveProfile(store, input) {
|
|
105
|
+
const aliasTarget = getAliasTarget(input);
|
|
106
|
+
const directProfile = store.get(input);
|
|
107
|
+
// Both alias and config name exist → ask
|
|
108
|
+
if (aliasTarget && directProfile && aliasTarget !== input) {
|
|
109
|
+
console.log(chalk.yellow(t("alias.conflict", { name: input, target: aliasTarget })));
|
|
110
|
+
console.log(` ${chalk.cyan("1)")} ${t("alias.conflict_alias", { target: aliasTarget })}`);
|
|
111
|
+
console.log(` ${chalk.cyan("2)")} ${t("alias.conflict_config", { name: input })}`);
|
|
112
|
+
const choice = await ask(t("alias.choose_conflict"));
|
|
113
|
+
if (choice === "1") {
|
|
114
|
+
const profile = store.get(aliasTarget);
|
|
115
|
+
if (!profile) {
|
|
116
|
+
console.log(chalk.red(t("error.alias_target_missing", { alias: input, target: aliasTarget })));
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return profile;
|
|
120
|
+
}
|
|
121
|
+
return directProfile;
|
|
122
|
+
}
|
|
123
|
+
// Alias exists → resolve
|
|
124
|
+
if (aliasTarget) {
|
|
125
|
+
const profile = store.get(aliasTarget);
|
|
126
|
+
if (profile)
|
|
127
|
+
return profile;
|
|
128
|
+
console.log(chalk.red(t("error.alias_target_missing", { alias: input, target: aliasTarget })));
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
// Direct match
|
|
132
|
+
if (directProfile)
|
|
133
|
+
return directProfile;
|
|
134
|
+
// Fuzzy matching
|
|
135
|
+
const allNames = store.list().map((p) => p.name);
|
|
136
|
+
const suggestions = findSuggestions(input, allNames);
|
|
137
|
+
console.log(chalk.red(t("error.not_found", { name: input })));
|
|
138
|
+
if (suggestions.length === 1) {
|
|
139
|
+
console.log(chalk.yellow(t("suggest.did_you_mean", { name: chalk.bold(suggestions[0]) })));
|
|
140
|
+
}
|
|
141
|
+
else if (suggestions.length > 1) {
|
|
142
|
+
console.log(chalk.yellow(t("suggest.did_you_mean_header")));
|
|
143
|
+
for (const s of suggestions) {
|
|
144
|
+
console.log(` - ${chalk.bold(s)}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
console.log(chalk.gray(t("suggest.use_list")));
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
// ccm init
|
|
153
|
+
program
|
|
154
|
+
.command("init")
|
|
155
|
+
.description(t("init.description"))
|
|
156
|
+
.action(async () => {
|
|
157
|
+
if (ccSwitchExists()) {
|
|
158
|
+
const use = await ask(t("init.cc_switch_found"));
|
|
159
|
+
if (use.toLowerCase() !== "n") {
|
|
160
|
+
writeRc({ mode: "cc-switch" });
|
|
161
|
+
const { CcSwitchStore } = await import("./store/cc-switch.js");
|
|
162
|
+
const store = new CcSwitchStore();
|
|
163
|
+
const profiles = store.list();
|
|
164
|
+
const current = store.getCurrent();
|
|
165
|
+
console.log(chalk.green(t("init.done_cc_switch")));
|
|
166
|
+
console.log(chalk.green(t("init.imported", { count: String(profiles.length) })));
|
|
167
|
+
if (current) {
|
|
168
|
+
console.log(chalk.gray(t("init.current", { name: current })));
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
console.log(chalk.gray(t("init.no_current")));
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
writeRc({ mode: "standalone" });
|
|
177
|
+
console.log(chalk.green(t("init.done_standalone")));
|
|
178
|
+
});
|
|
179
|
+
// ccm config
|
|
180
|
+
program
|
|
181
|
+
.command("config")
|
|
182
|
+
.description(t("config.description"))
|
|
183
|
+
.action(async () => {
|
|
184
|
+
const rc = readRc();
|
|
185
|
+
if (!rc) {
|
|
186
|
+
console.log(chalk.yellow(t("common.not_init")));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
console.log(t("config.current_mode", { mode: chalk.cyan(rc.mode) }));
|
|
190
|
+
const confirm = await ask(t("config.switch_confirm"));
|
|
191
|
+
if (confirm.toLowerCase() === "y") {
|
|
192
|
+
const newMode = rc.mode === "cc-switch" ? "standalone" : "cc-switch";
|
|
193
|
+
if (newMode === "cc-switch" && !ccSwitchExists()) {
|
|
194
|
+
console.log(chalk.red(t("config.cc_switch_not_installed")));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
writeRc({ mode: newMode });
|
|
198
|
+
console.log(chalk.green(t("config.switched", { mode: newMode })));
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
// ccm list
|
|
202
|
+
program
|
|
203
|
+
.command("list")
|
|
204
|
+
.alias("ls")
|
|
205
|
+
.description(t("list.description"))
|
|
206
|
+
.action(async () => {
|
|
207
|
+
const store = ensureStore();
|
|
208
|
+
const profiles = store.list();
|
|
209
|
+
const current = store.getCurrent();
|
|
210
|
+
if (profiles.length === 0) {
|
|
211
|
+
console.log(chalk.yellow(t("list.empty")));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Helper: apply selected profile
|
|
215
|
+
const switchTo = (name) => {
|
|
216
|
+
if (name === current)
|
|
217
|
+
return;
|
|
218
|
+
const profile = store.get(name);
|
|
219
|
+
applyProfile(profile.settingsConfig);
|
|
220
|
+
store.setCurrent(profile.name);
|
|
221
|
+
const env = (profile.settingsConfig.env || {});
|
|
222
|
+
const model = env["ANTHROPIC_MODEL"] || t("common.model_default");
|
|
223
|
+
console.log(chalk.green(t("use.done", { name: chalk.bold(profile.name) })));
|
|
224
|
+
console.log(` ${t("common.model")}: ${chalk.cyan(model)}`);
|
|
225
|
+
console.log(chalk.gray(` ${t("use.restart")}`));
|
|
226
|
+
};
|
|
227
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
228
|
+
if (isInteractive) {
|
|
229
|
+
const options = profiles.map((p) => {
|
|
230
|
+
const isCurrent = p.name === current;
|
|
231
|
+
const env = (p.settingsConfig.env || {});
|
|
232
|
+
const model = env["ANTHROPIC_MODEL"] || t("common.model_default");
|
|
233
|
+
const baseUrl = env["ANTHROPIC_BASE_URL"] || "default";
|
|
234
|
+
const tag = isCurrent ? ` ${t("list.current_marker")}` : "";
|
|
235
|
+
return {
|
|
236
|
+
label: `${p.name}${tag}`,
|
|
237
|
+
hint: `${t("common.model")}: ${model} ${t("common.source")}: ${baseUrl}`,
|
|
238
|
+
value: p.name,
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
const initial = profiles.findIndex((p) => p.name === current);
|
|
242
|
+
const selected = await clack.select({
|
|
243
|
+
message: "",
|
|
244
|
+
options,
|
|
245
|
+
initialValue: initial >= 0 ? profiles[initial].name : profiles[0].name,
|
|
246
|
+
});
|
|
247
|
+
if (clack.isCancel(selected)) {
|
|
248
|
+
clack.cancel(t("list.cancelled"));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
switchTo(selected);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
// Fallback: numbered list + type to select
|
|
255
|
+
console.log(chalk.bold(`\n${t("list.header")}\n`));
|
|
256
|
+
profiles.forEach((p, i) => {
|
|
257
|
+
const isCurrent = p.name === current;
|
|
258
|
+
const marker = isCurrent ? chalk.green("● ") : " ";
|
|
259
|
+
const name = isCurrent ? chalk.green.bold(p.name) : p.name;
|
|
260
|
+
const env = (p.settingsConfig.env || {});
|
|
261
|
+
const model = env["ANTHROPIC_MODEL"] || t("common.model_default");
|
|
262
|
+
const baseUrl = env["ANTHROPIC_BASE_URL"] || "default";
|
|
263
|
+
console.log(`${marker}${chalk.gray(`${i + 1}.`)} ${name}`);
|
|
264
|
+
console.log(` ${t("common.model")}: ${chalk.cyan(model)} ${t("common.source")}: ${chalk.gray(baseUrl)}`);
|
|
265
|
+
});
|
|
266
|
+
console.log();
|
|
267
|
+
const input = await ask(t("list.choose_number"));
|
|
268
|
+
if (!input)
|
|
269
|
+
return;
|
|
270
|
+
const idx = parseInt(input, 10) - 1;
|
|
271
|
+
if (isNaN(idx) || idx < 0 || idx >= profiles.length) {
|
|
272
|
+
console.log(chalk.red(t("error.invalid_choice")));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
switchTo(profiles[idx].name);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
// ccm current
|
|
279
|
+
program
|
|
280
|
+
.command("current")
|
|
281
|
+
.description(t("current.description"))
|
|
282
|
+
.action(() => {
|
|
283
|
+
const store = ensureStore();
|
|
284
|
+
const currentName = store.getCurrent();
|
|
285
|
+
if (!currentName) {
|
|
286
|
+
console.log(chalk.yellow(t("current.none")));
|
|
287
|
+
console.log(chalk.gray(`\n${t("current.settings_header")}`));
|
|
288
|
+
const settings = readClaudeSettings();
|
|
289
|
+
const env = (settings.env || {});
|
|
290
|
+
console.log(formatEnv(env));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const profile = store.get(currentName);
|
|
294
|
+
if (!profile) {
|
|
295
|
+
console.log(chalk.yellow(t("current.not_exist", { name: currentName })));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
console.log(`\n${t("current.header", { name: chalk.green.bold(profile.name) })}\n`);
|
|
299
|
+
const env = (profile.settingsConfig.env || {});
|
|
300
|
+
console.log(formatEnv(env));
|
|
301
|
+
if (profile.settingsConfig.model) {
|
|
302
|
+
console.log(` ${chalk.gray("model")}: ${profile.settingsConfig.model}`);
|
|
303
|
+
}
|
|
304
|
+
console.log();
|
|
305
|
+
});
|
|
306
|
+
// ccm use <name>
|
|
307
|
+
program
|
|
308
|
+
.command("use <name>")
|
|
309
|
+
.description(t("use.description"))
|
|
310
|
+
.action(async (name) => {
|
|
311
|
+
const store = ensureStore();
|
|
312
|
+
const profile = await resolveProfile(store, name);
|
|
313
|
+
if (!profile)
|
|
314
|
+
return;
|
|
315
|
+
applyProfile(profile.settingsConfig);
|
|
316
|
+
store.setCurrent(profile.name);
|
|
317
|
+
const env = (profile.settingsConfig.env || {});
|
|
318
|
+
const model = env["ANTHROPIC_MODEL"] || t("common.model_default");
|
|
319
|
+
console.log(chalk.green(t("use.done", { name: chalk.bold(profile.name) })));
|
|
320
|
+
console.log(` ${t("common.model")}: ${chalk.cyan(model)}`);
|
|
321
|
+
console.log(chalk.gray(` ${t("use.restart")}`));
|
|
322
|
+
});
|
|
323
|
+
// ccm save <name>
|
|
324
|
+
program
|
|
325
|
+
.command("save <name>")
|
|
326
|
+
.description(t("save.description"))
|
|
327
|
+
.action((name) => {
|
|
328
|
+
const store = ensureStore();
|
|
329
|
+
const existing = store.get(name);
|
|
330
|
+
if (existing) {
|
|
331
|
+
console.log(chalk.yellow(t("save.overwrite", { name })));
|
|
332
|
+
}
|
|
333
|
+
const settings = readClaudeSettings();
|
|
334
|
+
const settingsConfig = {};
|
|
335
|
+
if (settings.env)
|
|
336
|
+
settingsConfig.env = settings.env;
|
|
337
|
+
if (settings.model)
|
|
338
|
+
settingsConfig.model = settings.model;
|
|
339
|
+
if (settings.hooks)
|
|
340
|
+
settingsConfig.hooks = settings.hooks;
|
|
341
|
+
if (settings.statusLine)
|
|
342
|
+
settingsConfig.statusLine = settings.statusLine;
|
|
343
|
+
store.save(name, settingsConfig);
|
|
344
|
+
store.setCurrent(name);
|
|
345
|
+
console.log(chalk.green(t("save.done", { name })));
|
|
346
|
+
});
|
|
347
|
+
// Helper: open editor with content, return parsed JSON or null
|
|
348
|
+
function openEditor(name, content) {
|
|
349
|
+
const tmpFile = join(tmpdir(), `ccm-${name}-${Date.now()}.json`);
|
|
350
|
+
writeFileSync(tmpFile, JSON.stringify(content, null, 2));
|
|
351
|
+
const editor = process.env.EDITOR || "vi";
|
|
352
|
+
const result = spawnSync(editor, [tmpFile], { stdio: "inherit" });
|
|
353
|
+
let parsed = null;
|
|
354
|
+
if (result.status === 0) {
|
|
355
|
+
try {
|
|
356
|
+
parsed = JSON.parse(readFileSync(tmpFile, "utf-8"));
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
console.log(chalk.red(t("add.json_parse_error")));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
unlinkSync(tmpFile);
|
|
364
|
+
}
|
|
365
|
+
catch { /* ignore */ }
|
|
366
|
+
return parsed;
|
|
367
|
+
}
|
|
368
|
+
// Helper: save and optionally switch after add
|
|
369
|
+
async function saveAndSwitch(store, name, settingsConfig) {
|
|
370
|
+
store.save(name, settingsConfig);
|
|
371
|
+
console.log(chalk.green(t("add.done", { name })));
|
|
372
|
+
const switchChoice = await ask(t("add.switch_confirm"));
|
|
373
|
+
if (switchChoice.toLowerCase() !== "n") {
|
|
374
|
+
applyProfile(settingsConfig);
|
|
375
|
+
store.setCurrent(name);
|
|
376
|
+
console.log(chalk.green(t("use.done", { name: chalk.bold(name) })));
|
|
377
|
+
console.log(chalk.gray(` ${t("use.restart")}`));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// ccm add
|
|
381
|
+
program
|
|
382
|
+
.command("add")
|
|
383
|
+
.alias("new")
|
|
384
|
+
.description(t("add.description"))
|
|
385
|
+
.action(async () => {
|
|
386
|
+
const store = ensureStore();
|
|
387
|
+
// 1. Ask name first
|
|
388
|
+
const name = await ask(t("add.prompt_name"));
|
|
389
|
+
if (!name) {
|
|
390
|
+
console.log(chalk.red(t("add.name_required")));
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
// Check if exists
|
|
394
|
+
const existing = store.get(name);
|
|
395
|
+
if (existing) {
|
|
396
|
+
const overwrite = await ask(t("add.already_exists", { name }));
|
|
397
|
+
if (overwrite.toLowerCase() !== "y") {
|
|
398
|
+
console.log(chalk.gray(t("add.cancelled")));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// 2. Choose mode
|
|
403
|
+
console.log(`\n${chalk.bold(t("add.mode_select"))}\n`);
|
|
404
|
+
console.log(` ${chalk.cyan("1)")} ${t("add.mode_interactive")}`);
|
|
405
|
+
console.log(` ${chalk.cyan("2)")} ${t("add.mode_json")}\n`);
|
|
406
|
+
const mode = await ask(t("add.mode_choose"));
|
|
407
|
+
if (mode === "2") {
|
|
408
|
+
// JSON mode: open editor with template
|
|
409
|
+
const template = {
|
|
410
|
+
env: {
|
|
411
|
+
ANTHROPIC_BASE_URL: "",
|
|
412
|
+
ANTHROPIC_AUTH_TOKEN: "",
|
|
413
|
+
ANTHROPIC_MODEL: "",
|
|
414
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: "",
|
|
415
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: "",
|
|
416
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: "",
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
console.log(chalk.gray(t("add.json_template_hint")));
|
|
420
|
+
const edited = openEditor(name, template);
|
|
421
|
+
if (!edited)
|
|
422
|
+
return;
|
|
423
|
+
await saveAndSwitch(store, name, edited);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const steps = [
|
|
427
|
+
{ key: "ANTHROPIC_BASE_URL", prompt: t("add.prompt_base_url"), required: true },
|
|
428
|
+
{ key: "ANTHROPIC_AUTH_TOKEN", prompt: t("add.prompt_auth_token"), required: true },
|
|
429
|
+
{ key: "ANTHROPIC_MODEL", prompt: t("add.prompt_model"), required: true },
|
|
430
|
+
{ key: "ANTHROPIC_DEFAULT_OPUS_MODEL", prompt: t("add.prompt_default_opus"), required: false },
|
|
431
|
+
{ key: "ANTHROPIC_DEFAULT_SONNET_MODEL", prompt: t("add.prompt_default_sonnet"), required: false },
|
|
432
|
+
{ key: "ANTHROPIC_DEFAULT_HAIKU_MODEL", prompt: t("add.prompt_default_haiku"), required: false },
|
|
433
|
+
];
|
|
434
|
+
console.log(chalk.gray(t("add.back_hint")));
|
|
435
|
+
const values = {};
|
|
436
|
+
let i = 0;
|
|
437
|
+
while (i < steps.length) {
|
|
438
|
+
const step = steps[i];
|
|
439
|
+
const input = await ask(step.prompt);
|
|
440
|
+
if (input === "<") {
|
|
441
|
+
if (i > 0)
|
|
442
|
+
i--;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (step.required && !input) {
|
|
446
|
+
console.log(chalk.red(t("add.field_required", { field: step.key })));
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (input)
|
|
450
|
+
values[step.key] = input;
|
|
451
|
+
else
|
|
452
|
+
delete values[step.key];
|
|
453
|
+
i++;
|
|
454
|
+
}
|
|
455
|
+
// Build config
|
|
456
|
+
const env = {};
|
|
457
|
+
for (const [k, v] of Object.entries(values)) {
|
|
458
|
+
env[k] = v;
|
|
459
|
+
}
|
|
460
|
+
let settingsConfig = { env };
|
|
461
|
+
// Preview + optional edit
|
|
462
|
+
console.log(`\n${chalk.bold(t("add.preview_header"))}\n`);
|
|
463
|
+
console.log(JSON.stringify(settingsConfig, null, 2));
|
|
464
|
+
console.log();
|
|
465
|
+
const editChoice = await ask(t("add.edit_confirm"));
|
|
466
|
+
if (editChoice.toLowerCase() === "y") {
|
|
467
|
+
const edited = openEditor(name, settingsConfig);
|
|
468
|
+
if (edited)
|
|
469
|
+
settingsConfig = edited;
|
|
470
|
+
}
|
|
471
|
+
await saveAndSwitch(store, name, settingsConfig);
|
|
472
|
+
});
|
|
473
|
+
// ccm show <name>
|
|
474
|
+
program
|
|
475
|
+
.command("show [name]")
|
|
476
|
+
.description(t("show.description"))
|
|
477
|
+
.action(async (name) => {
|
|
478
|
+
const store = ensureStore();
|
|
479
|
+
if (!name) {
|
|
480
|
+
const currentName = store.getCurrent();
|
|
481
|
+
if (!currentName) {
|
|
482
|
+
console.log(chalk.yellow(t("show.no_current")));
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
name = currentName;
|
|
486
|
+
}
|
|
487
|
+
const profile = await resolveProfile(store, name);
|
|
488
|
+
if (!profile)
|
|
489
|
+
return;
|
|
490
|
+
console.log(`\n${chalk.bold(profile.name)}\n`);
|
|
491
|
+
const env = (profile.settingsConfig.env || {});
|
|
492
|
+
console.log(formatEnv(env));
|
|
493
|
+
if (profile.settingsConfig.model) {
|
|
494
|
+
console.log(` ${chalk.gray("model")}: ${profile.settingsConfig.model}`);
|
|
495
|
+
}
|
|
496
|
+
console.log();
|
|
497
|
+
});
|
|
498
|
+
// ccm modify [name]
|
|
499
|
+
program
|
|
500
|
+
.command("modify [name]")
|
|
501
|
+
.alias("edit")
|
|
502
|
+
.description(t("modify.description"))
|
|
503
|
+
.action(async (name) => {
|
|
504
|
+
const store = ensureStore();
|
|
505
|
+
const profiles = store.list();
|
|
506
|
+
const current = store.getCurrent();
|
|
507
|
+
// 1. Select profile
|
|
508
|
+
if (!name) {
|
|
509
|
+
if (profiles.length === 0) {
|
|
510
|
+
console.log(chalk.yellow(t("list.empty")));
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
514
|
+
if (isInteractive) {
|
|
515
|
+
const options = profiles.map((p) => {
|
|
516
|
+
const isCurrent = p.name === current;
|
|
517
|
+
const env = (p.settingsConfig.env || {});
|
|
518
|
+
const model = env["ANTHROPIC_MODEL"] || t("common.model_default");
|
|
519
|
+
const tag = isCurrent ? ` ${t("list.current_marker")}` : "";
|
|
520
|
+
return {
|
|
521
|
+
label: `${p.name}${tag}`,
|
|
522
|
+
hint: `${t("common.model")}: ${model}`,
|
|
523
|
+
value: p.name,
|
|
524
|
+
};
|
|
525
|
+
});
|
|
526
|
+
const selected = await clack.select({
|
|
527
|
+
message: "",
|
|
528
|
+
options,
|
|
529
|
+
});
|
|
530
|
+
if (clack.isCancel(selected)) {
|
|
531
|
+
clack.cancel(t("list.cancelled"));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
name = selected;
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
console.log(chalk.bold(`\n${t("list.header")}\n`));
|
|
538
|
+
profiles.forEach((p, i) => {
|
|
539
|
+
const isCurrent = p.name === current;
|
|
540
|
+
const marker = isCurrent ? chalk.green("● ") : " ";
|
|
541
|
+
const label = isCurrent ? chalk.green.bold(p.name) : p.name;
|
|
542
|
+
const env = (p.settingsConfig.env || {});
|
|
543
|
+
const model = env["ANTHROPIC_MODEL"] || t("common.model_default");
|
|
544
|
+
console.log(`${marker}${chalk.gray(`${i + 1}.`)} ${label}`);
|
|
545
|
+
console.log(` ${t("common.model")}: ${chalk.cyan(model)}`);
|
|
546
|
+
});
|
|
547
|
+
console.log();
|
|
548
|
+
const input = await ask(t("list.choose_number"));
|
|
549
|
+
if (!input)
|
|
550
|
+
return;
|
|
551
|
+
const idx = parseInt(input, 10) - 1;
|
|
552
|
+
if (isNaN(idx) || idx < 0 || idx >= profiles.length) {
|
|
553
|
+
console.log(chalk.red(t("error.invalid_choice")));
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
name = profiles[idx].name;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const profile = await resolveProfile(store, name);
|
|
560
|
+
if (!profile)
|
|
561
|
+
return;
|
|
562
|
+
const currentEnv = (profile.settingsConfig.env || {});
|
|
563
|
+
// 2. Choose mode
|
|
564
|
+
console.log(`\n${chalk.bold(t("add.mode_select"))}\n`);
|
|
565
|
+
console.log(` ${chalk.cyan("1)")} ${t("add.mode_interactive")}`);
|
|
566
|
+
console.log(` ${chalk.cyan("2)")} ${t("add.mode_json")}\n`);
|
|
567
|
+
const mode = await ask(t("add.mode_choose"));
|
|
568
|
+
let settingsConfig;
|
|
569
|
+
if (mode === "2") {
|
|
570
|
+
// JSON mode
|
|
571
|
+
const edited = openEditor(profile.name, profile.settingsConfig);
|
|
572
|
+
if (!edited)
|
|
573
|
+
return;
|
|
574
|
+
settingsConfig = edited;
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
const steps = [
|
|
578
|
+
{ key: "ANTHROPIC_BASE_URL", prompt: "ANTHROPIC_BASE_URL", required: true },
|
|
579
|
+
{ key: "ANTHROPIC_AUTH_TOKEN", prompt: "ANTHROPIC_AUTH_TOKEN", required: true },
|
|
580
|
+
{ key: "ANTHROPIC_MODEL", prompt: "ANTHROPIC_MODEL", required: true },
|
|
581
|
+
{ key: "ANTHROPIC_DEFAULT_OPUS_MODEL", prompt: "ANTHROPIC_DEFAULT_OPUS_MODEL", required: false },
|
|
582
|
+
{ key: "ANTHROPIC_DEFAULT_SONNET_MODEL", prompt: "ANTHROPIC_DEFAULT_SONNET_MODEL", required: false },
|
|
583
|
+
{ key: "ANTHROPIC_DEFAULT_HAIKU_MODEL", prompt: "ANTHROPIC_DEFAULT_HAIKU_MODEL", required: false },
|
|
584
|
+
];
|
|
585
|
+
console.log(chalk.gray(t("add.back_hint")));
|
|
586
|
+
const values = { ...currentEnv };
|
|
587
|
+
let i = 0;
|
|
588
|
+
while (i < steps.length) {
|
|
589
|
+
const step = steps[i];
|
|
590
|
+
const cur = currentEnv[step.key] || "";
|
|
591
|
+
const hint = cur ? chalk.gray(` [${cur}]`) : "";
|
|
592
|
+
const input = await ask(`${step.prompt}${hint}: `);
|
|
593
|
+
if (input === "<") {
|
|
594
|
+
if (i > 0)
|
|
595
|
+
i--;
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (input) {
|
|
599
|
+
values[step.key] = input;
|
|
600
|
+
}
|
|
601
|
+
else if (step.required && !cur) {
|
|
602
|
+
console.log(chalk.red(t("add.field_required", { field: step.key })));
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
// empty input + has current value → keep current (already in values)
|
|
606
|
+
i++;
|
|
607
|
+
}
|
|
608
|
+
const env = {};
|
|
609
|
+
for (const [k, v] of Object.entries(values)) {
|
|
610
|
+
if (v)
|
|
611
|
+
env[k] = v;
|
|
612
|
+
}
|
|
613
|
+
settingsConfig = { ...profile.settingsConfig, env };
|
|
614
|
+
}
|
|
615
|
+
// 3. Preview
|
|
616
|
+
console.log(`\n${chalk.bold(t("add.preview_header"))}\n`);
|
|
617
|
+
console.log(JSON.stringify(settingsConfig, null, 2));
|
|
618
|
+
console.log();
|
|
619
|
+
// 4. Optional editor (only for step mode)
|
|
620
|
+
if (mode !== "2") {
|
|
621
|
+
const editChoice = await ask(t("add.edit_confirm"));
|
|
622
|
+
if (editChoice.toLowerCase() === "y") {
|
|
623
|
+
const edited = openEditor(profile.name, settingsConfig);
|
|
624
|
+
if (edited)
|
|
625
|
+
settingsConfig = edited;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// 5. Save
|
|
629
|
+
store.save(profile.name, settingsConfig);
|
|
630
|
+
console.log(chalk.green(t("modify.done", { name: profile.name })));
|
|
631
|
+
// 6. Switch if not current
|
|
632
|
+
if (profile.name !== current) {
|
|
633
|
+
const switchChoice = await ask(t("add.switch_confirm"));
|
|
634
|
+
if (switchChoice.toLowerCase() !== "n") {
|
|
635
|
+
applyProfile(settingsConfig);
|
|
636
|
+
store.setCurrent(profile.name);
|
|
637
|
+
console.log(chalk.green(t("use.done", { name: chalk.bold(profile.name) })));
|
|
638
|
+
console.log(chalk.gray(` ${t("use.restart")}`));
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
applyProfile(settingsConfig);
|
|
643
|
+
console.log(chalk.gray(` ${t("use.restart")}`));
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
// ccm remove [name]
|
|
647
|
+
program
|
|
648
|
+
.command("remove [name]")
|
|
649
|
+
.alias("rm")
|
|
650
|
+
.description(t("remove.description"))
|
|
651
|
+
.action(async (name) => {
|
|
652
|
+
const store = ensureStore();
|
|
653
|
+
const profiles = store.list();
|
|
654
|
+
const current = store.getCurrent();
|
|
655
|
+
if (!name) {
|
|
656
|
+
if (profiles.length === 0) {
|
|
657
|
+
console.log(chalk.yellow(t("list.empty")));
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
661
|
+
if (isInteractive) {
|
|
662
|
+
const options = profiles.map((p) => {
|
|
663
|
+
const isCurrent = p.name === current;
|
|
664
|
+
const env = (p.settingsConfig.env || {});
|
|
665
|
+
const model = env["ANTHROPIC_MODEL"] || t("common.model_default");
|
|
666
|
+
const tag = isCurrent ? ` ${t("list.current_marker")}` : "";
|
|
667
|
+
return {
|
|
668
|
+
label: `${p.name}${tag}`,
|
|
669
|
+
hint: `${t("common.model")}: ${model}`,
|
|
670
|
+
value: p.name,
|
|
671
|
+
};
|
|
672
|
+
});
|
|
673
|
+
const selected = await clack.select({
|
|
674
|
+
message: "",
|
|
675
|
+
options,
|
|
676
|
+
});
|
|
677
|
+
if (clack.isCancel(selected)) {
|
|
678
|
+
clack.cancel(t("list.cancelled"));
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
name = selected;
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
console.log(chalk.bold(`\n${t("list.header")}\n`));
|
|
685
|
+
profiles.forEach((p, i) => {
|
|
686
|
+
const isCurrent = p.name === current;
|
|
687
|
+
const marker = isCurrent ? chalk.green("● ") : " ";
|
|
688
|
+
const label = isCurrent ? chalk.green.bold(p.name) : p.name;
|
|
689
|
+
const env = (p.settingsConfig.env || {});
|
|
690
|
+
const model = env["ANTHROPIC_MODEL"] || t("common.model_default");
|
|
691
|
+
console.log(`${marker}${chalk.gray(`${i + 1}.`)} ${label}`);
|
|
692
|
+
console.log(` ${t("common.model")}: ${chalk.cyan(model)}`);
|
|
693
|
+
});
|
|
694
|
+
console.log();
|
|
695
|
+
const input = await ask(t("list.choose_number"));
|
|
696
|
+
if (!input)
|
|
697
|
+
return;
|
|
698
|
+
const idx = parseInt(input, 10) - 1;
|
|
699
|
+
if (isNaN(idx) || idx < 0 || idx >= profiles.length) {
|
|
700
|
+
console.log(chalk.red(t("error.invalid_choice")));
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
name = profiles[idx].name;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
// Check if name is an alias
|
|
707
|
+
const aliasTarget = getAliasTarget(name);
|
|
708
|
+
if (aliasTarget) {
|
|
709
|
+
console.log(chalk.yellow(t("alias.is_alias", { name, target: aliasTarget })));
|
|
710
|
+
console.log(`\n${t("alias.rm_which")}\n`);
|
|
711
|
+
console.log(` ${chalk.cyan("1)")} ${t("alias.rm_alias", { name })}`);
|
|
712
|
+
console.log(` ${chalk.cyan("2)")} ${t("alias.rm_config", { target: aliasTarget })}`);
|
|
713
|
+
const choice = await ask(t("alias.rm_choose"));
|
|
714
|
+
if (choice === "1") {
|
|
715
|
+
const rc = readRc();
|
|
716
|
+
delete rc.aliases[name];
|
|
717
|
+
writeRc(rc);
|
|
718
|
+
console.log(chalk.green(t("alias.rm_done", { short: name })));
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
// choice === "2" → delete the config
|
|
722
|
+
name = aliasTarget;
|
|
723
|
+
}
|
|
724
|
+
const profile = await resolveProfile(store, name);
|
|
725
|
+
if (!profile)
|
|
726
|
+
return;
|
|
727
|
+
const confirm = await ask(t("remove.confirm", { name: profile.name }));
|
|
728
|
+
if (confirm.toLowerCase() !== "y")
|
|
729
|
+
return;
|
|
730
|
+
store.remove(profile.name);
|
|
731
|
+
console.log(chalk.green(t("remove.done", { name: profile.name })));
|
|
732
|
+
});
|
|
733
|
+
// ccm alias
|
|
734
|
+
const aliasCmd = program
|
|
735
|
+
.command("alias")
|
|
736
|
+
.description(t("alias.description"));
|
|
737
|
+
aliasCmd
|
|
738
|
+
.command("set <short> <name>")
|
|
739
|
+
.description(t("alias.set_description"))
|
|
740
|
+
.action((short, name) => {
|
|
741
|
+
const store = ensureStore();
|
|
742
|
+
if (!store.get(name)) {
|
|
743
|
+
const allNames = store.list().map((p) => p.name);
|
|
744
|
+
const suggestions = findSuggestions(name, allNames);
|
|
745
|
+
console.log(chalk.red(t("error.not_found", { name })));
|
|
746
|
+
if (suggestions.length > 0) {
|
|
747
|
+
console.log(chalk.yellow(t("suggest.did_you_mean", { name: suggestions.join(", ") })));
|
|
748
|
+
}
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const rc = readRc();
|
|
752
|
+
rc.aliases = rc.aliases || {};
|
|
753
|
+
rc.aliases[short] = name;
|
|
754
|
+
writeRc(rc);
|
|
755
|
+
console.log(chalk.green(t("alias.set_done", { short: chalk.bold(short), name })));
|
|
756
|
+
});
|
|
757
|
+
aliasCmd
|
|
758
|
+
.command("rm <short>")
|
|
759
|
+
.description(t("alias.rm_description"))
|
|
760
|
+
.action((short) => {
|
|
761
|
+
const rc = readRc();
|
|
762
|
+
if (!rc?.aliases?.[short]) {
|
|
763
|
+
console.log(chalk.red(t("alias.rm_not_found", { short })));
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
delete rc.aliases[short];
|
|
767
|
+
writeRc(rc);
|
|
768
|
+
console.log(chalk.green(t("alias.rm_done", { short })));
|
|
769
|
+
});
|
|
770
|
+
aliasCmd
|
|
771
|
+
.command("list")
|
|
772
|
+
.alias("ls")
|
|
773
|
+
.description(t("alias.list_description"))
|
|
774
|
+
.action(() => {
|
|
775
|
+
const rc = readRc();
|
|
776
|
+
const aliases = rc?.aliases || {};
|
|
777
|
+
const entries = Object.entries(aliases);
|
|
778
|
+
if (entries.length === 0) {
|
|
779
|
+
console.log(chalk.yellow(t("alias.list_empty")));
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
console.log(chalk.bold(`\n${t("alias.list_header")}\n`));
|
|
783
|
+
for (const [short, name] of entries) {
|
|
784
|
+
console.log(` ${chalk.cyan.bold(short)} → ${name}`);
|
|
785
|
+
}
|
|
786
|
+
console.log();
|
|
787
|
+
});
|
|
788
|
+
// Default: ccm alias (no subcommand) → show list
|
|
789
|
+
aliasCmd.action(() => {
|
|
790
|
+
aliasCmd.commands.find((c) => c.name() === "list").parseAsync([]);
|
|
791
|
+
});
|
|
792
|
+
// ccm locale
|
|
793
|
+
const localeCmd = program
|
|
794
|
+
.command("locale")
|
|
795
|
+
.description(t("locale.description"));
|
|
796
|
+
localeCmd
|
|
797
|
+
.command("set <lang>")
|
|
798
|
+
.description(t("locale.set_description"))
|
|
799
|
+
.action((lang) => {
|
|
800
|
+
if (lang !== "zh" && lang !== "en") {
|
|
801
|
+
console.log(chalk.red(t("locale.set_invalid", { locale: lang })));
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
switchLocale(lang);
|
|
805
|
+
});
|
|
806
|
+
const SUPPORTED_LOCALES = [
|
|
807
|
+
{ code: "zh", label: "中文" },
|
|
808
|
+
{ code: "en", label: "English" },
|
|
809
|
+
];
|
|
810
|
+
const switchLocale = (code) => {
|
|
811
|
+
const rc = readRc();
|
|
812
|
+
if (!rc) {
|
|
813
|
+
console.log(chalk.yellow(t("common.not_init")));
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
rc.locale = code;
|
|
817
|
+
writeRc(rc);
|
|
818
|
+
setLocale(code);
|
|
819
|
+
console.log(chalk.green(t("locale.set_done", { locale: code })));
|
|
820
|
+
};
|
|
821
|
+
localeCmd
|
|
822
|
+
.command("list")
|
|
823
|
+
.alias("ls")
|
|
824
|
+
.description(t("locale.list_description"))
|
|
825
|
+
.action(async () => {
|
|
826
|
+
const rc = readRc();
|
|
827
|
+
const current = rc?.locale || "zh";
|
|
828
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
829
|
+
if (isInteractive) {
|
|
830
|
+
const options = SUPPORTED_LOCALES.map(({ code, label }) => {
|
|
831
|
+
const isCurrent = code === current;
|
|
832
|
+
const tag = isCurrent ? ` ${t("locale.list_current_marker")}` : "";
|
|
833
|
+
return { label: `${code} - ${label}${tag}`, value: code };
|
|
834
|
+
});
|
|
835
|
+
const selected = await clack.select({
|
|
836
|
+
message: "",
|
|
837
|
+
options,
|
|
838
|
+
initialValue: current,
|
|
839
|
+
});
|
|
840
|
+
if (clack.isCancel(selected) || selected === current)
|
|
841
|
+
return;
|
|
842
|
+
switchLocale(selected);
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
console.log(chalk.bold(`\n${t("locale.list_header")}\n`));
|
|
846
|
+
SUPPORTED_LOCALES.forEach(({ code, label }, i) => {
|
|
847
|
+
const isCurrent = code === current;
|
|
848
|
+
const marker = isCurrent ? chalk.green("● ") : " ";
|
|
849
|
+
const name = isCurrent ? chalk.green.bold(`${code} - ${label}`) : `${code} - ${label}`;
|
|
850
|
+
const tag = isCurrent ? chalk.gray(` ${t("locale.list_current_marker")}`) : "";
|
|
851
|
+
console.log(`${marker}${chalk.gray(`${i + 1}.`)} ${name}${tag}`);
|
|
852
|
+
});
|
|
853
|
+
console.log();
|
|
854
|
+
const input = await ask(t("locale.choose_number"));
|
|
855
|
+
if (!input)
|
|
856
|
+
return;
|
|
857
|
+
const idx = parseInt(input, 10) - 1;
|
|
858
|
+
if (isNaN(idx) || idx < 0 || idx >= SUPPORTED_LOCALES.length) {
|
|
859
|
+
console.log(chalk.red(t("error.invalid_choice")));
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const selected = SUPPORTED_LOCALES[idx].code;
|
|
863
|
+
if (selected === current)
|
|
864
|
+
return;
|
|
865
|
+
switchLocale(selected);
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
// Default: ccm locale (no subcommand) → show list
|
|
869
|
+
localeCmd.action(() => {
|
|
870
|
+
localeCmd.commands.find((c) => c.name() === "list").parseAsync([]);
|
|
871
|
+
});
|
|
872
|
+
program.parse();
|