codex-slot 0.1.16 → 0.1.17

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.
@@ -11,9 +11,13 @@ const node_net_1 = __importDefault(require("node:net"));
11
11
  const node_path_1 = __importDefault(require("node:path"));
12
12
  const node_child_process_1 = require("node:child_process");
13
13
  const undici_1 = require("undici");
14
+ const account_service_1 = require("./account-service");
15
+ const account_store_1 = require("../account-store");
14
16
  const codex_config_1 = require("../codex-config");
17
+ const codex_auth_1 = require("../codex-auth");
15
18
  const cli_helpers_1 = require("../cli-helpers");
16
19
  const config_1 = require("../config");
20
+ const scheduler_1 = require("../scheduler");
17
21
  const STARTUP_POLL_INTERVAL_MS = 100;
18
22
  const STARTUP_TIMEOUT_MS = 5000;
19
23
  /**
@@ -163,6 +167,41 @@ function rollbackFailedStart(pid, previousConfig) {
163
167
  node_fs_1.default.rmSync((0, config_1.getPidPath)(), { force: true });
164
168
  (0, config_1.saveConfig)(previousConfig);
165
169
  (0, codex_config_1.deactivateManagedCodexConfig)();
170
+ (0, codex_auth_1.deactivateManagedCodexAuth)();
171
+ }
172
+ /**
173
+ * 选择一个可用于接管主 `~/.codex` 登录态的账号。
174
+ *
175
+ * 业务规则:
176
+ * 1. 优先复用当前调度器已经选中的最佳账号,保证 CLI 与代理请求走同一身份。
177
+ * 2. 若当前没有可调度账号,则回退到首个启用且本地工作空间仍存在的账号。
178
+ * 3. 若仍无可用账号,则返回 `null`,此时仅接管 provider 配置,不强行覆盖主登录态。
179
+ *
180
+ * @returns 选中的受管账号;若不存在可接管账号则返回 `null`。
181
+ * @throws 无显式抛出。
182
+ */
183
+ function resolveManagedAuthAccount() {
184
+ const selected = (0, scheduler_1.pickBestAccount)()?.account;
185
+ if (selected && (0, account_store_1.hasCompleteCodexAuthState)(selected.codex_home)) {
186
+ return selected;
187
+ }
188
+ return ((0, account_service_1.listAccounts)().find((account) => account.enabled &&
189
+ node_fs_1.default.existsSync(account.codex_home) &&
190
+ (0, account_store_1.hasCompleteCodexAuthState)(account.codex_home)) ?? null);
191
+ }
192
+ /**
193
+ * 将主 `~/.codex` 登录态切换到当前受管账号,供 `codex_apps` 等依赖主登录态的链路复用。
194
+ *
195
+ * @returns 实际接管的账号;若没有合适账号则返回 `null`。
196
+ * @throws 当目标账号目录缺少完整登录态时抛出异常。
197
+ */
198
+ function applyManagedAuthIfPossible() {
199
+ const account = resolveManagedAuthAccount();
200
+ if (!account) {
201
+ return null;
202
+ }
203
+ (0, codex_auth_1.applyManagedCodexAuth)(account.codex_home, { sourceAccountId: account.id });
204
+ return account;
166
205
  }
