@duanluan/codex-plus-plus-launcher 1.2.14 → 1.2.18

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.
@@ -1 +1,3 @@
1
- __version__ = "1.2.14"
1
+ __version__ = "1.2.18"
2
+
3
+
@@ -14,6 +14,8 @@ from codex_plus_plus_launcher.runtime import (
14
14
  legacy_auto_inject_state,
15
15
  load_install_state,
16
16
  runtime_paths,
17
+ shortcut_sidecar_install_root,
18
+ shortcut_sidecar_version,
17
19
  )
18
20
 
19
21
 
@@ -31,15 +33,34 @@ def find_codex_binary() -> str | None:
31
33
  return None
32
34
 
33
35
 
36
+ def _sidecar_drift_value(installed: str | None, bundled: str | None) -> str:
37
+ """Compute the sidecar_drift status surfaced by doctor.
38
+
39
+ - "none": both versions are known and equal
40
+ - "mismatch:installed=X,bundled=Y": both known but different
41
+ - "unknown": either version is missing (older install / preflight running
42
+ from source tree without bundled sidecar)
43
+ """
44
+ if not installed or not bundled:
45
+ return "unknown"
46
+ if installed == bundled:
47
+ return "none"
48
+ return f"mismatch:installed={installed},bundled={bundled}"
49
+
50
+
34
51
  def doctor_report(paths: RuntimePaths | None = None) -> dict[str, str]:
35
52
  resolved_paths = paths or runtime_paths()
36
53
  installation = detect_codex_installation()
37
54
  state = load_install_state(resolved_paths)
38
55
 
56
+ bundled_version = bundled_upstream_version()
57
+ installed_sidecar_version = shortcut_sidecar_version()
58
+ sidecar_root = shortcut_sidecar_install_root()
59
+
39
60
  report = {
40
61
  "platform": sys.platform,
41
62
  "package_version": installed_package_version(),
42
- "bundled_upstream_version": bundled_upstream_version() or "missing",
63
+ "bundled_upstream_version": bundled_version or "missing",
43
64
  "global_command_version": global_command_version() or "missing",
44
65
  "host_python": find_host_python() or "missing",
45
66
  "codex_binary": find_codex_binary() or "missing",
@@ -50,6 +71,9 @@ def doctor_report(paths: RuntimePaths | None = None) -> dict[str, str]:
50
71
  "shortcut_state": str(state.get("shortcut_state") or "missing"),
51
72
  "start_menu_state": str(state.get("start_menu_state") or "missing"),
52
73
  "legacy_auto_inject": str(state.get("legacy_auto_inject_state") or legacy_auto_inject_state()),
74
+ "shortcut_sidecar_root": str(sidecar_root) if sidecar_root is not None else "missing",
75
+ "shortcut_sidecar_version": installed_sidecar_version or "unknown",
76
+ "sidecar_drift": _sidecar_drift_value(installed_sidecar_version, bundled_version),
53
77
  }
54
78
  if sys.platform == "win32":
55
79
  report["windows_app_dir"] = find_windows_codex_app_dir() or "missing"
@@ -590,6 +590,112 @@ def binary_version_from_path(path: Path | None) -> str | None:
590
590
  return None
591
591
 
592
592
 
593
+ SIDECAR_VERSION_STAMP_NAME = ".codexpp-sidecar-version"
594
+
595
+
596
+ def shortcut_sidecar_install_root() -> Path | None:
597
+ """Return the directory where the npm postinstall copied the sidecar.
598
+
599
+ Mirrors `installSidecarRoot` in npm/launcher.js. Used by doctor() to read
600
+ the stamp file written alongside the sidecar binary.
601
+ """
602
+ if sys.platform == "win32":
603
+ local = os.environ.get("LOCALAPPDATA")
604
+ if not local:
605
+ return None
606
+ return Path(local) / "Programs" / "Codex++"
607
+ if sys.platform == "darwin":
608
+ primary = Path("/Applications")
609
+ if (primary / SIDECAR_VERSION_STAMP_NAME).is_file() or (primary / "codex-plus-plus").is_file():
610
+ return primary
611
+ fallback = Path.home() / "Applications"
612
+ if (fallback / SIDECAR_VERSION_STAMP_NAME).is_file() or (fallback / "codex-plus-plus").is_file():
613
+ return fallback
614
+ return primary
615
+ xdg = os.environ.get("XDG_DATA_HOME") or str(Path.home() / ".local" / "share")
616
+ return Path(xdg) / "Codex++"
617
+
618
+
619
+ def shortcut_sidecar_binary(root: Path | None = None) -> Path | None:
620
+ base = root if root is not None else shortcut_sidecar_install_root()
621
+ if base is None:
622
+ return None
623
+ name = "codex-plus-plus.exe" if sys.platform == "win32" else "codex-plus-plus"
624
+ candidate = base / name
625
+ return candidate if candidate.is_file() else None
626
+
627
+
628
+ def _read_sidecar_version_stamp(root: Path) -> str | None:
629
+ stamp = root / SIDECAR_VERSION_STAMP_NAME
630
+ try:
631
+ text = stamp.read_text(encoding="utf-8")
632
+ except (OSError, UnicodeDecodeError):
633
+ return None
634
+ value = text.strip()
635
+ return value or None
636
+
637
+
638
+ def _read_pe_file_version(path: Path) -> str | None:
639
+ """Windows-only fallback: read FileVersion from the PE VS_FIXEDFILEINFO block.
640
+
641
+ Used when an older npm install root predates the stamp file. No new
642
+ dependencies — calls into version.dll via ctypes.
643
+ """
644
+ if sys.platform != "win32":
645
+ return None
646
+ try:
647
+ import ctypes
648
+ from ctypes import wintypes
649
+ except Exception:
650
+ return None
651
+ try:
652
+ version_dll = ctypes.WinDLL("version", use_last_error=True)
653
+ except OSError:
654
+ return None
655
+ target = str(path)
656
+ try:
657
+ size = version_dll.GetFileVersionInfoSizeW(target, None)
658
+ if not size:
659
+ return None
660
+ buffer = ctypes.create_string_buffer(size)
661
+ if not version_dll.GetFileVersionInfoW(target, 0, size, buffer):
662
+ return None
663
+ block_ptr = ctypes.c_void_p()
664
+ block_size = wintypes.UINT()
665
+ if not version_dll.VerQueryValueW(buffer, "\\", ctypes.byref(block_ptr), ctypes.byref(block_size)):
666
+ return None
667
+ if block_size.value < 16:
668
+ return None
669
+ raw = ctypes.string_at(block_ptr, block_size.value)
670
+ ms = int.from_bytes(raw[8:12], "little")
671
+ ls = int.from_bytes(raw[12:16], "little")
672
+ except Exception:
673
+ return None
674
+ parts = [ms >> 16, ms & 0xFFFF, ls >> 16, ls & 0xFFFF]
675
+ while len(parts) > 1 and parts[-1] == 0:
676
+ parts.pop()
677
+ return ".".join(str(part) for part in parts) or None
678
+
679
+
680
+ def shortcut_sidecar_version() -> str | None:
681
+ """Return the version of the installed sidecar copy, or None when unknown.
682
+
683
+ Prefers a stamp file written by npm postinstall (cross-platform). Falls
684
+ back to the PE VersionInfo on Windows for installs that predate the stamp.
685
+ """
686
+ root = shortcut_sidecar_install_root()
687
+ if root is None:
688
+ return None
689
+ stamp_version = _read_sidecar_version_stamp(root)
690
+ if stamp_version:
691
+ return stamp_version
692
+ if sys.platform == "win32":
693
+ binary = shortcut_sidecar_binary(root)
694
+ if binary is not None:
695
+ return _read_pe_file_version(binary)
696
+ return None
697
+
698
+
593
699
  def global_binary_candidate_dirs(current_binary: Path | None = None) -> list[Path]:
