@walkhi/code-relax 0.1.0-beta.30 → 0.1.0-beta.31

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 (185) hide show
  1. package/README.md +95 -89
  2. package/dist/bin/cleanup-accounts.mjs +63 -63
  3. package/dist/bin/codex-remote.mjs +374 -367
  4. package/dist/bin/manage-relay-admin.mjs +20 -20
  5. package/dist/bin/migrate-relay-db.mjs +109 -109
  6. package/dist/bin/publish-android-release.mjs +62 -58
  7. package/dist/bin/self-relay-admin.mjs +8 -8
  8. package/dist/bin/self-relay-demo.mjs +33 -33
  9. package/dist/bin/self-relay-server.mjs +66 -66
  10. package/dist/build-platform.json +1 -1
  11. package/dist/get-lan-networks.ps1 +40 -40
  12. package/dist/get-shared-service-url.ps1 +6 -6
  13. package/dist/shared/app-server-events.cjs +422 -412
  14. package/dist/shared/code-browser.cjs +159 -159
  15. package/dist/shared/file-changes.cjs +31 -31
  16. package/dist/shared/file-citations.cjs +117 -117
  17. package/dist/shared/file-mentions.cjs +92 -92
  18. package/dist/shared/message-delivery.cjs +54 -54
  19. package/dist/shared/p2p-data-channel.cjs +72 -72
  20. package/dist/shared/p2p-diagnostics.cjs +28 -28
  21. package/dist/shared/p2p-file-channel.cjs +74 -74
  22. package/dist/shared/plan-pricing.cjs +32 -32
  23. package/dist/shared/question-attention.cjs +40 -0
  24. package/dist/shared/server-requests.cjs +219 -219
  25. package/dist/shared/timeline-sync.cjs +173 -173
  26. package/dist/skills/code-relax/SKILL.md +22 -22
  27. package/dist/src/archived-threads.mjs +22 -0
  28. package/dist/src/background-process.mjs +18 -18
  29. package/dist/src/bounded-process-log.mjs +63 -63
  30. package/dist/src/celsius-client.mjs +107 -107
  31. package/dist/src/code-browser.mjs +100 -100
  32. package/dist/src/desktop-launcher.mjs +46 -46
  33. package/dist/src/dots-approval-timeline.mjs +99 -99
  34. package/dist/src/dots-avatar.mjs +42 -42
  35. package/dist/src/dots-chat-projection.mjs +50 -50
  36. package/dist/src/dots-chat-server.mjs +461 -461
  37. package/dist/src/dots-message-images.mjs +319 -319
  38. package/dist/src/dots-profile.mjs +77 -77
  39. package/dist/src/dots-room-activity.mjs +94 -94
  40. package/dist/src/dots-room-client.mjs +135 -135
  41. package/dist/src/dots-room-projection.mjs +94 -94
  42. package/dist/src/dots-schedules.mjs +66 -66
  43. package/dist/src/dots-summary-tasks.mjs +32 -32
  44. package/dist/src/dots-task-catalog.mjs +430 -430
  45. package/dist/src/dots-task-files.mjs +52 -52
  46. package/dist/src/execution-health.mjs +26 -26
  47. package/dist/src/file-transfer-range.mjs +22 -22
  48. package/dist/src/file-upload-session.mjs +130 -130
  49. package/dist/src/hosted-history-app-server.mjs +103 -103
  50. package/dist/src/hosted-history-cache.mjs +113 -113
  51. package/dist/src/lan-socket-server.mjs +265 -263
  52. package/dist/src/managed-app-server.mjs +244 -244
  53. package/dist/src/message-images.mjs +9 -9
  54. package/dist/src/message-upload-files.mjs +74 -74
  55. package/dist/src/onboarding.mjs +12 -12
  56. package/dist/src/plan-pricing.mjs +47 -47
  57. package/dist/src/platform/README.md +12 -12
  58. package/dist/src/platform/codex-protocol.mjs +46 -46
  59. package/dist/src/platform/codex-runtime.mjs +3 -3
  60. package/dist/src/platform/credential-store.mjs +3 -3
  61. package/dist/src/platform/desktop-host.mjs +3 -3
  62. package/dist/src/platform/desktop-project-registration.mjs +3 -3
  63. package/dist/src/platform/desktop-project-store.mjs +208 -149
  64. package/dist/src/platform/file-opener.mjs +9 -0
  65. package/dist/src/platform/linux/README.md +3 -3
  66. package/dist/src/platform/linux/codex-runtime.mjs +56 -0
  67. package/dist/src/platform/linux/credential-store.mjs +28 -0
  68. package/dist/src/platform/linux/desktop-host.mjs +83 -0
  69. package/dist/src/platform/linux/desktop-installation.mjs +36 -0
  70. package/dist/src/platform/linux/desktop-project-registration.mjs +11 -0
  71. package/dist/src/platform/linux/network-info.mjs +11 -0
  72. package/dist/src/platform/linux/process.mjs +42 -0
  73. package/dist/src/platform/linux/project-locations.mjs +17 -0
  74. package/dist/src/platform/linux/resident-startup.mjs +52 -0
  75. package/dist/src/platform/linux/retained-runtime.mjs +76 -0
  76. package/dist/src/platform/linux/service-host.mjs +18 -0
  77. package/dist/src/platform/linux/system.mjs +49 -0
  78. package/dist/src/platform/linux/user-environment.mjs +7 -0
  79. package/dist/src/platform/macos/README.md +3 -3
  80. package/dist/src/platform/macos/codex-runtime.mjs +125 -125
  81. package/dist/src/platform/macos/credential-store.mjs +10 -10
  82. package/dist/src/platform/macos/desktop-host.mjs +82 -82
  83. package/dist/src/platform/macos/desktop-project-registration.mjs +11 -10
  84. package/dist/src/platform/macos/network-info.mjs +8 -28
  85. package/dist/src/platform/macos/project-locations.mjs +8 -8
  86. package/dist/src/platform/macos/resident-startup.mjs +84 -84
  87. package/dist/src/platform/macos/retained-runtime.mjs +127 -127
  88. package/dist/src/platform/macos/service-host.mjs +18 -18
  89. package/dist/src/platform/macos/system.mjs +29 -29
  90. package/dist/src/platform/macos/user-environment.mjs +7 -7
  91. package/dist/src/platform/native-package.mjs +58 -46
  92. package/dist/src/platform/network-info.mjs +3 -3
  93. package/dist/src/platform/path-identity.mjs +5 -5
  94. package/dist/src/platform/posix/network-candidates.mjs +23 -0
  95. package/dist/src/platform/posix/runtime-paths.mjs +25 -25
  96. package/dist/src/platform/project-locations.mjs +3 -3
  97. package/dist/src/platform/runtime-paths.mjs +8 -8
  98. package/dist/src/platform/select.mjs +5 -5
  99. package/dist/src/platform/service-host.mjs +3 -3
  100. package/dist/src/platform/user-environment.mjs +3 -3
  101. package/dist/src/platform/windows/IsolatedProcess.cs +79 -79
  102. package/dist/src/platform/windows/activate-desktop.ps1 +48 -48
  103. package/dist/src/platform/windows/background-process.mjs +30 -30
  104. package/dist/src/platform/windows/codex-runtime.mjs +215 -215
  105. package/dist/src/platform/windows/credential-store.mjs +27 -27
  106. package/dist/src/platform/windows/credential-store.ps1 +49 -49
  107. package/dist/src/platform/windows/desktop-helper.mjs +179 -179
  108. package/dist/src/platform/windows/desktop-host.mjs +58 -58
  109. package/dist/src/platform/windows/desktop-info.ps1 +14 -14
  110. package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
  111. package/dist/src/platform/windows/desktop-runtime.ps1 +10 -10
  112. package/dist/src/platform/windows/desktop-shortcuts.ps1 +21 -21
  113. package/dist/src/platform/windows/dots-profile.mjs +49 -49
  114. package/dist/src/platform/windows/launch-worker.mjs +41 -41
  115. package/dist/src/platform/windows/network-info.mjs +34 -34
  116. package/dist/src/platform/windows/project-locations.mjs +16 -16
  117. package/dist/src/platform/windows/resident-start.js +20 -20
  118. package/dist/src/platform/windows/resident-start.ps1 +7 -7
  119. package/dist/src/platform/windows/resident-startup.ps1 +83 -83
  120. package/dist/src/platform/windows/resolve-state-root.ps1 +14 -14
  121. package/dist/src/platform/windows/runtime-paths.mjs +20 -20
  122. package/dist/src/platform/windows/rustdesk-video-probe.mjs +167 -167
  123. package/dist/src/platform/windows/service-host.mjs +31 -31
  124. package/dist/src/platform/windows/start-hidden-console.ps1 +49 -49
  125. package/dist/src/platform/windows/stop-desktop.ps1 +22 -22
  126. package/dist/src/platform/windows/user-environment.mjs +50 -50
  127. package/dist/src/platform/windows/user-environment.ps1 +41 -41
  128. package/dist/src/project-archive.mjs +30 -0
  129. package/dist/src/project-create.mjs +121 -119
  130. package/dist/src/project-management.mjs +59 -69
  131. package/dist/src/project-remove.mjs +55 -0
  132. package/dist/src/project-update.mjs +56 -0
  133. package/dist/src/runtime-discovery.mjs +47 -47
  134. package/dist/src/self-relay/account-cleanup.mjs +30 -30
  135. package/dist/src/self-relay/account-connections.mjs +52 -52
  136. package/dist/src/self-relay/admin-cloud.mjs +101 -101
  137. package/dist/src/self-relay/admin-page.mjs +80 -80
  138. package/dist/src/self-relay/admin-server.mjs +74 -74
  139. package/dist/src/self-relay/admin-state.mjs +127 -127
  140. package/dist/src/self-relay/admin-style.mjs +14 -14
  141. package/dist/src/self-relay/android-releases.mjs +61 -61
  142. package/dist/src/self-relay/client.mjs +61 -61
  143. package/dist/src/self-relay/connector.mjs +439 -438
  144. package/dist/src/self-relay/debug-allowlist.mjs +33 -33
  145. package/dist/src/self-relay/demo.mjs +37 -37
  146. package/dist/src/self-relay/device-info.mjs +27 -0
  147. package/dist/src/self-relay/device-store.mjs +92 -84
  148. package/dist/src/self-relay/diagnostics.mjs +54 -54
  149. package/dist/src/self-relay/email-accounts.mjs +183 -183
  150. package/dist/src/self-relay/file-wire.mjs +13 -13
  151. package/dist/src/self-relay/lan-authorizations.mjs +71 -42
  152. package/dist/src/self-relay/lifecycle.mjs +264 -250
  153. package/dist/src/self-relay/long-pairing.mjs +40 -40
  154. package/dist/src/self-relay/mysql-state.mjs +118 -105
  155. package/dist/src/self-relay/p2p-probe.mjs +159 -159
  156. package/dist/src/self-relay/privacy-page.mjs +26 -26
  157. package/dist/src/self-relay/public-releases.mjs +34 -0
  158. package/dist/src/self-relay/rtc-file-bridge.mjs +41 -41
  159. package/dist/src/self-relay/server.mjs +1006 -932
  160. package/dist/src/self-relay/usage-daily.mjs +55 -55
  161. package/dist/src/self-relay/usage-meter.mjs +141 -141
  162. package/dist/src/self-relay/windows-route-helper.mjs +75 -75
  163. package/dist/src/self-relay/wire.mjs +36 -36
  164. package/dist/src/server.mjs +1861 -1778
  165. package/dist/src/service-control.mjs +82 -82
  166. package/dist/src/service-doctor.mjs +110 -110
  167. package/dist/src/service-health.mjs +33 -33
  168. package/dist/src/service-install.mjs +91 -91
  169. package/dist/src/service-lifecycle.mjs +138 -138
  170. package/dist/src/service-restart.mjs +81 -81
  171. package/dist/src/shared-app-server.mjs +615 -611
  172. package/dist/src/shared-catalog.mjs +132 -132
  173. package/dist/src/shared-recovery.mjs +146 -146
  174. package/dist/src/shared-thread-stream.mjs +164 -164
  175. package/dist/src/skill-install.mjs +51 -51
  176. package/dist/src/thread-title-generation.mjs +150 -150
  177. package/dist/src/turn-model-history.mjs +47 -47
  178. package/dist/src/usage-reset.mjs +17 -17
  179. package/dist/src/workstation-downloads.mjs +120 -120
  180. package/dist/tools/rtc-route-helper.ps1 +99 -99
  181. package/dist/tools/start-rtc-route-helper.ps1 +15 -15
  182. package/dist/web/vendor/markdown-it.LICENSE.txt +22 -22
  183. package/dist/web/vendor/markdown-it.umd.min.js +13 -13
  184. package/package.json +7 -6
  185. package/tools/postinstall.mjs +127 -127
