ai-account-switch 1.5.7 → 1.6.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.
@@ -0,0 +1,997 @@
1
+ const chalk = require("chalk");
2
+ const inquirer = require("inquirer");
3
+ const ConfigManager = require("../config");
4
+ const { maskApiKey } = require("./helpers");
5
+ const { promptForModelGroup } = require("./model");
6
+
7
+ const config = new ConfigManager();
8
+
9
+ /**
10
+ * Add a new account
11
+ */
12
+ async function addAccount(name, options) {
13
+ // If name not provided, prompt for it
14
+ if (!name) {
15
+ const answers = await inquirer.prompt([
16
+ {
17
+ type: "input",
18
+ name: "accountName",
19
+ message: "Enter account name (请输入账号名称):",
20
+ validate: (input) =>
21
+ input.trim() !== "" ||
22
+ "Account name is required (账号名称不能为空)",
23
+ },
24
+ ]);
25
+ name = answers.accountName;
26
+ }
27
+
28
+ // Check if account already exists
29
+ if (config.accountExists(name)) {
30
+ const { overwrite } = await inquirer.prompt([
31
+ {
32
+ type: "confirm",
33
+ name: "overwrite",
34
+ message: `Account '${name}' already exists. Overwrite? (账号 '${name}' 已存在。是否覆盖?)`,
35
+ default: false,
36
+ },
37
+ ]);
38
+
39
+ if (!overwrite) {
40
+ console.log(chalk.yellow("Operation cancelled. (操作已取消。)"));
41
+ return;
42
+ }
43
+ }
44
+
45
+ // Prompt for account type first
46
+ const typeAnswer = await inquirer.prompt([
47
+ {
48
+ type: "list",
49
+ name: "type",
50
+ message: "Select account type (请选择账号类型):",
51
+ choices: ["Claude", "Codex", "CCR", "Droids", "Other"],
52
+ default: "Claude",
53
+ },
54
+ ]);
55
+
56
+ // Show configuration tips based on account type
57
+ if (typeAnswer.type === "Codex") {
58
+ console.log(
59
+ chalk.cyan("\n📝 Codex Configuration Tips (Codex 配置提示):")
60
+ );
61
+ console.log(
62
+ chalk.gray(
63
+ " • API URL should include the full path (e.g., https://api.example.com/v1) (API URL 应包含完整路径,例如: https://api.example.com/v1)"
64
+ )
65
+ );
66
+ console.log(
67
+ chalk.gray(
68
+ " • AIS will automatically add /v1 if missing (AIS 会自动添加 /v1 如果缺失)"
69
+ )
70
+ );
71
+ console.log(
72
+ chalk.gray(
73
+ " • Codex uses OpenAI-compatible API format (Codex 使用 OpenAI 兼容的 API 格式)\n"
74
+ )
75
+ );
76
+ } else if (typeAnswer.type === "Droids") {
77
+ console.log(
78
+ chalk.cyan("\n📝 Droids Configuration Tips (Droids 配置提示):")
79
+ );
80
+ console.log(
81
+ chalk.gray(
82
+ " • Droids configuration will be stored in .droids/config.json (Droids 配置将存储在 .droids/config.json)"
83
+ )
84
+ );
85
+ console.log(
86
+ chalk.gray(
87
+ " • API URL is optional (defaults to Droids default endpoint) (API URL 是可选的,默认使用 Droids 默认端点)"
88
+ )
89
+ );
90
+ console.log(
91
+ chalk.gray(
92
+ " • You can configure custom models and settings (您可以配置自定义模型和设置)\n"
93
+ )
94
+ );
95
+ } else if (typeAnswer.type === "CCR") {
96
+ console.log(
97
+ chalk.cyan("\n📝 CCR Configuration Tips (CCR 配置提示):")
98
+ );
99
+ console.log(
100
+ " • CCR configuration will be stored in ~/.claude-code-router/config.json (CCR 配置将存储在 ~/.claude-code-router/config.json)"
101
+ );
102
+ console.log(
103
+ " • You need to provide Provider name and models (您需要提供 Provider 名称和模型列表)"
104
+ );
105
+ console.log(
106
+ " • Router configuration will be automatically updated (Router 配置将自动更新)\n"
107
+ );
108
+ }
109
+
110
+ // Prompt for remaining account details
111
+ const accountData = await inquirer.prompt([
112
+ {
113
+ type: "input",
114
+ name: "apiKey",
115
+ message: "Enter API Key (请输入 API Key):",
116
+ validate: (input) =>
117
+ input.trim() !== "" || "API Key is required (API Key 不能为空)",
118
+ },
119
+ {
120
+ type: "input",
121
+ name: "apiUrl",
122
+ message:
123
+ typeAnswer.type === "Codex"
124
+ ? "Enter API URL (请输入 API URL) (e.g., https://api.example.com or https://api.example.com/v1) :"
125
+ : typeAnswer.type === "CCR"
126
+ ? "Enter API URL (请输入 API URL):"
127
+ : "Enter API URL (optional) (请输入 API URL,可选):",
128
+ default: typeAnswer.type === "CCR" ? "http://localhost:3000/v1/chat/completions" : "",
129
+ },
130
+ {
131
+ type: "input",
132
+ name: "email",
133
+ message: "Enter associated email (optional) (请输入关联邮箱,可选):",
134
+ default: "",
135
+ },
136
+ {
137
+ type: "input",
138
+ name: "description",
139
+ message: "Enter description (optional) (请输入描述,可选):",
140
+ default: "",
141
+ },
142
+ {
143
+ type: "confirm",
144
+ name: "addCustomEnv",
145
+ message:
146
+ "Add custom environment variables (是否添加自定义环境变量?) ? (e.g., CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC)",
147
+ default: false,
148
+ },
149
+ ]);
150
+
151
+ // Merge type into accountData
152
+ accountData.type = typeAnswer.type;
153
+
154
+ // Handle custom environment variables
155
+ if (accountData.addCustomEnv) {
156
+ accountData.customEnv = {};
157
+ let addMore = true;
158
+
159
+ console.log(
160
+ chalk.cyan(
161
+ "\n💡 Tip (提示): Enter in format KEY=VALUE, e.g., CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 (请使用 KEY=VALUE 格式输入)"
162
+ )
163
+ );
164
+ console.log(
165
+ chalk.cyan(" Or leave empty to finish (留空则完成添加)\n")
166
+ );
167
+
168
+ while (addMore) {
169
+ const envInput = await inquirer.prompt([
170
+ {
171
+ type: "input",
172
+ name: "envVar",
173
+ message:
174
+ "Environment variable (KEY=VALUE format) (环境变量,KEY=VALUE 格式):",
175
+ validate: (input) => {
176
+ // Allow empty input to skip
177
+ if (!input.trim()) return true;
178
+
179
+ // Check if input contains '='
180
+ if (!input.includes("=")) {
181
+ return "Invalid format. Use KEY=VALUE format (e.g., MY_VAR=value) (格式无效。请使用 KEY=VALUE 格式,例如: MY_VAR=value)";
182
+ }
183
+
184
+ const [key, ...valueParts] = input.split("=");
185
+ const value = valueParts.join("="); // In case value contains '='
186
+
187
+ if (!key.trim()) {
188
+ return "Variable name cannot be empty (变量名不能为空)";
189
+ }
190
+
191
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(key.trim())) {
192
+ return "Invalid variable name. Use uppercase letters, numbers, and underscores (e.g., MY_VAR) (变量名无效。请使用大写字母、数字和下划线,例如: MY_VAR)";
193
+ }
194
+
195
+ if (!value.trim()) {
196
+ return "Variable value cannot be empty (变量值不能为空)";
197
+ }
198
+
199
+ return true;
200
+ },
201
+ },
202
+ ]);
203
+
204
+ // If user left input empty, skip adding more
205
+ if (!envInput.envVar.trim()) {
206
+ break;
207
+ }
208
+
209
+ // Parse KEY=VALUE
210
+ const [key, ...valueParts] = envInput.envVar.split("=");
211
+ const value = valueParts.join("="); // In case value contains '='
212
+
213
+ accountData.customEnv[key.trim()] = value.trim();
214
+
215
+ // Display currently added variables
216
+ console.log(
217
+ chalk.green("\n✓ Added (已添加):"),
218
+ chalk.cyan(`${key.trim()}=${value.trim()}`)
219
+ );
220
+
221
+ if (Object.keys(accountData.customEnv).length > 0) {
222
+ console.log(
223
+ chalk.bold(
224
+ "\n📋 Current environment variables (当前环境变量):"
225
+ )
226
+ );
227
+ Object.entries(accountData.customEnv).forEach(([k, v]) => {
228
+ console.log(chalk.gray(" •"), chalk.cyan(`${k}=${v}`));
229
+ });
230
+ console.log("");
231
+ }
232
+
233
+ const { continueAdding } = await inquirer.prompt([
234
+ {
235
+ type: "confirm",
236
+ name: "continueAdding",
237
+ message:
238
+ "Add another environment variable? (是否继续添加环境变量?)",
239
+ default: false,
240
+ },
241
+ ]);
242
+
243
+ addMore = continueAdding;
244
+ }
245
+
246
+ if (Object.keys(accountData.customEnv).length > 0) {
247
+ console.log(
248
+ chalk.green(
249
+ `\n✓ Total: ${
250
+ Object.keys(accountData.customEnv).length
251
+ } custom environment variable(s) added (总计: 已添加 ${
252
+ Object.keys(accountData.customEnv).length
253
+ } 个自定义环境变量)\n`
254
+ )
255
+ );
256
+ } else {
257
+ console.log(
258
+ chalk.yellow(
259
+ "\n⚠ No custom environment variables added (未添加自定义环境变量)\n"
260
+ )
261
+ );
262
+ }
263
+ }
264
+
265
+ // Remove the addCustomEnv flag before saving
266
+ delete accountData.addCustomEnv;
267
+
268
+ // Handle model configuration based on account type
269
+ if (accountData.type === "Claude") {
270
+ // Claude uses complex model groups
271
+ accountData.modelGroups = {};
272
+ accountData.activeModelGroup = null;
273
+
274
+ // Prompt for model group configuration
275
+ const { createModelGroup } = await inquirer.prompt([
276
+ {
277
+ type: "confirm",
278
+ name: "createModelGroup",
279
+ message:
280
+ "Do you want to create a model group? (Recommended) (是否创建模型组?推荐)",
281
+ default: true,
282
+ },
283
+ ]);
284
+
285
+ if (createModelGroup) {
286
+ const groupName = "default";
287
+ const modelGroupConfig = await promptForModelGroup();
288
+
289
+ if (Object.keys(modelGroupConfig).length > 0) {
290
+ accountData.modelGroups[groupName] = modelGroupConfig;
291
+ accountData.activeModelGroup = groupName;
292
+ console.log(
293
+ chalk.green(`\n✓ Created model group '${groupName}'`)
294
+ );
295
+ }
296
+ }
297
+ } else if (accountData.type === "Codex" || accountData.type === "Droids") {
298
+ // Codex and Droids use simple model field
299
+ const { addModel } = await inquirer.prompt([
300
+ {
301
+ type: "confirm",
302
+ name: "addModel",
303
+ message:
304
+ "Do you want to specify a model? (Optional) (是否指定模型?可选)",
305
+ default: false,
306
+ },
307
+ ]);
308
+
309
+ if (addModel) {
310
+ const { model } = await inquirer.prompt([
311
+ {
312
+ type: "input",
313
+ name: "model",
314
+ message: "Enter model name (请输入模型名称):",
315
+ default: "",
316
+ },
317
+ ]);
318
+
319
+ if (model.trim()) {
320
+ accountData.model = model.trim();
321
+ console.log(
322
+ chalk.green(
323
+ `\n✓ Model set to (模型已设置为): ${accountData.model}`
324
+ )
325
+ );
326
+ }
327
+ }
328
+ } else if (accountData.type === "CCR") {
329
+ // CCR needs provider name and models configuration
330
+ const ccrConfig = await inquirer.prompt([
331
+ {
332
+ type: "input",
333
+ name: "providerName",
334
+ message: "Enter Provider name (请输入 Provider 名称):",
335
+ validate: (input) =>
336
+ input.trim() !== "" || "Provider name is required (Provider 名称不能为空)",
337
+ },
338
+ {
339
+ type: "input",
340
+ name: "defaultModel",
341
+ message: "Enter default model (请输入 default 模型):",
342
+ validate: (input) =>
343
+ input.trim() !== "" || "Default model is required (默认模型不能为空)",
344
+ },
345
+ {
346
+ type: "input",
347
+ name: "backgroundModel",
348
+ message: "Enter background model (请输入 background 模型):",
349
+ validate: (input) =>
350
+ input.trim() !== "" || "Background model is required (background 模型不能为空)",
351
+ },
352
+ {
353
+ type: "input",
354
+ name: "thinkModel",
355
+ message: "Enter think model (请输入 think 模型):",
356
+ validate: (input) =>
357
+ input.trim() !== "" || "Think model is required (think 模型不能为空)",
358
+ },
359
+ ]);
360
+
361
+ const models = [
362
+ ccrConfig.defaultModel.trim(),
363
+ ccrConfig.backgroundModel.trim(),
364
+ ccrConfig.thinkModel.trim()
365
+ ];
366
+ const uniqueModels = [...new Set(models)];
367
+
368
+ accountData.ccrConfig = {
369
+ providerName: ccrConfig.providerName.trim(),
370
+ models: uniqueModels,
371
+ defaultModel: ccrConfig.defaultModel.trim(),
372
+ backgroundModel: ccrConfig.backgroundModel.trim(),
373
+ thinkModel: ccrConfig.thinkModel.trim(),
374
+ };
375
+
376
+ console.log(
377
+ chalk.green(
378
+ `\n✓ CCR Provider: ${accountData.ccrConfig.providerName}`
379
+ )
380
+ );
381
+ console.log(
382
+ chalk.green(
383
+ `✓ Default Model: ${accountData.ccrConfig.defaultModel}`
384
+ )
385
+ );
386
+ console.log(
387
+ chalk.green(
388
+ `✓ Background Model: ${accountData.ccrConfig.backgroundModel}`
389
+ )
390
+ );
391
+ console.log(
392
+ chalk.green(
393
+ `✓ Think Model: ${accountData.ccrConfig.thinkModel}`
394
+ )
395
+ );
396
+ }
397
+
398
+ // Save account
399
+ config.addAccount(name, accountData);
400
+ console.log(
401
+ chalk.green(
402
+ `✓ Account '${name}' added successfully! (账号 '${name}' 添加成功!)`
403
+ )
404
+ );
405
+
406
+ // Show model configuration tips based on account type
407
+ if (accountData.type === "Claude" && accountData.activeModelGroup) {
408
+ console.log(
409
+ chalk.cyan(
410
+ `✓ Active model group (活动模型组): ${accountData.activeModelGroup}\n`
411
+ )
412
+ );
413
+ console.log(
414
+ chalk.cyan(
415
+ '💡 Tip (提示): Use "ais model add" to create more model groups (使用 "ais model add" 创建更多模型组)'
416
+ )
417
+ );
418
+ console.log(
419
+ chalk.cyan(
420
+ '💡 Tip (提示): Use "ais model list" to view all model groups (使用 "ais model list" 查看所有模型组)\n'
421
+ )
422
+ );
423
+ } else if (
424
+ (accountData.type === "Codex" || accountData.type === "Droids") &&
425
+ accountData.model
426
+ ) {
427
+ console.log(chalk.cyan(`✓ Model (模型): ${accountData.model}\n`));
428
+ } else if (accountData.type === "CCR" && accountData.ccrConfig) {
429
+ console.log(chalk.cyan(`✓ CCR Provider: ${accountData.ccrConfig.providerName}\n`));
430
+ }
431
+
432
+ // Show usage instructions based on account type
433
+ if (accountData.type === "Codex") {
434
+ console.log(
435
+ chalk.bold.cyan("\n📖 Codex Usage Instructions (Codex 使用说明):\n")
436
+ );
437
+ console.log(
438
+ chalk.white(
439
+ "1. Switch to this account in your project (在项目中切换到此账号):"
440
+ )
441
+ );
442
+ console.log(chalk.cyan(` ais use ${name}\n`));
443
+ console.log(
444
+ chalk.white(
445
+ "2. Use Codex with the generated profile (使用生成的配置文件运行 Codex):"
446
+ )
447
+ );
448
+ console.log(
449
+ chalk.cyan(` codex --profile ais_<project-name> "your prompt"\n`)
450
+ );
451
+ console.log(
452
+ chalk.white(
453
+ '3. The profile name will be shown when you run "ais use" (运行 "ais use" 时会显示配置文件名)\n'
454
+ )
455
+ );
456
+ } else if (accountData.type === "Claude") {
457
+ console.log(
458
+ chalk.bold.cyan(
459
+ "\n📖 Claude Usage Instructions (Claude 使用说明):\n"
460
+ )
461
+ );
462
+ console.log(
463
+ chalk.white(
464
+ "1. Switch to this account in your project (在项目中切换到此账号):"
465
+ )
466
+ );
467
+ console.log(chalk.cyan(` ais use ${name}\n`));
468
+ console.log(
469
+ chalk.white(
470
+ "2. Start Claude Code in your project directory (在项目目录中启动 Claude Code)"
471
+ )
472
+ );
473
+ console.log(
474
+ chalk.white(
475
+ "3. Claude Code will automatically use the project configuration (Claude Code 将自动使用项目配置)\n"
476
+ )
477
+ );
478
+ } else if (accountData.type === "Droids") {
479
+ console.log(
480
+ chalk.bold.cyan(
481
+ "\n📖 Droids Usage Instructions (Droids 使用说明):\n"
482
+ )
483
+ );
484
+ console.log(
485
+ chalk.white(
486
+ "1. Switch to this account in your project (在项目中切换到此账号):"
487
+ )
488
+ );
489
+ console.log(chalk.cyan(` ais use ${name}\n`));
490
+ console.log(
491
+ chalk.white(
492
+ "2. Start Droids in your project directory (在项目目录中启动 Droids)"
493
+ )
494
+ );
495
+ console.log(
496
+ chalk.white(
497
+ "3. Droids will automatically use the configuration from .droids/config.json (Droids 将自动使用 .droids/config.json 中的配置)\n"
498
+ )
499
+ );
500
+ } else if (accountData.type === "CCR") {
501
+ console.log(
502
+ chalk.bold.cyan(
503
+ "\n📖 CCR Usage Instructions (CCR 使用说明):\n"
504
+ )
505
+ );
506
+ console.log(
507
+ chalk.white(
508
+ "1. Switch to this account in your project (在项目中切换到此账号):"
509
+ )
510
+ );
511
+ console.log(chalk.cyan(` ais use ${name}\n`));
512
+ console.log(
513
+ chalk.white(
514
+ "2. CCR configuration will be updated in ~/.claude-code-router/config.json (CCR 配置将更新到 ~/.claude-code-router/config.json)\n"
515
+ )
516
+ );
517
+ }
518
+ }
519
+
520
+ /**
521
+ * List all accounts
522
+ */
523
+ function listAccounts() {
524
+ const accounts = config.getAllAccounts();
525
+ const accountNames = Object.keys(accounts);
526
+
527
+ if (accountNames.length === 0) {
528
+ console.log(
529
+ chalk.yellow(
530
+ 'No accounts found. Use "ais add" to add an account. (未找到账号。请使用 "ais add" 添加账号。)'
531
+ )
532
+ );
533
+ return;
534
+ }
535
+
536
+ const currentProject = config.getProjectAccount();
537
+
538
+ console.log(chalk.bold("\n📋 Available Accounts (可用账号):\n"));
539
+
540
+ accountNames.forEach((name) => {
541
+ const account = accounts[name];
542
+ const isActive = currentProject && currentProject.name === name;
543
+ const marker = isActive ? chalk.green("● ") : " ";
544
+ const nameDisplay = isActive
545
+ ? chalk.green.bold(name)
546
+ : chalk.cyan(name);
547
+
548
+ console.log(`${marker}${nameDisplay}`);
549
+ console.log(` Type: ${account.type}`);
550
+ console.log(` API Key: ${maskApiKey(account.apiKey)}`);
551
+ if (account.email) console.log(` Email: ${account.email}`);
552
+ if (account.description)
553
+ console.log(` Description: ${account.description}`);
554
+ if (account.customEnv && Object.keys(account.customEnv).length > 0) {
555
+ console.log(
556
+ ` Custom Env: ${Object.keys(account.customEnv).join(", ")}`
557
+ );
558
+ }
559
+ // Display model configuration based on account type
560
+ if (
561
+ account.type === "Claude" &&
562
+ account.modelGroups &&
563
+ Object.keys(account.modelGroups).length > 0
564
+ ) {
565
+ const groupNames = Object.keys(account.modelGroups);
566
+ const activeMarker = account.activeModelGroup
567
+ ? ` (active: ${account.activeModelGroup})`
568
+ : "";
569
+ console.log(
570
+ ` Model Groups: ${groupNames.join(", ")}${activeMarker}`
571
+ );
572
+ } else if (
573
+ (account.type === "Codex" || account.type === "Droids") &&
574
+ account.model
575
+ ) {
576
+ console.log(` Model: ${account.model}`);
577
+ }
578
+ console.log(
579
+ ` Created: ${new Date(account.createdAt).toLocaleString()}`
580
+ );
581
+ console.log("");
582
+ });
583
+
584
+ if (currentProject) {
585
+ console.log(
586
+ chalk.green(
587
+ `✓ Current project is using (当前项目正在使用): ${currentProject.name}\n`
588
+ )
589
+ );
590
+ } else {
591
+ console.log(
592
+ chalk.yellow(
593
+ '⚠ No account set for current project. Use "ais use <account>" to set one. (当前项目未设置账号。请使用 "ais use <账号名>" 设置。)\n'
594
+ )
595
+ );
596
+ }
597
+ }
598
+
599
+ /**
600
+ * Switch to a specific account for current project
601
+ */
602
+ async function useAccount(name) {
603
+ if (!name) {
604
+ // If no name provided, show interactive selection
605
+ const accounts = config.getAllAccounts();
606
+ const accountNames = Object.keys(accounts);
607
+
608
+ if (accountNames.length === 0) {
609
+ console.log(
610
+ chalk.yellow(
611
+ 'No accounts found. Use "ais add" to add an account first. (未找到账号。请先使用 "ais add" 添加账号。)'
612
+ )
613
+ );
614
+ return;
615
+ }
616
+
617
+ const answers = await inquirer.prompt([
618
+ {
619
+ type: "list",
620
+ name: "accountName",
621
+ message: "Select an account to use (请选择要使用的账号):",
622
+ choices: accountNames,
623
+ },
624
+ ]);
625
+
626
+ name = answers.accountName;
627
+ }
628
+
629
+ if (!config.accountExists(name)) {
630
+ console.log(
631
+ chalk.red(`✗ Account '${name}' not found. (未找到账号 '${name}'。)`)
632
+ );
633
+ console.log(
634
+ chalk.yellow(
635
+ 'Use "ais list" to see available accounts. (请使用 "ais list" 查看可用账号。)'
636
+ )
637
+ );
638
+ return;
639
+ }
640
+
641
+ const success = config.setProjectAccount(name);
642
+ if (success) {
643
+ const fs = require("fs");
644
+ const path = require("path");
645
+ const { execSync } = require("child_process");
646
+ const account = config.getAccount(name);
647
+
648
+ console.log(
649
+ chalk.green(
650
+ `✓ Switched to account '${name}' for current project. (已为当前项目切换到账号 '${name}'。)`
651
+ )
652
+ );
653
+ console.log(chalk.yellow(`Project (项目): ${process.cwd()}`));
654
+
655
+ // Restart CCR if account type is CCR
656
+ if (account && account.type === "CCR") {
657
+ try {
658
+ console.log(chalk.cyan("\n🔄 Restarting CCR Router... (重启 CCR Router...)"));
659
+ execSync("ccr restart", { stdio: "inherit" });
660
+ console.log(chalk.green("✓ CCR Router restarted successfully (CCR Router 重启成功)\n"));
661
+ } catch (error) {
662
+ console.log(chalk.yellow("⚠ Failed to restart CCR Router automatically (自动重启 CCR Router 失败)"));
663
+ console.log(chalk.yellow(" Please run manually: ccr restart (请手动运行: ccr restart)\n"));
664
+ }
665
+ }
666
+
667
+ // Show different messages based on account type
668
+ if (account && account.type === "Codex") {
669
+ const profileFile = path.join(process.cwd(), ".codex-profile");
670
+ if (fs.existsSync(profileFile)) {
671
+ const profileName = fs.readFileSync(profileFile, "utf8").trim();
672
+ console.log(
673
+ chalk.cyan(
674
+ `✓ Codex profile created (Codex 配置文件已创建): ${profileName}`
675
+ )
676
+ );
677
+ console.log("");
678
+ console.log(chalk.bold.cyan("📖 Next Steps (下一步):"));
679
+ console.log(
680
+ chalk.yellow(
681
+ ` Start interactive session (启动交互式会话): ${chalk.bold(
682
+ `codex --profile ${profileName}`
683
+ )}`
684
+ )
685
+ );
686
+ console.log(
687
+ chalk.white(
688
+ " This will enter project-level interactive mode (这将进入项目级交互模式)"
689
+ )
690
+ );
691
+ }
692
+ } else if (account && account.type === "Droids") {
693
+ console.log(
694
+ chalk.cyan(
695
+ `✓ Droids configuration generated at (Droids 配置已生成至): .droids/config.json`
696
+ )
697
+ );
698
+ console.log("");
699
+ console.log(chalk.bold.cyan("📖 Next Steps (下一步):"));
700
+ console.log(
701
+ chalk.yellow(
702
+ ` Start interactive session (启动交互式会话): ${chalk.bold(
703
+ "droid"
704
+ )}`
705
+ )
706
+ );
707
+ console.log(
708
+ chalk.white(
709
+ " This will enter project-level interactive mode (这将进入项目级交互模式)"
710
+ )
711
+ );
712
+ console.log(
713
+ chalk.white(
714
+ " Droids will automatically use the configuration from .droids/config.json (Droids 将自动使用 .droids/config.json 中的配置)"
715
+ )
716
+ );
717
+ } else if (account && account.type === "CCR") {
718
+ console.log(
719
+ chalk.cyan(
720
+ `✓ CCR configuration updated at (CCR 配置已更新至): ~/.claude-code-router/config.json`
721
+ )
722
+ );
723
+ console.log(
724
+ chalk.cyan(
725
+ `✓ Claude configuration generated at (Claude 配置已生成至): .claude/settings.local.json`
726
+ )
727
+ );
728
+ console.log("");
729
+ console.log(chalk.bold.cyan("📖 Next Steps (下一步):"));
730
+ console.log(
731
+ chalk.yellow(
732
+ ` Start interactive session (启动交互式会话): ${chalk.bold(
733
+ "claude"
734
+ )}`
735
+ )
736
+ );
737
+ console.log(
738
+ chalk.white(
739
+ " This will enter project-level interactive mode (这将进入项目级交互模式)"
740
+ )
741
+ );
742
+ console.log(
743
+ chalk.white(
744
+ " Claude Code will use CCR Router to route requests (Claude Code 将使用 CCR Router 路由请求)"
745
+ )
746
+ );
747
+ } else {
748
+ console.log(
749
+ chalk.cyan(
750
+ `✓ Claude configuration generated at (Claude 配置已生成至): .claude/settings.local.json`
751
+ )
752
+ );
753
+ console.log("");
754
+ console.log(chalk.bold.cyan("📖 Next Steps (下一步):"));
755
+ console.log(
756
+ chalk.yellow(
757
+ ` Start interactive session (启动交互式会话): ${chalk.bold(
758
+ "claude"
759
+ )}`
760
+ )
761
+ );
762
+ console.log(
763
+ chalk.white(
764
+ " This will enter project-level interactive mode (这将进入项目级交互模式)"
765
+ )
766
+ );
767
+ console.log(
768
+ chalk.white(
769
+ " Claude Code will automatically use the project configuration (Claude Code 将自动使用项目配置)"
770
+ )
771
+ );
772
+ }
773
+
774
+ // Check if .gitignore was updated
775
+ const gitignorePath = path.join(process.cwd(), ".gitignore");
776
+ const gitDir = path.join(process.cwd(), ".git");
777
+ if (fs.existsSync(gitDir) && fs.existsSync(gitignorePath)) {
778
+ console.log("");
779
+ console.log(
780
+ chalk.cyan(
781
+ `✓ Updated .gitignore to exclude AIS configuration files (已更新 .gitignore 以排除 AIS 配置文件)`
782
+ )
783
+ );
784
+ }
785
+ } else {
786
+ console.log(chalk.red("✗ Failed to set account. (设置账号失败。)"));
787
+ }
788
+ }
789
+
790
+ /**
791
+ * Show current project's account info
792
+ */
793
+ function showInfo() {
794
+ const projectAccount = config.getProjectAccount();
795
+
796
+ if (!projectAccount) {
797
+ console.log(
798
+ chalk.yellow(
799
+ "⚠ No account set for current project. (当前项目未设置账号。)"
800
+ )
801
+ );
802
+ console.log(chalk.yellow(`Project (项目): ${process.cwd()}`));
803
+ console.log(
804
+ chalk.cyan(
805
+ '\nUse "ais use <account>" to set an account for this project. (使用 "ais use <账号名>" 为此项目设置账号。)'
806
+ )
807
+ );
808
+ return;
809
+ }
810
+
811
+ console.log(
812
+ chalk.bold("\n📌 Current Project Account Info (当前项目账号信息):\n")
813
+ );
814
+ console.log(
815
+ `${chalk.cyan("Account Name:")} ${chalk.green.bold(
816
+ projectAccount.name
817
+ )}`
818
+ );
819
+ console.log(`${chalk.cyan("Type:")} ${projectAccount.type}`);
820
+ console.log(
821
+ `${chalk.cyan("API Key:")} ${maskApiKey(projectAccount.apiKey)}`
822
+ );
823
+ if (projectAccount.apiUrl)
824
+ console.log(`${chalk.cyan("API URL:")} ${projectAccount.apiUrl}`);
825
+ if (projectAccount.email)
826
+ console.log(`${chalk.cyan("Email:")} ${projectAccount.email}`);
827
+ if (projectAccount.description)
828
+ console.log(
829
+ `${chalk.cyan("Description:")} ${projectAccount.description}`
830
+ );
831
+ if (
832
+ projectAccount.customEnv &&
833
+ Object.keys(projectAccount.customEnv).length > 0
834
+ ) {
835
+ console.log(`${chalk.cyan("Custom Environment Variables:")}`);
836
+ Object.entries(projectAccount.customEnv).forEach(([key, value]) => {
837
+ console.log(` ${chalk.gray("•")} ${key}: ${value}`);
838
+ });
839
+ }
840
+ // Display model configuration based on account type
841
+ if (
842
+ projectAccount.type === "Claude" &&
843
+ projectAccount.modelGroups &&
844
+ Object.keys(projectAccount.modelGroups).length > 0
845
+ ) {
846
+ console.log(`${chalk.cyan("Model Groups:")}`);
847
+ Object.keys(projectAccount.modelGroups).forEach((groupName) => {
848
+ const isActive = projectAccount.activeModelGroup === groupName;
849
+ const marker = isActive ? chalk.green("● ") : " ";
850
+ console.log(
851
+ `${marker}${isActive ? chalk.green.bold(groupName) : groupName}`
852
+ );
853
+ });
854
+ if (projectAccount.activeModelGroup) {
855
+ console.log(
856
+ `${chalk.cyan("Active Model Group:")} ${chalk.green.bold(
857
+ projectAccount.activeModelGroup
858
+ )}`
859
+ );
860
+ }
861
+ } else if (
862
+ (projectAccount.type === "Codex" || projectAccount.type === "Droids") &&
863
+ projectAccount.model
864
+ ) {
865
+ console.log(`${chalk.cyan("Model:")} ${projectAccount.model}`);
866
+ }
867
+ console.log(
868
+ `${chalk.cyan("Set At:")} ${new Date(
869
+ projectAccount.setAt
870
+ ).toLocaleString()}`
871
+ );
872
+ console.log(`${chalk.cyan("Project Root:")} ${projectAccount.projectRoot}`);
873
+ console.log(`${chalk.cyan("Current Directory:")} ${process.cwd()}\n`);
874
+ }
875
+
876
+ /**
877
+ * Remove an account
878
+ */
879
+ async function removeAccount(name) {
880
+ if (!name) {
881
+ const accounts = config.getAllAccounts();
882
+ const accountNames = Object.keys(accounts);
883
+
884
+ if (accountNames.length === 0) {
885
+ console.log(chalk.yellow("No accounts found. (未找到账号。)"));
886
+ return;
887
+ }
888
+
889
+ const answers = await inquirer.prompt([
890
+ {
891
+ type: "list",
892
+ name: "accountName",
893
+ message: "Select an account to remove (请选择要删除的账号):",
894
+ choices: accountNames,
895
+ },
896
+ ]);
897
+
898
+ name = answers.accountName;
899
+ }
900
+
901
+ if (!config.accountExists(name)) {
902
+ console.log(
903
+ chalk.red(`✗ Account '${name}' not found. (未找到账号 '${name}'。)`)
904
+ );
905
+ return;
906
+ }
907
+
908
+ const { confirm } = await inquirer.prompt([
909
+ {
910
+ type: "confirm",
911
+ name: "confirm",
912
+ message: `Are you sure you want to remove account '${name}'? (确定要删除账号 '${name}' 吗?)`,
913
+ default: false,
914
+ },
915
+ ]);
916
+
917
+ if (!confirm) {
918
+ console.log(chalk.yellow("Operation cancelled. (操作已取消。)"));
919
+ return;
920
+ }
921
+
922
+ const success = config.removeAccount(name);
923
+ if (success) {
924
+ console.log(
925
+ chalk.green(
926
+ `✓ Account '${name}' removed successfully. (账号 '${name}' 删除成功。)`
927
+ )
928
+ );
929
+ } else {
930
+ console.log(chalk.red("✗ Failed to remove account. (删除账号失败。)"));
931
+ }
932
+ }
933
+
934
+ /**
935
+ * Show current account for current project
936
+ */
937
+ function showCurrent() {
938
+ const projectAccount = config.getProjectAccount();
939
+
940
+ if (!projectAccount) {
941
+ console.log(
942
+ chalk.yellow(
943
+ "⚠ No account set for current project. (当前项目未设置账号。)"
944
+ )
945
+ );
946
+ return;
947
+ }
948
+
949
+ console.log(
950
+ chalk.green(
951
+ `Current account (当前账号): ${chalk.bold(projectAccount.name)}`
952
+ )
953
+ );
954
+ }
955
+
956
+ /**
957
+ * Export account configuration
958
+ */
959
+ function exportAccount(name) {
960
+ if (!name) {
961
+ console.log(
962
+ chalk.red("Please specify an account name. (请指定账号名称。)")
963
+ );
964
+ console.log(
965
+ chalk.cyan(
966
+ "Usage: ais export <account-name> (用法: ais export <账号名>)"
967
+ )
968
+ );
969
+ return;
970
+ }
971
+
972
+ const account = config.getAccount(name);
973
+ if (!account) {
974
+ console.log(
975
+ chalk.red(`✗ Account '${name}' not found. (未找到账号 '${name}'。)`)
976
+ );
977
+ return;
978
+ }
979
+
980
+ console.log(
981
+ chalk.bold(
982
+ `\n📤 Export for account '${name}' (账号 '${name}' 的导出数据):\n`
983
+ )
984
+ );
985
+ console.log(JSON.stringify({ [name]: account }, null, 2));
986
+ console.log("");
987
+ }
988
+
989
+ module.exports = {
990
+ addAccount,
991
+ listAccounts,
992
+ useAccount,
993
+ showInfo,
994
+ removeAccount,
995
+ showCurrent,
996
+ exportAccount,
997
+ };