594
700
  candidate_dirs: list[Path] = []
595
701
  if current_binary is not None:
@@ -13,8 +13,30 @@ function packageVersion(root = packageRoot()) {
13
13
  return JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version;
14
14
  }
15
15
 
16
+ function upstreamRefFromMetadata(root = packageRoot()) {
17
+ for (const relativePath of [
18
+ path.join('upstream-bin', 'upstream-release.json'),
19
+ path.join('.github', 'upstream-sync', 'latest.json'),
20
+ ]) {
21
+ const metadataPath = path.join(root, relativePath);
22
+ if (!fs.existsSync(metadataPath)) {
23
+ continue;
24
+ }
25
+ try {
26
+ const payload = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
27
+ const ref = payload.ref || payload.version || payload.tag || payload.upstream_version;
28
+ if (ref) {
29
+ return String(ref);
30
+ }
31
+ } catch (_error) {
32
+ // Ignore malformed local metadata and fall back to the package version.
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+
16
38
  function defaultUpstreamRef(root = packageRoot()) {
17
- return `v${packageVersion(root)}`;
39
+ return upstreamRefFromMetadata(root) || `v${packageVersion(root)}`;
18
40
  }
19
41
 
20
42
  function run(command, args, options = {}) {
@@ -125,4 +147,4 @@ if (require.main === module) {
125
147
  }
126
148
  }
127
149
 
128
- module.exports = { applyPluginUnlockPatch, buildTargetArgs, cargoTargetForKey, defaultUpstreamRef, main, packageBuildArgs, platformKey };
150
+ module.exports = { applyPluginUnlockPatch, buildTargetArgs, cargoTargetForKey, defaultUpstreamRef, main, packageBuildArgs, platformKey, upstreamRefFromMetadata };
package/npm/i18n.js CHANGED
@@ -18,6 +18,9 @@ const messages = {
18
18
  shortcutInstallFailed: '创建 Codex++ 快捷方式失败',
19
19
  retryingMirror: '直连失败,正在尝试 GitHub 镜像',
20
20
  retryingForceReinstall: '检测到损坏的 pip 安装记录,正在尝试跳过卸载直接覆盖安装',
21
+ sidecarSelfHealOk: 'Codex++: 已自动更新本地 sidecar',
22
+ sidecarSelfHealLocked: 'Codex++: 检测到本地 sidecar 与 npm 版本不一致,但当前正在运行;本次仍使用旧版启动。请退出 Codex 后重新执行 cxpp launch 完成升级。',
23
+ sidecarSelfHealFailed: 'Codex++: sidecar 自愈失败,仍使用旧版启动',
21
24
  },
22
25
  en: {
23
26
  missingPython: 'missing command: python3, python, or py',
@@ -38,6 +41,9 @@ const messages = {
38
41
  shortcutInstallFailed: 'failed to create Codex++ shortcuts',
39
42
  retryingMirror: 'direct GitHub download failed, retrying with mirrors',
40
43
  retryingForceReinstall: 'detected a broken pip installation record, retrying without uninstall',
44
+ sidecarSelfHealOk: 'Codex++: refreshed the local sidecar to match the npm package',
45
+ sidecarSelfHealLocked: 'Codex++: the local sidecar is out of date but is currently running; using the old version for this launch. Quit Codex and re-run cxpp launch to finish the upgrade.',
46
+ sidecarSelfHealFailed: 'Codex++: sidecar self-heal failed, falling back to the old version',
41
47
  },
42
48
  };
43
49
 
package/npm/launcher.js CHANGED
@@ -8,6 +8,7 @@ const { t } = require('./i18n.js');
8
8
  const SUPPORTED_PLATFORMS = new Set(['win32-x64', 'darwin-x64', 'darwin-arm64', 'linux-x64']);
9
9
  const SILENT_BINARY = 'codex-plus-plus';
10
10
  const MANAGER_BINARY = 'codex-plus-plus-manager';
11
+ const SIDECAR_VERSION_STAMP = '.codexpp-sidecar-version';
11
12
  const SILENT_NAME = 'Codex++';
12
13
  const MANAGER_NAME = 'Codex++ 管理工具';
13
14
  const LINUX_SHIM_DIR_NAME = 'codex-desktop-linux-shim';
@@ -15,6 +16,8 @@ const LINUX_SHIM_BINARY = 'codex.exe';
15
16
  const LINUX_DESKTOP_ENTRY = 'codex-plus-plus.desktop';
16
17
  const PLUGIN_AUTH_UNLOCK_FILE = 'plugin-auth-unlocked.js';
17
18
  const PLUGIN_AUTH_UNLOCK_CONTENT = 'function e(e){return false}export{e as t};\n';
19
+ const WINDOWS_LAUNCH_SCRIPT = 'launch-codexpp.vbs';
20
+ const CODEX_PLUS_MENU_SELECTOR = '#codex-plus-menu, [data-codex-plus-menu="true"]';
18
21
 
19
22
  function optionValue(options, key, fallback) {
20
23
  const value = options[key];
@@ -303,6 +306,532 @@ function terminateProcesses(processes, options = {}) {
303
306
  stdio: 'ignore',
304
307
  windowsHide: true,
305
308
  });
309
+ const taskkillArgs = ['/F'];
310
+ for (const id of ids) {
311
+ taskkillArgs.push('/PID', String(id));
312
+ }
313
+ run('taskkill.exe', taskkillArgs, {
314
+ encoding: 'utf8',
315
+ env,
316
+ stdio: 'ignore',
317
+ windowsHide: true,
318
+ });
319
+ }
320
+
321
+ function terminateSidecarsByImageName(options = {}) {
322
+ const platform = optionValue(options, 'platform', process.platform);
323
+ if (platform !== 'win32') {
324
+ return false;
325
+ }
326
+ if (typeof options.terminateSidecarsByImageName === 'function') {
327
+ return Boolean(options.terminateSidecarsByImageName(options));
328
+ }
329
+ const run = options.spawnSync || spawnSync;
330
+ const result = run('taskkill.exe', ['/F', '/IM', 'codex-plus-plus.exe'], {
331
+ encoding: 'utf8',
332
+ env: options.env || process.env,
333
+ stdio: 'ignore',
334
+ windowsHide: true,
335
+ });
336
+ return Boolean(result && result.status === 0);
337
+ }
338
+
339
+ function findRunningCodexProcesses(options = {}) {
340
+ const platform = optionValue(options, 'platform', process.platform);
341
+ if (platform !== 'win32') {
342
+ return [];
343
+ }
344
+ if (typeof options.findRunningCodexProcesses === 'function') {
345
+ return options.findRunningCodexProcesses(options) || [];
346
+ }
347
+
348
+ const run = options.processSpawnSync || (options.spawnSync ? null : spawnSync);
349
+ if (!run) {
350
+ return [];
351
+ }
352
+ const script = [
353
+ '$ErrorActionPreference = "SilentlyContinue"',
354
+ 'Get-CimInstance Win32_Process | Where-Object {',
355
+ ' $_.Name -ieq "Codex.exe"',
356
+ '} | Select-Object ProcessId,Name,ExecutablePath,CommandLine | ConvertTo-Json -Compress',
357
+ ].join('\n');
358
+ const result = run('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script], {
359
+ encoding: 'utf8',
360
+ windowsHide: true,
361
+ });
362
+ if (!result || result.status !== 0) {
363
+ return [];
364
+ }
365
+ try {
366
+ return parsePowerShellJson(result.stdout);
367
+ } catch (_error) {
368
+ return [];
369
+ }
370
+ }
371
+
372
+ function findRunningSidecarProcesses(options = {}) {
373
+ const platform = optionValue(options, 'platform', process.platform);
374
+ if (platform !== 'win32') {
375
+ return [];
376
+ }
377
+ if (typeof options.findRunningSidecarProcesses === 'function') {
378
+ return options.findRunningSidecarProcesses(options) || [];
379
+ }
380
+
381
+ const run = options.processSpawnSync || (options.spawnSync ? null : spawnSync);
382
+ if (!run) {
383
+ return [];
384
+ }
385
+ const script = [
386
+ '$ErrorActionPreference = "SilentlyContinue"',
387
+ 'Get-CimInstance Win32_Process | Where-Object {',
388
+ ' $_.Name -ieq "codex-plus-plus.exe"',
389
+ '} | Select-Object ProcessId,Name,ExecutablePath,CommandLine | ConvertTo-Json -Compress',
390
+ ].join('\n');
391
+ const result = run('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script], {
392
+ encoding: 'utf8',
393
+ windowsHide: true,
394
+ });
395
+ if (!result || result.status !== 0) {
396
+ return [];
397
+ }
398
+ try {
399
+ return parsePowerShellJson(result.stdout);
400
+ } catch (_error) {
401
+ return [];
402
+ }
403
+ }
404
+
405
+ function cdpTargetsAvailable(debugPort, options = {}) {
406
+ if (typeof options.cdpTargetsAvailable === 'function') {
407
+ return options.cdpTargetsAvailable(debugPort, options);
408
+ }
409
+ const run = options.processSpawnSync || (options.spawnSync ? null : spawnSync);
410
+ if (!run) {
411
+ return true;
412
+ }
413
+ const env = { ...(options.env || process.env), CODEXPP_DEBUG_PORT: String(debugPort) };
414
+ const script = [
415
+ '$ErrorActionPreference = "SilentlyContinue"',
416
+ '$port = [int]$env:CODEXPP_DEBUG_PORT',
417
+ 'foreach ($hostName in @("127.0.0.1", "[::1]")) {',
418
+ ' try {',
419
+ ' $response = Invoke-WebRequest -UseBasicParsing -Uri "http://$hostName`:$port/json" -TimeoutSec 2',
420
+ ' if ($response.StatusCode -eq 200 -and $response.Content -match "webSocketDebuggerUrl") { exit 0 }',
421
+ ' } catch {}',
422
+ '}',
423
+ 'exit 1',
424
+ ].join('\n');
425
+ const result = run('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script], {
426
+ encoding: 'utf8',
427
+ env,
428
+ windowsHide: true,
429
+ });
430
+ return Boolean(result && result.status === 0);
431
+ }
432
+
433
+ function codexPlusUiInjectionProbeScript() {
434
+ return String.raw`
435
+ const http = require('node:http');
436
+ const net = require('node:net');
437
+ const crypto = require('node:crypto');
438
+
439
+ const debugPort = Number(process.env.CODEXPP_DEBUG_PORT || '0');
440
+ const selector = process.env.CODEXPP_MENU_SELECTOR || '#codex-plus-menu, [data-codex-plus-menu="true"]';
441
+
442
+ function exitSoon(code) {
443
+ try {
444
+ process.exitCode = code;
445
+ } finally {
446
+ process.exit(code);
447
+ }
448
+ }
449
+
450
+ function readJson(pathname) {
451
+ return new Promise((resolve, reject) => {
452
+ const request = http.get({
453
+ host: '127.0.0.1',
454
+ port: debugPort,
455
+ path: pathname,
456
+ timeout: 1500,
457
+ }, (response) => {
458
+ const chunks = [];
459
+ response.setEncoding('utf8');
460
+ response.on('data', (chunk) => chunks.push(chunk));
461
+ response.on('end', () => {
462
+ try {
463
+ resolve(JSON.parse(chunks.join('')));
464
+ } catch (error) {
465
+ reject(error);
466
+ }
467
+ });
468
+ });
469
+ request.on('timeout', () => request.destroy(new Error('timeout')));
470
+ request.on('error', reject);
471
+ });
472
+ }
473
+
474
+ function parseWebSocketTarget(webSocketDebuggerUrl) {
475
+ const parsed = new URL(webSocketDebuggerUrl);
476
+ if (parsed.protocol !== 'ws:') {
477
+ throw new Error('unsupported websocket protocol');
478
+ }
479
+ return {
480
+ host: parsed.hostname,
481
+ port: Number(parsed.port || 80),
482
+ path: (parsed.pathname || '/') + (parsed.search || ''),
483
+ };
484
+ }
485
+
486
+ function encodeClientTextFrame(text) {
487
+ const payload = Buffer.from(text, 'utf8');
488
+ if (payload.length > 65535) {
489
+ throw new Error('websocket payload too large');
490
+ }
491
+ const headerLength = payload.length < 126 ? 2 : 4;
492
+ const frame = Buffer.alloc(headerLength + 4 + payload.length);
493
+ frame[0] = 0x81;
494
+ let offset = 2;
495
+ if (payload.length < 126) {
496
+ frame[1] = 0x80 | payload.length;
497
+ } else {
498
+ frame[1] = 0x80 | 126;
499
+ frame.writeUInt16BE(payload.length, 2);
500
+ offset = 4;
501
+ }
502
+ const mask = crypto.randomBytes(4);
503
+ mask.copy(frame, offset);
504
+ for (let index = 0; index < payload.length; index += 1) {
505
+ frame[offset + 4 + index] = payload[index] ^ mask[index % 4];
506
+ }
507
+ return frame;
508
+ }
509
+
510
+ function evaluateMenuState(webSocketDebuggerUrl) {
511
+ return new Promise((resolve, reject) => {
512
+ let target;
513
+ try {
514
+ target = parseWebSocketTarget(webSocketDebuggerUrl);
515
+ } catch (error) {
516
+ reject(error);
517
+ return;
518
+ }
519
+ const socket = net.createConnection({ host: target.host, port: target.port });
520
+ let buffer = Buffer.alloc(0);
521
+ let upgraded = false;
522
+ let settled = false;
523
+ const timer = setTimeout(() => {
524
+ finish(new Error('CDP evaluate timed out'));
525
+ }, 2500);
526
+
527
+ function finish(error, value) {
528
+ if (settled) {
529
+ return;
530
+ }
531
+ settled = true;
532
+ clearTimeout(timer);
533
+ socket.destroy();
534
+ if (error) {
535
+ reject(error);
536
+ } else {
537
+ resolve(value);
538
+ }
539
+ }
540
+
541
+ function sendEvaluateRequest() {
542
+ socket.write(encodeClientTextFrame(JSON.stringify({
543
+ id: 1,
544
+ method: 'Runtime.evaluate',
545
+ params: {
546
+ expression: '(() => Boolean(document.querySelector(' + JSON.stringify(selector) + ')))()',
547
+ returnByValue: true,
548
+ awaitPromise: true,
549
+ allowUnsafeEvalBlockedByCSP: true,
550
+ },
551
+ })));
552
+ }
553
+
554
+ function readFrames() {
555
+ while (buffer.length >= 2) {
556
+ const first = buffer[0];
557
+ const second = buffer[1];
558
+ const opcode = first & 0x0f;
559
+ let length = second & 0x7f;
560
+ let offset = 2;
561
+ if (length === 126) {
562
+ if (buffer.length < offset + 2) return;
563
+ length = buffer.readUInt16BE(offset);
564
+ offset += 2;
565
+ } else if (length === 127) {
566
+ if (buffer.length < offset + 8) return;
567
+ const high = buffer.readUInt32BE(offset);
568
+ const low = buffer.readUInt32BE(offset + 4);
569
+ if (high !== 0) {
570
+ finish(new Error('websocket frame too large'));
571
+ return;
572
+ }
573
+ length = low;
574
+ offset += 8;
575
+ }
576
+ const masked = Boolean(second & 0x80);
577
+ let mask = null;
578
+ if (masked) {
579
+ if (buffer.length < offset + 4) return;
580
+ mask = buffer.subarray(offset, offset + 4);
581
+ offset += 4;
582
+ }
583
+ if (buffer.length < offset + length) return;
584
+ const payload = Buffer.from(buffer.subarray(offset, offset + length));
585
+ buffer = buffer.subarray(offset + length);
586
+ if (mask) {
587
+ for (let index = 0; index < payload.length; index += 1) {
588
+ payload[index] ^= mask[index % 4];
589
+ }
590
+ }
591
+ if (opcode === 0x8) {
592
+ finish(new Error('CDP websocket closed'));
593
+ return;
594
+ }
595
+ if (opcode !== 0x1 && opcode !== 0x0) {
596
+ continue;
597
+ }
598
+ let payloadJson;
599
+ try {
600
+ payloadJson = JSON.parse(payload.toString('utf8'));
601
+ } catch (_error) {
602
+ continue;
603
+ }
604
+ if (payloadJson.id !== 1) {
605
+ continue;
606
+ }
607
+ if (payloadJson.error) {
608
+ finish(new Error(payloadJson.error.message || 'CDP evaluate failed'));
609
+ return;
610
+ }
611
+ finish(null, Boolean(payloadJson.result && payloadJson.result.result && payloadJson.result.result.value));
612
+ return;
613
+ }
614
+ }
615
+
616
+ socket.on('connect', () => {
617
+ const key = crypto.randomBytes(16).toString('base64');
618
+ socket.write([
619
+ 'GET ' + target.path + ' HTTP/1.1',
620
+ 'Host: ' + target.host + ':' + target.port,
621
+ 'Upgrade: websocket',
622
+ 'Connection: Upgrade',
623
+ 'Sec-WebSocket-Key: ' + key,
624
+ 'Sec-WebSocket-Version: 13',
625
+ '',
626
+ '',
627
+ ].join('\r\n'));
628
+ });
629
+
630
+ socket.on('data', (chunk) => {
631
+ buffer = Buffer.concat([buffer, chunk]);
632
+ if (!upgraded) {
633
+ const headerEnd = buffer.indexOf('\r\n\r\n');
634
+ if (headerEnd === -1) {
635
+ return;
636
+ }
637
+ const header = buffer.subarray(0, headerEnd).toString('latin1');
638
+ buffer = buffer.subarray(headerEnd + 4);
639
+ if (!/^HTTP\/1\.[01] 101\b/.test(header)) {
640
+ finish(new Error('CDP websocket upgrade failed'));
641
+ return;
642
+ }
643
+ upgraded = true;
644
+ sendEvaluateRequest();
645
+ }
646
+ readFrames();
647
+ });
648
+
649
+ socket.on('error', (error) => {
650
+ finish(error);
651
+ });
652
+
653
+ socket.on('close', () => {
654
+ if (!settled) {
655
+ finish(new Error('CDP websocket closed'));
656
+ return;
657
+ }
658
+ });
659
+ });
660
+ }
661
+
662
+ (async () => {
663
+ if (!Number.isInteger(debugPort) || debugPort <= 0) {
664
+ exitSoon(3);
665
+ }
666
+ const targets = await readJson('/json/list').catch(() => readJson('/json'));
667
+ const list = Array.isArray(targets) ? targets : [];
668
+ const target = list.find((candidate) => candidate.type === 'page' && candidate.webSocketDebuggerUrl)
669
+ || list.find((candidate) => candidate.webSocketDebuggerUrl);
670
+ if (!target) {
671
+ exitSoon(3);
672
+ }
673
+ const present = await evaluateMenuState(target.webSocketDebuggerUrl);
674
+ exitSoon(present ? 0 : 2);
675
+ })().catch(() => exitSoon(3));
676
+ `;
677
+ }
678
+
679
+ function codexPlusUiInjectionState(debugPort, options = {}) {
680
+ if (typeof options.codexPlusUiInjectionState === 'function') {
681
+ return options.codexPlusUiInjectionState(debugPort, options);
682
+ }
683
+ const run = options.processSpawnSync || (options.spawnSync || options.spawn ? null : spawnSync);
684
+ if (!run) {
685
+ return 'unknown';
686
+ }
687
+ const env = {
688
+ ...(options.env || process.env),
689
+ CODEXPP_DEBUG_PORT: String(debugPort),
690
+ CODEXPP_MENU_SELECTOR: CODEX_PLUS_MENU_SELECTOR,
691
+ };
692
+ const result = run(process.execPath, ['-e', codexPlusUiInjectionProbeScript()], {
693
+ encoding: 'utf8',
694
+ env,
695
+ timeout: optionValue(options, 'uiInjectionProbeTimeoutMs', 4000),
696
+ windowsHide: true,
697
+ });
698
+ if (!result) {
699
+ return 'unknown';
700
+ }
701
+ if (result.status === 0) {
702
+ return 'present';
703
+ }
704
+ if (result.status === 2) {
705
+ return 'missing';
706
+ }
707
+ return 'unknown';
708
+ }
709
+
710
+ function launchDebugPort(args = [], fallback = 9229) {
711
+ for (let index = 0; index < args.length; index += 1) {
712
+ const arg = String(args[index] || '');
713
+ if (arg === '--debug-port' && args[index + 1]) {
714
+ const parsed = Number(args[index + 1]);
715
+ if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) {
716
+ return parsed;
717
+ }
718
+ }
719
+ const match = arg.match(/^--remote-debugging-port=(\d+)$/) || arg.match(/^--debug-port=(\d+)$/);
720
+ if (match) {
721
+ const parsed = Number(match[1]);
722
+ if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) {
723
+ return parsed;
724
+ }
725
+ }
726
+ }
727
+ return fallback;
728
+ }
729
+
730
+ function codexProcessDebugPorts(processes = []) {
731
+ const ports = [];
732
+ for (const processInfo of processes) {
733
+ const commandLine = String(processInfo.CommandLine || '');
734
+ const match = commandLine.match(/--remote-debugging-port=(\d+)/);
735
+ if (!match) {
736
+ continue;
737
+ }
738
+ const parsed = Number(match[1]);
739
+ if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535 && !ports.includes(parsed)) {
740
+ ports.push(parsed);
741
+ }
742
+ }
743
+ return ports;
744
+ }
745
+
746
+ function preflightWindowsCodexLaunch(args = [], options = {}) {
747
+ const platform = optionValue(options, 'platform', process.platform);
748
+ const env = options.env || process.env;
749
+ if (platform !== 'win32' || isTruthyEnv(env.CODEXPP_DISABLE_CDP_PREFLIGHT)) {
750
+ return { action: 'noop' };
751
+ }
752
+
753
+ const processes = findRunningCodexProcesses(options);
754
+ if (processes.length === 0) {
755
+ return { action: 'noop' };
756
+ }
757
+
758
+ const requestedDebugPort = launchDebugPort(args);
759
+ const runningDebugPorts = codexProcessDebugPorts(processes);
760
+ const portsToCheck = runningDebugPorts.length > 0 ? runningDebugPorts : [requestedDebugPort];
761
+ if (runningDebugPorts.length > 0) {
762
+ const availableDebugPort = portsToCheck.find((debugPort) => cdpTargetsAvailable(debugPort, options));
763
+ if (availableDebugPort) {
764
+ const uiState = codexPlusUiInjectionState(availableDebugPort, options);
765
+ if (uiState !== 'missing') {
766
+ return { action: 'noop', debugPort: availableDebugPort, processCount: processes.length, uiState };
767
+ }
768
+
769
+ const sidecars = findRunningSidecarProcesses(options);
770
+ if (sidecars.length > 0) {
771
+ const terminate = options.terminateProcesses || terminateProcesses;
772
+ terminate(sidecars, options);
773
+ return {
774
+ action: 'terminated_stale_sidecar',
775
+ debugPort: availableDebugPort,
776
+ processCount: processes.length,
777
+ sidecarCount: sidecars.length,
778
+ uiState,
779
+ };
780
+ }
781
+
782
+ terminateSidecarsByImageName(options);
783
+ return {
784
+ action: 'terminated_stale_sidecar_by_name',
785
+ debugPort: availableDebugPort,
786
+ processCount: processes.length,
787
+ sidecarCount: 0,
788
+ uiState,
789
+ };
790
+ }
791
+ }
792
+
793
+ const sidecars = findRunningSidecarProcesses(options);
794
+ if (sidecars.length > 0) {
795
+ const terminate = options.terminateProcesses || terminateProcesses;
796
+ terminate(sidecars, options);
797
+ return {
798
+ action: 'terminated_sidecar_for_unavailable_cdp',
799
+ debugPort: portsToCheck[0],
800
+ processCount: processes.length,
801
+ sidecarCount: sidecars.length,
802
+ };
803
+ }
804
+
805
+ return { action: 'cdp_unavailable', debugPort: portsToCheck[0], processCount: processes.length };
806
+ }
807
+
808
+ function shouldPassPreflightDebugPort(preflight) {
809
+ return Boolean(
810
+ preflight
811
+ && preflight.uiState === 'missing'
812
+ && Number.isInteger(preflight.debugPort)
813
+ && preflight.debugPort > 0
814
+ && preflight.debugPort <= 65535,
815
+ );
816
+ }
817
+
818
+ function sidecarArgsWithDebugPort(args = [], debugPort) {
819
+ if (!Number.isInteger(debugPort) || debugPort <= 0 || debugPort > 65535) {
820
+ return args;
821
+ }
822
+ const filtered = [];
823
+ for (let index = 0; index < args.length; index += 1) {
824
+ const arg = String(args[index] || '');
825
+ if (arg === '--debug-port') {
826
+ index += 1;
827
+ continue;
828
+ }
829
+ if (/^--debug-port=\d+$/.test(arg) || /^--remote-debugging-port=\d+$/.test(arg)) {
830
+ continue;
831
+ }
832
+ filtered.push(args[index]);
833
+ }
834
+ return ['--debug-port', String(debugPort), ...filtered];
306
835
  }
