codex-to-im 1.0.31 → 1.0.33

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.mjs CHANGED
@@ -15,6 +15,22 @@ import { fileURLToPath } from "node:url";
15
15
  import fs from "node:fs";
16
16
  import os from "node:os";
17
17
  import path from "node:path";
18
+
19
+ // src/runtime-options.ts
20
+ function parseSandboxMode(value) {
21
+ if (value === "read-only" || value === "workspace-write" || value === "danger-full-access") {
22
+ return value;
23
+ }
24
+ return void 0;
25
+ }
26
+ function parseReasoningEffort(value) {
27
+ if (value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh") {
28
+ return value;
29
+ }
30
+ return void 0;
31
+ }
32
+
33
+ // src/config.ts
18
34
  var DEFAULT_CTI_HOME = path.join(os.homedir(), ".codex-to-im");
19
35
  var DEFAULT_WORKSPACE_ROOT = path.join(os.homedir(), "cx2im");
20
36
  var CTI_HOME = process.env.CTI_HOME || DEFAULT_CTI_HOME;
@@ -61,18 +77,6 @@ function parsePositiveInt(value) {
61
77
  if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
62
78
  return Math.floor(parsed);
63
79
  }
64
- function parseSandboxMode(value) {
65
- if (value === "read-only" || value === "workspace-write" || value === "danger-full-access") {
66
- return value;
67
- }
68
- return void 0;
69
- }
70
- function parseReasoningEffort(value) {
71
- if (value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh") {
72
- return value;
73
- }
74
- return void 0;
75
- }
76
80
  function normalizeFeishuSite(value) {
77
81
  const normalized = (value || "").trim().replace(/\/+$/, "").toLowerCase();
78
82
  if (!normalized) return "feishu";
@@ -228,12 +232,14 @@ var runtimeDir = path2.join(CTI_HOME, "runtime");
228
232
  var logsDir = path2.join(CTI_HOME, "logs");
229
233
  var bridgePidFile = path2.join(runtimeDir, "bridge.pid");
230
234
  var bridgeStatusFile = path2.join(runtimeDir, "status.json");
235
+ var bridgeStartLockFile = path2.join(runtimeDir, "bridge.start.lock");
231
236
  var uiStatusFile = path2.join(runtimeDir, "ui-server.json");
232
237
  var uiPort = 4781;
233
238
  var bridgeAutostartTaskName = "CodexToIMBridge";
234
239
  var bridgeAutostartLauncherFile = path2.join(runtimeDir, "bridge-autostart.ps1");
235
240
  var npmUninstallLogFile = path2.join(runtimeDir, "npm-uninstall.log");
236
241
  var WINDOWS_HIDE = process.platform === "win32" ? { windowsHide: true } : {};
242
+ var BRIDGE_START_LOCK_STALE_MS = 3e4;
237
243
  function ensureDirs() {
238
244
  fs2.mkdirSync(runtimeDir, { recursive: true });
239
245
  fs2.mkdirSync(logsDir, { recursive: true });
@@ -287,6 +293,70 @@ function clearBridgePidFile() {
287
293
  } catch {
288
294
  }
289
295
  }
296
+ function readBridgeStartLock(filePath = bridgeStartLockFile) {
297
+ const parsed = readJsonFile(filePath, null);
298
+ const pid = Number(parsed?.pid);
299
+ const createdAt = typeof parsed?.createdAt === "string" ? parsed.createdAt : "";
300
+ if (!Number.isFinite(pid) || pid <= 0 || !createdAt) return null;
301
+ return { pid, createdAt };
302
+ }
303
+ function isBridgeStartLockStale(lock, options = {}) {
304
+ if (!lock) return true;
305
+ const nowMs = options.nowMs ?? Date.now();
306
+ const staleMs = options.staleMs ?? BRIDGE_START_LOCK_STALE_MS;
307
+ const isAlive = options.isAlive ?? isProcessAlive;
308
+ const createdAtMs = Date.parse(lock.createdAt);
309
+ if (!Number.isFinite(createdAtMs)) return true;
310
+ if (!isAlive(lock.pid)) return true;
311
+ return nowMs - createdAtMs > staleMs;
312
+ }
313
+ function tryAcquireBridgeStartLock(options = {}) {
314
+ const filePath = options.filePath ?? bridgeStartLockFile;
315
+ const ownerPid = options.ownerPid ?? process.pid;
316
+ const nowMs = options.nowMs ?? Date.now();
317
+ const payload = {
318
+ pid: ownerPid,
319
+ createdAt: new Date(nowMs).toISOString()
320
+ };
321
+ for (let attempt = 0; attempt < 2; attempt += 1) {
322
+ try {
323
+ fs2.writeFileSync(filePath, JSON.stringify(payload, null, 2), { encoding: "utf-8", flag: "wx" });
324
+ return { acquired: true };
325
+ } catch (error) {
326
+ const code = error.code;
327
+ if (code !== "EEXIST") throw error;
328
+ const existing2 = readBridgeStartLock(filePath);
329
+ if (!isBridgeStartLockStale(existing2, {
330
+ nowMs,
331
+ staleMs: options.staleMs,
332
+ isAlive: options.isAlive
333
+ })) {
334
+ return { acquired: false, holderPid: existing2?.pid };
335
+ }
336
+ try {
337
+ fs2.unlinkSync(filePath);
338
+ } catch {
339
+ }
340
+ }
341
+ }
342
+ const existing = readBridgeStartLock(filePath);
343
+ return { acquired: false, holderPid: existing?.pid };
344
+ }
345
+ function releaseBridgeStartLock(filePath = bridgeStartLockFile, ownerPid = process.pid) {
346
+ const existing = readBridgeStartLock(filePath);
347
+ if (!existing) {
348
+ try {
349
+ fs2.unlinkSync(filePath);
350
+ } catch {
351
+ }
352
+ return;
353
+ }
354
+ if (existing.pid !== ownerPid) return;
355
+ try {
356
+ fs2.unlinkSync(filePath);
357
+ } catch {
358
+ }
359
+ }
290
360
  function sleep(ms) {
291
361
  return new Promise((resolve) => setTimeout(resolve, ms));
292
362
  }
@@ -515,6 +585,21 @@ async function waitForBridgeRunning(timeoutMs = 2e4) {
515
585
  }
516
586
  return getBridgeStatus();
517
587
  }
588
+ async function waitForBridgeStartupTurn(timeoutMs = 2e4) {
589
+ const startedAt = Date.now();
590
+ while (Date.now() - startedAt < timeoutMs) {
591
+ const status = getBridgeStatus();
592
+ if (status.running) return status;
593
+ const lock = readBridgeStartLock();
594
+ if (!lock) return status;
595
+ if (isBridgeStartLockStale(lock)) {
596
+ releaseBridgeStartLock();
597
+ return getBridgeStatus();
598
+ }
599
+ await sleep(300);
600
+ }
601
+ return getBridgeStatus();
602
+ }
518
603
  async function waitForUiServer(timeoutMs = 15e3) {
519
604
  const startedAt = Date.now();
520
605
  while (Date.now() - startedAt < timeoutMs) {
@@ -543,27 +628,52 @@ async function startBridge() {
543
628
  if (preflightFailure) {
544
629
  throw new Error(preflightFailure);
545
630
  }
546
- const daemonEntry = path2.join(packageRoot, "dist", "daemon.mjs");
547
- if (!fs2.existsSync(daemonEntry)) {
548
- throw new Error(`Daemon bundle not found at ${daemonEntry}. Run npm run build first.`);
631
+ let startLockHeld = false;
632
+ let lockState = tryAcquireBridgeStartLock();
633
+ if (!lockState.acquired) {
634
+ const status = await waitForBridgeStartupTurn();
635
+ if (status.running) return status;
636
+ lockState = tryAcquireBridgeStartLock();
637
+ if (!lockState.acquired) {
638
+ throw new Error(
639
+ describeBridgeActivationFailure(status, config.channels) || `\u53E6\u4E00\u4E2A\u6865\u63A5\u670D\u52A1\u542F\u52A8\u8BF7\u6C42\u4ECD\u5728\u8FDB\u884C\u4E2D\uFF08PID: ${lockState.holderPid || "unknown"}\uFF09\u3002\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002`
640
+ );
641
+ }
549
642
  }
550
- const stdoutFd = fs2.openSync(path2.join(logsDir, "bridge-launcher.out.log"), "a");
551
- const stderrFd = fs2.openSync(path2.join(logsDir, "bridge-launcher.err.log"), "a");
552
- const child = spawn(process.execPath, [daemonEntry], {
553
- cwd: packageRoot,
554
- detached: true,
555
- env: buildDaemonEnv(),
556
- stdio: ["ignore", stdoutFd, stderrFd],
557
- ...WINDOWS_HIDE
558
- });
559
- child.unref();
560
- const status = await waitForBridgeRunning();
561
- if (!status.running) {
562
- throw new Error(
563
- describeBridgeActivationFailure(status, config.channels) || "Bridge failed to report running=true."
564
- );
643
+ startLockHeld = true;
644
+ try {
645
+ const currentAfterLock = getBridgeStatus();
646
+ const extraAlivePidsAfterLock = getTrackedBridgePids(currentAfterLock).filter((pid) => pid !== currentAfterLock.pid && isProcessAlive(pid));
647
+ if (currentAfterLock.running && extraAlivePidsAfterLock.length === 0) return currentAfterLock;
648
+ if (currentAfterLock.running && extraAlivePidsAfterLock.length > 0) {
649
+ await stopBridge();
650
+ }
651
+ const daemonEntry = path2.join(packageRoot, "dist", "daemon.mjs");
652
+ if (!fs2.existsSync(daemonEntry)) {
653
+ throw new Error(`Daemon bundle not found at ${daemonEntry}. Run npm run build first.`);
654
+ }
655
+ const stdoutFd = fs2.openSync(path2.join(logsDir, "bridge-launcher.out.log"), "a");
656
+ const stderrFd = fs2.openSync(path2.join(logsDir, "bridge-launcher.err.log"), "a");
657
+ const child = spawn(process.execPath, [daemonEntry], {
658
+ cwd: packageRoot,
659
+ detached: true,
660
+ env: buildDaemonEnv(),
661
+ stdio: ["ignore", stdoutFd, stderrFd],
662
+ ...WINDOWS_HIDE
663
+ });
664
+ child.unref();
665
+ const status = await waitForBridgeRunning();
666
+ if (!status.running) {
667
+ throw new Error(
668
+ describeBridgeActivationFailure(status, config.channels) || "Bridge failed to report running=true."
669
+ );
670
+ }
671
+ return status;
672
+ } finally {
673
+ if (startLockHeld) {
674
+ releaseBridgeStartLock();
675
+ }
565
676
  }
566
- return status;
567
677
  }
568
678
  async function stopBridge() {
569
679
  const status = readJsonFile(bridgeStatusFile, { running: false });