@duanluan/codex-plus-plus-launcher 1.2.15 → 1.2.19

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.15"
1
+ __version__ = "1.2.19"
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,551 @@ 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
+ const HTTP_HOSTS = ['localhost', '127.0.0.1', '::1'];
451
+
452
+ function readJsonFromHost(host, pathname) {
453
+ return new Promise((resolve, reject) => {
454
+ const request = http.get({
455
+ host,
456
+ port: debugPort,
457
+ path: pathname,
458
+ timeout: 1500,
459
+ }, (response) => {
460
+ const chunks = [];
461
+ response.setEncoding('utf8');
462
+ response.on('data', (chunk) => chunks.push(chunk));
463
+ response.on('end', () => {
464
+ try {
465
+ resolve(JSON.parse(chunks.join('')));
466
+ } catch (error) {
467
+ reject(error);
468
+ }
469
+ });
470
+ });
471
+ request.on('timeout', () => request.destroy(new Error('timeout')));
472
+ request.on('error', reject);
473
+ });
474
+ }
475
+
476
+ async function readJson(pathname) {
477
+ let lastError = null;
478
+ for (const host of HTTP_HOSTS) {
479
+ try {
480
+ return await readJsonFromHost(host, pathname);
481
+ } catch (error) {
482
+ lastError = error;
483
+ }
484
+ }
485
+ throw lastError || new Error('CDP HTTP unavailable');
486
+ }
487
+
488
+ function parseWebSocketTarget(webSocketDebuggerUrl) {
489
+ const parsed = new URL(webSocketDebuggerUrl);
490
+ if (parsed.protocol !== 'ws:') {
491
+ throw new Error('unsupported websocket protocol');
492
+ }
493
+ const host = parsed.hostname.startsWith('[') && parsed.hostname.endsWith(']')
494
+ ? parsed.hostname.slice(1, -1)
495
+ : parsed.hostname;
496
+ const hostHeader = host.includes(':') ? '[' + host + ']' : host;
497
+ return {
498
+ host,
499
+ hostHeader,
500
+ port: Number(parsed.port || 80),
501
+ path: (parsed.pathname || '/') + (parsed.search || ''),
502
+ };
503
+ }
504
+
505
+ function encodeClientTextFrame(text) {
506
+ const payload = Buffer.from(text, 'utf8');
507
+ if (payload.length > 65535) {
508
+ throw new Error('websocket payload too large');
509
+ }
510
+ const headerLength = payload.length < 126 ? 2 : 4;
511
+ const frame = Buffer.alloc(headerLength + 4 + payload.length);
512
+ frame[0] = 0x81;
513
+ let offset = 2;
514
+ if (payload.length < 126) {
515
+ frame[1] = 0x80 | payload.length;
516
+ } else {
517
+ frame[1] = 0x80 | 126;
518
+ frame.writeUInt16BE(payload.length, 2);
519
+ offset = 4;
520
+ }
521
+ const mask = crypto.randomBytes(4);
522
+ mask.copy(frame, offset);
523
+ for (let index = 0; index < payload.length; index += 1) {
524
+ frame[offset + 4 + index] = payload[index] ^ mask[index % 4];
525
+ }
526
+ return frame;
527
+ }
528
+
529
+ function evaluateMenuState(webSocketDebuggerUrl) {
530
+ return new Promise((resolve, reject) => {
531
+ let target;
532
+ try {
533
+ target = parseWebSocketTarget(webSocketDebuggerUrl);
534
+ } catch (error) {
535
+ reject(error);
536
+ return;
537
+ }
538
+ const socket = net.createConnection({ host: target.host, port: target.port });
539
+ let buffer = Buffer.alloc(0);
540
+ let upgraded = false;
541
+ let settled = false;
542
+ const timer = setTimeout(() => {
543
+ finish(new Error('CDP evaluate timed out'));
544
+ }, 2500);
545
+
546
+ function finish(error, value) {
547
+ if (settled) {
548
+ return;
549
+ }
550
+ settled = true;
551
+ clearTimeout(timer);
552
+ socket.destroy();
553
+ if (error) {
554
+ reject(error);
555
+ } else {
556
+ resolve(value);
557
+ }
558
+ }
559
+
560
+ function sendEvaluateRequest() {
561
+ socket.write(encodeClientTextFrame(JSON.stringify({
562
+ id: 1,
563
+ method: 'Runtime.evaluate',
564
+ params: {
565
+ expression: '(() => Boolean(document.querySelector(' + JSON.stringify(selector) + ')))()',
566
+ returnByValue: true,
567
+ awaitPromise: true,
568
+ allowUnsafeEvalBlockedByCSP: true,
569
+ },
570
+ })));
571
+ }
572
+
573
+ function readFrames() {
574
+ while (buffer.length >= 2) {
575
+ const first = buffer[0];
576
+ const second = buffer[1];
577
+ const opcode = first & 0x0f;
578
+ let length = second & 0x7f;
579
+ let offset = 2;
580
+ if (length === 126) {
581
+ if (buffer.length < offset + 2) return;
582
+ length = buffer.readUInt16BE(offset);
583
+ offset += 2;
584
+ } else if (length === 127) {
585
+ if (buffer.length < offset + 8) return;
586
+ const high = buffer.readUInt32BE(offset);
587
+ const low = buffer.readUInt32BE(offset + 4);
588
+ if (high !== 0) {
589
+ finish(new Error('websocket frame too large'));
590
+ return;
591
+ }
592
+ length = low;
593
+ offset += 8;
594
+ }
595
+ const masked = Boolean(second & 0x80);
596
+ let mask = null;
597
+ if (masked) {
598
+ if (buffer.length < offset + 4) return;
599
+ mask = buffer.subarray(offset, offset + 4);
600
+ offset += 4;
601
+ }
602
+ if (buffer.length < offset + length) return;
603
+ const payload = Buffer.from(buffer.subarray(offset, offset + length));
604
+ buffer = buffer.subarray(offset + length);
605
+ if (mask) {
606
+ for (let index = 0; index < payload.length; index += 1) {
607
+ payload[index] ^= mask[index % 4];
608
+ }
609
+ }
610
+ if (opcode === 0x8) {
611
+ finish(new Error('CDP websocket closed'));
612
+ return;
613
+ }
614
+ if (opcode !== 0x1 && opcode !== 0x0) {
615
+ continue;
616
+ }
617
+ let payloadJson;
618
+ try {
619
+ payloadJson = JSON.parse(payload.toString('utf8'));
620
+ } catch (_error) {
621
+ continue;
622
+ }
623
+ if (payloadJson.id !== 1) {
624
+ continue;
625
+ }
626
+ if (payloadJson.error) {
627
+ finish(new Error(payloadJson.error.message || 'CDP evaluate failed'));
628
+ return;
629
+ }
630
+ finish(null, Boolean(payloadJson.result && payloadJson.result.result && payloadJson.result.result.value));
631
+ return;
632
+ }
633
+ }
634
+
635
+ socket.on('connect', () => {
636
+ const key = crypto.randomBytes(16).toString('base64');
637
+ socket.write([
638
+ 'GET ' + target.path + ' HTTP/1.1',
639
+ 'Host: ' + target.hostHeader + ':' + target.port,
640
+ 'Upgrade: websocket',
641
+ 'Connection: Upgrade',
642
+ 'Sec-WebSocket-Key: ' + key,
643
+ 'Sec-WebSocket-Version: 13',
644
+ '',
645
+ '',
646
+ ].join('\r\n'));
647
+ });
648
+
649
+ socket.on('data', (chunk) => {
650
+ buffer = Buffer.concat([buffer, chunk]);
651
+ if (!upgraded) {
652
+ const headerEnd = buffer.indexOf('\r\n\r\n');
653
+ if (headerEnd === -1) {
654
+ return;
655
+ }
656
+ const header = buffer.subarray(0, headerEnd).toString('latin1');
657
+ buffer = buffer.subarray(headerEnd + 4);
658
+ if (!/^HTTP\/1\.[01] 101\b/.test(header)) {
659
+ finish(new Error('CDP websocket upgrade failed'));
660
+ return;
661
+ }
662
+ upgraded = true;
663
+ sendEvaluateRequest();
664
+ }
665
+ readFrames();
666
+ });
667
+
668
+ socket.on('error', (error) => {
669
+ finish(error);
670
+ });
671
+
672
+ socket.on('close', () => {
673
+ if (!settled) {
674
+ finish(new Error('CDP websocket closed'));
675
+ return;
676
+ }
677
+ });
678
+ });
679
+ }
680
+
681
+ (async () => {
682
+ if (!Number.isInteger(debugPort) || debugPort <= 0) {
683
+ exitSoon(3);
684
+ }
685
+ const targets = await readJson('/json/list').catch(() => readJson('/json'));
686
+ const list = Array.isArray(targets) ? targets : [];
687
+ const target = list.find((candidate) => candidate.type === 'page' && candidate.webSocketDebuggerUrl)
688
+ || list.find((candidate) => candidate.webSocketDebuggerUrl);
689
+ if (!target) {
690
+ exitSoon(3);
691
+ }
692
+ const present = await evaluateMenuState(target.webSocketDebuggerUrl);
693
+ exitSoon(present ? 0 : 2);
694
+ })().catch(() => exitSoon(3));
695
+ `;
696
+ }
697
+
698
+ function codexPlusUiInjectionState(debugPort, options = {}) {
699
+ if (typeof options.codexPlusUiInjectionState === 'function') {
700
+ return options.codexPlusUiInjectionState(debugPort, options);
701
+ }
702
+ const run = options.processSpawnSync || (options.spawnSync || options.spawn ? null : spawnSync);
703
+ if (!run) {
704
+ return 'unknown';
705
+ }
706
+ const env = {
707
+ ...(options.env || process.env),
708
+ CODEXPP_DEBUG_PORT: String(debugPort),
709
+ CODEXPP_MENU_SELECTOR: CODEX_PLUS_MENU_SELECTOR,
710
+ };
711
+ const result = run(process.execPath, ['-e', codexPlusUiInjectionProbeScript()], {
712
+ encoding: 'utf8',
713
+ env,
714
+ timeout: optionValue(options, 'uiInjectionProbeTimeoutMs', 4000),
715
+ windowsHide: true,
716
+ });
717
+ if (!result) {
718
+ return 'unknown';
719
+ }
720
+ if (result.status === 0) {
721
+ return 'present';
722
+ }
723
+ if (result.status === 2) {
724
+ return 'missing';
725
+ }
726
+ return 'unknown';
727
+ }
728
+
729
+ function launchDebugPort(args = [], fallback = 9229) {
730
+ for (let index = 0; index < args.length; index += 1) {
731
+ const arg = String(args[index] || '');
732
+ if (arg === '--debug-port' && args[index + 1]) {
733
+ const parsed = Number(args[index + 1]);
734
+ if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) {
735
+ return parsed;
736
+ }
737
+ }
738
+ const match = arg.match(/^--remote-debugging-port=(\d+)$/) || arg.match(/^--debug-port=(\d+)$/);
739
+ if (match) {
740
+ const parsed = Number(match[1]);
741
+ if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) {
742
+ return parsed;
743
+ }
744
+ }
745
+ }
746
+ return fallback;
747
+ }
748
+
749
+ function codexProcessDebugPorts(processes = []) {
750
+ const ports = [];
751
+ for (const processInfo of processes) {
752
+ const commandLine = String(processInfo.CommandLine || '');
753
+ const match = commandLine.match(/--remote-debugging-port=(\d+)/);
754
+ if (!match) {
755
+ continue;
756
+ }
757
+ const parsed = Number(match[1]);
758
+ if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535 && !ports.includes(parsed)) {
759
+ ports.push(parsed);
760
+ }
761
+ }
762
+ return ports;
763
+ }
764
+
765
+ function preflightWindowsCodexLaunch(args = [], options = {}) {
766
+ const platform = optionValue(options, 'platform', process.platform);
767
+ const env = options.env || process.env;
768
+ if (platform !== 'win32' || isTruthyEnv(env.CODEXPP_DISABLE_CDP_PREFLIGHT)) {
769
+ return { action: 'noop' };
770
+ }
771
+
772
+ const processes = findRunningCodexProcesses(options);
773
+ if (processes.length === 0) {
774
+ return { action: 'noop' };
775
+ }
776
+
777
+ const requestedDebugPort = launchDebugPort(args);
778
+ const runningDebugPorts = codexProcessDebugPorts(processes);
779
+ const portsToCheck = runningDebugPorts.length > 0 ? runningDebugPorts : [requestedDebugPort];
780
+ if (runningDebugPorts.length > 0) {
781
+ const availableDebugPort = portsToCheck.find((debugPort) => cdpTargetsAvailable(debugPort, options));
782
+ if (availableDebugPort) {
783
+ const uiState = codexPlusUiInjectionState(availableDebugPort, options);
784
+ if (uiState !== 'missing') {
785
+ return { action: 'noop', debugPort: availableDebugPort, processCount: processes.length, uiState };
786
+ }
787
+
788
+ const sidecars = findRunningSidecarProcesses(options);
789
+ if (sidecars.length > 0) {
790
+ const terminate = options.terminateProcesses || terminateProcesses;
791
+ terminate(sidecars, options);
792
+ return {
793
+ action: 'terminated_stale_sidecar',
794
+ debugPort: availableDebugPort,
795
+ processCount: processes.length,
796
+ sidecarCount: sidecars.length,
797
+ uiState,
798
+ };
799
+ }
800
+
801
+ terminateSidecarsByImageName(options);
802
+ return {
803
+ action: 'terminated_stale_sidecar_by_name',
804
+ debugPort: availableDebugPort,
805
+ processCount: processes.length,
806
+ sidecarCount: 0,
807
+ uiState,
808
+ };
809
+ }
810
+ }
811
+
812
+ const sidecars = findRunningSidecarProcesses(options);
813
+ if (sidecars.length > 0) {
814
+ const terminate = options.terminateProcesses || terminateProcesses;
815
+ terminate(sidecars, options);
816
+ return {
817
+ action: 'terminated_sidecar_for_unavailable_cdp',
818
+ debugPort: portsToCheck[0],
819
+ processCount: processes.length,
820
+ sidecarCount: sidecars.length,
821
+ };
822
+ }
823
+
824
+ return { action: 'cdp_unavailable', debugPort: portsToCheck[0], processCount: processes.length };
825
+ }
826
+
827
+ function shouldPassPreflightDebugPort(preflight) {
828
+ return Boolean(
829
+ preflight
830
+ && preflight.uiState === 'missing'
831
+ && Number.isInteger(preflight.debugPort)
832
+ && preflight.debugPort > 0
833
+ && preflight.debugPort <= 65535,
834
+ );
835
+ }
836
+
837
+ function sidecarArgsWithDebugPort(args = [], debugPort) {
838
+ if (!Number.isInteger(debugPort) || debugPort <= 0 || debugPort > 65535) {
839
+ return args;
840
+ }
841
+ const filtered = [];
842
+ for (let index = 0; index < args.length; index += 1) {
843
+ const arg = String(args[index] || '');
844
+ if (arg === '--debug-port') {
845
+ index += 1;
846
+ continue;
847
+ }
848
+ if (/^--debug-port=\d+$/.test(arg) || /^--remote-debugging-port=\d+$/.test(arg)) {
849
+ continue;
850
+ }
851
+ filtered.push(args[index]);
852
+ }
853
+ return ['--debug-port', String(debugPort), ...filtered];
306
854
  }
307
855
 
308
856
  function isTruthyEnv(value) {
@@ -478,10 +1026,106 @@ async function installSidecars(options = {}) {
478
1026
  fsImpl.copyFileSync(icon, iconTarget);
479
1027
  installed.icon = iconTarget;
480
1028
  }
1029
+ try {
1030
+ fsImpl.writeFileSync(
1031
+ path.join(installRoot, SIDECAR_VERSION_STAMP),
1032
+ packageVersion(options) + '\n',
1033
+ );
1034
+ } catch (_error) {
1035
+ // Stamp is best-effort; doctor will fall back to platform probes.
1036
+ }
481
1037
  installed.installRoot = installRoot;
482
1038
  return installed;
483
1039
  }
484
1040
 
1041
+ function readInstalledSidecarVersion(options = {}) {
1042
+ const fsImpl = options.fs || fs;
1043
+ const platform = optionValue(options, 'platform', process.platform);
1044
+ const installRoot = optionValue(options, 'installRoot', () => detectedInstallRoot(options));
1045
+ const stampPath = path.join(installRoot, SIDECAR_VERSION_STAMP);
1046
+ try {
1047
+ const raw = fsImpl.readFileSync(stampPath, 'utf8');
1048
+ const first = String(raw).split(/\r?\n/)[0].trim();
1049
+ if (first) {
1050
+ return first;
1051
+ }
1052
+ } catch (_error) {
1053
+ // stamp absent or unreadable; fall through to platform probe
1054
+ }
1055
+ if (platform !== 'win32') {
1056
+ return null;
1057
+ }
1058
+ const silent = installedSidecarPath('silent', { ...options, installRoot });
1059
+ if (!fsImpl.existsSync(silent)) {
1060
+ return null;
1061
+ }
1062
+ const run = options.spawnSync || spawnSync;
1063
+ try {
1064
+ const result = run(
1065
+ 'powershell',
1066
+ ['-NoProfile', '-NonInteractive', '-Command', '(Get-Item -LiteralPath $args[0]).VersionInfo.FileVersion', '--', silent],
1067
+ { encoding: 'utf8', windowsHide: true },
1068
+ );
1069
+ if (result && result.status === 0) {
1070
+ const out = String(result.stdout || '').trim();
1071
+ if (out) {
1072
+ return out;
1073
+ }
1074
+ }
1075
+ } catch (_error) {
1076
+ // probe failed; treat as unknown
1077
+ }
1078
+ return null;
1079
+ }
1080
+
1081
+ function computeSidecarDrift(options = {}) {
1082
+ const platform = optionValue(options, 'platform', process.platform);
1083
+ const arch = optionValue(options, 'arch', process.arch);
1084
+ if (!SUPPORTED_PLATFORMS.has(platformKey(platform, arch))) {
1085
+ return 'unsupported';
1086
+ }
1087
+ const fsImpl = options.fs || fs;
1088
+ const installRoot = optionValue(options, 'installRoot', () => detectedInstallRoot(options));
1089
+ const silent = installedSidecarPath('silent', { ...options, installRoot });
1090
+ if (!fsImpl.existsSync(silent)) {
1091
+ return 'mismatch';
1092
+ }
1093
+ const bundled = packageVersion(options);
1094
+ const installed = readInstalledSidecarVersion({ ...options, installRoot });
1095
+ if (!installed) {
1096
+ return 'unknown';
1097
+ }
1098
+ return installed === bundled ? 'none' : 'mismatch';
1099
+ }
1100
+
1101
+ async function ensureSidecarsFresh(options = {}) {
1102
+ const drift = computeSidecarDrift(options);
1103
+ if (drift === 'none' || drift === 'unsupported') {
1104
+ return { action: 'noop', drift };
1105
+ }
1106
+ const env = options.env || process.env;
1107
+ const before = readInstalledSidecarVersion(options);
1108
+ const after = packageVersion(options);
1109
+ const stderr = options.stderr || process.stderr;
1110
+ try {
1111
+ await installSidecars({
1112
+ ...options,
1113
+ promptYesNo: () => false,
1114
+ promptYesNoWindowsPopup: () => false,
1115
+ });
1116
+ const arrow = before ? `${before} -> ${after}` : `-> ${after}`;
1117
+ stderr.write(`${t('sidecarSelfHealOk', env)} (${arrow})\n`);
1118
+ return { action: 'reinstalled', drift, before, after };
1119
+ } catch (error) {
1120
+ if (error && error.code === 'CODEXPP_LOCKED_OLD_BINARY') {
1121
+ stderr.write(`${t('sidecarSelfHealLocked', env)}\n`);
1122
+ return { action: 'locked', drift, error };
1123
+ }
1124
+ stderr.write(`${t('sidecarSelfHealFailed', env)}: ${(error && error.message) || String(error)}\n`);
1125
+ return { action: 'failed', drift, error };
1126
+ }
1127
+ }
1128
+
485
1129
  function normalizeExecutablePath(candidate) {
486
1130
  return path.resolve(String(candidate || ''));
487
1131
  }
@@ -920,6 +1564,55 @@ function psQuote(value) {
920
1564
  return `'${String(value).replace(/'/g, "''")}'`;