167
206
  /**
168
207
  * 为后台服务挑选最终启动端口。
@@ -225,6 +264,7 @@ async function startManagedService(portOverride) {
225
264
  // 每次真正启动服务前都轮换一次本地 api_key,并让受管 config.toml 使用同一新值。
226
265
  const persistedConfig = (0, config_1.rotateServerApiKey)(config);
227
266
  (0, codex_config_1.applyManagedCodexConfig)(undefined, { config: persistedConfig });
267
+ applyManagedAuthIfPossible();
228
268
  const logPath = (0, config_1.getServiceLogPath)();
229
269
  const logFd = node_fs_1.default.openSync(logPath, "a");
230
270
  const serveEntrypoint = resolveServeEntrypoint();
@@ -267,10 +307,12 @@ function stopManagedService() {
267
307
  const pid = getRunningPid();
268
308
  if (!pid) {
269
309
  (0, codex_config_1.deactivateManagedCodexConfig)();
310
+ (0, codex_auth_1.deactivateManagedCodexAuth)();
270
311
  return { stoppedPid: null };
271
312
  }
272
313
  process.kill(pid, "SIGTERM");
273
314
  node_fs_1.default.rmSync((0, config_1.getPidPath)(), { force: true });
274
315
  (0, codex_config_1.deactivateManagedCodexConfig)();
316
+ (0, codex_auth_1.deactivateManagedCodexAuth)();
275
317
  return { stoppedPid: pid };
276
318
  }
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getDefaultCodexHome = getDefaultCodexHome;
7
+ exports.applyManagedCodexAuth = applyManagedCodexAuth;
8
+ exports.deactivateManagedCodexAuth = deactivateManagedCodexAuth;
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const account_store_1 = require("./account-store");
12
+ const state_1 = require("./state");
13
+ /**
14
+ * 返回默认的 Codex HOME 目录。
15
+ *
16
+ * @returns 当前进程 HOME;未设置时返回空字符串。
17
+ */
18
+ function getDefaultCodexHome() {
19
+ return process.env.HOME ?? "";
20
+ }
21
+ /**
22
+ * 读取目标 HOME 下 `.codex/accounts` 目录中的所有 `.auth.json` 文件内容。
23
+ *
24
+ * @param codexHome 目标 HOME 目录。
25
+ * @returns 文件名到原始文本的映射;目录不存在时返回空对象。
26
+ */
27
+ function snapshotAccountAuthFiles(codexHome) {
28
+ const accountsDir = node_path_1.default.join((0, account_store_1.getCodexDataDir)(codexHome), "accounts");
29
+ if (!node_fs_1.default.existsSync(accountsDir)) {
30
+ return {};
31
+ }
32
+ const snapshots = {};
33
+ for (const entry of node_fs_1.default.readdirSync(accountsDir, { withFileTypes: true })) {
34
+ if (!entry.isFile() || !entry.name.endsWith(".auth.json")) {
35
+ continue;
36
+ }
37
+ snapshots[entry.name] = node_fs_1.default.readFileSync(node_path_1.default.join(accountsDir, entry.name), "utf8");
38
+ }
39
+ return snapshots;
40
+ }
41
+ /**
42
+ * 基于当前目标 HOME 生成登录态恢复快照。
43
+ *
44
+ * @param targetHome 需要接管的主 HOME 目录。
45
+ * @param sourceAccountId 可选的来源账号标识,仅用于状态记录。
46
+ * @returns 用于 stop 恢复的快照。
47
+ */
48
+ function buildManagedAuthSnapshot(targetHome, sourceAccountId) {
49
+ const codexDir = (0, account_store_1.getCodexDataDir)(targetHome);
50
+ const authPath = node_path_1.default.join(codexDir, "auth.json");
51
+ const registryPath = node_path_1.default.join(codexDir, "accounts", "registry.json");
52
+ return {
53
+ target_home: targetHome,
54
+ source_account_id: sourceAccountId ?? null,
55
+ original_auth_file: node_fs_1.default.existsSync(authPath) ? node_fs_1.default.readFileSync(authPath, "utf8") : null,
56
+ original_registry_file: node_fs_1.default.existsSync(registryPath) ? node_fs_1.default.readFileSync(registryPath, "utf8") : null,
57
+ original_account_auth_files: snapshotAccountAuthFiles(targetHome)
58
+ };
59
+ }
60
+ /**
61
+ * 将指定文件恢复为快照内容;快照为空时删除目标文件。
62
+ *
63
+ * @param targetFile 目标文件路径。
64
+ * @param content 原始文件内容;为 `null` 时表示恢复为不存在。
65
+ * @returns 无返回值。
66
+ */
67
+ function restoreSnapshotFile(targetFile, content) {
68
+ if (content === null) {
69
+ node_fs_1.default.rmSync(targetFile, { force: true });
70
+ return;
71
+ }
72
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(targetFile), { recursive: true });
73
+ node_fs_1.default.writeFileSync(targetFile, content, "utf8");
74
+ }
75
+ /**
76
+ * 将主 HOME 中多余的 `.auth.json` 文件删除,再恢复快照中记录的文件集合。
77
+ *
78
+ * @param targetHome 目标 HOME 目录。
79
+ * @param snapshot 登录态快照。
80
+ * @returns 无返回值。
81
+ */
82
+ function restoreAccountAuthFiles(targetHome, snapshot) {
83
+ const accountsDir = node_path_1.default.join((0, account_store_1.getCodexDataDir)(targetHome), "accounts");
84
+ node_fs_1.default.mkdirSync(accountsDir, { recursive: true });
85
+ for (const entry of node_fs_1.default.readdirSync(accountsDir, { withFileTypes: true })) {
86
+ if (!entry.isFile() || !entry.name.endsWith(".auth.json")) {
87
+ continue;
88
+ }
89
+ if (!(entry.name in snapshot.original_account_auth_files)) {
90
+ node_fs_1.default.rmSync(node_path_1.default.join(accountsDir, entry.name), { force: true });
91
+ }
92
+ }
93
+ for (const [fileName, content] of Object.entries(snapshot.original_account_auth_files)) {
94
+ restoreSnapshotFile(node_path_1.default.join(accountsDir, fileName), content);
95
+ }
96
+ }
97
+ /**
98
+ * 将主 `~/.codex` 登录态切换到指定受管账号,供 `codex_apps` 等依赖主登录态的能力复用。
99
+ *
100
+ * @param sourceHome 来源账号 HOME。
101
+ * @param options 可选控制项;可指定目标 HOME 与来源账号标识。
102
+ * @returns 实际接管的目标 HOME 路径。
103
+ */
104
+ function applyManagedCodexAuth(sourceHome, options) {
105
+ const targetHome = options?.targetHome ?? getDefaultCodexHome();
106
+ const previousState = (0, state_1.getManagedCodexAuthState)();
107
+ const snapshot = previousState && previousState.target_home === targetHome
108
+ ? previousState
109
+ : buildManagedAuthSnapshot(targetHome, options?.sourceAccountId);
110
+ (0, account_store_1.cloneCodexAuthState)(sourceHome, targetHome);
111
+ (0, state_1.setManagedCodexAuthState)({
112
+ ...snapshot,
113
+ source_account_id: options?.sourceAccountId ?? snapshot.source_account_id ?? null
114
+ });
115
+ return targetHome;
116
+ }
117
+ /**
118
+ * 恢复主 `~/.codex` 登录态到 cslot 接管前的原始状态。
119
+ *
120
+ * @returns 恢复的目标 HOME 路径;若没有接管快照则返回 `null`。
121
+ */
122
+ function deactivateManagedCodexAuth() {
123
+ const snapshot = (0, state_1.getManagedCodexAuthState)();
124
+ if (!snapshot) {
125
+ return null;
126
+ }
127
+ const targetHome = snapshot.target_home;
128
+ const codexDir = (0, account_store_1.getCodexDataDir)(targetHome);
129
+ restoreSnapshotFile(node_path_1.default.join(codexDir, "auth.json"), snapshot.original_auth_file);
130
+ restoreSnapshotFile(node_path_1.default.join(codexDir, "accounts", "registry.json"), snapshot.original_registry_file);
131
+ restoreAccountAuthFiles(targetHome, snapshot);
132
+ (0, state_1.clearManagedCodexAuthState)();
133
+ return targetHome;
134
+ }
@@ -73,9 +73,31 @@ function buildManagedProviderBlock(eol, config) {
73
73
  `base_url = "http://${config.server.host}:${config.server.port}/v1"`,
74
74
  'wire_api = "responses"',
75
75
  `experimental_bearer_token = "${config.server.api_key}"`,
76
+ "[model_providers.cslot.http_headers]",
77
+ `Authorization = "Bearer ${config.server.api_key}"`,
76
78
  PROVIDER_BLOCK_END_MARKER
77
79
  ].join(eol);
