@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/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();