921
1565
  }
922
1566
 
1567
+ function windowsCommandLineArg(value) {
1568
+ const raw = String(value);
1569
+ if (raw && !/[ \t"]/.test(raw)) {
1570
+ return raw;
1571
+ }
1572
+ let output = '"';
1573
+ let backslashes = 0;
1574
+ for (const ch of raw) {
1575
+ if (ch === '\\') {
1576
+ backslashes += 1;
1577
+ } else if (ch === '"') {
1578
+ output += '\\'.repeat(backslashes * 2 + 1) + '"';
1579
+ backslashes = 0;
1580
+ } else {
1581
+ output += '\\'.repeat(backslashes) + ch;
1582
+ backslashes = 0;
1583
+ }
1584
+ }
1585
+ output += '\\'.repeat(backslashes * 2) + '"';
1586
+ return output;
1587
+ }
1588
+
1589
+ function vbsString(value) {
1590
+ return `"${String(value).replace(/"/g, '""')}"`;
1591
+ }
1592
+
1593
+ function windowsWscriptPath(options = {}) {
1594
+ const env = options.env || process.env;
1595
+ return path.join(env.SystemRoot || env.SYSTEMROOT || 'C:\\Windows', 'System32', 'wscript.exe');
1596
+ }
1597
+
1598
+ function writeWindowsLaunchScript(installed, options = {}) {
1599
+ const fsImpl = options.fs || fs;
1600
+ const installRoot = installed.installRoot || path.dirname(installed.silent);
1601
+ const scriptPath = optionValue(options, 'windowsLaunchScriptPath', () => path.join(installRoot, WINDOWS_LAUNCH_SCRIPT));
1602
+ const nodePath = optionValue(options, 'nodePath', process.execPath);
1603
+ const cxppScript = path.join(optionValue(options, 'packageRoot', packageRoot), 'npm', 'cxpp.js');
1604
+ const commandLine = [nodePath, cxppScript, 'launch'].map(windowsCommandLineArg).join(' ');
1605
+ const contents = [
1606
+ 'Set shell = CreateObject("WScript.Shell")',
1607
+ `shell.CurrentDirectory = ${vbsString(installRoot)}`,
1608
+ `shell.Run ${vbsString(commandLine)}, 0, False`,
1609
+ '',
1610
+ ].join('\r\n');
1611
+ fsImpl.mkdirSync(path.dirname(scriptPath), { recursive: true });
1612
+ fsImpl.writeFileSync(scriptPath, contents, 'utf8');
1613
+ return scriptPath;
1614
+ }
1615
+
923
1616
  function windowsEntrypointPaths(options = {}) {
924
1617
  const env = options.env || process.env;
925
1618
  const desktop = optionValue(options, 'desktopDir', () => path.join(os.homedir(), 'Desktop'));
@@ -940,17 +1633,39 @@ function installWindowsEntrypoints(installed, options = {}) {
940
1633
  const paths = windowsEntrypointPaths(options);
941
1634
  fsImpl.mkdirSync(path.dirname(paths.desktopSilent), { recursive: true });
942
1635
  fsImpl.mkdirSync(paths.startMenu, { recursive: true });
1636
+ const launchScript = writeWindowsLaunchScript(installed, options);
1637
+ const wscript = windowsWscriptPath(options);
1638
+ const installRoot = installed.installRoot || path.dirname(installed.silent);
943
1639
  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
- }));
1640
+ {
1641
+ path: paths.desktopSilent,
1642
+ target: wscript,
1643
+ arguments: windowsCommandLineArg(launchScript),
1644
+ workingDirectory: installRoot,
1645
+ description: 'Launch Codex++ silently',
1646
+ icon: installed.silent,
1647
+ },
1648
+ {
1649
+ path: paths.desktopManager,
1650
+ target: installed.manager,
1651
+ description: 'Open Codex++ management tool',
1652
+ icon: installed.manager,
1653
+ },
1654
+ {
1655
+ path: paths.startMenuSilent,
1656
+ target: wscript,
1657
+ arguments: windowsCommandLineArg(launchScript),
1658
+ workingDirectory: installRoot,
1659
+ description: 'Launch Codex++ silently',
1660
+ icon: installed.silent,
1661
+ },
1662
+ {
1663
+ path: paths.startMenuManager,
1664
+ target: installed.manager,
1665
+ description: 'Open Codex++ management tool',
1666
+ icon: installed.manager,
1667
+ },
1668
+ ];
954
1669
  const result = run('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', windowsShortcutScript(specs)], {
955
1670
  encoding: 'utf8',
956
1671
  windowsHide: true,
@@ -1284,25 +1999,37 @@ async function runLauncher(args = [], options = {}) {
1284
1999
  return { status: 0 };
1285
2000
  }
1286
2001
  if (command === 'launch' || command === 'run') {
2002
+ await ensureSidecarsFresh(options);
1287
2003
  const passthrough = args.slice(1);
1288
- return spawnSidecar('silent', passthrough[0] === '--' ? passthrough.slice(1) : passthrough, options);
2004
+ const sidecarArgs = passthrough[0] === '--' ? passthrough.slice(1) : passthrough;
2005
+ const preflight = preflightWindowsCodexLaunch(sidecarArgs, options);
2006
+ const launchArgs = shouldPassPreflightDebugPort(preflight)
2007
+ ? sidecarArgsWithDebugPort(sidecarArgs, preflight.debugPort)
2008
+ : sidecarArgs;
2009
+ return spawnSidecar('silent', launchArgs, options);
1289
2010
  }
1290
2011
  if (command === 'manager') {
2012
+ await ensureSidecarsFresh(options);
1291
2013
  return spawnSidecar('manager', args.slice(1), options);
1292
2014
  }
2015
+ await ensureSidecarsFresh(options);
1293
2016
  return spawnSidecar('silent', args, options);
1294
2017
  }
