@walkhi/code-relax 0.1.0-beta.50 → 0.1.0-beta.59

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.
Files changed (101) hide show
  1. package/README.md +105 -105
  2. package/dist/bin/cleanup-accounts.mjs +63 -63
  3. package/dist/bin/codex-remote.mjs +403 -403
  4. package/dist/bin/publish-android-release.mjs +63 -63
  5. package/dist/bin/release-upload-server.mjs +190 -190
  6. package/dist/bin/set-relay-admin-password.mjs +11 -0
  7. package/dist/build-platform.json +1 -1
  8. package/dist/get-shared-service-url.ps1 +14 -14
  9. package/dist/shared/code-browser.cjs +159 -159
  10. package/dist/shared/file-changes.cjs +31 -31
  11. package/dist/shared/file-citations.cjs +117 -117
  12. package/dist/shared/file-mentions.cjs +92 -92
  13. package/dist/shared/message-delivery.cjs +115 -115
  14. package/dist/shared/p2p-file-channel.cjs +87 -87
  15. package/dist/shared/release-version.cjs +31 -0
  16. package/dist/shared/server-requests.cjs +311 -311
  17. package/dist/shared/timeline-sync.cjs +173 -173
  18. package/dist/skills/code-relax/SKILL.md +22 -22
  19. package/dist/src/bounded-process-log.mjs +63 -63
  20. package/dist/src/celsius-client.mjs +107 -107
  21. package/dist/src/dots-approval-timeline.mjs +99 -99
  22. package/dist/src/dots-avatar.mjs +42 -42
  23. package/dist/src/dots-chat-projection.mjs +50 -50
  24. package/dist/src/dots-chat-server.mjs +483 -483
  25. package/dist/src/dots-message-images.mjs +319 -319
  26. package/dist/src/dots-profile.mjs +77 -77
  27. package/dist/src/dots-room-activity.mjs +172 -172
  28. package/dist/src/dots-room-client.mjs +137 -135
  29. package/dist/src/dots-room-projection.mjs +94 -94
  30. package/dist/src/dots-schedules.mjs +66 -66
  31. package/dist/src/dots-summary-tasks.mjs +32 -32
  32. package/dist/src/dots-task-catalog.mjs +430 -430
  33. package/dist/src/file-transfer-range.mjs +22 -22
  34. package/dist/src/file-upload-session.mjs +146 -130
  35. package/dist/src/hosted-history-app-server.mjs +103 -103
  36. package/dist/src/hosted-history-cache.mjs +113 -113
  37. package/dist/src/lan-socket-server.mjs +338 -266
  38. package/dist/src/managed-app-server.mjs +373 -373
  39. package/dist/src/managed-endpoint.mjs +25 -25
  40. package/dist/src/message-upload-files.mjs +109 -109
  41. package/dist/src/onboarding.mjs +12 -12
  42. package/dist/src/platform/README.md +1 -1
  43. package/dist/src/platform/codex-protocol.mjs +46 -46
  44. package/dist/src/platform/desktop-project-store.mjs +219 -208
  45. package/dist/src/platform/linux/desktop-host.mjs +87 -87
  46. package/dist/src/platform/linux/process.mjs +53 -53
  47. package/dist/src/platform/linux/system.mjs +50 -50
  48. package/dist/src/platform/linux/user-environment.mjs +9 -9
  49. package/dist/src/platform/macos/codex-runtime.mjs +125 -125
  50. package/dist/src/platform/macos/desktop-host.mjs +86 -86
  51. package/dist/src/platform/macos/user-environment.mjs +9 -9
  52. package/dist/src/platform/native-package.mjs +58 -58
  53. package/dist/src/platform/windows/activate-desktop.ps1 +49 -49
  54. package/dist/src/platform/windows/codex-runtime.mjs +215 -215
  55. package/dist/src/platform/windows/credential-store.mjs +27 -27
  56. package/dist/src/platform/windows/credential-store.ps1 +49 -49
  57. package/dist/src/platform/windows/desktop-host.mjs +65 -65
  58. package/dist/src/platform/windows/desktop-info.ps1 +38 -38
  59. package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
  60. package/dist/src/platform/windows/desktop-shortcuts.ps1 +14 -14
  61. package/dist/src/platform/windows/dots-profile.mjs +49 -49
  62. package/dist/src/platform/windows/launch-worker.mjs +41 -41
  63. package/dist/src/platform/windows/project-locations.mjs +16 -16
  64. package/dist/src/platform/windows/service-host.mjs +35 -35
  65. package/dist/src/platform/windows/user-environment.mjs +52 -52
  66. package/dist/src/project-management.mjs +58 -58
  67. package/dist/src/runtime-discovery.mjs +47 -47
  68. package/dist/src/self-relay/account-cleanup.mjs +30 -30
  69. package/dist/src/self-relay/admin-auth.mjs +59 -0
  70. package/dist/src/self-relay/admin-login-page.mjs +7 -0
  71. package/dist/src/self-relay/admin-page.mjs +87 -80
  72. package/dist/src/self-relay/admin-server.mjs +49 -18
  73. package/dist/src/self-relay/admin-style.mjs +58 -14
  74. package/dist/src/self-relay/admin-workstations-page.mjs +95 -0
  75. package/dist/src/self-relay/admin-workstations.mjs +143 -0
  76. package/dist/src/self-relay/connection-reporting.mjs +38 -0
  77. package/dist/src/self-relay/connector.mjs +444 -439
  78. package/dist/src/self-relay/demo.mjs +31 -31
  79. package/dist/src/self-relay/diagnostics.mjs +54 -54
  80. package/dist/src/self-relay/email-accounts.mjs +183 -183
  81. package/dist/src/self-relay/file-wire.mjs +13 -13
  82. package/dist/src/self-relay/lifecycle.mjs +11 -0
  83. package/dist/src/self-relay/p2p-probe.mjs +168 -168
  84. package/dist/src/self-relay/phone-connections.mjs +55 -0
  85. package/dist/src/self-relay/privacy-page.mjs +26 -26
  86. package/dist/src/self-relay/public-releases.mjs +67 -67
  87. package/dist/src/self-relay/rtc-file-bridge.mjs +46 -41
  88. package/dist/src/self-relay/server.mjs +63 -3
  89. package/dist/src/self-relay/usage-meter.mjs +141 -141
  90. package/dist/src/server.mjs +23 -9
  91. package/dist/src/service-doctor.mjs +148 -148
  92. package/dist/src/service-install.mjs +97 -97
  93. package/dist/src/service-lifecycle.mjs +139 -139
  94. package/dist/src/service-update.mjs +6 -3
  95. package/dist/src/shared-app-server.mjs +683 -666
  96. package/dist/src/shared-catalog.mjs +132 -132
  97. package/dist/src/shared-thread-stream.mjs +165 -165
  98. package/dist/src/usage-reset.mjs +17 -17
  99. package/dist/src/workstation-downloads.mjs +120 -120
  100. package/package.json +4 -4
  101. package/tools/postinstall.mjs +186 -186