307
836
 
308
837
  function isTruthyEnv(value) {
@@ -478,10 +1007,106 @@ async function installSidecars(options = {}) {
478
1007
  fsImpl.copyFileSync(icon, iconTarget);
479
1008
  installed.icon = iconTarget;
480
1009
  }
1010
+ try {
1011
+ fsImpl.writeFileSync(
1012
+ path.join(installRoot, SIDECAR_VERSION_STAMP),
1013
+ packageVersion(options) + '\n',
1014
+ );
1015
+ } catch (_error) {
1016
+ // Stamp is best-effort; doctor will fall back to platform probes.
1017
+ }
481
1018
  installed.installRoot = installRoot;
482
1019
  return installed;
483
1020
  }
484
1021
 
1022
+ function readInstalledSidecarVersion(options = {}) {
1023
+ const fsImpl = options.fs || fs;
1024
+ const platform = optionValue(options, 'platform', process.platform);
1025
+ const installRoot = optionValue(options, 'installRoot', () => detectedInstallRoot(options));
1026
+ const stampPath = path.join(installRoot, SIDECAR_VERSION_STAMP);
1027
+ try {
1028
+ const raw = fsImpl.readFileSync(stampPath, 'utf8');
1029
+ const first = String(raw).split(/\r?\n/)[0].trim();
1030
+ if (first) {
1031
+ return first;
1032
+ }
1033
+ } catch (_error) {
1034
+ // stamp absent or unreadable; fall through to platform probe
1035
+ }
1036
+ if (platform !== 'win32') {
1037
+ return null;
1038
+ }
1039
+ const silent = installedSidecarPath('silent', { ...options, installRoot });
1040
+ if (!fsImpl.existsSync(silent)) {
1041
+ return null;
1042
+ }
1043
+ const run = options.spawnSync || spawnSync;
1044
+ try {
1045
+ const result = run(
1046
+ 'powershell',
1047
+ ['-NoProfile', '-NonInteractive', '-Command', '(Get-Item -LiteralPath $args[0]).VersionInfo.FileVersion', '--', silent],
1048
+ { encoding: 'utf8', windowsHide: true },
1049
+ );
1050
+ if (result && result.status === 0) {
1051
+ const out = String(result.stdout || '').trim();
1052
+ if (out) {
1053
+ return out;
1054
+ }
1055
+ }
1056
+ } catch (_error) {
1057
+ // probe failed; treat as unknown
1058
+ }
1059
+ return null;
1060
+ }
1061
+
1062
+ function computeSidecarDrift(options = {}) {
1063
+ const platform = optionValue(options, 'platform', process.platform);
1064
+ const arch = optionValue(options, 'arch', process.arch);
1065
+ if (!SUPPORTED_PLATFORMS.has(platformKey(platform, arch))) {
1066
+ return 'unsupported';
1067
+ }
1068
+ const fsImpl = options.fs || fs;
1069
+ const installRoot = optionValue(options, 'installRoot', () => detectedInstallRoot(options));
1070
+ const silent = installedSidecarPath('silent', { ...options, installRoot });
1071
+ if (!fsImpl.existsSync(silent)) {
1072
+ return 'mismatch';
1073
+ }
1074
+ const bundled = packageVersion(options);
1075
+ const installed = readInstalledSidecarVersion({ ...options, installRoot });
1076
+ if (!installed) {
1077
+ return 'unknown';
1078
+ }
1079
+ return installed === bundled ? 'none' : 'mismatch';
1080
+ }
1081
+
1082
+ async function ensureSidecarsFresh(options = {}) {
1083
+ const drift = computeSidecarDrift(options);
1084
+ if (drift === 'none' || drift === 'unsupported') {
1085
+ return { action: 'noop', drift };
1086
+ }
1087
+ const env = options.env || process.env;
1088
+ const before = readInstalledSidecarVersion(options);
1089
+ const after = packageVersion(options);
1090
+ const stderr = options.stderr || process.stderr;
1091
+ try {
1092
+ await installSidecars({
1093
+ ...options,
1094
+ promptYesNo: () => false,
1095
+ promptYesNoWindowsPopup: () => false,
1096
+ });
1097
+ const arrow = before ? `${before} -> ${after}` : `-> ${after}`;
1098
+ stderr.write(`${t('sidecarSelfHealOk', env)} (${arrow})\n`);
1099
+ return { action: 'reinstalled', drift, before, after };
1100
+ } catch (error) {
1101
+ if (error && error.code === 'CODEXPP_LOCKED_OLD_BINARY') {
1102
+ stderr.write(`${t('sidecarSelfHealLocked', env)}\n`);
1103
+ return { action: 'locked', drift, error };
1104
+ }
1105
+ stderr.write(`${t('sidecarSelfHealFailed', env)}: ${(error && error.message) || String(error)}\n`);
1106
+ return { action: 'failed', drift, error };
1107
+ }
1108
+ }
1109
+
485
1110
  function normalizeExecutablePath(candidate) {
486
1111
  return path.resolve(String(candidate || ''));
487
1112
  }
