@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.
- package/codex_plus_plus_launcher/__init__.py +3 -1
- package/codex_plus_plus_launcher/doctor.py +25 -1
- package/codex_plus_plus_launcher/runtime.py +106 -0
- package/npm/build-local-binary.cjs +24 -2
- package/npm/i18n.js +6 -0
- package/npm/launcher.js +725 -11
- package/package.json +3 -1
- package/patches/codex-plus-plus-plugin-unlock.patch +109 -4
- package/upstream-bin/darwin-arm64/codex-plus-plus +0 -0
- package/upstream-bin/darwin-arm64/codex-plus-plus-manager +0 -0
- package/upstream-bin/darwin-x64/codex-plus-plus +0 -0
- package/upstream-bin/darwin-x64/codex-plus-plus-manager +0 -0
- package/upstream-bin/linux-x64/codex-plus-plus +0 -0
- package/upstream-bin/upstream-release.json +5 -13
- package/upstream-bin/win32-x64/codex-plus-plus-manager.exe +0 -0
- package/upstream-bin/win32-x64/codex-plus-plus.exe +0 -0
|
@@ -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":
|
|
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
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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
|
-
|
|
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,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,
|
|
5
|
+
@@ -67,6 +67,7 @@
|
|
6
|
+
const codexServiceTierRequestOverrideVersion = "3";
|
|
7
|
+
const codexAppServerModelRequestPatchVersion = "1";
|
|
8
|
+
const codexPluginMarketplaceUnlockVersion = "10";
|
|
4
9
|
+ const codexPluginNavUnlockVersion = "2";
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
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
|
}
|
|
Binary file
|
|
Binary file
|