codex-slot 0.1.1 → 0.1.2
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 +31 -11
- package/dist/account-commands.js +91 -0
- package/dist/account-store.js +3 -2
- package/dist/app/account-service.js +121 -0
- package/dist/app/config-service.js +61 -0
- package/dist/app/service-lifecycle-service.js +116 -0
- package/dist/app/status-service.js +51 -0
- package/dist/cli-helpers.js +44 -0
- package/dist/cli.js +92 -575
- package/dist/codex-config.js +354 -0
- package/dist/config-command.js +44 -0
- package/dist/config.js +22 -23
- package/dist/login.js +3 -2
- package/dist/serve.js +2 -1
- package/dist/server.js +6 -5
- package/dist/service-control.js +43 -0
- package/dist/state.js +40 -4
- package/dist/status-command.js +216 -0
- package/dist/status.js +16 -6
- package/dist/text.js +33 -0
- package/dist/usage-sync.js +8 -7
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,609 +1,126 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
-
};
|
|
6
3
|
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
4
|
const commander_1 = require("commander");
|
|
12
|
-
const
|
|
5
|
+
const account_commands_1 = require("./account-commands");
|
|
6
|
+
const cli_helpers_1 = require("./cli-helpers");
|
|
13
7
|
const config_1 = require("./config");
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const usage_sync_1 = require("./usage-sync");
|
|
8
|
+
const service_control_1 = require("./service-control");
|
|
9
|
+
const status_command_1 = require("./status-command");
|
|
10
|
+
const text_1 = require("./text");
|
|
18
11
|
/**
|
|
19
|
-
*
|
|
12
|
+
* 为 CLI 程序注册根级帮助信息与统一示例。
|
|
20
13
|
*
|
|
21
|
-
* @
|
|
22
|
-
* @
|
|
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,无返回值。
|
|
14
|
+
* @param program Commander 程序实例。
|
|
15
|
+
* @returns 无返回值。
|
|
65
16
|
* @throws 无显式抛出。
|
|
66
17
|
*/
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
18
|
+
function configureRootProgram(program) {
|
|
19
|
+
program
|
|
20
|
+
.name("codex-slot")
|
|
21
|
+
.description((0, text_1.bi)("本地 Codex 多账号切换与状态管理工具", "Local Codex multi-account switcher"))
|
|
22
|
+
.helpOption("-h, --help", (0, text_1.bi)("显示帮助", "Show help"))
|
|
23
|
+
.version((0, cli_helpers_1.getCliVersion)());
|
|
24
|
+
program.addHelpText("after", [
|
|
25
|
+
"",
|
|
26
|
+
`${(0, text_1.bi)("示例", "Examples")}:`,
|
|
27
|
+
" cslot import work ~/workspace-home",
|
|
28
|
+
" cslot rename work work-main",
|
|
29
|
+
" cslot start --port 4399",
|
|
30
|
+
" cslot status --no-interactive",
|
|
31
|
+
"",
|
|
32
|
+
`${(0, text_1.bi)("说明", "Notes")}:`,
|
|
33
|
+
` ${(0, text_1.bi)("`import current ~` 里的 current 只是示例槽位名,不是内置账号。", "`current` in `import current ~` is only an example slot name, not a built-in account.")}`
|
|
34
|
+
].join("\n"));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 注册账号相关子命令。
|
|
38
|
+
*
|
|
39
|
+
* @param program Commander 程序实例。
|
|
40
|
+
* @returns 无返回值。
|
|
85
41
|
* @throws 无显式抛出。
|
|
86
42
|
*/
|
|
87
|
-
function
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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);
|
|
43
|
+
function registerAccountCommands(program) {
|
|
44
|
+
program
|
|
45
|
+
.command("add")
|
|
46
|
+
.description((0, text_1.bi)("登录并新增一个账号或工作空间", "Login and add a managed slot"))
|
|
47
|
+
.argument("<name>", (0, text_1.bi)("账号标识(本地槽位名)", "Local slot name"))
|
|
48
|
+
.action(async (name) => {
|
|
49
|
+
await (0, account_commands_1.handleAccountLogin)(name);
|
|
265
50
|
});
|
|
51
|
+
program
|
|
52
|
+
.command("del")
|
|
53
|
+
.description((0, text_1.bi)("删除一个已录入账号", "Remove a managed slot"))
|
|
54
|
+
.argument("[name]", (0, text_1.bi)("账号标识(本地槽位名),留空时列出全部", "Local slot name"))
|
|
55
|
+
.action(account_commands_1.handleAccountRemoveCommand);
|
|
56
|
+
program
|
|
57
|
+
.command("import")
|
|
58
|
+
.description((0, text_1.bi)("导入当前或指定 HOME 下的官方 codex 登录态", "Import official Codex auth state from the current or specified HOME"))
|
|
59
|
+
.argument("<name>", (0, text_1.bi)("账号标识(本地槽位名,例如 work/current)", "Local slot name, for example work/current"))
|
|
60
|
+
.argument("[codexHome]", (0, text_1.bi)("已有 HOME 目录,默认当前用户 HOME", "Source HOME, defaults to the current user HOME"))
|
|
61
|
+
.addHelpText("after", [
|
|
62
|
+
"",
|
|
63
|
+
`${(0, text_1.bi)("说明", "Note")}:`,
|
|
64
|
+
` ${(0, text_1.bi)("name 是你自定义的槽位名;`current` 不是系统保留字。", "`name` is your custom slot name; `current` is not a reserved keyword.")}`
|
|
65
|
+
].join("\n"))
|
|
66
|
+
.action(account_commands_1.handleAccountImport);
|
|
67
|
+
program
|
|
68
|
+
.command("rename")
|
|
69
|
+
.description((0, text_1.bi)("重命名一个已录入账号", "Rename a managed slot"))
|
|
70
|
+
.argument("<oldName>", (0, text_1.bi)("原槽位名", "Old slot name"))
|
|
71
|
+
.argument("<newName>", (0, text_1.bi)("新槽位名", "New slot name"))
|
|
72
|
+
.action(account_commands_1.handleAccountRename);
|
|
266
73
|
}
|
|
267
74
|
/**
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
* @param name 本地账号标识(等同于配置中的 name 字段)。
|
|
271
|
-
* @param codexHome 现有 HOME 目录;若未传则默认使用当前用户 HOME。
|
|
272
|
-
* @returns 无返回值。
|
|
273
|
-
*/
|
|
274
|
-
function handleAccountImport(name, codexHome) {
|
|
275
|
-
const sourceHome = codexHome ? (0, config_1.expandHome)(codexHome) : process.env.HOME ?? "";
|
|
276
|
-
const managedHome = (0, config_1.getManagedHome)(name);
|
|
277
|
-
(0, account_store_1.cloneCodexAuthState)(sourceHome, managedHome);
|
|
278
|
-
const account = (0, account_store_1.registerManagedAccount)(name, managedHome);
|
|
279
|
-
console.log(`账号已导入: ${account.id}`);
|
|
280
|
-
console.log(`来源 HOME: ${sourceHome}`);
|
|
281
|
-
console.log(`已复制到: ${account.codex_home}`);
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* 执行隔离登录流程,将账号录入到 cslot 管理目录。
|
|
285
|
-
*
|
|
286
|
-
* @param name 本地账号标识(等同于配置中的 name 字段)。
|
|
287
|
-
* @returns Promise,无返回值。
|
|
288
|
-
*/
|
|
289
|
-
async function handleAccountLogin(name) {
|
|
290
|
-
const home = await (0, login_1.loginManagedAccount)(name);
|
|
291
|
-
console.log(`登录完成,账号目录: ${home}`);
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* 删除配置中的账号项。
|
|
295
|
-
*
|
|
296
|
-
* @param name 本地账号标识(等同于配置中的 name 字段)。
|
|
297
|
-
* @returns 无返回值。
|
|
298
|
-
* @throws 当账号不存在时抛出错误。
|
|
299
|
-
*/
|
|
300
|
-
function handleAccountRemove(name) {
|
|
301
|
-
const removed = (0, account_store_1.removeManagedAccount)(name);
|
|
302
|
-
if (!removed) {
|
|
303
|
-
throw new Error(`未找到账号 ${name}`);
|
|
304
|
-
}
|
|
305
|
-
console.log(`已删除账号配置: ${removed.id}`);
|
|
306
|
-
}
|
|
307
|
-
/**
|
|
308
|
-
* del 子命令入口:在未提供 name 时先展示当前已录入账号列表,便于选择。
|
|
75
|
+
* 注册配置与状态相关子命令。
|
|
309
76
|
*
|
|
310
|
-
* @param
|
|
77
|
+
* @param program Commander 程序实例。
|
|
311
78
|
* @returns 无返回值。
|
|
79
|
+
* @throws 无显式抛出。
|
|
312
80
|
*/
|
|
313
|
-
function
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
console.log("当前已录入账号(name):");
|
|
321
|
-
for (const account of config.accounts) {
|
|
322
|
-
if (account.email) {
|
|
323
|
-
console.log(`- ${account.id} (${account.email})`);
|
|
324
|
-
}
|
|
325
|
-
else {
|
|
326
|
-
console.log(`- ${account.id}`);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
console.log("");
|
|
330
|
-
console.log("请使用以下命令删除指定账号,例如:");
|
|
331
|
-
console.log(" codex-slot del <name>");
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
handleAccountRemove(name);
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* 判断后台服务当前是否在运行。
|
|
338
|
-
*
|
|
339
|
-
* @returns 运行中的 PID;未运行时返回 `null`。
|
|
340
|
-
*/
|
|
341
|
-
function getRunningPid() {
|
|
342
|
-
const pidPath = (0, config_1.getPidPath)();
|
|
343
|
-
if (!node_fs_1.default.existsSync(pidPath)) {
|
|
344
|
-
return null;
|
|
345
|
-
}
|
|
346
|
-
const raw = node_fs_1.default.readFileSync(pidPath, "utf8").trim();
|
|
347
|
-
const pid = Number(raw);
|
|
348
|
-
if (!Number.isInteger(pid) || pid <= 0) {
|
|
349
|
-
node_fs_1.default.rmSync(pidPath, { force: true });
|
|
350
|
-
return null;
|
|
351
|
-
}
|
|
352
|
-
try {
|
|
353
|
-
process.kill(pid, 0);
|
|
354
|
-
return pid;
|
|
355
|
-
}
|
|
356
|
-
catch {
|
|
357
|
-
node_fs_1.default.rmSync(pidPath, { force: true });
|
|
358
|
-
return null;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* 后台启动 cslot 服务并写入 PID 文件。
|
|
363
|
-
*
|
|
364
|
-
* @returns Promise,无返回值。
|
|
365
|
-
* @throws 当服务已在运行或子进程启动失败时抛出异常。
|
|
366
|
-
*/
|
|
367
|
-
async function handleStart(portOverride) {
|
|
368
|
-
const config = (0, config_1.loadConfig)();
|
|
369
|
-
const port = portOverride ? Number(portOverride) : config.server.port;
|
|
370
|
-
if (portOverride) {
|
|
371
|
-
config.server.port = port;
|
|
372
|
-
(0, config_1.saveConfig)(config);
|
|
373
|
-
}
|
|
374
|
-
const runningPid = getRunningPid();
|
|
375
|
-
if (runningPid) {
|
|
376
|
-
console.log(`服务已在运行,PID=${runningPid}`);
|
|
377
|
-
if (portOverride) {
|
|
378
|
-
console.log(`已将新端口写入配置: ${port}`);
|
|
379
|
-
console.log("请先执行 cslot stop,再执行 cslot start 使新端口生效。");
|
|
380
|
-
}
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
applyManagedCodexConfig();
|
|
384
|
-
const logPath = (0, config_1.getServiceLogPath)();
|
|
385
|
-
const logFd = node_fs_1.default.openSync(logPath, "a");
|
|
386
|
-
const child = (0, node_child_process_1.spawn)(process.execPath, [__filename.replace(/cli\.js$/, "serve.js"), "--port", String(port)], {
|
|
387
|
-
detached: true,
|
|
388
|
-
stdio: ["ignore", logFd, logFd]
|
|
81
|
+
function registerRuntimeCommands(program) {
|
|
82
|
+
program
|
|
83
|
+
.command("status")
|
|
84
|
+
.description((0, text_1.bi)("刷新并查看所有已录入账号或工作空间的最新额度", "Refresh usage for all managed slots"))
|
|
85
|
+
.option("--no-interactive", (0, text_1.bi)("仅输出状态表,不进入交互式切换", "Print only"))
|
|
86
|
+
.action(async (options) => {
|
|
87
|
+
await (0, status_command_1.handleStatus)(options);
|
|
389
88
|
});
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
process.kill(pid, "SIGTERM");
|
|
409
|
-
node_fs_1.default.rmSync((0, config_1.getPidPath)(), { force: true });
|
|
410
|
-
deactivateManagedCodexConfig();
|
|
411
|
-
console.log(`服务已停止,PID=${pid}`);
|
|
412
|
-
}
|
|
413
|
-
/**
|
|
414
|
-
* 对正则元字符做转义,供动态构造匹配模式使用。
|
|
415
|
-
*
|
|
416
|
-
* @param input 原始字符串。
|
|
417
|
-
* @returns 经过转义后的安全正则片段。
|
|
418
|
-
*/
|
|
419
|
-
function escapeRegExp(input) {
|
|
420
|
-
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
421
|
-
}
|
|
422
|
-
function ensureParentDir(filePath) {
|
|
423
|
-
node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
|
|
424
|
-
}
|
|
425
|
-
function writeFileAtomic(targetFile, content) {
|
|
426
|
-
ensureParentDir(targetFile);
|
|
427
|
-
const tmpFile = `${targetFile}.tmp-${process.pid}-${Date.now()}`;
|
|
428
|
-
node_fs_1.default.writeFileSync(tmpFile, content, "utf8");
|
|
429
|
-
node_fs_1.default.renameSync(tmpFile, targetFile);
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* 返回默认的 `codex config.toml` 路径。
|
|
433
|
-
*
|
|
434
|
-
* @returns 默认 `config.toml` 绝对路径。
|
|
435
|
-
*/
|
|
436
|
-
function getDefaultCodexConfigPath() {
|
|
437
|
-
return node_path_1.default.join(process.env.HOME ?? "", ".codex", "config.toml");
|
|
438
|
-
}
|
|
439
|
-
/**
|
|
440
|
-
* 生成 cslot provider 配置块。
|
|
441
|
-
*
|
|
442
|
-
* @returns 可直接写入 `config.toml` 的配置块内容。
|
|
443
|
-
*/
|
|
444
|
-
function buildManagedConfigBlock() {
|
|
445
|
-
const config = (0, config_1.loadConfig)();
|
|
446
|
-
return [
|
|
447
|
-
"[model_providers.cslot]",
|
|
448
|
-
'name = "cslot"',
|
|
449
|
-
`base_url = "http://${config.server.host}:${config.server.port}/v1"`,
|
|
450
|
-
`http_headers = { Authorization = "Bearer ${config.server.api_key}" }`,
|
|
451
|
-
'wire_api = "responses"'
|
|
452
|
-
].join("\n");
|
|
453
|
-
}
|
|
454
|
-
/**
|
|
455
|
-
* 将 cslot provider 配置写入指定的 codex config.toml。
|
|
456
|
-
*
|
|
457
|
-
* @param targetPathOrDir 可选的 codex 配置目录或 config.toml 文件路径。
|
|
458
|
-
* @returns 实际写入的 `config.toml` 文件路径。
|
|
459
|
-
*/
|
|
460
|
-
function applyManagedCodexConfig(targetPathOrDir, options) {
|
|
461
|
-
const rawTarget = targetPathOrDir ? (0, config_1.expandHome)(targetPathOrDir) : getDefaultCodexConfigPath();
|
|
462
|
-
const targetFile = rawTarget.endsWith(".toml") ? rawTarget : node_path_1.default.join(rawTarget, "config.toml");
|
|
463
|
-
const block = buildManagedConfigBlock();
|
|
464
|
-
let original = "";
|
|
465
|
-
if (node_fs_1.default.existsSync(targetFile)) {
|
|
466
|
-
original = node_fs_1.default.readFileSync(targetFile, "utf8");
|
|
467
|
-
}
|
|
468
|
-
// 更稳定的策略:仅按 provider 名称和 model_provider 改动,不引入额外的 marker。
|
|
469
|
-
const lines = original.length > 0 ? original.split(/\r?\n/) : [];
|
|
470
|
-
let replacedModelProvider = false;
|
|
471
|
-
const modelProviderLine = 'model_provider = "cslot"';
|
|
472
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
473
|
-
const trimmed = lines[i].trim();
|
|
474
|
-
// 优先替换被注释掉的默认行(只替换第一处,减少对用户文件的干扰)。
|
|
475
|
-
if (!replacedModelProvider && /^#\s*model_provider\s*=/.test(trimmed)) {
|
|
476
|
-
lines[i] = modelProviderLine;
|
|
477
|
-
replacedModelProvider = true;
|
|
478
|
-
continue;
|
|
479
|
-
}
|
|
480
|
-
// 替换真实生效的 model_provider 行。
|
|
481
|
-
if (/^model_provider\s*=/.test(trimmed) && !trimmed.startsWith("#")) {
|
|
482
|
-
lines[i] = modelProviderLine;
|
|
483
|
-
replacedModelProvider = true;
|
|
484
|
-
continue;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
if (!replacedModelProvider) {
|
|
488
|
-
const firstNonEmptyIndex = lines.findIndex((line) => line.trim() !== "");
|
|
489
|
-
if (firstNonEmptyIndex >= 0) {
|
|
490
|
-
lines.splice(firstNonEmptyIndex, 0, modelProviderLine, "");
|
|
491
|
-
}
|
|
492
|
-
else {
|
|
493
|
-
lines.push(modelProviderLine, "");
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
// 替换或追加 [model_providers.cslot] 这一段配置。
|
|
497
|
-
const blockLines = block.split("\n");
|
|
498
|
-
let insertAfterIndex = -1;
|
|
499
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
500
|
-
const trimmed = lines[i].trim();
|
|
501
|
-
if (trimmed === "[model_providers.cslot]") {
|
|
502
|
-
let j = i;
|
|
503
|
-
while (j < lines.length) {
|
|
504
|
-
const currentTrimmed = lines[j].trim();
|
|
505
|
-
if (j > i && currentTrimmed.startsWith("[") && !currentTrimmed.startsWith("[[")) {
|
|
506
|
-
break;
|
|
507
|
-
}
|
|
508
|
-
insertAfterIndex = j;
|
|
509
|
-
j += 1;
|
|
510
|
-
}
|
|
511
|
-
// 删除旧的 cslot provider 表块,准备写入新的。
|
|
512
|
-
lines.splice(i, j - i);
|
|
513
|
-
insertAfterIndex = i - 1;
|
|
514
|
-
i = j - 1;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
if (insertAfterIndex >= 0) {
|
|
518
|
-
lines.splice(insertAfterIndex + 1, 0, "", ...blockLines);
|
|
519
|
-
}
|
|
520
|
-
else {
|
|
521
|
-
if (lines.length > 0 && lines[lines.length - 1].trim() !== "") {
|
|
522
|
-
lines.push("");
|
|
523
|
-
}
|
|
524
|
-
lines.push(...blockLines);
|
|
525
|
-
}
|
|
526
|
-
const nextContent = `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
|
|
527
|
-
writeFileAtomic(targetFile, nextContent);
|
|
528
|
-
if (!options?.silent) {
|
|
529
|
-
const config = (0, config_1.loadConfig)();
|
|
530
|
-
console.log(`已写入: ${targetFile}`);
|
|
531
|
-
console.log(`base_url=http://${config.server.host}:${config.server.port}/v1`);
|
|
532
|
-
console.log(`api_key=${config.server.api_key}`);
|
|
533
|
-
console.log("提示: start 会自动接管 codex provider,stop 会自动恢复。");
|
|
534
|
-
}
|
|
535
|
-
return targetFile;
|
|
536
|
-
}
|
|
537
|
-
/**
|
|
538
|
-
* 关闭 cslot 作为当前默认 provider 的接管状态。
|
|
539
|
-
*
|
|
540
|
-
* @returns 无返回值。
|
|
541
|
-
*/
|
|
542
|
-
function deactivateManagedCodexConfig() {
|
|
543
|
-
const targetFile = getDefaultCodexConfigPath();
|
|
544
|
-
if (!node_fs_1.default.existsSync(targetFile)) {
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
const original = node_fs_1.default.readFileSync(targetFile, "utf8");
|
|
548
|
-
const nextContent = original.replace(/^(\s*)model_provider\s*=\s*"cslot"\s*$/m, '$1# model_provider = "cslot"');
|
|
549
|
-
if (nextContent !== original) {
|
|
550
|
-
node_fs_1.default.writeFileSync(targetFile, nextContent, "utf8");
|
|
551
|
-
console.log(`已更新: ${targetFile}`);
|
|
552
|
-
}
|
|
89
|
+
program
|
|
90
|
+
.command("start")
|
|
91
|
+
.description((0, text_1.bi)("后台启动本地代理服务", "Start the local proxy in background"))
|
|
92
|
+
.option("--port <port>", (0, text_1.bi)("监听端口;会同步写入本地配置", "Listen port and save it to local config"))
|
|
93
|
+
.addHelpText("after", [
|
|
94
|
+
"",
|
|
95
|
+
`${(0, text_1.bi)("说明", "Notes")}:`,
|
|
96
|
+
` ${(0, text_1.bi)("start 会自动接管 `~/.codex/config.toml`,并在指定端口时自动写入该端口;stop 会恢复接管前内容。", "`start` will manage `~/.codex/config.toml` automatically, write the specified port when provided, and `stop` will restore the previous content.")}`,
|
|
97
|
+
].join("\n"))
|
|
98
|
+
.action(async (options) => {
|
|
99
|
+
await (0, service_control_1.handleStart)(options.port);
|
|
100
|
+
});
|
|
101
|
+
program
|
|
102
|
+
.command("stop")
|
|
103
|
+
.description((0, text_1.bi)("停止后台代理服务并恢复 codex 配置", "Stop the proxy and restore Codex config"))
|
|
104
|
+
.action(service_control_1.handleStop);
|
|
553
105
|
}
|
|
554
106
|
/**
|
|
555
|
-
* CLI
|
|
107
|
+
* CLI 主入口,负责初始化环境、注册命令并交给 Commander 分发执行。
|
|
556
108
|
*
|
|
557
109
|
* @returns Promise,无返回值。
|
|
558
110
|
* @throws 当命令执行失败时向上抛出异常。
|
|
559
111
|
*/
|
|
560
112
|
async function main() {
|
|
561
113
|
const program = new commander_1.Command();
|
|
562
|
-
// 禁用内置 help 子命令,仅保留 --help / -h 形式。
|
|
563
114
|
program.addHelpCommand(false);
|
|
564
|
-
(0, config_1.
|
|
115
|
+
(0, config_1.getCslotHome)();
|
|
565
116
|
(0, config_1.loadConfig)();
|
|
566
|
-
program
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
.version(getCliVersion());
|
|
570
|
-
program
|
|
571
|
-
.command("add")
|
|
572
|
-
.description("登录并新增一个账号或工作空间")
|
|
573
|
-
.argument("<name>", "账号标识(用于本地区分)")
|
|
574
|
-
.action(async (name) => {
|
|
575
|
-
await handleAccountLogin(name);
|
|
576
|
-
});
|
|
577
|
-
program
|
|
578
|
-
.command("del")
|
|
579
|
-
.description("删除一个已录入账号")
|
|
580
|
-
.argument("[name]", "账号标识(用于本地区分),留空时会列出当前所有账号")
|
|
581
|
-
.action(handleAccountRemoveCommand);
|
|
582
|
-
program
|
|
583
|
-
.command("import")
|
|
584
|
-
.description("导入当前或指定 HOME 下的官方 codex 登录态")
|
|
585
|
-
.argument("<name>", "账号标识(用于本地区分)")
|
|
586
|
-
.argument("[codexHome]", "已有 HOME 目录,默认当前用户 HOME")
|
|
587
|
-
.action(handleAccountImport);
|
|
588
|
-
program
|
|
589
|
-
.command("status")
|
|
590
|
-
.description("刷新并查看所有已录入账号或工作空间的最新额度")
|
|
591
|
-
.option("--no-interactive", "仅输出状态表,不进入交互式切换")
|
|
592
|
-
.action(async (options) => {
|
|
593
|
-
await handleStatus(options);
|
|
594
|
-
});
|
|
595
|
-
program
|
|
596
|
-
.command("start")
|
|
597
|
-
.description("后台启动本地代理服务")
|
|
598
|
-
.option("--port <port>", "监听端口")
|
|
599
|
-
.action(async (options) => {
|
|
600
|
-
await handleStart(options.port);
|
|
601
|
-
});
|
|
602
|
-
program.command("stop").description("停止后台代理服务").action(handleStop);
|
|
117
|
+
configureRootProgram(program);
|
|
118
|
+
registerAccountCommands(program);
|
|
119
|
+
registerRuntimeCommands(program);
|
|
603
120
|
await program.parseAsync(process.argv);
|
|
604
121
|
}
|
|
605
122
|
void main().catch((error) => {
|
|
606
123
|
const message = error instanceof Error ? error.message : String(error);
|
|
607
|
-
console.error(`codex-slot 执行失败: ${message}`);
|
|
124
|
+
console.error((0, text_1.bi)(`codex-slot 执行失败: ${message}`, `codex-slot failed: ${message}`));
|
|
608
125
|
process.exit(1);
|
|
609
126
|
});
|