@@ -1,53 +1,53 @@
1
- import fs from 'node:fs';
2
- import { managedEndpointUrl } from '../../managed-endpoint.mjs';
3
-
4
- const gone = error => ['ENOENT', 'ESRCH'].includes(error.code);
5
- let bootId;
6
- export function processInfo(pid) {
7
- if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error('无效的进程 ID。');
8
- const root = `/proc/${pid}`;
9
- try {
10
- if (fs.statSync(root).uid !== process.getuid()) return null;
11
- const stat = fs.readFileSync(`${root}/stat`, 'utf8');
12
- const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
13
- if (fields[0] === 'Z') return null;
14
- const ticks = fields[19];
15
- bootId ??= fs.readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim();
16
- const created = `${bootId}:${ticks}`;
17
- const executable = fs.readlinkSync(`${root}/exe`);
18
- const args = fs.readFileSync(`${root}/cmdline`, 'utf8').split('\0').filter(Boolean);
19
- const after = fs.readFileSync(`${root}/stat`, 'utf8');
20
- if (after.slice(after.lastIndexOf(')') + 2).split(' ')[19] !== ticks) return null;
21
- return { pid, created, executable, args, command: args.join(' ') };
22
- } catch (error) { if (gone(error)) return null; throw error; }
23
- }
24
- export const sameProcess = (a, b) => !!a && !!b && a.pid === b.pid && a.created === b.created
25
- && a.executable === b.executable && JSON.stringify(a.args) === JSON.stringify(b.args);
26
- export function stopProcess(pid, expected) {
27
- if (expected?.pid !== pid || !sameProcess(expected, processInfo(pid))) throw new Error('进程身份已变化,未结束进程。');
28
- process.kill(pid, 'SIGTERM');
29
- return { stopped: true };
30
- }
31
- function hasManagedSocket(pid, url, listener) {
32
- const endpoint = managedEndpointUrl(url);
33
- if (!endpoint) return false;
34
- const port = Number(endpoint.slice(endpoint.lastIndexOf(':') + 1)).toString(16).toUpperCase().padStart(4, '0');
35
- try {
36
- const sockets = new Set(fs.readdirSync(`/proc/${pid}/fd`).flatMap(fd => {
37
- try { return /^socket:\[(\d+)\]$/.exec(fs.readlinkSync(`/proc/${pid}/fd/${fd}`))?.slice(1) || []; }
38
- catch (error) { if (gone(error)) return []; throw error; }
39
- }));
40
- return ['tcp', 'tcp6'].some(table => fs.readFileSync(`/proc/${pid}/net/${table}`, 'utf8').trim().split('\n').slice(1).some(line => {
41
- const fields = line.trim().split(/\s+/);
42
- return fields[3] === (listener ? '0A' : '01') && sockets.has(fields[9])
43
- && [`0100007F:${port}`, `0000000000000000FFFF00000100007F:${port}`].includes(fields[listener ? 1 : 2]);
44
- }));
45
- } catch (error) { if (gone(error)) return false; throw error; }
46
- }
47
-
48
- export const connectedToManagedServer = (pid, url) => hasManagedSocket(pid, url, false);
49
- export function ownsManagedListener(expected, url) {
50
- return !!expected && sameProcess(expected, processInfo(expected.pid))
51
- && hasManagedSocket(expected.pid, url, true)
52
- && sameProcess(expected, processInfo(expected.pid));
53
- }
1
+ import fs from 'node:fs';
2
+ import { managedEndpointUrl } from '../../managed-endpoint.mjs';
3
+
4
+ const gone = error => ['ENOENT', 'ESRCH'].includes(error.code);
5
+ let bootId;
6
+ export function processInfo(pid) {
7
+ if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error('无效的进程 ID。');
8
+ const root = `/proc/${pid}`;
9
+ try {
10
+ if (fs.statSync(root).uid !== process.getuid()) return null;
11
+ const stat = fs.readFileSync(`${root}/stat`, 'utf8');
12
+ const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
13
+ if (fields[0] === 'Z') return null;
14
+ const ticks = fields[19];
15
+ bootId ??= fs.readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim();
16
+ const created = `${bootId}:${ticks}`;
17
+ const executable = fs.readlinkSync(`${root}/exe`);
18
+ const args = fs.readFileSync(`${root}/cmdline`, 'utf8').split('\0').filter(Boolean);
19
+ const after = fs.readFileSync(`${root}/stat`, 'utf8');
20
+ if (after.slice(after.lastIndexOf(')') + 2).split(' ')[19] !== ticks) return null;
21
+ return { pid, created, executable, args, command: args.join(' ') };
22
+ } catch (error) { if (gone(error)) return null; throw error; }
23
+ }
24
+ export const sameProcess = (a, b) => !!a && !!b && a.pid === b.pid && a.created === b.created
25
+ && a.executable === b.executable && JSON.stringify(a.args) === JSON.stringify(b.args);
26
+ export function stopProcess(pid, expected) {
27
+ if (expected?.pid !== pid || !sameProcess(expected, processInfo(pid))) throw new Error('进程身份已变化,未结束进程。');
28
+ process.kill(pid, 'SIGTERM');
29
+ return { stopped: true };
30
+ }
31
+ function hasManagedSocket(pid, url, listener) {
32
+ const endpoint = managedEndpointUrl(url);
33
+ if (!endpoint) return false;
34
+ const port = Number(endpoint.slice(endpoint.lastIndexOf(':') + 1)).toString(16).toUpperCase().padStart(4, '0');
35
+ try {
36
+ const sockets = new Set(fs.readdirSync(`/proc/${pid}/fd`).flatMap(fd => {
37
+ try { return /^socket:\[(\d+)\]$/.exec(fs.readlinkSync(`/proc/${pid}/fd/${fd}`))?.slice(1) || []; }
38
+ catch (error) { if (gone(error)) return []; throw error; }
39
+ }));
40
+ return ['tcp', 'tcp6'].some(table => fs.readFileSync(`/proc/${pid}/net/${table}`, 'utf8').trim().split('\n').slice(1).some(line => {
41
+ const fields = line.trim().split(/\s+/);
42
+ return fields[3] === (listener ? '0A' : '01') && sockets.has(fields[9])
43
+ && [`0100007F:${port}`, `0000000000000000FFFF00000100007F:${port}`].includes(fields[listener ? 1 : 2]);
44
+ }));
45
+ } catch (error) { if (gone(error)) return false; throw error; }
46
+ }
47
+
48
+ export const connectedToManagedServer = (pid, url) => hasManagedSocket(pid, url, false);
49
+ export function ownsManagedListener(expected, url) {
50
+ return !!expected && sameProcess(expected, processInfo(expected.pid))
51
+ && hasManagedSocket(expected.pid, url, true)
52
+ && sameProcess(expected, processInfo(expected.pid));
53
+ }
@@ -1,50 +1,50 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { installedDesktop, verifyDesktopPackage } from './desktop-installation.mjs';
4
- import { processInfo, sameProcess, stopProcess, connectedToManagedServer, ownsManagedListener } from './process.mjs';
5
- import { startBackground } from '../../background-process.mjs';
6
-
7
- // Package upgrades unlink the old executable while its process stays alive.
8
- // Match that process for Desktop discovery, but retain its exact /proc identity
9
- // for sameProcess/stopProcess so a changed process is never signalled blindly.
10
- const desktopExecutableMatches = (actual, installed) => actual === installed || actual === `${installed} (deleted)`;
11
-
12
- export async function systemRequest(request) {
13
- if (request.operation === 'owns-listener') return ownsManagedListener(request.expected, request.url);
14
- if (request.operation === 'process') return processInfo(request.pid);
15
- if (request.operation === 'stop') return stopProcess(request.expected?.pid, request.expected);
16
- const installed = installedDesktop();
17
- if (request.operation === 'desktop-info') {
18
- const running = [];
19
- if (installed) for (const name of fs.readdirSync('/proc').filter(name => /^\d+$/.test(name))) {
20
- try {
21
- if (fs.statSync(`/proc/${name}`).uid !== process.getuid()
22
- || !desktopExecutableMatches(fs.readlinkSync(`/proc/${name}/exe`), installed.executable)) continue;
23
- } catch (error) { if (['ENOENT', 'ESRCH', 'EACCES'].includes(error.code)) continue; throw error; }
24
- const info = processInfo(Number(name));
25
- if (info && desktopExecutableMatches(info.executable, installed.executable) && !info.args.some(arg => arg.startsWith('--type='))) running.push(info);
26
- }
27
- if (running.length > 1) throw new Error('发现多个 ChatGPT 桌面实例,请先关闭多余实例。');
28
- const info = running[0] || null;
29
- const connected = info ? connectedToManagedServer(info.pid, request.url) : false;
30
- if (info && !sameProcess(info, processInfo(info.pid))) throw new Error('核对期间桌面进程发生变化。');
31
- return { ...(installed || { desktop: null, version: '', build: '' }), running: !!info,
32
- finishedLaunching: connected, connectedToManagedServer: connected, process: info };
33
- }
34
- if (!installed || installed.desktop !== request.bundlePath) throw new Error('桌面版安装位置已变化。');
35
- if (request.operation === 'desktop-start') {
36
- verifyDesktopPackage(installed.desktop);
37
- if (!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) throw new Error('请在 Linux 图形桌面的当前用户会话中运行 relax setup。');
38
- const environment = { ...process.env, ...request.environment };
39
- delete environment.CODEX_APP_SERVER_FORCE_CLI;
40
- delete environment.CODEX_APP_TOOLS_PIPE_PATH;
41
- const pid = await startBackground(path.join(installed.desktop, 'codex-launcher'), [], environment);
42
- return { pid, started: true };
43
- }
44
- if (request.operation === 'desktop-close') {
45
- // Electron's POSIX signal handler calls Browser::Quit for SIGTERM. Wait
46
- // for exit before touching the store; never escalate to SIGKILL.
47
- return stopProcess(request.expected?.pid, request.expected);
48
- }
49
- throw new Error(`不支持的 Linux 系统操作:${request.operation}`);
50
- }
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { installedDesktop, verifyDesktopPackage } from './desktop-installation.mjs';
4
+ import { processInfo, sameProcess, stopProcess, connectedToManagedServer, ownsManagedListener } from './process.mjs';
5
+ import { startBackground } from '../../background-process.mjs';
6
+
7
+ // Package upgrades unlink the old executable while its process stays alive.
8
+ // Match that process for Desktop discovery, but retain its exact /proc identity
9
+ // for sameProcess/stopProcess so a changed process is never signalled blindly.
10
+ const desktopExecutableMatches = (actual, installed) => actual === installed || actual === `${installed} (deleted)`;
11
+
12
+ export async function systemRequest(request) {
13
+ if (request.operation === 'owns-listener') return ownsManagedListener(request.expected, request.url);
14
+ if (request.operation === 'process') return processInfo(request.pid);
15
+ if (request.operation === 'stop') return stopProcess(request.expected?.pid, request.expected);
16
+ const installed = installedDesktop();
17
+ if (request.operation === 'desktop-info') {
18
+ const running = [];
19
+ if (installed) for (const name of fs.readdirSync('/proc').filter(name => /^\d+$/.test(name))) {
20
+ try {
21
+ if (fs.statSync(`/proc/${name}`).uid !== process.getuid()
22
+ || !desktopExecutableMatches(fs.readlinkSync(`/proc/${name}/exe`), installed.executable)) continue;
23
+ } catch (error) { if (['ENOENT', 'ESRCH', 'EACCES'].includes(error.code)) continue; throw error; }
24
+ const info = processInfo(Number(name));
25
+ if (info && desktopExecutableMatches(info.executable, installed.executable) && !info.args.some(arg => arg.startsWith('--type='))) running.push(info);
26
+ }
27
+ if (running.length > 1) throw new Error('发现多个 ChatGPT 桌面实例,请先关闭多余实例。');
28
+ const info = running[0] || null;
29
+ const connected = info ? connectedToManagedServer(info.pid, request.url) : false;
30
+ if (info && !sameProcess(info, processInfo(info.pid))) throw new Error('核对期间桌面进程发生变化。');
31
+ return { ...(installed || { desktop: null, version: '', build: '' }), running: !!info,
32
+ finishedLaunching: connected, connectedToManagedServer: connected, process: info };
33
+ }
34
+ if (!installed || installed.desktop !== request.bundlePath) throw new Error('桌面版安装位置已变化。');
35
+ if (request.operation === 'desktop-start') {
36
+ verifyDesktopPackage(installed.desktop);
37
+ if (!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) throw new Error('请在 Linux 图形桌面的当前用户会话中运行 relax setup。');
38
+ const environment = { ...process.env, ...request.environment };
39
+ delete environment.CODEX_APP_SERVER_FORCE_CLI;
40
+ delete environment.CODEX_APP_TOOLS_PIPE_PATH;
41
+ const pid = await startBackground(path.join(installed.desktop, 'codex-launcher'), [], environment);
42
+ return { pid, started: true };
43
+ }
44
+ if (request.operation === 'desktop-close') {
45
+ // Electron's POSIX signal handler calls Browser::Quit for SIGTERM. Wait
46
+ // for exit before touching the store; never escalate to SIGKILL.
47
+ return stopProcess(request.expected?.pid, request.expected);
48
+ }
49
+ throw new Error(`不支持的 Linux 系统操作:${request.operation}`);
50
+ }
@@ -1,9 +1,9 @@
1
- import { managedEndpointUrl } from '../../managed-endpoint.mjs';
2
- // Only the new Desktop process receives the managed connection settings.
3
- // Do not mutate shell profiles or the login session's global environment.
4
- export async function configureManagedEnvironment(stateRoot, url) {
5
- if (!managedEndpointUrl(url)) throw new Error('未确认 Codex 内核地址。');
6
- return { configured: true, changed: false, owned: false, persistent: false,
7
- scope: 'application-launch', value: url };
8
- }
9
- export async function clearManagedEnvironment() { return { removed: false, preserved: false, reason: 'application-launch-only' }; }
1
+ import { managedEndpointUrl } from '../../managed-endpoint.mjs';
2
+ // Only the new Desktop process receives the managed connection settings.
3
+ // Do not mutate shell profiles or the login session's global environment.
4
+ export async function configureManagedEnvironment(stateRoot, url) {
5
+ if (!managedEndpointUrl(url)) throw new Error('未确认 Codex 内核地址。');
6
+ return { configured: true, changed: false, owned: false, persistent: false,
7
+ scope: 'application-launch', value: url };
8
+ }
9
+ export async function clearManagedEnvironment() { return { removed: false, preserved: false, reason: 'application-launch-only' }; }
@@ -1,125 +1,125 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { createRequire } from 'node:module';
5
- import { execFile, execFileSync } from 'node:child_process';
6
- import { promisify } from 'node:util';
7
- import { compareVersions, verifyProtocol } from '../codex-protocol.mjs';
8
- import { readRecord, writeRecord } from '../../service-lifecycle.mjs';
9
- import { retainRuntime, runtimeFingerprint, packageRoot } from './retained-runtime.mjs';
10
- export { runtimeFingerprint, pruneRetainedDesktopRuntimes } from './retained-runtime.mjs';
11
-
12
- const exec = promisify(execFile);
13
- export const codeModeHostPath = executable => path.join(packageRoot(executable), 'bin/codex-code-mode-host');
14
- const fileExists = file => { try { return fs.statSync(file).isFile(); } catch (error) { if (error.code === 'ENOENT') return false; throw error; } };
15
- export function nativeExecutable(file) {
16
- if (!fileExists(file)) return false;
17
- const descriptor = fs.openSync(file, 'r');
18
- try {
19
- const magic = Buffer.alloc(4);
20
- if (fs.readSync(descriptor, magic, 0, 4, 0) !== 4) return false;
21
- return [0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe, 0xcafebabe, 0xbebafeca, 0xcafebabf, 0xbfbafeca].includes(magic.readUInt32BE());
22
- } finally { fs.closeSync(descriptor); }
23
- }
24
- export function runtimeFilesAvailable(executable) {
25
- try { return nativeExecutable(executable) && nativeExecutable(codeModeHostPath(executable)); }
26
- catch (error) { if (error.code === 'EACCES') throw error; return false; }
27
- }
28
- function nativeEntry(file) {
29
- if (!fileExists(file)) return null;
30
- file = fs.realpathSync(file);
31
- if (nativeExecutable(file)) return file;
32
- if (path.basename(path.dirname(file)) === 'bin') {
33
- const bundle = path.resolve(file, '../../CodexCLI.app/Contents/MacOS/codex');
34
- if (nativeExecutable(bundle)) return bundle;
35
- }
36
- return null;
37
- }
38
-
39
- // Match the official CLI's native package mapping. Never launch the npm JS shim as a kernel.
40
- export function discoverCodex(environment = process.env, { home = os.homedir(), arch = process.arch } = {}) {
41
- if (!['arm64', 'x64'].includes(arch)) throw new Error(`不支持的 Mac CPU:${arch}`);
42
- const found = new Map();
43
- const add = (file, source) => {
44
- file = nativeEntry(file);
45
- if (!file) return;
46
- fs.accessSync(file, fs.constants.X_OK);
47
- const executable = fs.realpathSync(file);
48
- if (!found.has(executable)) found.set(executable, { executable, source });
49
- };
50
- if (environment.CODE_RELAX_CODEX_PATH) {
51
- const file = environment.CODE_RELAX_CODEX_PATH;
52
- if (!path.isAbsolute(file) || path.basename(file) !== 'codex') throw new Error('CODE_RELAX_CODEX_PATH 必须是原生 codex 程序的绝对路径。');
53
- add(file, 'explicit');
54
- if (!found.size) throw new Error('指定的 Codex 原生程序不存在或不是 Mach-O 可执行文件。');
55
- return [...found.values()];
56
- }
57
- for (const directory of ['/Applications', path.join(home, 'Applications')]) {
58
- for (const name of ['ChatGPT.app', 'Codex.app']) {
59
- const bundle = path.join(directory, name);
60
- if (!fileExists(path.join(bundle, 'Contents/Info.plist'))) continue;
61
- const id = execFileSync('/usr/bin/plutil', ['-extract', 'CFBundleIdentifier', 'raw', '-o', '-', path.join(bundle, 'Contents/Info.plist')], { encoding: 'utf8', timeout: 5000 }).trim();
62
- if (id !== 'com.openai.codex') continue;
63
- add(path.join(bundle, 'Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex'), 'desktop');
64
- }
65
- }
66
- const codexHome = environment.CODEX_HOME?.trim() || path.join(home, '.codex');
67
- const directories = [...String(environment.PATH || '').split(path.delimiter), environment.CODEX_INSTALL_DIR,
68
- path.join(home, '.local/bin'), path.join(codexHome, 'packages/standalone/current/bin')].filter(item => item && path.isAbsolute(item));
69
- const target = `${arch === 'arm64' ? 'aarch64' : 'x86_64'}-apple-darwin`;
70
- for (const directory of new Set(directories)) {
71
- const entry = path.join(directory, 'codex');
72
- if (!fileExists(entry)) continue;
73
- if (nativeEntry(entry)) { add(entry, 'cli'); continue; }
74
- let root = path.dirname(fs.realpathSync(entry));
75
- while (true) {
76
- const packageFile = path.join(root, 'package.json');
77
- if (fileExists(packageFile) && readRecord(packageFile)?.name === '@openai/codex') {
78
- let vendor = path.join(root, 'vendor');
79
- try { vendor = path.join(path.dirname(createRequire(packageFile).resolve(`@openai/codex-darwin-${arch}/package.json`)), 'vendor'); }
80
- catch (error) { if (error.code !== 'MODULE_NOT_FOUND') throw error; }
81
- add(path.join(vendor, target, 'bin/codex'), 'npm');
82
- break;
83
- }
84
- const parent = path.dirname(root);
85
- if (parent === root) break;
86
- root = parent;
87
- }
88
- }
89
- return [...found.values()];
90
- }
91
-
92
- export async function resolveCodexRuntime(stateRoot, { refresh = false, environment = process.env } = {}) {
93
- fs.mkdirSync(stateRoot, { recursive: true, mode: 0o700 });
94
- const recordFile = path.join(stateRoot, 'codex-runtime.json');
95
- const saved = readRecord(recordFile);
96
- const codexHome = path.resolve(environment.CODEX_HOME?.trim() || saved?.codexHome || path.join(os.homedir(), '.codex'));
97
- const explicit = environment.CODE_RELAX_CODEX_PATH || (saved?.source === 'explicit' ? saved.sourceExecutable : '');
98
- if (!refresh && saved?.executable && (!explicit || explicit === saved.sourceExecutable) && codexHome === saved.codexHome
99
- && runtimeFilesAvailable(saved.executable) && saved.fingerprint === runtimeFingerprint(saved.executable)) return saved;
100
- const candidates = [], rejected = [];
101
- for (const candidate of discoverCodex({ ...environment, CODEX_HOME: codexHome, CODE_RELAX_CODEX_PATH: explicit })) {
102
- try {
103
- if (candidate.source === 'desktop') {
104
- const bundle = path.resolve(packageRoot(candidate.executable), '../../..');
105
- await exec('/usr/bin/codesign', ['--verify', '--deep', '--strict', '-R=identifier \"com.openai.codex\" and anchor apple generic and certificate leaf[subject.OU] = \"2DC432GLL2\"', bundle], { timeout: 30000 });
106
- }
107
- if (!runtimeFilesAvailable(candidate.executable)) throw new Error('缺少配套原生 codex-code-mode-host');
108
- const { stdout } = await exec(candidate.executable, ['--version'], { timeout: 10000, env: { ...environment, CODEX_HOME: codexHome } });
109
- const version = /^codex-cli (\d+\.\d+\.\d+(?:-[\w.]+)?)/m.exec(stdout)?.[1];
110
- if (!version) throw new Error('无法识别 Codex 版本');
111
- candidates.push({ ...candidate, version });
112
- } catch (error) { rejected.push(`${candidate.executable}: ${error.message}`); }
113
- }
114
- candidates.sort((a, b) => Number(b.source === 'desktop') - Number(a.source === 'desktop') || compareVersions(b.version, a.version));
115
- for (const candidate of candidates) {
116
- try {
117
- const retained = await retainRuntime(candidate.executable, stateRoot, candidate.version);
118
- await verifyProtocol(retained.executable, stateRoot);
119
- const result = { ...candidate, ...retained, codexHome, fingerprint: runtimeFingerprint(retained.executable) };
120
- writeRecord(recordFile, result);
121
- return result;
122
- } catch (error) { rejected.push(`${candidate.executable}: ${error.message}`); }
123
- }
124
- throw new Error(`未找到具备所需协议能力的 Mac Codex 内核,请安装官方 Codex 应用或完整 CLI 并登录账号。${rejected.length ? '\n' + rejected.join('\n') : ''}`);
125
- }
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { createRequire } from 'node:module';
5
+ import { execFile, execFileSync } from 'node:child_process';
6
+ import { promisify } from 'node:util';
7
+ import { compareVersions, verifyProtocol } from '../codex-protocol.mjs';
8
+ import { readRecord, writeRecord } from '../../service-lifecycle.mjs';
9
+ import { retainRuntime, runtimeFingerprint, packageRoot } from './retained-runtime.mjs';
10
+ export { runtimeFingerprint, pruneRetainedDesktopRuntimes } from './retained-runtime.mjs';
11
+
12
+ const exec = promisify(execFile);
13
+ export const codeModeHostPath = executable => path.join(packageRoot(executable), 'bin/codex-code-mode-host');
14
+ const fileExists = file => { try { return fs.statSync(file).isFile(); } catch (error) { if (error.code === 'ENOENT') return false; throw error; } };
15
+ export function nativeExecutable(file) {
16
+ if (!fileExists(file)) return false;
17
+ const descriptor = fs.openSync(file, 'r');
18
+ try {
19
+ const magic = Buffer.alloc(4);
20
+ if (fs.readSync(descriptor, magic, 0, 4, 0) !== 4) return false;
21
+ return [0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe, 0xcafebabe, 0xbebafeca, 0xcafebabf, 0xbfbafeca].includes(magic.readUInt32BE());
22
+ } finally { fs.closeSync(descriptor); }
23
+ }
24
+ export function runtimeFilesAvailable(executable) {
25
+ try { return nativeExecutable(executable) && nativeExecutable(codeModeHostPath(executable)); }
26
+ catch (error) { if (error.code === 'EACCES') throw error; return false; }
27
+ }
28
+ function nativeEntry(file) {
29
+ if (!fileExists(file)) return null;
30
+ file = fs.realpathSync(file);
31
+ if (nativeExecutable(file)) return file;
32
+ if (path.basename(path.dirname(file)) === 'bin') {
33
+ const bundle = path.resolve(file, '../../CodexCLI.app/Contents/MacOS/codex');
34
+ if (nativeExecutable(bundle)) return bundle;
35
+ }
36
+ return null;
37
+ }
38
+
39
+ // Match the official CLI's native package mapping. Never launch the npm JS shim as a kernel.
40
+ export function discoverCodex(environment = process.env, { home = os.homedir(), arch = process.arch } = {}) {
41
+ if (!['arm64', 'x64'].includes(arch)) throw new Error(`不支持的 Mac CPU:${arch}`);
42
+ const found = new Map();
43
+ const add = (file, source) => {
44
+ file = nativeEntry(file);
45
+ if (!file) return;
46
+ fs.accessSync(file, fs.constants.X_OK);
47
+ const executable = fs.realpathSync(file);
48
+ if (!found.has(executable)) found.set(executable, { executable, source });
49
+ };
50
+ if (environment.CODE_RELAX_CODEX_PATH) {
51
+ const file = environment.CODE_RELAX_CODEX_PATH;
52
+ if (!path.isAbsolute(file) || path.basename(file) !== 'codex') throw new Error('CODE_RELAX_CODEX_PATH 必须是原生 codex 程序的绝对路径。');
53
+ add(file, 'explicit');
54
+ if (!found.size) throw new Error('指定的 Codex 原生程序不存在或不是 Mach-O 可执行文件。');
55
+ return [...found.values()];
56
+ }
57
+ for (const directory of ['/Applications', path.join(home, 'Applications')]) {
58
+ for (const name of ['ChatGPT.app', 'Codex.app']) {
59
+ const bundle = path.join(directory, name);
60
+ if (!fileExists(path.join(bundle, 'Contents/Info.plist'))) continue;
61
+ const id = execFileSync('/usr/bin/plutil', ['-extract', 'CFBundleIdentifier', 'raw', '-o', '-', path.join(bundle, 'Contents/Info.plist')], { encoding: 'utf8', timeout: 5000 }).trim();
62
+ if (id !== 'com.openai.codex') continue;
63
+ add(path.join(bundle, 'Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex'), 'desktop');
64
+ }
65
+ }
66
+ const codexHome = environment.CODEX_HOME?.trim() || path.join(home, '.codex');
67
+ const directories = [...String(environment.PATH || '').split(path.delimiter), environment.CODEX_INSTALL_DIR,
68
+ path.join(home, '.local/bin'), path.join(codexHome, 'packages/standalone/current/bin')].filter(item => item && path.isAbsolute(item));
69
+ const target = `${arch === 'arm64' ? 'aarch64' : 'x86_64'}-apple-darwin`;
70
+ for (const directory of new Set(directories)) {
71
+ const entry = path.join(directory, 'codex');
72
+ if (!fileExists(entry)) continue;
73
+ if (nativeEntry(entry)) { add(entry, 'cli'); continue; }
74
+ let root = path.dirname(fs.realpathSync(entry));
75
+ while (true) {
76
+ const packageFile = path.join(root, 'package.json');
77
+ if (fileExists(packageFile) && readRecord(packageFile)?.name === '@openai/codex') {
78
+ let vendor = path.join(root, 'vendor');
79
+ try { vendor = path.join(path.dirname(createRequire(packageFile).resolve(`@openai/codex-darwin-${arch}/package.json`)), 'vendor'); }
80
+ catch (error) { if (error.code !== 'MODULE_NOT_FOUND') throw error; }
81
+ add(path.join(vendor, target, 'bin/codex'), 'npm');
82
+ break;
83
+ }
84
+ const parent = path.dirname(root);
85
+ if (parent === root) break;
86
+ root = parent;
87
+ }
88
+ }
89
+ return [...found.values()];
90
+ }
91
+
92
+ export async function resolveCodexRuntime(stateRoot, { refresh = false, environment = process.env } = {}) {
93
+ fs.mkdirSync(stateRoot, { recursive: true, mode: 0o700 });
94
+ const recordFile = path.join(stateRoot, 'codex-runtime.json');
95
+ const saved = readRecord(recordFile);
96
+ const codexHome = path.resolve(environment.CODEX_HOME?.trim() || saved?.codexHome || path.join(os.homedir(), '.codex'));
97
+ const explicit = environment.CODE_RELAX_CODEX_PATH || (saved?.source === 'explicit' ? saved.sourceExecutable : '');
98
+ if (!refresh && saved?.executable && (!explicit || explicit === saved.sourceExecutable) && codexHome === saved.codexHome
99
+ && runtimeFilesAvailable(saved.executable) && saved.fingerprint === runtimeFingerprint(saved.executable)) return saved;
100
+ const candidates = [], rejected = [];
101
+ for (const candidate of discoverCodex({ ...environment, CODEX_HOME: codexHome, CODE_RELAX_CODEX_PATH: explicit })) {
102
+ try {
103
+ if (candidate.source === 'desktop') {
104
+ const bundle = path.resolve(packageRoot(candidate.executable), '../../..');
105
+ await exec('/usr/bin/codesign', ['--verify', '--deep', '--strict', '-R=identifier \"com.openai.codex\" and anchor apple generic and certificate leaf[subject.OU] = \"2DC432GLL2\"', bundle], { timeout: 30000 });
106
+ }
107
+ if (!runtimeFilesAvailable(candidate.executable)) throw new Error('缺少配套原生 codex-code-mode-host');
108
+ const { stdout } = await exec(candidate.executable, ['--version'], { timeout: 10000, env: { ...environment, CODEX_HOME: codexHome } });
109
+ const version = /^codex-cli (\d+\.\d+\.\d+(?:-[\w.]+)?)/m.exec(stdout)?.[1];
110
+ if (!version) throw new Error('无法识别 Codex 版本');
111
+ candidates.push({ ...candidate, version });
112
+ } catch (error) { rejected.push(`${candidate.executable}: ${error.message}`); }
113
+ }
114
+ candidates.sort((a, b) => Number(b.source === 'desktop') - Number(a.source === 'desktop') || compareVersions(b.version, a.version));
115
+ for (const candidate of candidates) {
116
+ try {
117
+ const retained = await retainRuntime(candidate.executable, stateRoot, candidate.version);
118
+ await verifyProtocol(retained.executable, stateRoot);
119
+ const result = { ...candidate, ...retained, codexHome, fingerprint: runtimeFingerprint(retained.executable) };
120
+ writeRecord(recordFile, result);
121
+ return result;
122
+ } catch (error) { rejected.push(`${candidate.executable}: ${error.message}`); }
123
+ }
124
+ throw new Error(`未找到具备所需协议能力的 Mac Codex 内核,请安装官方 Codex 应用或完整 CLI 并登录账号。${rejected.length ? '\n' + rejected.join('\n') : ''}`);
125
+ }