78
80
  }
81
+ /**
82
+ * 判断候选表头是否属于指定父表的子表。
83
+ *
84
+ * @param parentHeader 父表表头,例如 `[model_providers.cslot]`。
85
+ * @param candidateHeader 当前扫描到的候选表头。
86
+ * @returns `true` 表示候选表头属于父表子表;否则返回 `false`。
87
+ */
88
+ function isChildTableHeader(parentHeader, candidateHeader) {
89
+ const normalizedParent = parentHeader.trim();
90
+ const normalizedCandidate = candidateHeader.trim();
91
+ if (!normalizedParent.startsWith("[") || !normalizedParent.endsWith("]")) {
92
+ return false;
93
+ }
94
+ if (!normalizedCandidate.startsWith("[") || !normalizedCandidate.endsWith("]")) {
95
+ return false;
96
+ }
97
+ const parentName = normalizedParent.slice(1, -1);
98
+ const candidateName = normalizedCandidate.slice(1, -1);
99
+ return candidateName.startsWith(`${parentName}.`);
100
+ }
79
101
  /**
80
102
  * 在文本中定位受 cslot 接管的块范围。
81
103
  *
@@ -206,7 +228,10 @@ function findTableSectionRange(content, header) {
206
228
  const line = lines[i];
207
229
  const lineEnd = offset + line.length;
208
230
  const trimmed = line.trim();
209
- if (i > startLineIndex && trimmed.startsWith("[") && !trimmed.startsWith("[[")) {
231
+ if (i > startLineIndex &&
232
+ trimmed.startsWith("[") &&
233
+ !trimmed.startsWith("[[") &&
234
+ !isChildTableHeader(header, trimmed)) {
210
235
  break;
211
236
  }
212
237
  endOffset = lineEnd;
package/dist/state.js CHANGED
@@ -14,8 +14,11 @@ exports.setUsageRefreshError = setUsageRefreshError;
14
14
  exports.clearUsageRefreshError = clearUsageRefreshError;
15
15
  exports.getUsageRefreshError = getUsageRefreshError;
16
16
  exports.getManagedCodexConfigState = getManagedCodexConfigState;
17
+ exports.getManagedCodexAuthState = getManagedCodexAuthState;
17
18
  exports.setManagedCodexConfigState = setManagedCodexConfigState;
19
+ exports.setManagedCodexAuthState = setManagedCodexAuthState;
18
20
  exports.clearManagedCodexConfigState = clearManagedCodexConfigState;
21
+ exports.clearManagedCodexAuthState = clearManagedCodexAuthState;
19
22
  const node_fs_1 = __importDefault(require("node:fs"));
20
23
  const node_path_1 = __importDefault(require("node:path"));
21
24
  const config_1 = require("./config");
@@ -34,6 +37,7 @@ function loadState() {
34
37
  account_blocks: {},
35
38
  usage_cache: {},
36
39
  usage_refresh_errors: {},
40
+ managed_codex_auth: null,
37
41
  managed_codex_config: null
38
42
  };
39
43
  }
@@ -44,12 +48,14 @@ function loadState() {
44
48
  account_blocks: {},
45
49
  usage_cache: {},
46
50
  usage_refresh_errors: {},
51
+ managed_codex_auth: null,
47
52
  managed_codex_config: null
48
53
  };
49
54
  return {
50
55
  account_blocks: parsed.account_blocks ?? {},
51
56
  usage_cache: parsed.usage_cache ?? {},
52
57
  usage_refresh_errors: parsed.usage_refresh_errors ?? {},
58
+ managed_codex_auth: parsed.managed_codex_auth ?? null,
53
59
  managed_codex_config: parsed.managed_codex_config ?? null
54
60
  };
55
61
  }
@@ -175,6 +181,15 @@ function getManagedCodexConfigState() {
175
181
  const state = loadState();
176
182
  return state.managed_codex_config ?? null;
177
183
  }
184
+ /**
185
+ * 读取当前记录的 Codex 主 HOME 登录态接管快照。
186
+ *
187
+ * @returns 最近一次接管时保存的登录态快照;不存在时返回 `null`。
188
+ */
189
+ function getManagedCodexAuthState() {
190
+ const state = loadState();
191
+ return state.managed_codex_auth ?? null;
192
+ }
178
193
  /**
179
194
  * 保存 Codex `config.toml` 接管快照,用于后续停止服务时精确恢复。
180
195
  *
@@ -186,6 +201,17 @@ function setManagedCodexConfigState(managedState) {
186
201
  state.managed_codex_config = managedState;
187
202
  saveState(state);
188
203
  }
204
+ /**
205
+ * 保存 Codex 主 HOME 登录态接管快照,用于 stop 时恢复原始登录态文件。
206
+ *
207
+ * @param managedState 接管前保存的原始登录态快照。
208
+ * @returns 无返回值。
209
+ */
210
+ function setManagedCodexAuthState(managedState) {
211
+ const state = loadState();
212
+ state.managed_codex_auth = managedState;
213
+ saveState(state);
214
+ }
189
215
  /**
190
216
  * 清理 Codex `config.toml` 接管快照。
191
217
  *
@@ -196,3 +222,13 @@ function clearManagedCodexConfigState() {
196
222
  state.managed_codex_config = null;
197
223
  saveState(state);
198
224
  }
225
+ /**
226
+ * 清理 Codex 主 HOME 登录态接管快照。
227
+ *
228
+ * @returns 无返回值。
229
+ */
230
+ function clearManagedCodexAuthState() {
231
+ const state = loadState();
232
+ state.managed_codex_auth = null;
233
+ saveState(state);
234
+ }
@@ -61,6 +61,18 @@ function renderSectionHeader(title, width, styled) {
61
61
  const line = `${"-".repeat(side)}${plainLabel}${"-".repeat(side)}`;
62
62
  return paint(line.slice(0, targetWidth), ANSI.cyan, styled);
63
63
  }