@@ -920,6 +1545,55 @@ function psQuote(value) {
920
1545
  return `'${String(value).replace(/'/g, "''")}'`;
921
1546
  }
922
1547
 
1548
+ function windowsCommandLineArg(value) {
1549
+ const raw = String(value);
1550
+ if (raw && !/[ \t"]/.test(raw)) {
1551
+ return raw;
1552
+ }
1553
+ let output = '"';
1554
+ let backslashes = 0;
1555
+ for (const ch of raw) {
1556
+ if (ch === '\\') {
1557
+ backslashes += 1;
1558
+ } else if (ch === '"') {
1559
+ output += '\\'.repeat(backslashes * 2 + 1) + '"';
1560
+ backslashes = 0;
1561
+ } else {
1562
+ output += '\\'.repeat(backslashes) + ch;
1563
+ backslashes = 0;
1564
+ }
1565
+ }
1566
+ output += '\\'.repeat(backslashes * 2) + '"';
1567
+ return output;
1568
+ }
1569
+
1570
+ function vbsString(value) {
1571
+ return `"${String(value).replace(/"/g, '""')}"`;
1572
+ }
1573
+
1574
+ function windowsWscriptPath(options = {}) {
1575
+ const env = options.env || process.env;
1576
+ return path.join(env.SystemRoot || env.SYSTEMROOT || 'C:\\Windows', 'System32', 'wscript.exe');
1577
+ }
1578
+
1579
+ function writeWindowsLaunchScript(installed, options = {}) {
1580
+ const fsImpl = options.fs || fs;
1581
+ const installRoot = installed.installRoot || path.dirname(installed.silent);
1582
+ const scriptPath = optionValue(options, 'windowsLaunchScriptPath', () => path.join(installRoot, WINDOWS_LAUNCH_SCRIPT));
1583
+ const nodePath = optionValue(options, 'nodePath', process.execPath);
1584
+ const cxppScript = path.join(optionValue(options, 'packageRoot', packageRoot), 'npm', 'cxpp.js');
1585
+ const commandLine = [nodePath, cxppScript, 'launch'].map(windowsCommandLineArg).join(' ');
1586
+ const contents = [
1587
+ 'Set shell = CreateObject("WScript.Shell")',
1588
+ `shell.CurrentDirectory = ${vbsString(installRoot)}`,
1589
+ `shell.Run ${vbsString(commandLine)}, 0, False`,
1590
+ '',
1591
+ ].join('\r\n');
1592
+ fsImpl.mkdirSync(path.dirname(scriptPath), { recursive: true });
1593
+ fsImpl.writeFileSync(scriptPath, contents, 'utf8');
1594
+ return scriptPath;
1595
+ }
1596
+
923
1597
  function windowsEntrypointPaths(options = {}) {
924
1598
  const env = options.env || process.env;
925
1599
  const desktop = optionValue(options, 'desktopDir', () => path.join(os.homedir(), 'Desktop'));
@@ -940,17 +1614,39 @@ function installWindowsEntrypoints(installed, options = {}) {
940
1614
  const paths = windowsEntrypointPaths(options);
941
1615
  fsImpl.mkdirSync(path.dirname(paths.desktopSilent), { recursive: true });
942
1616
  fsImpl.mkdirSync(paths.startMenu, { recursive: true });
1617
+ const launchScript = writeWindowsLaunchScript(installed, options);
1618
+ const wscript = windowsWscriptPath(options);
1619
+ const installRoot = installed.installRoot || path.dirname(installed.silent);
943
1620
  const specs = [
944
- [paths.desktopSilent, installed.silent, 'Launch Codex++ silently'],
945
- [paths.desktopManager, installed.manager, 'Open Codex++ management tool'],
946
- [paths.startMenuSilent, installed.silent, 'Launch Codex++ silently'],
947
- [paths.startMenuManager, installed.manager, 'Open Codex++ management tool'],
948
- ].map(([shortcutPath, target, description]) => ({
949
- path: shortcutPath,
950
- target,
951
- description,
952
- icon: target,
953
- }));
1621
+ {
1622
+ path: paths.desktopSilent,
1623
+ target: wscript,
1624
+ arguments: windowsCommandLineArg(launchScript),
1625
+ workingDirectory: installRoot,
1626
+ description: 'Launch Codex++ silently',
1627
+ icon: installed.silent,
1628
+ },
1629
+ {
1630
+ path: paths.desktopManager,
1631
+ target: installed.manager,
1632
+ description: 'Open Codex++ management tool',
1633
+ icon: installed.manager,
1634
+ },
1635
+ {
1636
+ path: paths.startMenuSilent,
1637
+ target: wscript,
1638
+ arguments: windowsCommandLineArg(launchScript),
1639
+ workingDirectory: installRoot,
1640
+ description: 'Launch Codex++ silently',
1641
+ icon: installed.silent,
1642
+ },
1643
+ {
1644
+ path: paths.startMenuManager,
1645
+ target: installed.manager,
1646
+ description: 'Open Codex++ management tool',
1647
+ icon: installed.manager,
1648
+ },
1649
+ ];
954
1650
  const result = run('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', windowsShortcutScript(specs)], {
955
1651
  encoding: 'utf8',
956
1652
  windowsHide: true,
@@ -1284,25 +1980,37 @@ async function runLauncher(args = [], options = {}) {
1284
1980
  return { status: 0 };
1285
1981
  }
1286
1982
  if (command === 'launch' || command === 'run') {
1983
+ await ensureSidecarsFresh(options);
1287
1984
  const passthrough = args.slice(1);
1288
- return spawnSidecar('silent', passthrough[0] === '--' ? passthrough.slice(1) : passthrough, options);
1985
+ const sidecarArgs = passthrough[0] === '--' ? passthrough.slice(1) : passthrough;
1986
+ const preflight = preflightWindowsCodexLaunch(sidecarArgs, options);
1987
+ const launchArgs = shouldPassPreflightDebugPort(preflight)
1988
+ ? sidecarArgsWithDebugPort(sidecarArgs, preflight.debugPort)
1989
+ : sidecarArgs;
1990
+ return spawnSidecar('silent', launchArgs, options);
1289
1991
  }
1290
1992
  if (command === 'manager') {
1993
+ await ensureSidecarsFresh(options);
1291
1994
  return spawnSidecar('manager', args.slice(1), options);
1292
1995
  }
1996
+ await ensureSidecarsFresh(options);
1293
1997
  return spawnSidecar('silent', args, options);
1294
1998
  }
1295
1999
 
1296
2000
  module.exports = {
2001
+ SIDECAR_VERSION_STAMP,
1297
2002
  SUPPORTED_PLATFORMS,
1298
2003
  bundledSidecarPath,
1299
2004
  bundledUpstreamVersion,
2005
+ computeSidecarDrift,
1300
2006
  copyReplacingChangedFile,
1301
2007
  defaultInstallRoot,
1302
2008
  doctorReport,
2009
+ ensureSidecarsFresh,
1303
2010
  executableName,
1304
2011
  filesMatch,
1305
2012
  findRunningProcessesForPath,
2013
+ findRunningSidecarProcesses,
1306
2014
  installApp,
1307
2015
  installEntrypoints,
1308
2016
  installSidecars,
@@ -1315,14 +2023,20 @@ module.exports = {
1315
2023
  fallbackMacInstallRoot,
1316
2024
  macAppRoot,
1317
2025
  platformKey,
2026
+ preflightWindowsCodexLaunch,
2027
+ codexPlusUiInjectionState,
2028
+ sidecarArgsWithDebugPort,
1318
2029
  promptYesNoWindowsPopup,
2030
+ readInstalledSidecarVersion,
1319
2031
  removePath,
1320
2032
  runLauncher,
1321
2033
  spawnSidecar,
1322
2034
  terminateProcesses,
2035
+ terminateSidecarsByImageName,
1323
2036
  upstreamBinDir,
1324
2037
  upstreamMetadata,
1325
2038
  upstreamMetadataPath,
2039
+ writeWindowsLaunchScript,
1326
2040
  windowsShortcutScript,
1327
2041
  windowsEntrypointPaths,
1328
2042
  writeMacAppBundle,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duanluan/codex-plus-plus-launcher",
3
- "version": "1.2.14",
3
+ "version": "1.2.18",
4
4
  "description": "Install and launch Codex++ from npm",
5
5
  "bin": {
6
6
  "cxpp": "npm/cxpp.js",
@@ -51,3 +51,5 @@
51
51
  ],
52
52
  "license": "MIT"
53
53
  }
54
+
55
+
@@ -1,8 +1,19 @@
1
+ diff --git a/assets/inject/renderer-inject.js b/assets/inject/renderer-inject.js
2
+ index fa775b1..920c25e 100644
1
3
  --- a/assets/inject/renderer-inject.js
2
4
  +++ b/assets/inject/renderer-inject.js
3
- @@ -67,0 +68 @@
5
+ @@ -67,6 +67,7 @@
6
+ const codexServiceTierRequestOverrideVersion = "3";
7
+ const codexAppServerModelRequestPatchVersion = "1";
8
+ const codexPluginMarketplaceUnlockVersion = "10";
4
9
  + const codexPluginNavUnlockVersion = "2";
5
- @@ -2701,0 +2703,154 @@
10
+ const codexThreadScrollMaxEntries = 120;
11
+ const codexThreadScrollSaveThrottleMs = 120;
12
+ const codexThreadScrollRestoreWindowMs = 3200;
13
+ @@ -2702,6 +2703,160 @@
14
+ return true;
15
+ }
16
+
6
17
  + function pluginNavButtonCandidates() {
7
18
  + return Array.from(document.querySelectorAll(selectors.pluginNavButton))
8
19
  + .filter((button) => button instanceof HTMLElement && isPluginNavButton(button));
@@ -157,8 +168,102 @@
157
168
  + }, codexForcePluginInstallRefreshIntervalMs);
158
169
  + }
