codex-slot 0.1.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/cli.js ADDED
@@ -0,0 +1,580 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const node_child_process_1 = require("node:child_process");
10
+ const node_readline_1 = __importDefault(require("node:readline"));
11
+ const commander_1 = require("commander");
12
+ const account_store_1 = require("./account-store");
13
+ const config_1 = require("./config");
14
+ const login_1 = require("./login");
15
+ const scheduler_1 = require("./scheduler");
16
+ const status_1 = require("./status");
17
+ const usage_sync_1 = require("./usage-sync");
18
+ /**
19
+ * 读取当前 CLI 的发布版本号,优先与 npm 包元数据保持一致。
20
+ *
21
+ * @returns string,当前包版本号;当 package.json 不可读或字段缺失时返回 `0.0.0`。
22
+ * @throws 无显式抛出;内部异常会被吞掉并回退到默认版本号。
23
+ */
24
+ function getCliVersion() {
25
+ try {
26
+ const packageJsonPath = node_path_1.default.resolve(__dirname, "../package.json");
27
+ // 直接读取发布包内的 package.json,避免 CLI 版本号与 npm 发布版本脱节。
28
+ const packageJsonContent = node_fs_1.default.readFileSync(packageJsonPath, "utf8");
29
+ const packageJson = JSON.parse(packageJsonContent);
30
+ if (typeof packageJson.version === "string" && packageJson.version.trim().length > 0) {
31
+ return packageJson.version;
32
+ }
33
+ }
34
+ catch {
35
+ // 读取失败时使用保底版本,避免 `-V` 命令直接异常退出。
36
+ }
37
+ return "0.0.0";
38
+ }
39
+ /**
40
+ * 进入交互式全屏缓冲区,并隐藏光标,确保后续重绘始终基于固定画布。
41
+ *
42
+ * @returns void,无返回值。
43
+ * @throws 无显式抛出。
44
+ */
45
+ function enterInteractiveScreen() {
46
+ // 切到 alternate screen,避免在主终端历史中反复覆盖导致画面抖动。
47
+ process.stdout.write("\x1b[?1049h");
48
+ process.stdout.write("\x1b[?25l");
49
+ }
50
+ /**
51
+ * 退出交互式全屏缓冲区,并恢复光标显示。
52
+ *
53
+ * @returns void,无返回值。
54
+ * @throws 无显式抛出。
55
+ */
56
+ function leaveInteractiveScreen() {
57
+ process.stdout.write("\x1b[?25h");
58
+ process.stdout.write("\x1b[?1049l");
59
+ }
60
+ /**
61
+ * 在交互式全屏缓冲区中从左上角整块重绘内容。
62
+ *
63
+ * @param lines 待输出的文本行数组。
64
+ * @returns void,无返回值。
65
+ * @throws 无显式抛出。
66
+ */
67
+ function renderInteractiveScreen(lines) {
68
+ // 每次都回到左上角并清屏,避免依赖“上一帧占了多少行”的脆弱回退逻辑。
69
+ node_readline_1.default.cursorTo(process.stdout, 0, 0);
70
+ node_readline_1.default.clearScreenDown(process.stdout);
71
+ process.stdout.write(lines.join("\n"));
72
+ process.stdout.write("\n");
73
+ }
74
+ /**
75
+ * 计算交互式状态面板的初始光标位置。
76
+ *
77
+ * 业务含义:
78
+ * 1. 优先将光标定位到当前自动调度选中的账号,确保首屏焦点与 `selected=` 一致。
79
+ * 2. 若当前没有自动选中账号,则回退到首个可用账号,方便直接切换。
80
+ * 3. 若仍不存在可用账号,则回退到首个已启用账号;最后兜底为列表第一项。
81
+ *
82
+ * @param accounts 已按展示顺序排好的账号列表;不能为空,元素需包含启用状态与账号 id。
83
+ * @param statuses 当前账号运行时状态快照;用于判断账号是否可用。
84
+ * @returns number,初始光标所在的数组下标;当未命中任何候选时返回 `0`。
85
+ * @throws 无显式抛出。
86
+ */
87
+ function resolveInitialCursorIndex(accounts, statuses) {
88
+ const selected = (0, scheduler_1.pickBestAccount)();
89
+ if (selected) {
90
+ const selectedIndex = accounts.findIndex((account) => account.id === selected.account.id);
91
+ if (selectedIndex >= 0) {
92
+ return selectedIndex;
93
+ }
94
+ }
95
+ const statusById = new Map(statuses.map((item) => [item.id, item]));
96
+ // 当前有可用账号时,优先聚焦到第一个可用账号,减少用户额外移动光标的成本。
97
+ const availableIndex = accounts.findIndex((account) => statusById.get(account.id)?.isAvailable);
98
+ if (availableIndex >= 0) {
99
+ return availableIndex;
100
+ }
101
+ // 若所有账号都不可用,至少默认落在首个已启用账号上,避免首屏停在明显不可操作的禁用项。
102
+ const enabledIndex = accounts.findIndex((account) => account.enabled);
103
+ if (enabledIndex >= 0) {
104
+ return enabledIndex;
105
+ }
106
+ return 0;
107
+ }
108
+ /**
109
+ * 刷新所有已录入账号的远端额度,并输出最新状态表格。
110
+ *
111
+ * @returns Promise,无返回值。
112
+ */
113
+ async function handleStatus(options) {
114
+ await (0, usage_sync_1.refreshAllAccountUsage)();
115
+ const statuses = (0, status_1.collectAccountStatuses)();
116
+ const interactive = options?.interactive ?? true;
117
+ if (interactive) {
118
+ await handleInteractiveToggle(statuses);
119
+ return;
120
+ }
121
+ const selected = (0, scheduler_1.pickBestAccount)();
122
+ const displayStatuses = statuses.map((item) => ({
123
+ ...item,
124
+ name: item.id === selected?.account.id ? `${item.name}*` : item.name
125
+ }));
126
+ const available = statuses.filter((item) => item.isAvailable).length;
127
+ const fiveHourLimited = statuses.filter((item) => item.isFiveHourLimited && !item.isWeeklyLimited).length;
128
+ const weeklyLimited = statuses.filter((item) => item.isWeeklyLimited).length;
129
+ console.log((0, status_1.renderStatusTable)(displayStatuses));
130
+ console.log("");
131
+ console.log(`available=${available} 5h_limited=${fiveHourLimited} weekly_limited=${weeklyLimited}`);
132
+ console.log(`selected=${selected ? selected.account.name : "none"}`);
133
+ }
134
+ /**
135
+ * 进入账号启用状态的交互式切换界面,并在用户确认退出后恢复终端状态。
136
+ *
137
+ * @param initialStatuses 进入交互前刚刷新的账号状态快照,用于首屏复用同一块展示区域。
138
+ * @returns Promise,在用户按下 `Enter`、`q` 或 `Ctrl+C` 退出交互后完成。
139
+ * @throws 无显式抛出;终端读写异常将沿调用链透出。
140
+ */
141
+ async function handleInteractiveToggle(initialStatuses) {
142
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
143
+ console.log("当前环境不支持交互式操作,请直接编辑配置文件或使用 --no-interactive 选项。");
144
+ return;
145
+ }
146
+ const stdin = process.stdin;
147
+ node_readline_1.default.emitKeypressEvents(stdin);
148
+ stdin.setRawMode?.(true);
149
+ const config = (0, config_1.loadConfig)();
150
+ if (config.accounts.length === 0) {
151
+ console.log("当前没有已录入账号。");
152
+ stdin.setRawMode?.(false);
153
+ return;
154
+ }
155
+ // 按名称排序,方便浏览。
156
+ const accounts = [...config.accounts].sort((a, b) => a.name.localeCompare(b.name));
157
+ let cursor = resolveInitialCursorIndex(accounts, initialStatuses ?? (0, status_1.collectAccountStatuses)());
158
+ let changed = false;
159
+ enterInteractiveScreen();
160
+ return await new Promise((resolve) => {
161
+ let closed = false;
162
+ const render = () => {
163
+ const latestStatuses = (0, status_1.collectAccountStatuses)();
164
+ const selected = (0, scheduler_1.pickBestAccount)();
165
+ const statusSource = changed ? latestStatuses : (initialStatuses ?? latestStatuses);
166
+ const statusById = new Map(statusSource.map((item) => [item.id, item]));
167
+ const autoSelectedId = selected?.account.id ?? null;
168
+ // 交互态按账号列表顺序重组表格行,确保选择框与状态信息严格同行显示。
169
+ const displayStatuses = accounts
170
+ .map((account) => {
171
+ const status = statusById.get(account.id);
172
+ if (!status) {
173
+ return null;
174
+ }
175
+ return {
176
+ ...status,
177
+ name: account.id === autoSelectedId ? `${status.name}*` : status.name
178
+ };
179
+ })
180
+ .filter((item) => item !== null);
181
+ const available = statusSource.filter((item) => item.isAvailable).length;
182
+ const fiveHourLimited = statusSource.filter((item) => item.isFiveHourLimited && !item.isWeeklyLimited).length;
183
+ const weeklyLimited = statusSource.filter((item) => item.isWeeklyLimited).length;
184
+ const lines = [
185
+ (0, status_1.renderStatusTable)(displayStatuses, {
186
+ selectorColumn: {
187
+ enabledById: Object.fromEntries(accounts.map((account) => [account.id, account.enabled])),
188
+ cursorAccountId: accounts[cursor]?.id ?? null
189
+ }
190
+ }),
191
+ "",
192
+ `available=${available} 5h_limited=${fiveHourLimited} weekly_limited=${weeklyLimited}`,
193
+ `selected=${selected ? selected.account.name : "none"}`,
194
+ "",
195
+ "空格切换当前行启用状态,Enter / q 退出。"
196
+ ];
197
+ renderInteractiveScreen(lines);
198
+ };
199
+ const applyChanges = () => {
200
+ if (!changed) {
201
+ return;
202
+ }
203
+ const latest = (0, config_1.loadConfig)();
204
+ for (const account of accounts) {
205
+ const index = latest.accounts.findIndex((item) => item.id === account.id);
206
+ if (index >= 0) {
207
+ latest.accounts[index] = account;
208
+ }
209
+ }
210
+ (0, config_1.saveConfig)(latest);
211
+ changed = false;
212
+ initialStatuses = (0, status_1.collectAccountStatuses)();
213
+ };
214
+ const exitInteractive = () => {
215
+ if (closed) {
216
+ return;
217
+ }
218
+ closed = true;
219
+ // 退出前先持久化本轮勾选变更,避免用户误以为切换未生效。
220
+ applyChanges();
221
+ stdin.off("keypress", onKeypress);
222
+ stdin.setRawMode?.(false);
223
+ stdin.pause();
224
+ leaveInteractiveScreen();
225
+ console.log("已退出账号启用状态编辑。");
226
+ resolve();
227
+ };
228
+ const onKeypress = (_str, key) => {
229
+ if (key.name === "up") {
230
+ // 顶部账号继续按上键时保持当前位置,避免交互焦点在首尾之间循环跳转。
231
+ const nextCursor = Math.max(0, cursor - 1);
232
+ if (nextCursor !== cursor) {
233
+ cursor = nextCursor;
234
+ render();
235
+ }
236
+ return;
237
+ }
238
+ if (key.name === "down") {
239
+ // 底部账号继续按下键时保持当前位置,避免用户误以为光标异常回绕。
240
+ const nextCursor = Math.min(accounts.length - 1, cursor + 1);
241
+ if (nextCursor !== cursor) {
242
+ cursor = nextCursor;
243
+ render();
244
+ }
245
+ return;
246
+ }
247
+ if (key.name === "space") {
248
+ // 空格直接切换当前选中账号的启用状态,并立即写回配置。
249
+ accounts[cursor].enabled = !accounts[cursor].enabled;
250
+ changed = true;
251
+ applyChanges();
252
+ render();
253
+ return;
254
+ }
255
+ if (key.name === "return" || key.name === "enter") {
256
+ exitInteractive();
257
+ return;
258
+ }
259
+ if (key.name === "q" || (key.ctrl && key.name === "c")) {
260
+ exitInteractive();
261
+ }
262
+ };
263
+ render();
264
+ stdin.on("keypress", onKeypress);
265
+ });
266
+ }
267
+ /**
268
+ * 将已有的 Codex HOME 目录中的登录态复制到 cslot 自己的隔离目录并纳入管理。
269
+ *
270
+ * @param accountId 本地账号标识。
271
+ * @param codexHome 现有 HOME 目录;若未传则默认使用当前用户 HOME。
272
+ * @returns 无返回值。
273
+ */
274
+ function handleAccountImport(accountId, codexHome) {
275
+ const sourceHome = codexHome ? (0, config_1.expandHome)(codexHome) : process.env.HOME ?? "";
276
+ const managedHome = (0, config_1.getManagedHome)(accountId);
277
+ (0, account_store_1.cloneCodexAuthState)(sourceHome, managedHome);
278
+ const account = (0, account_store_1.registerManagedAccount)(accountId, managedHome);
279
+ console.log(`账号已导入: ${account.id}`);
280
+ console.log(`来源 HOME: ${sourceHome}`);
281
+ console.log(`已复制到: ${account.codex_home}`);
282
+ }
283
+ /**
284
+ * 执行隔离登录流程,将账号录入到 cslot 管理目录。
285
+ *
286
+ * @param accountId 本地账号标识。
287
+ * @returns Promise,无返回值。
288
+ */
289
+ async function handleAccountLogin(accountId) {
290
+ const home = await (0, login_1.loginManagedAccount)(accountId);
291
+ console.log(`登录完成,账号目录: ${home}`);
292
+ }
293
+ /**
294
+ * 删除配置中的账号项。
295
+ *
296
+ * @param accountId 本地账号标识。
297
+ * @returns 无返回值。
298
+ * @throws 当账号不存在时抛出错误。
299
+ */
300
+ function handleAccountRemove(accountId) {
301
+ const removed = (0, account_store_1.removeManagedAccount)(accountId);
302
+ if (!removed) {
303
+ throw new Error(`未找到账号 ${accountId}`);
304
+ }
305
+ console.log(`已删除账号配置: ${removed.id}`);
306
+ }
307
+ /**
308
+ * 判断后台服务当前是否在运行。
309
+ *
310
+ * @returns 运行中的 PID;未运行时返回 `null`。
311
+ */
312
+ function getRunningPid() {
313
+ const pidPath = (0, config_1.getPidPath)();
314
+ if (!node_fs_1.default.existsSync(pidPath)) {
315
+ return null;
316
+ }
317
+ const raw = node_fs_1.default.readFileSync(pidPath, "utf8").trim();
318
+ const pid = Number(raw);
319
+ if (!Number.isInteger(pid) || pid <= 0) {
320
+ node_fs_1.default.rmSync(pidPath, { force: true });
321
+ return null;
322
+ }
323
+ try {
324
+ process.kill(pid, 0);
325
+ return pid;
326
+ }
327
+ catch {
328
+ node_fs_1.default.rmSync(pidPath, { force: true });
329
+ return null;
330
+ }
331
+ }
332
+ /**
333
+ * 后台启动 cslot 服务并写入 PID 文件。
334
+ *
335
+ * @returns Promise,无返回值。
336
+ * @throws 当服务已在运行或子进程启动失败时抛出异常。
337
+ */
338
+ async function handleStart(portOverride) {
339
+ const config = (0, config_1.loadConfig)();
340
+ const port = portOverride ? Number(portOverride) : config.server.port;
341
+ if (portOverride) {
342
+ config.server.port = port;
343
+ (0, config_1.saveConfig)(config);
344
+ }
345
+ const runningPid = getRunningPid();
346
+ if (runningPid) {
347
+ console.log(`服务已在运行,PID=${runningPid}`);
348
+ if (portOverride) {
349
+ console.log(`已将新端口写入配置: ${port}`);
350
+ console.log("请先执行 cslot stop,再执行 cslot start 使新端口生效。");
351
+ }
352
+ return;
353
+ }
354
+ applyManagedCodexConfig();
355
+ const logPath = (0, config_1.getServiceLogPath)();
356
+ const logFd = node_fs_1.default.openSync(logPath, "a");
357
+ const child = (0, node_child_process_1.spawn)(process.execPath, [__filename.replace(/cli\.js$/, "serve.js"), "--port", String(port)], {
358
+ detached: true,
359
+ stdio: ["ignore", logFd, logFd]
360
+ });
361
+ child.unref();
362
+ node_fs_1.default.writeFileSync((0, config_1.getPidPath)(), `${child.pid}\n`, "utf8");
363
+ console.log(`服务已启动: http://${config.server.host}:${port}`);
364
+ console.log(`PID: ${child.pid}`);
365
+ console.log(`日志: ${logPath}`);
366
+ }
367
+ /**
368
+ * 停止后台运行的 cslot 服务。
369
+ *
370
+ * @returns 无返回值。
371
+ */
372
+ function handleStop() {
373
+ const pid = getRunningPid();
374
+ if (!pid) {
375
+ console.log("服务未运行");
376
+ deactivateManagedCodexConfig();
377
+ return;
378
+ }
379
+ process.kill(pid, "SIGTERM");
380
+ node_fs_1.default.rmSync((0, config_1.getPidPath)(), { force: true });
381
+ deactivateManagedCodexConfig();
382
+ console.log(`服务已停止,PID=${pid}`);
383
+ }
384
+ /**
385
+ * 对正则元字符做转义,供动态构造匹配模式使用。
386
+ *
387
+ * @param input 原始字符串。
388
+ * @returns 经过转义后的安全正则片段。
389
+ */
390
+ function escapeRegExp(input) {
391
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
392
+ }
393
+ function ensureParentDir(filePath) {
394
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
395
+ }
396
+ function writeFileAtomic(targetFile, content) {
397
+ ensureParentDir(targetFile);
398
+ const tmpFile = `${targetFile}.tmp-${process.pid}-${Date.now()}`;
399
+ node_fs_1.default.writeFileSync(tmpFile, content, "utf8");
400
+ node_fs_1.default.renameSync(tmpFile, targetFile);
401
+ }
402
+ /**
403
+ * 返回默认的 `codex config.toml` 路径。
404
+ *
405
+ * @returns 默认 `config.toml` 绝对路径。
406
+ */
407
+ function getDefaultCodexConfigPath() {
408
+ return node_path_1.default.join(process.env.HOME ?? "", ".codex", "config.toml");
409
+ }
410
+ /**
411
+ * 生成 cslot provider 配置块。
412
+ *
413
+ * @returns 可直接写入 `config.toml` 的配置块内容。
414
+ */
415
+ function buildManagedConfigBlock() {
416
+ const config = (0, config_1.loadConfig)();
417
+ return [
418
+ "[model_providers.cslot]",
419
+ 'name = "cslot"',
420
+ `base_url = "http://${config.server.host}:${config.server.port}/v1"`,
421
+ `http_headers = { Authorization = "Bearer ${config.server.api_key}" }`,
422
+ 'wire_api = "responses"'
423
+ ].join("\n");
424
+ }
425
+ /**
426
+ * 将 cslot provider 配置写入指定的 codex config.toml。
427
+ *
428
+ * @param targetPathOrDir 可选的 codex 配置目录或 config.toml 文件路径。
429
+ * @returns 实际写入的 `config.toml` 文件路径。
430
+ */
431
+ function applyManagedCodexConfig(targetPathOrDir, options) {
432
+ const rawTarget = targetPathOrDir ? (0, config_1.expandHome)(targetPathOrDir) : getDefaultCodexConfigPath();
433
+ const targetFile = rawTarget.endsWith(".toml") ? rawTarget : node_path_1.default.join(rawTarget, "config.toml");
434
+ const block = buildManagedConfigBlock();
435
+ let original = "";
436
+ if (node_fs_1.default.existsSync(targetFile)) {
437
+ original = node_fs_1.default.readFileSync(targetFile, "utf8");
438
+ }
439
+ // 更稳定的策略:仅按 provider 名称和 model_provider 改动,不引入额外的 marker。
440
+ const lines = original.length > 0 ? original.split(/\r?\n/) : [];
441
+ let replacedModelProvider = false;
442
+ const modelProviderLine = 'model_provider = "cslot"';
443
+ for (let i = 0; i < lines.length; i += 1) {
444
+ const trimmed = lines[i].trim();
445
+ // 优先替换被注释掉的默认行(只替换第一处,减少对用户文件的干扰)。
446
+ if (!replacedModelProvider && /^#\s*model_provider\s*=/.test(trimmed)) {
447
+ lines[i] = modelProviderLine;
448
+ replacedModelProvider = true;
449
+ continue;
450
+ }
451
+ // 替换真实生效的 model_provider 行。
452
+ if (/^model_provider\s*=/.test(trimmed) && !trimmed.startsWith("#")) {
453
+ lines[i] = modelProviderLine;
454
+ replacedModelProvider = true;
455
+ continue;
456
+ }
457
+ }
458
+ if (!replacedModelProvider) {
459
+ const firstNonEmptyIndex = lines.findIndex((line) => line.trim() !== "");
460
+ if (firstNonEmptyIndex >= 0) {
461
+ lines.splice(firstNonEmptyIndex, 0, modelProviderLine, "");
462
+ }
463
+ else {
464
+ lines.push(modelProviderLine, "");
465
+ }
466
+ }
467
+ // 替换或追加 [model_providers.cslot] 这一段配置。
468
+ const blockLines = block.split("\n");
469
+ let insertAfterIndex = -1;
470
+ for (let i = 0; i < lines.length; i += 1) {
471
+ const trimmed = lines[i].trim();
472
+ if (trimmed === "[model_providers.cslot]") {
473
+ let j = i;
474
+ while (j < lines.length) {
475
+ const currentTrimmed = lines[j].trim();
476
+ if (j > i && currentTrimmed.startsWith("[") && !currentTrimmed.startsWith("[[")) {
477
+ break;
478
+ }
479
+ insertAfterIndex = j;
480
+ j += 1;
481
+ }
482
+ // 删除旧的 cslot provider 表块,准备写入新的。
483
+ lines.splice(i, j - i);
484
+ insertAfterIndex = i - 1;
485
+ i = j - 1;
486
+ }
487
+ }
488
+ if (insertAfterIndex >= 0) {
489
+ lines.splice(insertAfterIndex + 1, 0, "", ...blockLines);
490
+ }
491
+ else {
492
+ if (lines.length > 0 && lines[lines.length - 1].trim() !== "") {
493
+ lines.push("");
494
+ }
495
+ lines.push(...blockLines);
496
+ }
497
+ const nextContent = `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
498
+ writeFileAtomic(targetFile, nextContent);
499
+ if (!options?.silent) {
500
+ const config = (0, config_1.loadConfig)();
501
+ console.log(`已写入: ${targetFile}`);
502
+ console.log(`base_url=http://${config.server.host}:${config.server.port}/v1`);
503
+ console.log(`api_key=${config.server.api_key}`);
504
+ console.log("提示: start 会自动接管 codex provider,stop 会自动恢复。");
505
+ }
506
+ return targetFile;
507
+ }
508
+ /**
509
+ * 关闭 cslot 作为当前默认 provider 的接管状态。
510
+ *
511
+ * @returns 无返回值。
512
+ */
513
+ function deactivateManagedCodexConfig() {
514
+ const targetFile = getDefaultCodexConfigPath();
515
+ if (!node_fs_1.default.existsSync(targetFile)) {
516
+ return;
517
+ }
518
+ const original = node_fs_1.default.readFileSync(targetFile, "utf8");
519
+ const nextContent = original.replace(/^(\s*)model_provider\s*=\s*"cslot"\s*$/m, '$1# model_provider = "cslot"');
520
+ if (nextContent !== original) {
521
+ node_fs_1.default.writeFileSync(targetFile, nextContent, "utf8");
522
+ console.log(`已更新: ${targetFile}`);
523
+ }
524
+ }
525
+ /**
526
+ * CLI 主入口,负责命令注册与执行。
527
+ *
528
+ * @returns Promise,无返回值。
529
+ * @throws 当命令执行失败时向上抛出异常。
530
+ */
531
+ async function main() {
532
+ const program = new commander_1.Command();
533
+ // 禁用内置 help 子命令,仅保留 --help / -h 形式。
534
+ program.addHelpCommand(false);
535
+ (0, config_1.getCodexSwHome)();
536
+ (0, config_1.loadConfig)();
537
+ program
538
+ .name("codex-slot")
539
+ .description("本地 Codex 多账号切换与状态管理工具")
540
+ .version(getCliVersion());
541
+ program
542
+ .command("add")
543
+ .description("登录并新增一个账号或工作空间")
544
+ .argument("<accountId>", "账号标识")
545
+ .action(async (accountId) => {
546
+ await handleAccountLogin(accountId);
547
+ });
548
+ program
549
+ .command("del")
550
+ .description("删除一个已录入账号")
551
+ .argument("<accountId>", "账号标识")
552
+ .action(handleAccountRemove);
553
+ program
554
+ .command("import")
555
+ .description("导入当前或指定 HOME 下的官方 codex 登录态")
556
+ .argument("<accountId>", "账号标识")
557
+ .argument("[codexHome]", "已有 HOME 目录,默认当前用户 HOME")
558
+ .action(handleAccountImport);
559
+ program
560
+ .command("status")
561
+ .description("刷新并查看所有已录入账号或工作空间的最新额度")
562
+ .option("--no-interactive", "仅输出状态表,不进入交互式切换")
563
+ .action(async (options) => {
564
+ await handleStatus(options);
565
+ });
566
+ program
567
+ .command("start")
568
+ .description("后台启动本地代理服务")
569
+ .option("--port <port>", "监听端口")
570
+ .action(async (options) => {
571
+ await handleStart(options.port);
572
+ });
573
+ program.command("stop").description("停止后台代理服务").action(handleStop);
574
+ await program.parseAsync(process.argv);
575
+ }
576
+ void main().catch((error) => {
577
+ const message = error instanceof Error ? error.message : String(error);
578
+ console.error(`codex-slot 执行失败: ${message}`);
579
+ process.exit(1);
580
+ });