@@ -0,0 +1,11 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import { networkCandidates } from '../posix/network-candidates.mjs';
4
+
5
+ export async function lanNetworks() {
6
+ const hardware = fs.readdirSync('/sys/class/net').filter(name => fs.existsSync(`/sys/class/net/${name}/device`)).map(interfaceName => ({
7
+ interfaceName, networkName: interfaceName,
8
+ networkType: fs.existsSync(`/sys/class/net/${interfaceName}/wireless`) ? 'wifi' : 'ethernet',
9
+ }));
10
+ return networkCandidates(hardware, os.networkInterfaces());
11
+ }
@@ -0,0 +1,42 @@
1
+ import fs from 'node:fs';
2
+
3
+ const gone = error => ['ENOENT', 'ESRCH'].includes(error.code);
4
+ let bootId;
5
+ export function processInfo(pid) {
6
+ if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error('无效的进程 ID。');
7
+ const root = `/proc/${pid}`;
8
+ try {
9
+ if (fs.statSync(root).uid !== process.getuid()) return null;
10
+ const stat = fs.readFileSync(`${root}/stat`, 'utf8');
11
+ const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
12
+ if (fields[0] === 'Z') return null;
13
+ const ticks = fields[19];
14
+ bootId ??= fs.readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim();
15
+ const created = `${bootId}:${ticks}`;
16
+ const executable = fs.readlinkSync(`${root}/exe`);
17
+ const args = fs.readFileSync(`${root}/cmdline`, 'utf8').split('\0').filter(Boolean);
18
+ const after = fs.readFileSync(`${root}/stat`, 'utf8');
19
+ if (after.slice(after.lastIndexOf(')') + 2).split(' ')[19] !== ticks) return null;
20
+ return { pid, created, executable, args, command: args.join(' ') };
21
+ } catch (error) { if (gone(error)) return null; throw error; }
22
+ }
23
+ export const sameProcess = (a, b) => !!a && !!b && a.pid === b.pid && a.created === b.created
24
+ && a.executable === b.executable && JSON.stringify(a.args) === JSON.stringify(b.args);
25
+ export function stopProcess(pid, expected) {
26
+ if (expected?.pid !== pid || !sameProcess(expected, processInfo(pid))) throw new Error('进程身份已变化,未结束进程。');
27
+ process.kill(pid, 'SIGTERM');
28
+ return { stopped: true };
29
+ }
30
+ export function connectedToManagedServer(pid) {
31
+ try {
32
+ const sockets = new Set(fs.readdirSync(`/proc/${pid}/fd`).flatMap(fd => {
33
+ try { return /^socket:\[(\d+)\]$/.exec(fs.readlinkSync(`/proc/${pid}/fd/${fd}`))?.slice(1) || []; }
34
+ catch (error) { if (gone(error)) return []; throw error; }
35
+ }));
36
+ return ['tcp', 'tcp6'].some(table => fs.readFileSync(`/proc/${pid}/net/${table}`, 'utf8').trim().split('\n').slice(1).some(line => {
37
+ const fields = line.trim().split(/\s+/);
38
+ return fields[3] === '01' && sockets.has(fields[9])
39
+ && ['0100007F:B30F', '0000000000000000FFFF00000100007F:B30F'].includes(fields[2]);
40
+ }));
41
+ } catch (error) { if (gone(error)) return false; throw error; }
42
+ }
@@ -0,0 +1,17 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ export async function projectLocations() {
6
+ const home = os.homedir();
7
+ const config = path.isAbsolute(process.env.XDG_CONFIG_HOME || '') ? process.env.XDG_CONFIG_HOME : path.join(home, '.config');
8
+ let text = '';
9
+ try { text = fs.readFileSync(path.join(config, 'user-dirs.dirs'), 'utf8'); }
10
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
11
+ const directory = (name, fallback) => {
12
+ const match = new RegExp(`^XDG_${name}_DIR="([^"\\n]+)"$`, 'm').exec(text);
13
+ const value = match?.[1].replace(/^\$HOME(?=\/|$)/, home);
14
+ return value && path.isAbsolute(value) && !value.includes('$') ? value : path.join(home, fallback);
15
+ };
16
+ return { home, downloads: directory('DOWNLOAD', 'Downloads'), defaultParent: path.join(directory('DOCUMENTS', 'Documents'), 'ChatGPT'), roots: ['/'] };
17
+ }
@@ -0,0 +1,52 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { assertPlainPath } from '../../skill-install.mjs';
5
+ import { runtimePaths } from '../runtime-paths.mjs';
6
+ import { readRecord } from '../../service-lifecycle.mjs';
7
+
8
+ // XDG autostart inherits the actual graphical session (Wayland/X11 and the
9
+ // unlocked Secret Service). No system daemon, sudo, or shell profile mutation.
10
+ const quote = value => {
11
+ if (/[\u0000-\u001f\u007f]/.test(value)) throw new Error('登录启动路径含无效控制字符。');
12
+ return `"${value.replace(/%/g, '%%').replace(/[\\"`$]/g, char => `\\${char}`).replace(/\\/g, '\\\\')}"`;
13
+ };
14
+ export function autostartEntry({ serviceRoot, nodeExecutable, environment = process.env }) {
15
+ const entry = path.join(serviceRoot, 'dist/bin/codex-remote.mjs');
16
+ const variables = {};
17
+ for (const name of ['CODEX_HOME', 'XDG_DATA_HOME', 'XDG_STATE_HOME', 'XDG_CONFIG_HOME'])
18
+ if (environment[name]) variables[name] = environment[name];
19
+ const stablePath = String(environment.PATH || '').split(':').filter(value => value.startsWith('/') && !value.endsWith('/node_modules/.bin')).join(':');
20
+ if (stablePath) variables.PATH = stablePath;
21
+ const command = [quote(nodeExecutable), quote(entry), 'start', '--json'].join(' ');
22
+ const assignments = Object.entries(variables).map(([key, value]) => quote(`${key}=${value}`));
23
+ return `[Desktop Entry]\nType=Application\nName=Code Relax\nComment=Code Relax workstation\nExec=/usr/bin/env ${[...assignments, command].join(' ')}\nTerminal=false\nX-CodeRelax-Owner=${encodeURIComponent(path.resolve(serviceRoot))}\n`;
24
+ }
25
+ export async function residentStartup(installation, action = 'status') {
26
+ if (!['status', 'enable', 'disable'].includes(action)) throw new Error('无效的登录启动操作。');
27
+ if (process.getuid() === 0) throw new Error('请以普通 Linux 桌面用户安装,不使用 sudo。');
28
+ const config = path.isAbsolute(process.env.XDG_CONFIG_HOME || '') ? process.env.XDG_CONFIG_HOME : path.join(os.homedir(), '.config');
29
+ const file = path.join(config, 'autostart/cn.walkhi.code-relax.desktop');
30
+ assertPlainPath(file);
31
+ const runtime = readRecord(path.join(runtimePaths().stateRoot, 'codex-runtime.json'));
32
+ const environment = { ...process.env, ...(runtime?.codexHome ? { CODEX_HOME: runtime.codexHome } : {}) };
33
+ const expected = autostartEntry({ ...installation, environment });
34
+ let current = '';
35
+ try { current = fs.readFileSync(file, 'utf8'); }
36
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
37
+ const marker = `X-CodeRelax-Owner=${encodeURIComponent(path.resolve(installation.serviceRoot))}`;
38
+ if (current && !current.split(/\r?\n/).includes(marker)) throw new Error('同名登录启动项不属于此安装,未修改。');
39
+ const command = `${quote(installation.nodeExecutable)} ${quote(path.join(installation.serviceRoot, 'dist/bin/codex-remote.mjs'))} start --json`;
40
+ const execLine = current.split(/\r?\n/).find(line => line.startsWith('Exec=')) || '';
41
+ if (action === 'status') return { enabled: execLine.startsWith('Exec=/usr/bin/env ') && execLine.endsWith(command)
42
+ && !/^(?:Hidden=true|X-GNOME-Autostart-enabled=false)$/m.test(current), path: file, mechanism: 'XDG Autostart' };
43
+ if (action === 'disable') {
44
+ if (current) fs.unlinkSync(file);
45
+ return { enabled: false, removed: !!current, path: file };
46
+ }
47
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
48
+ const temporary = `${file}.${process.pid}.tmp`;
49
+ try { fs.writeFileSync(temporary, expected, { flag: 'wx', mode: 0o600 }); fs.renameSync(temporary, file); }
50
+ finally { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); }
51
+ return { enabled: true, path: file, mechanism: 'XDG Autostart', appliesAtNextLogin: true };
52
+ }
@@ -0,0 +1,76 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { readRecord, writeRecord } from '../../service-lifecycle.mjs';
5
+
6
+ const owner = 'code-relax-linux-runtime';
7
+ const manifestName = '.code-relax-runtime.json';
8
+ // Linux Desktop currently ships a flat, statically linked runtime, unlike Mac.
9
+ const names = ['codex', 'codex-code-mode-host', 'rg'];
10
+ export function runtimeFingerprint(executable) {
11
+ return JSON.stringify(names.map(name => {
12
+ const stat = fs.statSync(path.join(path.dirname(executable), name));
13
+ return [name, stat.size, stat.mtimeMs, stat.ctimeMs, stat.ino, stat.mode & 0o777];
14
+ }));
15
+ }
16
+ async function digestFiles(root) {
17
+ const files = [];
18
+ for (const name of names) {
19
+ const file = path.join(root, name), hash = createHash('sha256');
20
+ const before = fs.lstatSync(file);
21
+ if (!before.isFile()) throw new Error(`Linux 内核组件不是普通文件:${name}`);
22
+ for await (const chunk of fs.createReadStream(file)) hash.update(chunk);
23
+ const after = fs.statSync(file);
24
+ if (before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs)
25
+ throw new Error('Linux 内核更新正在替换文件,请稍后重试。');
26
+ files.push({ name, size: after.size, mode: after.mode & 0o777, sha256: hash.digest('hex') });
27
+ }
28
+ return files;
29
+ }
30
+ const treeHash = files => createHash('sha256').update(JSON.stringify(files)).digest('hex');
31
+ export async function retainRuntime(executable, stateRoot, version) {
32
+ const source = path.dirname(executable), sourceFingerprint = runtimeFingerprint(executable);
33
+ const files = await digestFiles(source), digest = treeHash(files);
34
+ if (sourceFingerprint !== runtimeFingerprint(executable)) throw new Error('Linux 内核在核对期间发生变化。');
35
+ const parent = path.join(stateRoot, 'linux-runtimes'), target = path.join(parent, digest);
36
+ const result = { executable: path.join(target, 'codex'), sourceExecutable: executable, sourceFingerprint };
37
+ if (fs.existsSync(target)) {
38
+ if (readRecord(path.join(target, manifestName))?.owner !== owner || treeHash(await digestFiles(target)) !== digest)
39
+ throw new Error('保留的 Linux 内核不完整,未覆盖运行中的文件。');
40
+ return result;
41
+ }
42
+ fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
43
+ const staging = path.join(parent, `.${randomUUID()}.tmp`), created = [];
44
+ fs.mkdirSync(staging, { mode: 0o700 });
45
+ try {
46
+ for (const file of files) {
47
+ const destination = path.join(staging, file.name);
48
+ fs.copyFileSync(path.join(source, file.name), destination, fs.constants.COPYFILE_EXCL | fs.constants.COPYFILE_FICLONE);
49
+ created.push(file.name); fs.chmodSync(destination, file.mode);
50
+ }
51
+ if (treeHash(await digestFiles(staging)) !== digest || sourceFingerprint !== runtimeFingerprint(executable))
52
+ throw new Error('Linux 内核复制期间发生变化,未使用混合版本。');
53
+ writeRecord(path.join(staging, manifestName), { owner, digest, version, files });
54
+ created.push(manifestName);
55
+ fs.renameSync(staging, target);
56
+ return result;
57
+ } catch (error) {
58
+ if (fs.existsSync(staging)) { for (const name of created) fs.unlinkSync(path.join(staging, name)); fs.rmdirSync(staging); }
59
+ throw error;
60
+ }
61
+ }
62
+ export async function pruneRetainedDesktopRuntimes(stateRoot, keepExecutable) {
63
+ const parent = path.resolve(stateRoot, 'linux-runtimes'), keep = path.dirname(path.resolve(keepExecutable));
64
+ if (path.dirname(keep) !== parent || !/^[a-f0-9]{64}$/.test(path.basename(keep)) || !fs.existsSync(parent)) return;
65
+ for (const entry of fs.readdirSync(parent, { withFileTypes: true })) {
66
+ const directory = path.join(parent, entry.name);
67
+ if (!entry.isDirectory() || !/^[a-f0-9]{64}$/.test(entry.name) || directory === keep) continue;
68
+ const record = readRecord(path.join(directory, manifestName));
69
+ if (record?.owner !== owner || record.digest !== entry.name) continue;
70
+ if (fs.readdirSync(directory).sort().join('|') !== [...names, manifestName].sort().join('|')) continue;
71
+ const actual = await digestFiles(directory);
72
+ if (treeHash(actual) !== entry.name || JSON.stringify(record.files) !== JSON.stringify(actual)) continue;
73
+ for (const name of [...names, manifestName]) fs.unlinkSync(path.join(directory, name));
74
+ fs.rmdirSync(directory);
75
+ }
76
+ }
@@ -0,0 +1,18 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { runtimePaths } from '../runtime-paths.mjs';
4
+ import { readRecord } from '../../service-lifecycle.mjs';
5
+ import { lanNetworks } from './network-info.mjs';
6
+ import { desktopHost } from './desktop-host.mjs';
7
+
8
+ export { lanNetworks };
9
+ export const lanAddress = async () => (await lanNetworks())[0]?.address || '';
10
+ export const sharedServiceUrl = () => desktopHost.sharedServiceUrl();
11
+ export async function bridgeEnvironment() {
12
+ const environment = { ...process.env };
13
+ const installed = readRecord(path.join(runtimePaths().serviceRoot, 'installation.json'));
14
+ if (!installed?.nodeExecutable || !fs.existsSync(installed.nodeExecutable)) throw new Error('安装时选用的 Node 不存在,请重新执行 relax setup。');
15
+ environment.CODEX_MCP_NODE_PATH = installed.nodeExecutable;
16
+ delete environment.CODEX_APP_TOOLS_PIPE_PATH;
17
+ return { executable: installed.nodeExecutable, environment };
18
+ }
@@ -0,0 +1,49 @@
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 } 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 === 'process') return processInfo(request.pid);
14
+ if (request.operation === 'stop') return stopProcess(request.expected?.pid, request.expected);
15
+ const installed = installedDesktop();
16
+ if (request.operation === 'desktop-info') {
17
+ const running = [];
18
+ if (installed) for (const name of fs.readdirSync('/proc').filter(name => /^\d+$/.test(name))) {
19
+ try {
20
+ if (fs.statSync(`/proc/${name}`).uid !== process.getuid()
21
+ || !desktopExecutableMatches(fs.readlinkSync(`/proc/${name}/exe`), installed.executable)) continue;
22
+ } catch (error) { if (['ENOENT', 'ESRCH', 'EACCES'].includes(error.code)) continue; throw error; }
23
+ const info = processInfo(Number(name));
24
+ if (info && desktopExecutableMatches(info.executable, installed.executable) && !info.args.some(arg => arg.startsWith('--type='))) running.push(info);
25
+ }
26
+ if (running.length > 1) throw new Error('发现多个 ChatGPT 桌面实例,请先关闭多余实例。');
27
+ const info = running[0] || null;
28
+ const connected = info ? connectedToManagedServer(info.pid) : false;
29
+ if (info && !sameProcess(info, processInfo(info.pid))) throw new Error('核对期间桌面进程发生变化。');
30
+ return { ...(installed || { desktop: null, version: '', build: '' }), running: !!info,
31
+ finishedLaunching: connected, connectedToManagedServer: connected, process: info };
32
+ }
33
+ if (!installed || installed.desktop !== request.bundlePath) throw new Error('桌面版安装位置已变化。');
34
+ if (request.operation === 'desktop-start') {
35
+ verifyDesktopPackage(installed.desktop);
36
+ if (!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) throw new Error('请在 Linux 图形桌面的当前用户会话中运行 relax setup。');
37
+ const environment = { ...process.env, ...request.environment };
38
+ delete environment.CODEX_APP_SERVER_FORCE_CLI;
39
+ delete environment.CODEX_APP_TOOLS_PIPE_PATH;
40
+ const pid = await startBackground(path.join(installed.desktop, 'codex-launcher'), [], environment);
41
+ return { pid, started: true };
42
+ }
43
+ if (request.operation === 'desktop-close') {
44
+ // Electron's POSIX signal handler calls Browser::Quit for SIGTERM. Wait
45
+ // for exit before touching the store; never escalate to SIGKILL.
46
+ return stopProcess(request.expected?.pid, request.expected);
47
+ }
48
+ throw new Error(`不支持的 Linux 系统操作:${request.operation}`);
49
+ }
@@ -0,0 +1,7 @@
1
+ // Only the new Desktop process receives the managed connection settings.
2
+ // Do not mutate shell profiles or the login session's global environment.
3
+ export async function configureManagedEnvironment() {
4
+ return { configured: true, changed: false, owned: false, persistent: false,
5
+ scope: 'application-launch', value: 'ws://127.0.0.1:45839' };
6
+ }
7
+ export async function clearManagedEnvironment() { return { removed: false, preserved: false, reason: 'application-launch-only' }; }
@@ -1,3 +1,3 @@
1
- # macos 被控主机(预留)
2
-
3
- 仅规划 Codex 的系统路径、进程/IPC 和运行环境适配,尚未实现。接入排在控制端扩展之后;不预设支持 Desktop 共享任务。
1
+ # macos 被控主机(预留)
2
+
3
+ 仅规划 Codex 的系统路径、进程/IPC 和运行环境适配,尚未实现。接入排在控制端扩展之后;不预设支持 Desktop 共享任务。
@@ -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
+ }
@@ -1,10 +1,10 @@
1
- import path from 'node:path';
2
- import { createHash } from 'node:crypto';
3
- import { runtimePaths } from '../runtime-paths.mjs';
4
- import { systemRequest } from './system.mjs';
5
-
6
- export function credentialStore(action, record, root = runtimePaths().credentialsRoot) {
7
- if (!['load', 'save'].includes(action)) throw new Error('无效的钥匙串操作。');
8
- const account = createHash('sha256').update(path.resolve(root)).digest('hex');
9
- return systemRequest({ operation: 'credentials', action, account, record });
10
- }
1
+ import path from 'node:path';
2
+ import { createHash } from 'node:crypto';
3
+ import { runtimePaths } from '../runtime-paths.mjs';
4
+ import { systemRequest } from './system.mjs';
5
+
6
+ export function credentialStore(action, record, root = runtimePaths().credentialsRoot) {
7
+ if (!['load', 'save'].includes(action)) throw new Error('无效的钥匙串操作。');
8
+ const account = createHash('sha256').update(path.resolve(root)).digest('hex');
9
+ return systemRequest({ operation: 'credentials', action, account, record });
10
+ }