@walkhi/code-relax 0.1.0-beta.26 → 0.1.0-beta.28

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 (63) hide show
  1. package/README.md +80 -67
  2. package/dist/bin/codex-remote.mjs +286 -239
  3. package/dist/shared/app-server-events.cjs +102 -16
  4. package/dist/shared/code-browser.cjs +159 -0
  5. package/dist/shared/file-changes.cjs +31 -0
  6. package/dist/shared/file-citations.cjs +117 -0
  7. package/dist/shared/file-mentions.cjs +80 -35
  8. package/dist/shared/message-delivery.cjs +38 -0
  9. package/dist/shared/p2p-file-channel.cjs +74 -0
  10. package/dist/shared/server-requests.cjs +149 -41
  11. package/dist/shared/service-endpoints.cjs +11 -11
  12. package/dist/shared/timeline-sync.cjs +173 -156
  13. package/dist/skills/code-relax/SKILL.md +2 -1
  14. package/dist/src/bounded-process-log.mjs +63 -0
  15. package/dist/src/celsius-client.mjs +107 -0
  16. package/dist/src/code-browser.mjs +101 -0
  17. package/dist/src/desktop-launcher.mjs +30 -30
  18. package/dist/src/dots-approval-timeline.mjs +99 -0
  19. package/dist/src/dots-avatar.mjs +42 -0
  20. package/dist/src/dots-chat-projection.mjs +50 -0
  21. package/dist/src/dots-chat-server.mjs +412 -0
  22. package/dist/src/dots-message-images.mjs +319 -0
  23. package/dist/src/dots-profile.mjs +77 -0
  24. package/dist/src/dots-room-activity.mjs +94 -0
  25. package/dist/src/dots-room-client.mjs +135 -0
  26. package/dist/src/dots-room-projection.mjs +94 -0
  27. package/dist/src/dots-schedules.mjs +66 -0
  28. package/dist/src/dots-summary-tasks.mjs +32 -0
  29. package/dist/src/dots-task-catalog.mjs +411 -0
  30. package/dist/src/file-transfer-range.mjs +22 -0
  31. package/dist/src/file-upload-session.mjs +130 -124
  32. package/dist/src/hosted-history-app-server.mjs +103 -0
  33. package/dist/src/hosted-history-cache.mjs +113 -0
  34. package/dist/src/lan-socket-server.mjs +205 -160
  35. package/dist/src/managed-app-server.mjs +241 -238
  36. package/dist/src/message-upload-files.mjs +74 -0
  37. package/dist/src/platform/windows/activate-desktop.ps1 +48 -0
  38. package/dist/src/platform/windows/codex-runtime.mjs +253 -251
  39. package/dist/src/platform/windows/desktop-host.mjs +38 -32
  40. package/dist/src/platform/windows/desktop-info.ps1 +2 -2
  41. package/dist/src/platform/windows/desktop-project-registration.mjs +153 -0
  42. package/dist/src/platform/windows/dots-profile.mjs +49 -0
  43. package/dist/src/platform/windows/launch-worker.mjs +41 -42
  44. package/dist/src/platform/windows/project-locations.mjs +16 -0
  45. package/dist/src/project-create.mjs +119 -0
  46. package/dist/src/project-management.mjs +69 -0
  47. package/dist/src/self-relay/account-connections.mjs +52 -0
  48. package/dist/src/self-relay/admin-page.mjs +80 -80
  49. package/dist/src/self-relay/admin-style.mjs +14 -9
  50. package/dist/src/self-relay/connector.mjs +22 -4
  51. package/dist/src/self-relay/file-wire.mjs +13 -0
  52. package/dist/src/self-relay/p2p-probe.mjs +152 -123
  53. package/dist/src/self-relay/rtc-file-bridge.mjs +41 -0
  54. package/dist/src/self-relay/server.mjs +894 -849
  55. package/dist/src/server.mjs +463 -148
  56. package/dist/src/service-doctor.mjs +72 -61
  57. package/dist/src/service-lifecycle.mjs +51 -43
  58. package/dist/src/shared-app-server.mjs +166 -107
  59. package/dist/src/shared-catalog.mjs +132 -40
  60. package/dist/src/shared-thread-stream.mjs +103 -25
  61. package/dist/src/usage-reset.mjs +17 -0
  62. package/dist/src/workstation-downloads.mjs +120 -0
  63. package/package.json +1 -1
@@ -1,251 +1,253 @@
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 } from 'node:child_process';
6
- import { createHash, randomUUID } from 'node:crypto';
7
- import { fileURLToPath } from 'node:url';
8
- import { promisify } from 'node:util';
9
- import { readRecord, writeRecord } from '../../service-lifecycle.mjs';
10
-
11
- const exec = promisify(execFile);
12
- const requiredMethods = ['thread/start', 'thread/resume', 'thread/list', 'thread/turns/list',
13
- 'thread/items/list', 'turn/start', 'turn/steer', 'turn/interrupt', 'model/list', 'project/list',
14
- 'thread/settings/update', 'configRequirements/read', 'thread/queue/list', 'thread/queue/add',
15
- 'thread/queue/delete', 'thread/queue/start', 'thread/read', 'thread/unsubscribe', 'config/read', 'account/rateLimits/read'];
16
- export const codeModeHostPath = executable => path.join(path.dirname(executable), 'codex-code-mode-host.exe');
17
- export const runtimeFilesAvailable = executable => fs.existsSync(executable) && fs.existsSync(codeModeHostPath(executable));
18
- const fingerprint = executable => [executable, codeModeHostPath(executable)]
19
- .map(file => { const s = fs.statSync(file); return `${s.size}:${s.mtimeMs}`; }).join('|');
20
- const powershell = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
21
- const desktopRuntimeScript = path.join(path.dirname(fileURLToPath(import.meta.url)), 'desktop-runtime.ps1');
22
-
23
- async function fileHash(file) {
24
- const hash = createHash('sha256');
25
- for await (const chunk of fs.createReadStream(file)) hash.update(chunk);
26
- return hash.digest('hex');
27
- }
28
-
29
- async function installedDesktopRuntime() {
30
- const { stdout } = await exec(powershell, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
31
- '-File', desktopRuntimeScript], { windowsHide: true, encoding: 'utf8', timeout: 30000 });
32
- return JSON.parse(stdout.trim());
33
- }
34
-
35
- async function matchesInstalledDesktop(executable, installed) {
36
- if (!installed || !runtimeFilesAvailable(executable) || !runtimeFilesAvailable(installed.executable)) return false;
37
- const files = [[executable, installed.executable], [codeModeHostPath(executable), installed.codeModeHost]];
38
- for (const [candidate, bundled] of files) {
39
- if (fs.statSync(candidate).size !== fs.statSync(bundled).size || await fileHash(candidate) !== await fileHash(bundled)) return false;
40
- }
41
- return true;
42
- }
43
-
44
- function runtimeFiles(directory, relative = '') {
45
- const files = [];
46
- for (const entry of fs.readdirSync(path.join(directory, relative), { withFileTypes: true })) {
47
- const name = path.join(relative, entry.name);
48
- if (entry.isDirectory()) files.push(...runtimeFiles(directory, name));
49
- else if (entry.isFile()) files.push(name);
50
- else throw new Error(`Codex 内核目录包含不支持的文件类型:${name}`);
51
- }
52
- return files.sort();
53
- }
54
-
55
- function runtimeDirectories(files) {
56
- const directories = new Set();
57
- for (const file of files) {
58
- let directory = path.dirname(file);
59
- while (directory !== '.') { directories.add(directory); directory = path.dirname(directory); }
60
- }
61
- return [...directories].sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
62
- }
63
-
64
- async function retainDesktopRuntime(executable, installed, stateRoot) {
65
- if (!/^[\w.-]+$/.test(installed.package)) throw new Error('Codex Desktop 安装包名称无效。');
66
- const source = path.dirname(executable);
67
- const files = runtimeFiles(source);
68
- if (files.includes('runtime.json')) throw new Error('Codex 内核目录与保留清单文件名冲突。');
69
- const directories = runtimeDirectories(files);
70
- const root = path.join(stateRoot, 'desktop-runtimes');
71
- const target = path.join(root, installed.package);
72
- const retained = path.join(target, 'codex.exe');
73
- if (fs.existsSync(target)) {
74
- const manifest = readRecord(path.join(target, 'runtime.json'));
75
- if (manifest?.package !== installed.package || JSON.stringify(manifest.files) !== JSON.stringify(files)
76
- || JSON.stringify(runtimeFiles(target)) !== JSON.stringify([...files, 'runtime.json'].sort())
77
- || !await matchesInstalledDesktop(retained, installed)) {
78
- throw new Error(`已保留的 Codex 内核文件不完整:${target}`);
79
- }
80
- return retained;
81
- }
82
- fs.mkdirSync(root, { recursive: true });
83
- const staging = path.join(root, `.${installed.package}.${randomUUID()}.tmp`);
84
- fs.mkdirSync(staging);
85
- const created = [];
86
- try {
87
- for (const directory of directories) fs.mkdirSync(path.join(staging, directory));
88
- for (const name of files) {
89
- const from = path.join(source, name), to = path.join(staging, name);
90
- created.push(to);
91
- try { fs.linkSync(from, to); }
92
- catch (error) {
93
- if (error.code !== 'EXDEV') throw error;
94
- fs.copyFileSync(from, to);
95
- }
96
- }
97
- if (!await matchesInstalledDesktop(path.join(staging, 'codex.exe'), installed)) {
98
- throw new Error('保留的 Codex 内核与当前安装包不一致。');
99
- }
100
- const manifestPath = path.join(staging, 'runtime.json');
101
- writeRecord(manifestPath, { package: installed.package, files });
102
- created.push(manifestPath);
103
- fs.renameSync(staging, target);
104
- return retained;
105
- } catch (error) {
106
- for (const file of created) fs.rmSync(file, { force: true });
107
- for (const directory of directories.reverse()) {
108
- const nested = path.join(staging, directory);
109
- if (fs.existsSync(nested)) fs.rmdirSync(nested);
110
- }
111
- fs.rmdirSync(staging);
112
- throw error;
113
- }
114
- }
115
-
116
- export function pruneRetainedDesktopRuntimes(stateRoot, keepExecutable) {
117
- const root = path.join(stateRoot, 'desktop-runtimes');
118
- if (!fs.existsSync(root)) return;
119
- const keep = path.resolve(path.dirname(keepExecutable)).toLowerCase();
120
- for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
121
- if (!entry.isDirectory() || !/^OpenAI\.Codex_[\w.-]+$/.test(entry.name)) continue;
122
- const directory = path.join(root, entry.name);
123
- if (path.resolve(directory).toLowerCase() === keep || fs.lstatSync(directory).isSymbolicLink()) continue;
124
- const manifest = readRecord(path.join(directory, 'runtime.json'));
125
- const files = manifest?.files;
126
- if (manifest?.package !== entry.name || !Array.isArray(files) || !files.includes('codex.exe')
127
- || !files.includes('codex-code-mode-host.exe') || new Set(files).size !== files.length
128
- || !files.every(name => typeof name === 'string' && name !== 'runtime.json'
129
- && path.resolve(directory, name).toLowerCase().startsWith(`${path.resolve(directory).toLowerCase()}${path.sep}`))) continue;
130
- const expected = [...files, 'runtime.json'].sort();
131
- if (JSON.stringify(runtimeFiles(directory)) !== JSON.stringify(expected)) continue;
132
- if (!fs.lstatSync(path.join(directory, 'runtime.json')).isFile()
133
- || !files.every(name => fs.lstatSync(path.join(directory, name)).isFile())) continue;
134
- for (const name of files) fs.unlinkSync(path.join(directory, name));
135
- fs.unlinkSync(path.join(directory, 'runtime.json'));
136
- for (const name of runtimeDirectories(files).reverse()) fs.rmdirSync(path.join(directory, name));
137
- fs.rmdirSync(directory);
138
- }
139
- }
140
-
141
- // Resolve native binaries, never execute a shell shim from a background task.
142
- export function discoverCodex(environment = process.env) {
143
- const found = new Map();
144
- const add = (file, source) => {
145
- if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return;
146
- const executable = fs.realpathSync(file);
147
- if (!found.has(executable.toLowerCase()) || source === 'desktop') found.set(executable.toLowerCase(), { executable, source });
148
- };
149
- if (environment.CODE_RELAX_CODEX_PATH) {
150
- const file = environment.CODE_RELAX_CODEX_PATH;
151
- if (!path.isAbsolute(file) || path.basename(file).toLowerCase() !== 'codex.exe') throw new Error('CODE_RELAX_CODEX_PATH 必须是 codex.exe 的绝对路径。');
152
- add(file, 'explicit');
153
- if (!found.size) throw new Error('CODE_RELAX_CODEX_PATH 指定的程序不存在。');
154
- return [...found.values()];
155
- }
156
- const local = environment.LOCALAPPDATA || path.join(os.homedir(), 'AppData/Local');
157
- const home = environment.CODEX_HOME || path.join(os.homedir(), '.codex');
158
- const directories = [...String(environment.PATH || environment.Path || '').split(';').filter(Boolean),
159
- environment.CODEX_INSTALL_DIR, path.join(local, 'Programs/OpenAI/Codex/bin'),
160
- path.join(home, 'packages/standalone/current/bin')].filter(Boolean);
161
- for (const directory of directories) {
162
- add(path.join(directory, 'codex.exe'), 'cli');
163
- const packageFile = path.join(directory, 'node_modules/@openai/codex/package.json');
164
- if (!fs.existsSync(packageFile)) continue;
165
- const root = path.dirname(fs.realpathSync(packageFile));
166
- let vendor = path.join(root, 'vendor');
167
- try { vendor = path.join(path.dirname(createRequire(packageFile).resolve('@openai/codex-win32-x64/package.json')), 'vendor'); }
168
- catch (error) { if (error.code !== 'MODULE_NOT_FOUND') throw error; }
169
- add(path.join(vendor, 'x86_64-pc-windows-msvc/bin/codex.exe'), 'npm');
170
- }
171
- const desktopRoot = path.join(local, 'OpenAI/Codex/bin');
172
- add(path.join(desktopRoot, 'codex.exe'), 'desktop');
173
- if (fs.existsSync(desktopRoot)) for (const entry of fs.readdirSync(desktopRoot, { withFileTypes: true })) {
174
- if (entry.isDirectory()) add(path.join(desktopRoot, entry.name, 'codex.exe'), 'desktop');
175
- }
176
- return [...found.values()];
177
- }
178
-
179
- function compareVersions(a, b) {
180
- const parse = v => v.split(/[.-]/).map(part => /^\d+$/.test(part) ? Number(part) : part);
181
- const aa = parse(a), bb = parse(b);
182
- for (let i = 0; i < Math.max(aa.length, bb.length); i++) {
183
- if (aa[i] === bb[i]) continue;
184
- if (aa[i] === undefined) return 1;
185
- if (bb[i] === undefined) return -1;
186
- return typeof aa[i] === 'number' && typeof bb[i] === 'number' ? aa[i] - bb[i] : String(aa[i]).localeCompare(String(bb[i]));
187
- }
188
- return 0;
189
- }
190
-
191
- async function verifyProtocol(executable, stateRoot) {
192
- const directory = fs.mkdtempSync(path.join(stateRoot, 'cli-schema-'));
193
- try {
194
- await exec(executable, ['app-server', 'generate-json-schema', '--experimental', '--out', directory],
195
- { windowsHide: true, timeout: 30000, maxBuffer: 4 * 1024 * 1024 });
196
- const request = fs.readFileSync(path.join(directory, 'ClientRequest.json'), 'utf8');
197
- const missing = requiredMethods.filter(method => !request.includes(`"${method}"`));
198
- const turns = fs.readFileSync(path.join(directory, 'v2/ThreadTurnsListParams.json'), 'utf8');
199
- if (!turns.includes('"itemsView"') || !turns.includes('"summary"')) missing.push('turn summary pagination');
200
- if (missing.length) throw new Error(`缺少协议能力:${missing.join(', ')}`);
201
- } finally {
202
- const remove = dir => {
203
- for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
204
- const file = path.join(dir, item.name);
205
- if (item.isDirectory()) remove(file); else fs.unlinkSync(file);
206
- }
207
- fs.rmdirSync(dir);
208
- };
209
- remove(directory);
210
- }
211
- }
212
-
213
- export async function resolveCodexRuntime(stateRoot, { refresh = false, environment = process.env } = {}) {
214
- fs.mkdirSync(stateRoot, { recursive: true });
215
- const recordFile = path.join(stateRoot, 'codex-runtime.json');
216
- const saved = readRecord(recordFile);
217
- environment = { ...environment,
218
- CODEX_HOME: environment.CODEX_HOME || saved?.codexHome || path.join(os.homedir(), '.codex'),
219
- CODE_RELAX_CODEX_PATH: environment.CODE_RELAX_CODEX_PATH || (saved?.source === 'explicit' ? saved.executable : '') };
220
- if (!refresh && saved?.executable && runtimeFilesAvailable(saved.executable)
221
- && saved.fingerprint === fingerprint(saved.executable)) return saved;
222
- const candidates = [], rejected = [];
223
- let installed = null;
224
- try { installed = await installedDesktopRuntime(); }
225
- catch (error) { rejected.push(`无法读取当前 Codex Desktop 安装包:${error.message}`); }
226
- for (const candidate of discoverCodex(environment)) {
227
- try {
228
- if (!runtimeFilesAvailable(candidate.executable)) throw new Error('缺少同目录的 codex-code-mode-host.exe');
229
- if (candidate.source === 'desktop' && !await matchesInstalledDesktop(candidate.executable, installed)) {
230
- throw new Error('不是当前 Codex Desktop 安装包配套的 CLI');
231
- }
232
- const { stdout } = await exec(candidate.executable, ['--version'], { windowsHide: true, timeout: 10000 });
233
- const version = /^codex-cli (\d+\.\d+\.\d+(?:-[\w.]+)?)/m.exec(stdout)?.[1];
234
- if (!version) throw new Error('无法识别版本');
235
- candidates.push({ ...candidate, version });
236
- } catch (error) { rejected.push(`${candidate.executable}: ${error.message}`); }
237
- }
238
- candidates.sort((a, b) => Number(b.source === 'desktop') - Number(a.source === 'desktop') || compareVersions(b.version, a.version));
239
- for (const candidate of candidates) {
240
- try {
241
- const executable = candidate.source === 'desktop'
242
- ? await retainDesktopRuntime(candidate.executable, installed, stateRoot) : candidate.executable;
243
- await verifyProtocol(executable, stateRoot);
244
- const result = { ...candidate, executable, fingerprint: fingerprint(executable),
245
- codexHome: path.resolve(environment.CODEX_HOME || path.join(os.homedir(), '.codex')) };
246
- writeRecord(recordFile, result);
247
- return result;
248
- } catch (error) { rejected.push(`${candidate.executable}: ${error.message}`); }
249
- }
250
- throw new Error(`未找到兼容的 Codex CLI,请安装或更新 Codex CLI 后执行 relax setup。${rejected.length ? '\n' + rejected.join('\n') : ''}`);
251
- }
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 } from 'node:child_process';
6
+ import { createHash, randomUUID } from 'node:crypto';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { promisify } from 'node:util';
9
+ import { readRecord, writeRecord } from '../../service-lifecycle.mjs';
10
+
11
+ const exec = promisify(execFile);
12
+ const requiredMethods = ['thread/start', 'thread/resume', 'thread/list', 'thread/turns/list',
13
+ 'thread/items/list', 'turn/start', 'turn/steer', 'turn/interrupt', 'model/list', 'project/list',
14
+ 'project/create', 'project/update', 'project/move',
15
+ 'fs/getMetadata', 'fs/readDirectory', 'fs/createDirectory', 'command/exec',
16
+ 'thread/settings/update', 'configRequirements/read', 'thread/queue/list', 'thread/queue/add',
17
+ 'thread/queue/delete', 'thread/queue/start', 'thread/read', 'thread/unsubscribe', 'config/read', 'account/rateLimits/read'];
18
+ export const codeModeHostPath = executable => path.join(path.dirname(executable), 'codex-code-mode-host.exe');
19
+ export const runtimeFilesAvailable = executable => fs.existsSync(executable) && fs.existsSync(codeModeHostPath(executable));
20
+ const fingerprint = executable => [executable, codeModeHostPath(executable)]
21
+ .map(file => { const s = fs.statSync(file); return `${s.size}:${s.mtimeMs}`; }).join('|');
22
+ const powershell = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
23
+ const desktopRuntimeScript = path.join(path.dirname(fileURLToPath(import.meta.url)), 'desktop-runtime.ps1');
24
+
25
+ async function fileHash(file) {
26
+ const hash = createHash('sha256');
27
+ for await (const chunk of fs.createReadStream(file)) hash.update(chunk);
28
+ return hash.digest('hex');
29
+ }
30
+
31
+ async function installedDesktopRuntime() {
32
+ const { stdout } = await exec(powershell, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
33
+ '-File', desktopRuntimeScript], { windowsHide: true, encoding: 'utf8', timeout: 30000 });
34
+ return JSON.parse(stdout.trim());
35
+ }
36
+
37
+ async function matchesInstalledDesktop(executable, installed) {
38
+ if (!installed || !runtimeFilesAvailable(executable) || !runtimeFilesAvailable(installed.executable)) return false;
39
+ const files = [[executable, installed.executable], [codeModeHostPath(executable), installed.codeModeHost]];
40
+ for (const [candidate, bundled] of files) {
41
+ if (fs.statSync(candidate).size !== fs.statSync(bundled).size || await fileHash(candidate) !== await fileHash(bundled)) return false;
42
+ }
43
+ return true;
44
+ }
45
+
46
+ function runtimeFiles(directory, relative = '') {
47
+ const files = [];
48
+ for (const entry of fs.readdirSync(path.join(directory, relative), { withFileTypes: true })) {
49
+ const name = path.join(relative, entry.name);
50
+ if (entry.isDirectory()) files.push(...runtimeFiles(directory, name));
51
+ else if (entry.isFile()) files.push(name);
52
+ else throw new Error(`Codex 内核目录包含不支持的文件类型:${name}`);
53
+ }
54
+ return files.sort();
55
+ }
56
+
57
+ function runtimeDirectories(files) {
58
+ const directories = new Set();
59
+ for (const file of files) {
60
+ let directory = path.dirname(file);
61
+ while (directory !== '.') { directories.add(directory); directory = path.dirname(directory); }
62
+ }
63
+ return [...directories].sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
64
+ }
65
+
66
+ async function retainDesktopRuntime(executable, installed, stateRoot) {
67
+ if (!/^[\w.-]+$/.test(installed.package)) throw new Error('Codex Desktop 安装包名称无效。');
68
+ const source = path.dirname(executable);
69
+ const files = runtimeFiles(source);
70
+ if (files.includes('runtime.json')) throw new Error('Codex 内核目录与保留清单文件名冲突。');
71
+ const directories = runtimeDirectories(files);
72
+ const root = path.join(stateRoot, 'desktop-runtimes');
73
+ const target = path.join(root, installed.package);
74
+ const retained = path.join(target, 'codex.exe');
75
+ if (fs.existsSync(target)) {
76
+ const manifest = readRecord(path.join(target, 'runtime.json'));
77
+ if (manifest?.package !== installed.package || JSON.stringify(manifest.files) !== JSON.stringify(files)
78
+ || JSON.stringify(runtimeFiles(target)) !== JSON.stringify([...files, 'runtime.json'].sort())
79
+ || !await matchesInstalledDesktop(retained, installed)) {
80
+ throw new Error(`已保留的 Codex 内核文件不完整:${target}`);
81
+ }
82
+ return retained;
83
+ }
84
+ fs.mkdirSync(root, { recursive: true });
85
+ const staging = path.join(root, `.${installed.package}.${randomUUID()}.tmp`);
86
+ fs.mkdirSync(staging);
87
+ const created = [];
88
+ try {
89
+ for (const directory of directories) fs.mkdirSync(path.join(staging, directory));
90
+ for (const name of files) {
91
+ const from = path.join(source, name), to = path.join(staging, name);
92
+ created.push(to);
93
+ try { fs.linkSync(from, to); }
94
+ catch (error) {
95
+ if (error.code !== 'EXDEV') throw error;
96
+ fs.copyFileSync(from, to);
97
+ }
98
+ }
99
+ if (!await matchesInstalledDesktop(path.join(staging, 'codex.exe'), installed)) {
100
+ throw new Error('保留的 Codex 内核与当前安装包不一致。');
101
+ }
102
+ const manifestPath = path.join(staging, 'runtime.json');
103
+ writeRecord(manifestPath, { package: installed.package, files });
104
+ created.push(manifestPath);
105
+ fs.renameSync(staging, target);
106
+ return retained;
107
+ } catch (error) {
108
+ for (const file of created) fs.rmSync(file, { force: true });
109
+ for (const directory of directories.reverse()) {
110
+ const nested = path.join(staging, directory);
111
+ if (fs.existsSync(nested)) fs.rmdirSync(nested);
112
+ }
113
+ fs.rmdirSync(staging);
114
+ throw error;
115
+ }
116
+ }
117
+
118
+ export function pruneRetainedDesktopRuntimes(stateRoot, keepExecutable) {
119
+ const root = path.join(stateRoot, 'desktop-runtimes');
120
+ if (!fs.existsSync(root)) return;
121
+ const keep = path.resolve(path.dirname(keepExecutable)).toLowerCase();
122
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
123
+ if (!entry.isDirectory() || !/^OpenAI\.Codex_[\w.-]+$/.test(entry.name)) continue;
124
+ const directory = path.join(root, entry.name);
125
+ if (path.resolve(directory).toLowerCase() === keep || fs.lstatSync(directory).isSymbolicLink()) continue;
126
+ const manifest = readRecord(path.join(directory, 'runtime.json'));
127
+ const files = manifest?.files;
128
+ if (manifest?.package !== entry.name || !Array.isArray(files) || !files.includes('codex.exe')
129
+ || !files.includes('codex-code-mode-host.exe') || new Set(files).size !== files.length
130
+ || !files.every(name => typeof name === 'string' && name !== 'runtime.json'
131
+ && path.resolve(directory, name).toLowerCase().startsWith(`${path.resolve(directory).toLowerCase()}${path.sep}`))) continue;
132
+ const expected = [...files, 'runtime.json'].sort();
133
+ if (JSON.stringify(runtimeFiles(directory)) !== JSON.stringify(expected)) continue;
134
+ if (!fs.lstatSync(path.join(directory, 'runtime.json')).isFile()
135
+ || !files.every(name => fs.lstatSync(path.join(directory, name)).isFile())) continue;
136
+ for (const name of files) fs.unlinkSync(path.join(directory, name));
137
+ fs.unlinkSync(path.join(directory, 'runtime.json'));
138
+ for (const name of runtimeDirectories(files).reverse()) fs.rmdirSync(path.join(directory, name));
139
+ fs.rmdirSync(directory);
140
+ }
141
+ }
142
+
143
+ // Resolve native binaries, never execute a shell shim from a background task.
144
+ export function discoverCodex(environment = process.env) {
145
+ const found = new Map();
146
+ const add = (file, source) => {
147
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return;
148
+ const executable = fs.realpathSync(file);
149
+ if (!found.has(executable.toLowerCase()) || source === 'desktop') found.set(executable.toLowerCase(), { executable, source });
150
+ };
151
+ if (environment.CODE_RELAX_CODEX_PATH) {
152
+ const file = environment.CODE_RELAX_CODEX_PATH;
153
+ if (!path.isAbsolute(file) || path.basename(file).toLowerCase() !== 'codex.exe') throw new Error('CODE_RELAX_CODEX_PATH 必须是 codex.exe 的绝对路径。');
154
+ add(file, 'explicit');
155
+ if (!found.size) throw new Error('CODE_RELAX_CODEX_PATH 指定的程序不存在。');
156
+ return [...found.values()];
157
+ }
158
+ const local = environment.LOCALAPPDATA || path.join(os.homedir(), 'AppData/Local');
159
+ const home = environment.CODEX_HOME || path.join(os.homedir(), '.codex');
160
+ const directories = [...String(environment.PATH || environment.Path || '').split(';').filter(Boolean),
161
+ environment.CODEX_INSTALL_DIR, path.join(local, 'Programs/OpenAI/Codex/bin'),
162
+ path.join(home, 'packages/standalone/current/bin')].filter(Boolean);
163
+ for (const directory of directories) {
164
+ add(path.join(directory, 'codex.exe'), 'cli');
165
+ const packageFile = path.join(directory, 'node_modules/@openai/codex/package.json');
166
+ if (!fs.existsSync(packageFile)) continue;
167
+ const root = path.dirname(fs.realpathSync(packageFile));
168
+ let vendor = path.join(root, 'vendor');
169
+ try { vendor = path.join(path.dirname(createRequire(packageFile).resolve('@openai/codex-win32-x64/package.json')), 'vendor'); }
170
+ catch (error) { if (error.code !== 'MODULE_NOT_FOUND') throw error; }
171
+ add(path.join(vendor, 'x86_64-pc-windows-msvc/bin/codex.exe'), 'npm');
172
+ }
173
+ const desktopRoot = path.join(local, 'OpenAI/Codex/bin');
174
+ add(path.join(desktopRoot, 'codex.exe'), 'desktop');
175
+ if (fs.existsSync(desktopRoot)) for (const entry of fs.readdirSync(desktopRoot, { withFileTypes: true })) {
176
+ if (entry.isDirectory()) add(path.join(desktopRoot, entry.name, 'codex.exe'), 'desktop');
177
+ }
178
+ return [...found.values()];
179
+ }
180
+
181
+ function compareVersions(a, b) {
182
+ const parse = v => v.split(/[.-]/).map(part => /^\d+$/.test(part) ? Number(part) : part);
183
+ const aa = parse(a), bb = parse(b);
184
+ for (let i = 0; i < Math.max(aa.length, bb.length); i++) {
185
+ if (aa[i] === bb[i]) continue;
186
+ if (aa[i] === undefined) return 1;
187
+ if (bb[i] === undefined) return -1;
188
+ return typeof aa[i] === 'number' && typeof bb[i] === 'number' ? aa[i] - bb[i] : String(aa[i]).localeCompare(String(bb[i]));
189
+ }
190
+ return 0;
191
+ }
192
+
193
+ async function verifyProtocol(executable, stateRoot) {
194
+ const directory = fs.mkdtempSync(path.join(stateRoot, 'cli-schema-'));
195
+ try {
196
+ await exec(executable, ['app-server', 'generate-json-schema', '--experimental', '--out', directory],
197
+ { windowsHide: true, timeout: 30000, maxBuffer: 4 * 1024 * 1024 });
198
+ const request = fs.readFileSync(path.join(directory, 'ClientRequest.json'), 'utf8');
199
+ const missing = requiredMethods.filter(method => !request.includes(`"${method}"`));
200
+ const turns = fs.readFileSync(path.join(directory, 'v2/ThreadTurnsListParams.json'), 'utf8');
201
+ if (!turns.includes('"itemsView"') || !turns.includes('"summary"')) missing.push('turn summary pagination');
202
+ if (missing.length) throw new Error(`缺少协议能力:${missing.join(', ')}`);
203
+ } finally {
204
+ const remove = dir => {
205
+ for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
206
+ const file = path.join(dir, item.name);
207
+ if (item.isDirectory()) remove(file); else fs.unlinkSync(file);
208
+ }
209
+ fs.rmdirSync(dir);
210
+ };
211
+ remove(directory);
212
+ }
213
+ }
214
+
215
+ export async function resolveCodexRuntime(stateRoot, { refresh = false, environment = process.env } = {}) {
216
+ fs.mkdirSync(stateRoot, { recursive: true });
217
+ const recordFile = path.join(stateRoot, 'codex-runtime.json');
218
+ const saved = readRecord(recordFile);
219
+ environment = { ...environment,
220
+ CODEX_HOME: environment.CODEX_HOME || saved?.codexHome || path.join(os.homedir(), '.codex'),
221
+ CODE_RELAX_CODEX_PATH: environment.CODE_RELAX_CODEX_PATH || (saved?.source === 'explicit' ? saved.executable : '') };
222
+ if (!refresh && saved?.executable && runtimeFilesAvailable(saved.executable)
223
+ && saved.fingerprint === fingerprint(saved.executable)) return saved;
224
+ const candidates = [], rejected = [];
225
+ let installed = null;
226
+ try { installed = await installedDesktopRuntime(); }
227
+ catch (error) { rejected.push(`无法读取当前 Codex Desktop 安装包:${error.message}`); }
228
+ for (const candidate of discoverCodex(environment)) {
229
+ try {
230
+ if (!runtimeFilesAvailable(candidate.executable)) throw new Error('缺少同目录的 codex-code-mode-host.exe');
231
+ if (candidate.source === 'desktop' && !await matchesInstalledDesktop(candidate.executable, installed)) {
232
+ throw new Error('不是当前 Codex Desktop 安装包配套的 CLI');
233
+ }
234
+ const { stdout } = await exec(candidate.executable, ['--version'], { windowsHide: true, timeout: 10000 });
235
+ const version = /^codex-cli (\d+\.\d+\.\d+(?:-[\w.]+)?)/m.exec(stdout)?.[1];
236
+ if (!version) throw new Error('无法识别版本');
237
+ candidates.push({ ...candidate, version });
238
+ } catch (error) { rejected.push(`${candidate.executable}: ${error.message}`); }
239
+ }
240
+ candidates.sort((a, b) => Number(b.source === 'desktop') - Number(a.source === 'desktop') || compareVersions(b.version, a.version));
241
+ for (const candidate of candidates) {
242
+ try {
243
+ const executable = candidate.source === 'desktop'
244
+ ? await retainDesktopRuntime(candidate.executable, installed, stateRoot) : candidate.executable;
245
+ await verifyProtocol(executable, stateRoot);
246
+ const result = { ...candidate, executable, fingerprint: fingerprint(executable),
247
+ codexHome: path.resolve(environment.CODEX_HOME || path.join(os.homedir(), '.codex')) };
248
+ writeRecord(recordFile, result);
249
+ return result;
250
+ } catch (error) { rejected.push(`${candidate.executable}: ${error.message}`); }
251
+ }
252
+ throw new Error(`未找到兼容的 Codex CLI,请安装或更新 Codex CLI 后执行 relax setup。${rejected.length ? '\n' + rejected.join('\n') : ''}`);
253
+ }