1295
2018
 
1296
2019
  module.exports = {
2020
+ SIDECAR_VERSION_STAMP,
1297
2021
  SUPPORTED_PLATFORMS,
1298
2022
  bundledSidecarPath,
1299
2023
  bundledUpstreamVersion,
2024
+ computeSidecarDrift,
1300
2025
  copyReplacingChangedFile,
1301
2026
  defaultInstallRoot,
1302
2027
  doctorReport,
2028
+ ensureSidecarsFresh,
1303
2029
  executableName,
1304
2030
  filesMatch,
1305
2031
  findRunningProcessesForPath,
2032
+ findRunningSidecarProcesses,
1306
2033
  installApp,
1307
2034
  installEntrypoints,
1308
2035
  installSidecars,
@@ -1315,14 +2042,20 @@ module.exports = {
1315
2042
  fallbackMacInstallRoot,
1316
2043
  macAppRoot,
1317
2044
  platformKey,
2045
+ preflightWindowsCodexLaunch,
2046
+ codexPlusUiInjectionState,
2047
+ sidecarArgsWithDebugPort,
1318
2048
  promptYesNoWindowsPopup,
2049
+ readInstalledSidecarVersion,
1319
2050
  removePath,
1320
2051
  runLauncher,
1321
2052
  spawnSidecar,
1322
2053
  terminateProcesses,
2054
+ terminateSidecarsByImageName,
1323
2055
  upstreamBinDir,
1324
2056
  upstreamMetadata,
1325
2057
  upstreamMetadataPath,
2058
+ writeWindowsLaunchScript,
1326
2059
  windowsShortcutScript,
1327
2060
  windowsEntrypointPaths,
1328
2061
  writeMacAppBundle,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duanluan/codex-plus-plus-launcher",
3
- "version": "1.2.15",
3
+ "version": "1.2.19",
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
+ "upstream_version": "v1.2.15",
3
+ "package_version": "1.2.19",
4
+ "ref": "v1.2.15",
2
5
  "version": "v1.2.15",
3
- "html_url": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/tag/v1.2.15",
4
- "asset_name": "CodexPlusPlus-1.2.15-macos-arm64.dmg",
5
- "asset_url": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/download/v1.2.15/CodexPlusPlus-1.2.15-macos-arm64.dmg",
6
- "windows_asset_name": "CodexPlusPlus-1.2.15-windows-x64-setup.exe",
7
- "windows_asset_url": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/download/v1.2.15/CodexPlusPlus-1.2.15-windows-x64-setup.exe",
8
- "macos_x64_asset_name": "CodexPlusPlus-1.2.15-macos-x64.dmg",
9
- "macos_x64_asset_url": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/download/v1.2.15/CodexPlusPlus-1.2.15-macos-x64.dmg",
10
- "macos_arm64_asset_name": "CodexPlusPlus-1.2.15-macos-arm64.dmg",
11
- "macos_arm64_asset_url": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/download/v1.2.15/CodexPlusPlus-1.2.15-macos-arm64.dmg",
12
- "source_zip_url": "https://github.com/BigPizzaV3/CodexPlusPlus/archive/refs/tags/v1.2.15.zip",
13
- "install_spec": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/download/v1.2.15/CodexPlusPlus-1.2.15-macos-arm64.dmg",
14
6
  "commit": "324a350d00376d4f9565aeeae3c8820b0cbbbe2a",
15
7
  "repository": "BigPizzaV3/CodexPlusPlus"
16
8
  }