ccus-cli 0.2.29 → 0.2.30

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.
@@ -20,6 +20,7 @@ const paths_1 = require("./paths");
20
20
  // app-server 子进程参数:read-only + never,避免新版 Codex 拒绝已移除的 untrusted 审批策略。
21
21
  const CODEX_ARGS = ["-s", "read-only", "-a", "never", "app-server"];
22
22
  const DEFAULT_TIMEOUT_MS = 10_000;
23
+ const CLEANUP_TIMEOUT_MS = 2_000;
23
24
  // notify 每 turn 触发,额度缓存 5 分钟,命中秒回避免阻塞 Codex 主流程。
24
25
  const DEFAULT_TTL_MS = 5 * 60 * 1000;
25
26
  // ChatGPT 后端 wham/usage 回退:app-server 未返回可用额度时走,直连读 auth.json 的 OAuth token。
@@ -187,6 +188,38 @@ const defaultSpawn = (command, args, options) => {
187
188
  const merged = process.platform === "win32" ? { ...options, shell: true } : options;
188
189
  return (0, node_child_process_1.spawn)(command, args, merged);
189
190
  };
191
+ /** Windows 必须先清理整棵树,再关闭管道;先杀 shell 会失去后代进程的关联。 */
192
+ async function cleanupCodexChild(child, kill) {
193
+ try {
194
+ if (kill) {
195
+ if (process.platform === "win32" && child.pid !== undefined) {
196
+ await new Promise((resolve, reject) => {
197
+ (0, node_child_process_1.execFile)("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], {
198
+ windowsHide: true,
199
+ timeout: CLEANUP_TIMEOUT_MS,
200
+ }, (error) => error ? reject(error) : resolve());
201
+ });
202
+ }
203
+ else {
204
+ child.kill();
205
+ }
206
+ }
207
+ }
208
+ catch (error) {
209
+ (0, debug_1.debugLog)("codex", "process cleanup failed", error instanceof Error ? error.message : String(error));
210
+ try {
211
+ child.kill();
212
+ }
213
+ catch { /* 清理失败不覆盖查询结果。 */ }
214
+ }
215
+ finally {
216
+ // 即使 taskkill 失败,也不让后代继承的管道或进程句柄阻塞 Hook 退出。
217
+ child.stdin.destroy();
218
+ child.stdout.destroy();
219
+ child.stderr.destroy();
220
+ child.unref();
221
+ }
222
+ }
190
223
  /**
191
224
  * spawn `codex app-server` 走 JSON-RPC 握手 + `account/rateLimits/read` 拉额度。
192
225
  *
@@ -220,16 +253,12 @@ async function fetchCodexQuota(options = {}) {
220
253
  clearTimeout(timer);
221
254
  timer = null;
222
255
  }
223
- // settled 标志已保证 resolve 幂等,无需再手动摘 listener;kill 终止子进程后其 stdio 自然关闭。
224
- if (kill && child) {
225
- try {
226
- child.kill();
227
- }
228
- catch {
229
- // kill 失败不影响结果。
230
- }
256
+ if (child) {
257
+ void cleanupCodexChild(child, kill).then(() => resolve(outcome));
258
+ }
259
+ else {
260
+ resolve(outcome);
231
261
  }
232
- resolve(outcome);
233
262
  };
234
263
  try {
235
264
  child = spawnFn("codex", CODEX_ARGS, {
@@ -247,17 +276,30 @@ async function fetchCodexQuota(options = {}) {
247
276
  (0, debug_1.debugLog)("codex", "rpc timeout", { timeoutMs });
248
277
  finish(empty("error"), true);
249
278
  }, timeoutMs);
279
+ // 管道错误(例如清理期间的 EPIPE)是异步事件,不能仅靠 write 的 try/catch。
280
+ for (const stream of [child.stdin, child.stdout, child.stderr]) {
281
+ stream.on("error", (error) => {
282
+ (0, debug_1.debugLog)("codex", "stdio error", error.message);
283
+ finish(empty("error"), true);
284
+ });
285
+ }
286
+ child.stderr.resume();
250
287
  const send = (message) => {
288
+ if (settled)
289
+ return;
251
290
  try {
252
291
  child?.stdin.write(message);
253
292
  }
254
293
  catch (error) {
255
294
  (0, debug_1.debugLog)("codex", "stdin write failed", error instanceof Error ? error.message : String(error));
295
+ finish(empty("error"), true);
256
296
  }
257
297
  };
258
298
  const initId = (rpcId += 1);
259
299
  send(buildRpcMessage(initId, "initialize", { clientInfo: { name: "ccus", version: "1.0.0" } }));
260
300
  child.stdout.on("data", (chunk) => {
301
+ if (settled)
302
+ return;
261
303
  buffer += chunk.toString();
262
304
  let newline;
263
305
  while ((newline = buffer.indexOf("\n")) !== -1) {
@@ -292,13 +334,14 @@ async function fetchCodexQuota(options = {}) {
292
334
  const quota = parseRateLimitsResult(message.result);
293
335
  (0, debug_1.debugLog)("codex", "quota parsed", quota);
294
336
  finish({ ...quota, status: "ok" }, true);
337
+ return;
295
338
  }
296
339
  }
297
340
  });
298
341
  child.on("error", (err) => {
299
342
  const enoent = err.code === "ENOENT";
300
343
  (0, debug_1.debugLog)("codex", "spawn error", { code: err.code, message: err.message });
301
- finish(empty(enoent ? "unavailable" : "error"));
344
+ finish(empty(enoent ? "unavailable" : "error"), true);
302
345
  });
303
346
  child.on("close", (code) => {
304
347
  (0, debug_1.debugLog)("codex", "app-server exited before quota", { code });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccus-cli",
3
- "version": "0.2.29",
3
+ "version": "0.2.30",
4
4
  "description": "Claude Code statusline usage logger and dashboard CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {