codex-to-im 1.0.31 → 1.0.32

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
@@ -228,12 +228,14 @@ var runtimeDir = path2.join(CTI_HOME, "runtime");
228
228
  var logsDir = path2.join(CTI_HOME, "logs");
229
229
  var bridgePidFile = path2.join(runtimeDir, "bridge.pid");
230
230
  var bridgeStatusFile = path2.join(runtimeDir, "status.json");
231
+ var bridgeStartLockFile = path2.join(runtimeDir, "bridge.start.lock");
231
232
  var uiStatusFile = path2.join(runtimeDir, "ui-server.json");
232
233
  var uiPort = 4781;
233
234
  var bridgeAutostartTaskName = "CodexToIMBridge";
234
235
  var bridgeAutostartLauncherFile = path2.join(runtimeDir, "bridge-autostart.ps1");
235
236
  var npmUninstallLogFile = path2.join(runtimeDir, "npm-uninstall.log");
236
237
  var WINDOWS_HIDE = process.platform === "win32" ? { windowsHide: true } : {};
238
+ var BRIDGE_START_LOCK_STALE_MS = 3e4;
237
239
  function ensureDirs() {
238
240
  fs2.mkdirSync(runtimeDir, { recursive: true });
239
241
  fs2.mkdirSync(logsDir, { recursive: true });
@@ -287,6 +289,70 @@ function clearBridgePidFile() {
287
289
  } catch {
288
290
  }
289
291
  }
292
+ function readBridgeStartLock(filePath = bridgeStartLockFile) {
293
+ const parsed = readJsonFile(filePath, null);
294
+ const pid = Number(parsed?.pid);
295
+ const createdAt = typeof parsed?.createdAt === "string" ? parsed.createdAt : "";
296
+ if (!Number.isFinite(pid) || pid <= 0 || !createdAt) return null;
297
+ return { pid, createdAt };
298
+ }
299
+ function isBridgeStartLockStale(lock, options = {}) {
300
+ if (!lock) return true;
301
+ const nowMs = options.nowMs ?? Date.now();
302
+ const staleMs = options.staleMs ?? BRIDGE_START_LOCK_STALE_MS;
303
+ const isAlive = options.isAlive ?? isProcessAlive;
304
+ const createdAtMs = Date.parse(lock.createdAt);
305
+ if (!Number.isFinite(createdAtMs)) return true;
306
+ if (!isAlive(lock.pid)) return true;
307
+ return nowMs - createdAtMs > staleMs;
308
+ }
309
+ function tryAcquireBridgeStartLock(options = {}) {
310
+ const filePath = options.filePath ?? bridgeStartLockFile;
311
+ const ownerPid = options.ownerPid ?? process.pid;
312
+ const nowMs = options.nowMs ?? Date.now();
313
+ const payload = {
314
+ pid: ownerPid,
315
+ createdAt: new Date(nowMs).toISOString()
316
+ };
317
+ for (let attempt = 0; attempt < 2; attempt += 1) {
318
+ try {
319
+ fs2.writeFileSync(filePath, JSON.stringify(payload, null, 2), { encoding: "utf-8", flag: "wx" });
320
+ return { acquired: true };
321
+ } catch (error) {
322
+ const code = error.code;
323
+ if (code !== "EEXIST") throw error;
324
+ const existing2 = readBridgeStartLock(filePath);
325
+ if (!isBridgeStartLockStale(existing2, {
326
+ nowMs,
327
+ staleMs: options.staleMs,
328
+ isAlive: options.isAlive
329
+ })) {
330
+ return { acquired: false, holderPid: existing2?.pid };
331
+ }
332
+ try {
333
+ fs2.unlinkSync(filePath);
334
+ } catch {
335
+ }
336
+ }
337
+ }
338
+ const existing = readBridgeStartLock(filePath);
339
+ return { acquired: false, holderPid: existing?.pid };
340
+ }
341
+ function releaseBridgeStartLock(filePath = bridgeStartLockFile, ownerPid = process.pid) {
342
+ const existing = readBridgeStartLock(filePath);
343
+ if (!existing) {
344
+ try {
345
+ fs2.unlinkSync(filePath);
346
+ } catch {
347
+ }
348
+ return;
349
+ }
350
+ if (existing.pid !== ownerPid) return;
351
+ try {
352
+ fs2.unlinkSync(filePath);
353
+ } catch {
354
+ }
355
+ }
290
356
  function sleep(ms) {
291
357
  return new Promise((resolve) => setTimeout(resolve, ms));
292
358
  }
@@ -515,6 +581,21 @@ async function waitForBridgeRunning(timeoutMs = 2e4) {
515
581
  }
516
582
  return getBridgeStatus();
517
583
  }
584
+ async function waitForBridgeStartupTurn(timeoutMs = 2e4) {
585
+ const startedAt = Date.now();
586
+ while (Date.now() - startedAt < timeoutMs) {
587
+ const status = getBridgeStatus();
588
+ if (status.running) return status;
589
+ const lock = readBridgeStartLock();
590
+ if (!lock) return status;
591
+ if (isBridgeStartLockStale(lock)) {
592
+ releaseBridgeStartLock();
593
+ return getBridgeStatus();
594
+ }
595
+ await sleep(300);
596
+ }
597
+ return getBridgeStatus();
598
+ }
518
599
  async function waitForUiServer(timeoutMs = 15e3) {
519
600
  const startedAt = Date.now();
520
601
  while (Date.now() - startedAt < timeoutMs) {
@@ -543,27 +624,52 @@ async function startBridge() {
543
624
  if (preflightFailure) {
544
625
  throw new Error(preflightFailure);
545
626
  }
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.`);
627
+ let startLockHeld = false;
628
+ let lockState = tryAcquireBridgeStartLock();
629
+ if (!lockState.acquired) {
630
+ const status = await waitForBridgeStartupTurn();
631
+ if (status.running) return status;
632
+ lockState = tryAcquireBridgeStartLock();
633
+ if (!lockState.acquired) {
634
+ throw new Error(
635
+ 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`
636
+ );
637
+ }
549
638
  }
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
- );
639
+ startLockHeld = true;
640
+ try {
641
+ const currentAfterLock = getBridgeStatus();
642
+ const extraAlivePidsAfterLock = getTrackedBridgePids(currentAfterLock).filter((pid) => pid !== currentAfterLock.pid && isProcessAlive(pid));
643
+ if (currentAfterLock.running && extraAlivePidsAfterLock.length === 0) return currentAfterLock;
644
+ if (currentAfterLock.running && extraAlivePidsAfterLock.length > 0) {
645
+ await stopBridge();
646
+ }
647
+ const daemonEntry = path2.join(packageRoot, "dist", "daemon.mjs");
648
+ if (!fs2.existsSync(daemonEntry)) {
649
+ throw new Error(`Daemon bundle not found at ${daemonEntry}. Run npm run build first.`);
650
+ }
651
+ const stdoutFd = fs2.openSync(path2.join(logsDir, "bridge-launcher.out.log"), "a");
652
+ const stderrFd = fs2.openSync(path2.join(logsDir, "bridge-launcher.err.log"), "a");
653
+ const child = spawn(process.execPath, [daemonEntry], {
654
+ cwd: packageRoot,
655
+ detached: true,
656
+ env: buildDaemonEnv(),
657
+ stdio: ["ignore", stdoutFd, stderrFd],
658
+ ...WINDOWS_HIDE
659
+ });
660
+ child.unref();
661
+ const status = await waitForBridgeRunning();
662
+ if (!status.running) {
663
+ throw new Error(
664
+ describeBridgeActivationFailure(status, config.channels) || "Bridge failed to report running=true."
665
+ );
666
+ }
667
+ return status;
668
+ } finally {
669
+ if (startLockHeld) {
670
+ releaseBridgeStartLock();
671
+ }
565
672
  }
566
- return status;
567
673
  }
568
674
  async function stopBridge() {
569
675
  const status = readJsonFile(bridgeStatusFile, { running: false });
@@ -5611,12 +5611,14 @@ var runtimeDir = path4.join(CTI_HOME, "runtime");
5611
5611
  var logsDir = path4.join(CTI_HOME, "logs");
5612
5612
  var bridgePidFile = path4.join(runtimeDir, "bridge.pid");
5613
5613
  var bridgeStatusFile = path4.join(runtimeDir, "status.json");
5614
+ var bridgeStartLockFile = path4.join(runtimeDir, "bridge.start.lock");
5614
5615
  var uiStatusFile = path4.join(runtimeDir, "ui-server.json");
5615
5616
  var uiPort = 4781;
5616
5617
  var bridgeAutostartTaskName = "CodexToIMBridge";
5617
5618
  var bridgeAutostartLauncherFile = path4.join(runtimeDir, "bridge-autostart.ps1");
5618
5619
  var npmUninstallLogFile = path4.join(runtimeDir, "npm-uninstall.log");
5619
5620
  var WINDOWS_HIDE = process.platform === "win32" ? { windowsHide: true } : {};
5621
+ var BRIDGE_START_LOCK_STALE_MS = 3e4;
5620
5622
  function ensureDirs() {
5621
5623
  fs3.mkdirSync(runtimeDir, { recursive: true });
5622
5624
  fs3.mkdirSync(logsDir, { recursive: true });
@@ -5670,6 +5672,70 @@ function clearBridgePidFile() {
5670
5672
  } catch {
5671
5673
  }
5672
5674
  }
5675
+ function readBridgeStartLock(filePath = bridgeStartLockFile) {
5676
+ const parsed = readJsonFile(filePath, null);
5677
+ const pid = Number(parsed?.pid);
5678
+ const createdAt = typeof parsed?.createdAt === "string" ? parsed.createdAt : "";
5679
+ if (!Number.isFinite(pid) || pid <= 0 || !createdAt) return null;
5680
+ return { pid, createdAt };
5681
+ }
5682
+ function isBridgeStartLockStale(lock, options = {}) {
5683
+ if (!lock) return true;
5684
+ const nowMs = options.nowMs ?? Date.now();
5685
+ const staleMs = options.staleMs ?? BRIDGE_START_LOCK_STALE_MS;
5686
+ const isAlive = options.isAlive ?? isProcessAlive;
5687
+ const createdAtMs = Date.parse(lock.createdAt);
5688
+ if (!Number.isFinite(createdAtMs)) return true;
5689
+ if (!isAlive(lock.pid)) return true;
5690
+ return nowMs - createdAtMs > staleMs;
5691
+ }
5692
+ function tryAcquireBridgeStartLock(options = {}) {
5693
+ const filePath = options.filePath ?? bridgeStartLockFile;
5694
+ const ownerPid = options.ownerPid ?? process.pid;
5695
+ const nowMs = options.nowMs ?? Date.now();
5696
+ const payload = {
5697
+ pid: ownerPid,
5698
+ createdAt: new Date(nowMs).toISOString()
5699
+ };
5700
+ for (let attempt = 0; attempt < 2; attempt += 1) {
5701
+ try {
5702
+ fs3.writeFileSync(filePath, JSON.stringify(payload, null, 2), { encoding: "utf-8", flag: "wx" });
5703
+ return { acquired: true };
5704
+ } catch (error) {
5705
+ const code = error.code;
5706
+ if (code !== "EEXIST") throw error;
5707
+ const existing2 = readBridgeStartLock(filePath);
5708
+ if (!isBridgeStartLockStale(existing2, {
5709
+ nowMs,
5710
+ staleMs: options.staleMs,
5711
+ isAlive: options.isAlive
5712
+ })) {
5713
+ return { acquired: false, holderPid: existing2?.pid };
5714
+ }
5715
+ try {
5716
+ fs3.unlinkSync(filePath);
5717
+ } catch {
5718
+ }
5719
+ }
5720
+ }
5721
+ const existing = readBridgeStartLock(filePath);
5722
+ return { acquired: false, holderPid: existing?.pid };
5723
+ }
5724
+ function releaseBridgeStartLock(filePath = bridgeStartLockFile, ownerPid = process.pid) {
5725
+ const existing = readBridgeStartLock(filePath);
5726
+ if (!existing) {
5727
+ try {
5728
+ fs3.unlinkSync(filePath);
5729
+ } catch {
5730
+ }
5731
+ return;
5732
+ }
5733
+ if (existing.pid !== ownerPid) return;
5734
+ try {
5735
+ fs3.unlinkSync(filePath);
5736
+ } catch {
5737
+ }
5738
+ }
5673
5739
  function sleep(ms) {
5674
5740
  return new Promise((resolve) => setTimeout(resolve, ms));
5675
5741
  }
@@ -5792,6 +5858,21 @@ async function waitForBridgeRunning(timeoutMs = 2e4) {
5792
5858
  }
5793
5859
  return getBridgeStatus();
5794
5860
  }
5861
+ async function waitForBridgeStartupTurn(timeoutMs = 2e4) {
5862
+ const startedAt = Date.now();
5863
+ while (Date.now() - startedAt < timeoutMs) {
5864
+ const status = getBridgeStatus();
5865
+ if (status.running) return status;
5866
+ const lock = readBridgeStartLock();
5867
+ if (!lock) return status;
5868
+ if (isBridgeStartLockStale(lock)) {
5869
+ releaseBridgeStartLock();
5870
+ return getBridgeStatus();
5871
+ }
5872
+ await sleep(300);
5873
+ }
5874
+ return getBridgeStatus();
5875
+ }
5795
5876
  async function startBridge() {
5796
5877
  ensureDirs();
5797
5878
  const current = getBridgeStatus();
@@ -5805,27 +5886,52 @@ async function startBridge() {
5805
5886
  if (preflightFailure) {
5806
5887
  throw new Error(preflightFailure);
5807
5888
  }
5808
- const daemonEntry = path4.join(packageRoot, "dist", "daemon.mjs");
5809
- if (!fs3.existsSync(daemonEntry)) {
5810
- throw new Error(`Daemon bundle not found at ${daemonEntry}. Run npm run build first.`);
5811
- }
5812
- const stdoutFd = fs3.openSync(path4.join(logsDir, "bridge-launcher.out.log"), "a");
5813
- const stderrFd = fs3.openSync(path4.join(logsDir, "bridge-launcher.err.log"), "a");
5814
- const child = spawn(process.execPath, [daemonEntry], {
5815
- cwd: packageRoot,
5816
- detached: true,
5817
- env: buildDaemonEnv(),
5818
- stdio: ["ignore", stdoutFd, stderrFd],
5819
- ...WINDOWS_HIDE
5820
- });
5821
- child.unref();
5822
- const status = await waitForBridgeRunning();
5823
- if (!status.running) {
5824
- throw new Error(
5825
- describeBridgeActivationFailure(status, config.channels) || "Bridge failed to report running=true."
5826
- );
5889
+ let startLockHeld = false;
5890
+ let lockState = tryAcquireBridgeStartLock();
5891
+ if (!lockState.acquired) {
5892
+ const status = await waitForBridgeStartupTurn();
5893
+ if (status.running) return status;
5894
+ lockState = tryAcquireBridgeStartLock();
5895
+ if (!lockState.acquired) {
5896
+ throw new Error(
5897
+ 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`
5898
+ );
5899
+ }
5900
+ }
5901
+ startLockHeld = true;
5902
+ try {
5903
+ const currentAfterLock = getBridgeStatus();
5904
+ const extraAlivePidsAfterLock = getTrackedBridgePids(currentAfterLock).filter((pid) => pid !== currentAfterLock.pid && isProcessAlive(pid));
5905
+ if (currentAfterLock.running && extraAlivePidsAfterLock.length === 0) return currentAfterLock;
5906
+ if (currentAfterLock.running && extraAlivePidsAfterLock.length > 0) {
5907
+ await stopBridge();
5908
+ }
5909
+ const daemonEntry = path4.join(packageRoot, "dist", "daemon.mjs");
5910
+ if (!fs3.existsSync(daemonEntry)) {
5911
+ throw new Error(`Daemon bundle not found at ${daemonEntry}. Run npm run build first.`);
5912
+ }
5913
+ const stdoutFd = fs3.openSync(path4.join(logsDir, "bridge-launcher.out.log"), "a");
5914
+ const stderrFd = fs3.openSync(path4.join(logsDir, "bridge-launcher.err.log"), "a");
5915
+ const child = spawn(process.execPath, [daemonEntry], {
5916
+ cwd: packageRoot,
5917
+ detached: true,
5918
+ env: buildDaemonEnv(),
5919
+ stdio: ["ignore", stdoutFd, stderrFd],
5920
+ ...WINDOWS_HIDE
5921
+ });
5922
+ child.unref();
5923
+ const status = await waitForBridgeRunning();
5924
+ if (!status.running) {
5925
+ throw new Error(
5926
+ describeBridgeActivationFailure(status, config.channels) || "Bridge failed to report running=true."
5927
+ );
5928
+ }
5929
+ return status;
5930
+ } finally {
5931
+ if (startLockHeld) {
5932
+ releaseBridgeStartLock();
5933
+ }
5827
5934
  }
5828
- return status;
5829
5935
  }
5830
5936
  async function stopBridge() {
5831
5937
  const status = readJsonFile(bridgeStatusFile, { running: false });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-to-im",
3
- "version": "1.0.31",
3
+ "version": "1.0.32",
4
4
  "description": "Installable Codex-to-IM bridge with local setup UI and background service",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/zhangle1987/codex-to-im#readme",