64
+ /**
65
+ * 渲染轻量分隔线,用于账号主表与当前账号明细之间建立清晰层次。
66
+ *
67
+ * @param width 当前终端宽度。
68
+ * @param styled 是否启用 ANSI 样式。
69
+ * @returns 可直接打印的分隔线文本。
70
+ * @throws 无显式抛出。
71
+ */
72
+ function renderDivider(width, styled) {
73
+ const dividerWidth = Math.max(24, Math.min(width, 96));
74
+ return paint("-".repeat(dividerWidth), ANSI.dim, styled);
75
+ }
64
76
  /**
65
77
  * 渲染摘要区可读性更高的计数文本,并对关键指标做轻量着色。
66
78
  *
@@ -204,7 +216,9 @@ async function handleInteractiveToggle(initialStatuses) {
204
216
  }
205
217
  }),
206
218
  "",
207
- (0, status_1.renderStatusDetails)(currentItem, { maxWidth: screenWidth }),
219
+ renderDivider(screenWidth, styled),
220
+ renderSectionHeader("current", screenWidth, styled),
221
+ (0, status_1.renderStatusDetails)(currentItem, { maxWidth: screenWidth, header: false }),
208
222
  "",
209
223
  renderSectionHeader("summary", screenWidth, styled),
210
224
  renderSummaryLine(summary, narrowScreen, styled),
package/dist/status.js CHANGED
@@ -328,13 +328,16 @@ function renderStatusTable(statuses, options) {
328
328
  * @returns 适合直接打印的详情区文本。
329
329
  */
330
330
  function renderStatusDetails(item, options) {
331
+ const includeHeader = options?.header ?? true;
331
332
  if (!item) {
332
- return ["[ current ]", "slot -"].join("\n");
333
+ return [includeHeader ? "[ current ]" : "slot -", includeHeader ? "slot -" : ""]
334
+ .filter((line) => line.length > 0)
335
+ .join("\n");
333
336
  }
334
337
  const maxWidth = options?.maxWidth ?? Number.POSITIVE_INFINITY;
335
338
  const narrow = maxWidth < 72;
336
339
  const lines = [
337
- "[ current ]",
340
+ ...(includeHeader ? ["[ current ]"] : []),
338
341
  formatDetailLine("slot", `${item.name} plan=${item.plan}`, maxWidth),
339
342
  formatDetailLine("email", item.email ?? "-", maxWidth),
340
343
  formatDetailLine("status", resolveStatusLabel(item), maxWidth),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-slot",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "本地 Codex 多账号切换与状态管理工具",
5
5
  "type": "commonjs",
6
6
  "main": "dist/cli.js",