159
170
  +
160
- @@ -7592,0 +7748,2 @@
171
+ function restorePluginMarketplaceName(name) {
172
+ if (name === "codex-plus-openai-bundled") return "openai-bundled";
173
+ if (name === "codex-plus-openai-curated") return "openai-curated";
174
+ @@ -7659,6 +7814,8 @@
175
+ installStyle();
176
+ installCodexServiceTierDispatcherPatch();
177
+ installCodexPlusMenu();
161
178
  + unlockPluginNavButtons();
162
179
  + refreshPluginNavUnlockLoop();
163
- @@ -7647,0 +7805 @@
180
+ scheduleBackendHeartbeat();
181
+ installDeleteButtonEventDelegation();
182
+ updateThreadScrollHandlers();
183
+ @@ -8322,6 +8479,7 @@
184
+ ".composer-footer",
185
+ selectors.appHeader,
186
+ selectors.archiveNav,
164
187
  + selectors.pluginNavButton,
188
+ ...(pluginPatchDisabledInRelayMode() ? [] : [selectors.disabledInstallButton]),
189
+ ].join(", ");
190
+ }
191
+ diff --git a/crates/codex-plus-core/src/bridge.rs b/crates/codex-plus-core/src/bridge.rs
192
+ index 52ea388..b903025 100644
193
+ --- a/crates/codex-plus-core/src/bridge.rs
194
+ +++ b/crates/codex-plus-core/src/bridge.rs
195
+ @@ -57,6 +57,8 @@ pub fn bridge_health_check_script() -> &'static str {
196
+ (() => {
197
+ const bridge = window.__codexSessionDeleteBridge;
198
+ if (typeof bridge !== "function") return false;
199
+ + if (!window.__CODEX_PLUS_VERSION__) return false;
200
+ + if (!document.querySelector('#codex-plus-menu, [data-codex-plus-menu="true"]')) return false;
201
+ try {
202
+ return Promise.race([
203
+ Promise.resolve(bridge("/backend/status", {})).then((result) => !!result && result.status === "ok"),
204
+ diff --git a/crates/codex-plus-core/src/ports.rs b/crates/codex-plus-core/src/ports.rs
205
+ index cfa4933..5d33223 100644
206
+ --- a/crates/codex-plus-core/src/ports.rs
207
+ +++ b/crates/codex-plus-core/src/ports.rs
208
+ @@ -27,11 +27,11 @@ pub fn select_packaged_codex_debug_port(requested: u16) -> u16 {
209
+
210
+ pub fn select_packaged_codex_debug_port_with(
211
+ requested: u16,
212
+ - is_windows: bool,
213
+ - can_bind: impl Fn(u16) -> bool,
214
+ - find_available: impl Fn() -> u16,
215
+ + _is_windows: bool,
216
+ + _can_bind: impl Fn(u16) -> bool,
217
+ + _find_available: impl Fn() -> u16,
218
+ ) -> u16 {
219
+ - select_platform_loopback_port_with(requested, is_windows, can_bind, find_available)
220
+ + requested
221
+ }
222
+
223
+ pub fn select_platform_loopback_port_with(
224
+ diff --git a/crates/codex-plus-core/tests/cdp_bridge.rs b/crates/codex-plus-core/tests/cdp_bridge.rs
225
+ index c8fe983..c2f918e 100644
226
+ --- a/crates/codex-plus-core/tests/cdp_bridge.rs
227
+ +++ b/crates/codex-plus-core/tests/cdp_bridge.rs
228
+ @@ -830,11 +830,24 @@ fn bridge_health_check_script_uses_real_backend_round_trip() {
229
+ let script = bridge::bridge_health_check_script();
230
+
231
+ assert!(script.contains("__codexSessionDeleteBridge"));
232
+ + assert!(script.contains("__CODEX_PLUS_VERSION__"));
233
+ + assert!(script.contains("#codex-plus-menu"));
234
+ + assert!(script.contains("[data-codex-plus-menu=\"true\"]"));
235
+ assert!(script.contains("/backend/status"));
236
+ assert!(script.contains("Promise.race"));
237
+ assert!(script.contains("setTimeout"));
238
+ }
239
+
240
+ +#[test]
241
+ +fn bridge_health_check_script_rejects_bridge_only_renderer_state() {
242
+ + let script = bridge::bridge_health_check_script();
243
+ +
244
+ + assert!(script.contains("if (!window.__CODEX_PLUS_VERSION__) return false;"));
245
+ + assert!(script.contains(
246
+ + "if (!document.querySelector('#codex-plus-menu, [data-codex-plus-menu=\"true\"]')) return false;"
247
+ + ));
248
+ +}
249
+ +
250
+ #[test]
251
+ fn bridge_result_expressions_json_escape_inputs() {
252
+ let resolve = bridge::resolve_bridge_expression("request\"1", &json!({"status": "ok"}))
253
+ diff --git a/crates/codex-plus-core/tests/launcher.rs b/crates/codex-plus-core/tests/launcher.rs
254
+ index 5c13508..9f7319e 100644
255
+ --- a/crates/codex-plus-core/tests/launcher.rs
256
+ +++ b/crates/codex-plus-core/tests/launcher.rs
257
+ @@ -372,10 +372,10 @@ fn ports_windows_falls_back_to_ephemeral_when_requested_is_busy() {
258
+ }
259
+
260
+ #[test]
261
+ -fn ports_windows_packaged_debug_falls_back_to_ephemeral_when_requested_is_busy() {
262
+ +fn ports_windows_packaged_debug_keeps_requested_when_requested_is_busy() {
263
+ let selected = select_packaged_codex_debug_port_with(9229, true, |_| false, || 43001);
264
+
265
+ - assert_eq!(selected, 43001);
266
+ + assert_eq!(selected, 9229);
267
+ }
268
+
269
+ #[test]
@@ -1,16 +1,8 @@
1
1
  {
2
- "version": "v1.2.14",
3
- "html_url": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/tag/v1.2.14",
4
- "asset_name": "CodexPlusPlus-1.2.14-macos-arm64.dmg",
5
- "asset_url": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/download/v1.2.14/CodexPlusPlus-1.2.14-macos-arm64.dmg",
6
- "windows_asset_name": "CodexPlusPlus-1.2.14-windows-x64-setup.exe",
7
- "windows_asset_url": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/download/v1.2.14/CodexPlusPlus-1.2.14-windows-x64-setup.exe",
8
- "macos_x64_asset_name": "CodexPlusPlus-1.2.14-macos-x64.dmg",
9
- "macos_x64_asset_url": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/download/v1.2.14/CodexPlusPlus-1.2.14-macos-x64.dmg",
10
- "macos_arm64_asset_name": "CodexPlusPlus-1.2.14-macos-arm64.dmg",
11
- "macos_arm64_asset_url": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/download/v1.2.14/CodexPlusPlus-1.2.14-macos-arm64.dmg",
12
- "source_zip_url": "https://github.com/BigPizzaV3/CodexPlusPlus/archive/refs/tags/v1.2.14.zip",
13
- "install_spec": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/download/v1.2.14/CodexPlusPlus-1.2.14-macos-arm64.dmg",
14
- "commit": "76686f03cf7b2e8acd3f7894323989e02fa104d3",
2
+ "upstream_version": "v1.2.15",
3
+ "package_version": "1.2.18",
4
+ "ref": "v1.2.15",
5
+ "version": "v1.2.15",
6
+ "commit": "324a350d00376d4f9565aeeae3c8820b0cbbbe2a",
15
7
  "repository": "BigPizzaV3/CodexPlusPlus"
16
8
  }