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

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 (89) 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/build-platform.json +1 -1
  7. package/dist/get-shared-service-url.ps1 +14 -14
  8. package/dist/shared/code-browser.cjs +159 -159
  9. package/dist/shared/file-changes.cjs +31 -31
  10. package/dist/shared/file-citations.cjs +117 -117
  11. package/dist/shared/file-mentions.cjs +92 -92
  12. package/dist/shared/message-delivery.cjs +115 -115
  13. package/dist/shared/p2p-file-channel.cjs +87 -87
  14. package/dist/shared/server-requests.cjs +311 -311
  15. package/dist/shared/timeline-sync.cjs +173 -173
  16. package/dist/skills/code-relax/SKILL.md +22 -22
  17. package/dist/src/bounded-process-log.mjs +63 -63
  18. package/dist/src/celsius-client.mjs +107 -107
  19. package/dist/src/dots-approval-timeline.mjs +99 -99
  20. package/dist/src/dots-avatar.mjs +42 -42
  21. package/dist/src/dots-chat-projection.mjs +50 -50
  22. package/dist/src/dots-chat-server.mjs +483 -483
  23. package/dist/src/dots-message-images.mjs +319 -319
  24. package/dist/src/dots-profile.mjs +77 -77
  25. package/dist/src/dots-room-activity.mjs +172 -172
  26. package/dist/src/dots-room-client.mjs +135 -135
  27. package/dist/src/dots-room-projection.mjs +94 -94
  28. package/dist/src/dots-schedules.mjs +66 -66
  29. package/dist/src/dots-summary-tasks.mjs +32 -32
  30. package/dist/src/dots-task-catalog.mjs +430 -430
  31. package/dist/src/file-transfer-range.mjs +22 -22
  32. package/dist/src/file-upload-session.mjs +130 -130
  33. package/dist/src/hosted-history-app-server.mjs +103 -103
  34. package/dist/src/hosted-history-cache.mjs +113 -113
  35. package/dist/src/lan-socket-server.mjs +266 -266
  36. package/dist/src/managed-app-server.mjs +373 -373
  37. package/dist/src/managed-endpoint.mjs +25 -25
  38. package/dist/src/message-upload-files.mjs +109 -109
  39. package/dist/src/onboarding.mjs +12 -12
  40. package/dist/src/platform/README.md +1 -1
  41. package/dist/src/platform/codex-protocol.mjs +46 -46
  42. package/dist/src/platform/desktop-project-store.mjs +208 -208
  43. package/dist/src/platform/linux/desktop-host.mjs +87 -87
  44. package/dist/src/platform/linux/process.mjs +53 -53
  45. package/dist/src/platform/linux/system.mjs +50 -50
  46. package/dist/src/platform/linux/user-environment.mjs +9 -9
  47. package/dist/src/platform/macos/codex-runtime.mjs +125 -125
  48. package/dist/src/platform/macos/desktop-host.mjs +86 -86
  49. package/dist/src/platform/macos/user-environment.mjs +9 -9
  50. package/dist/src/platform/native-package.mjs +58 -58
  51. package/dist/src/platform/windows/activate-desktop.ps1 +49 -49
  52. package/dist/src/platform/windows/codex-runtime.mjs +215 -215
  53. package/dist/src/platform/windows/credential-store.mjs +27 -27
  54. package/dist/src/platform/windows/credential-store.ps1 +49 -49
  55. package/dist/src/platform/windows/desktop-host.mjs +65 -65
  56. package/dist/src/platform/windows/desktop-info.ps1 +38 -38
  57. package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
  58. package/dist/src/platform/windows/desktop-shortcuts.ps1 +14 -14
  59. package/dist/src/platform/windows/dots-profile.mjs +49 -49
  60. package/dist/src/platform/windows/launch-worker.mjs +41 -41
  61. package/dist/src/platform/windows/project-locations.mjs +16 -16
  62. package/dist/src/platform/windows/service-host.mjs +35 -35
  63. package/dist/src/platform/windows/user-environment.mjs +52 -52
  64. package/dist/src/project-management.mjs +58 -58
  65. package/dist/src/runtime-discovery.mjs +47 -47
  66. package/dist/src/self-relay/account-cleanup.mjs +30 -30
  67. package/dist/src/self-relay/admin-page.mjs +80 -80
  68. package/dist/src/self-relay/admin-style.mjs +14 -14
  69. package/dist/src/self-relay/connector.mjs +439 -439
  70. package/dist/src/self-relay/demo.mjs +31 -31
  71. package/dist/src/self-relay/diagnostics.mjs +54 -54
  72. package/dist/src/self-relay/email-accounts.mjs +183 -183
  73. package/dist/src/self-relay/file-wire.mjs +13 -13
  74. package/dist/src/self-relay/p2p-probe.mjs +168 -168
  75. package/dist/src/self-relay/privacy-page.mjs +26 -26
  76. package/dist/src/self-relay/public-releases.mjs +67 -67
  77. package/dist/src/self-relay/rtc-file-bridge.mjs +41 -41
  78. package/dist/src/self-relay/usage-meter.mjs +141 -141
  79. package/dist/src/server.mjs +5 -4
  80. package/dist/src/service-doctor.mjs +148 -148
  81. package/dist/src/service-install.mjs +97 -97
  82. package/dist/src/service-lifecycle.mjs +139 -139
  83. package/dist/src/shared-app-server.mjs +666 -666
  84. package/dist/src/shared-catalog.mjs +132 -132
  85. package/dist/src/shared-thread-stream.mjs +165 -165
  86. package/dist/src/usage-reset.mjs +17 -17
  87. package/dist/src/workstation-downloads.mjs +120 -120
  88. package/package.json +4 -4
  89. package/tools/postinstall.mjs +186 -186
@@ -1,215 +1,215 @@
1
- import fs from 'node:fs';
2
- import { compareVersions, verifyProtocol } from '../codex-protocol.mjs';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import { createRequire } from 'node:module';
6
- import { execFile } from 'node:child_process';
7
- import { createHash, randomUUID } from 'node:crypto';
8
- import { fileURLToPath } from 'node:url';
9
- import { promisify } from 'node:util';
10
- import { readRecord, writeRecord } from '../../service-lifecycle.mjs';
11
-
12
- const exec = promisify(execFile);
13
- export const codeModeHostPath = executable => path.join(path.dirname(executable), 'codex-code-mode-host.exe');
14
- export const runtimeFilesAvailable = executable => fs.existsSync(executable) && fs.existsSync(codeModeHostPath(executable));
15
- const fingerprint = executable => [executable, codeModeHostPath(executable)]
16
- .map(file => { const s = fs.statSync(file); return `${s.size}:${s.mtimeMs}`; }).join('|');
17
- export const runtimeFingerprint = fingerprint;
18
- const powershell = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
19
- const desktopRuntimeScript = path.join(path.dirname(fileURLToPath(import.meta.url)), 'desktop-runtime.ps1');
20
-
21
- async function fileHash(file) {
22
- const hash = createHash('sha256');
23
- for await (const chunk of fs.createReadStream(file)) hash.update(chunk);
24
- return hash.digest('hex');
25
- }
26
-
27
- async function installedDesktopRuntime() {
28
- const { stdout } = await exec(powershell, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
29
- '-File', desktopRuntimeScript], { windowsHide: true, encoding: 'utf8', timeout: 30000 });
30
- return JSON.parse(stdout.trim());
31
- }
32
-
33
- async function matchesInstalledDesktop(executable, installed) {
34
- if (!installed || !runtimeFilesAvailable(executable) || !runtimeFilesAvailable(installed.executable)) return false;
35
- const files = [[executable, installed.executable], [codeModeHostPath(executable), installed.codeModeHost]];
36
- for (const [candidate, bundled] of files) {
37
- if (fs.statSync(candidate).size !== fs.statSync(bundled).size || await fileHash(candidate) !== await fileHash(bundled)) return false;
38
- }
39
- return true;
40
- }
41
-
42
- function runtimeFiles(directory, relative = '') {
43
- const files = [];
44
- for (const entry of fs.readdirSync(path.join(directory, relative), { withFileTypes: true })) {
45
- const name = path.join(relative, entry.name);
46
- if (entry.isDirectory()) files.push(...runtimeFiles(directory, name));
47
- else if (entry.isFile()) files.push(name);
48
- else throw new Error(`Codex 内核目录包含不支持的文件类型:${name}`);
49
- }
50
- return files.sort();
51
- }
52
-
53
- function runtimeDirectories(files) {
54
- const directories = new Set();
55
- for (const file of files) {
56
- let directory = path.dirname(file);
57
- while (directory !== '.') { directories.add(directory); directory = path.dirname(directory); }
58
- }
59
- return [...directories].sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
60
- }
61
-
62
- async function retainDesktopRuntime(executable, installed, stateRoot) {
63
- if (!/^[\w.-]+$/.test(installed.package)) throw new Error('Codex Desktop 安装包名称无效。');
64
- const source = path.dirname(executable);
65
- const files = runtimeFiles(source);
66
- if (files.includes('runtime.json')) throw new Error('Codex 内核目录与保留清单文件名冲突。');
67
- const directories = runtimeDirectories(files);
68
- const root = path.join(stateRoot, 'desktop-runtimes');
69
- const target = path.join(root, installed.package);
70
- const retained = path.join(target, 'codex.exe');
71
- if (fs.existsSync(target)) {
72
- const manifest = readRecord(path.join(target, 'runtime.json'));
73
- if (manifest?.package !== installed.package || JSON.stringify(manifest.files) !== JSON.stringify(files)
74
- || JSON.stringify(runtimeFiles(target)) !== JSON.stringify([...files, 'runtime.json'].sort())
75
- || !await matchesInstalledDesktop(retained, installed)) {
76
- throw new Error(`已保留的 Codex 内核文件不完整:${target}`);
77
- }
78
- return retained;
79
- }
80
- fs.mkdirSync(root, { recursive: true });
81
- const staging = path.join(root, `.${installed.package}.${randomUUID()}.tmp`);
82
- fs.mkdirSync(staging);
83
- const created = [];
84
- try {
85
- for (const directory of directories) fs.mkdirSync(path.join(staging, directory));
86
- for (const name of files) {
87
- const from = path.join(source, name), to = path.join(staging, name);
88
- created.push(to);
89
- try { fs.linkSync(from, to); }
90
- catch (error) {
91
- if (error.code !== 'EXDEV') throw error;
92
- fs.copyFileSync(from, to);
93
- }
94
- }
95
- if (!await matchesInstalledDesktop(path.join(staging, 'codex.exe'), installed)) {
96
- throw new Error('保留的 Codex 内核与当前安装包不一致。');
97
- }
98
- const manifestPath = path.join(staging, 'runtime.json');
99
- writeRecord(manifestPath, { package: installed.package, files });
100
- created.push(manifestPath);
101
- fs.renameSync(staging, target);
102
- return retained;
103
- } catch (error) {
104
- for (const file of created) fs.rmSync(file, { force: true });
105
- for (const directory of directories.reverse()) {
106
- const nested = path.join(staging, directory);
107
- if (fs.existsSync(nested)) fs.rmdirSync(nested);
108
- }
109
- fs.rmdirSync(staging);
110
- throw error;
111
- }
112
- }
113
-
114
- export function pruneRetainedDesktopRuntimes(stateRoot, keepExecutable) {
115
- const root = path.join(stateRoot, 'desktop-runtimes');
116
- if (!fs.existsSync(root)) return;
117
- const keep = path.resolve(path.dirname(keepExecutable)).toLowerCase();
118
- for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
119
- if (!entry.isDirectory() || !/^OpenAI\.Codex_[\w.-]+$/.test(entry.name)) continue;
120
- const directory = path.join(root, entry.name);
121
- if (path.resolve(directory).toLowerCase() === keep || fs.lstatSync(directory).isSymbolicLink()) continue;
122
- const manifest = readRecord(path.join(directory, 'runtime.json'));
123
- const files = manifest?.files;
124
- if (manifest?.package !== entry.name || !Array.isArray(files) || !files.includes('codex.exe')
125
- || !files.includes('codex-code-mode-host.exe') || new Set(files).size !== files.length
126
- || !files.every(name => typeof name === 'string' && name !== 'runtime.json'
127
- && path.resolve(directory, name).toLowerCase().startsWith(`${path.resolve(directory).toLowerCase()}${path.sep}`))) continue;
128
- const expected = [...files, 'runtime.json'].sort();
129
- if (JSON.stringify(runtimeFiles(directory)) !== JSON.stringify(expected)) continue;
130
- if (!fs.lstatSync(path.join(directory, 'runtime.json')).isFile()
131
- || !files.every(name => fs.lstatSync(path.join(directory, name)).isFile())) continue;
132
- for (const name of files) fs.unlinkSync(path.join(directory, name));
133
- fs.unlinkSync(path.join(directory, 'runtime.json'));
134
- for (const name of runtimeDirectories(files).reverse()) fs.rmdirSync(path.join(directory, name));
135
- fs.rmdirSync(directory);
136
- }
137
- }
138
-
139
- // Resolve native binaries, never execute a shell shim from a background task.
140
- export function discoverCodex(environment = process.env) {
141
- const found = new Map();
142
- const add = (file, source) => {
143
- if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return;
144
- const executable = fs.realpathSync(file);
145
- if (!found.has(executable.toLowerCase()) || source === 'desktop') found.set(executable.toLowerCase(), { executable, source });
146
- };
147
- if (environment.CODE_RELAX_CODEX_PATH) {
148
- const file = environment.CODE_RELAX_CODEX_PATH;
149
- if (!path.isAbsolute(file) || path.basename(file).toLowerCase() !== 'codex.exe') throw new Error('CODE_RELAX_CODEX_PATH 必须是 codex.exe 的绝对路径。');
150
- add(file, 'explicit');
151
- if (!found.size) throw new Error('CODE_RELAX_CODEX_PATH 指定的程序不存在。');
152
- return [...found.values()];
153
- }
154
- const local = environment.LOCALAPPDATA || path.join(os.homedir(), 'AppData/Local');
155
- const home = environment.CODEX_HOME || path.join(os.homedir(), '.codex');
156
- const directories = [...String(environment.PATH || environment.Path || '').split(';').filter(Boolean),
157
- environment.CODEX_INSTALL_DIR, path.join(local, 'Programs/OpenAI/Codex/bin'),
158
- path.join(home, 'packages/standalone/current/bin')].filter(Boolean);
159
- for (const directory of directories) {
160
- add(path.join(directory, 'codex.exe'), 'cli');
161
- const packageFile = path.join(directory, 'node_modules/@openai/codex/package.json');
162
- if (!fs.existsSync(packageFile)) continue;
163
- const root = path.dirname(fs.realpathSync(packageFile));
164
- let vendor = path.join(root, 'vendor');
165
- try { vendor = path.join(path.dirname(createRequire(packageFile).resolve('@openai/codex-win32-x64/package.json')), 'vendor'); }
166
- catch (error) { if (error.code !== 'MODULE_NOT_FOUND') throw error; }
167
- add(path.join(vendor, 'x86_64-pc-windows-msvc/bin/codex.exe'), 'npm');
168
- }
169
- const desktopRoot = path.join(local, 'OpenAI/Codex/bin');
170
- add(path.join(desktopRoot, 'codex.exe'), 'desktop');
171
- if (fs.existsSync(desktopRoot)) for (const entry of fs.readdirSync(desktopRoot, { withFileTypes: true })) {
172
- if (entry.isDirectory()) add(path.join(desktopRoot, entry.name, 'codex.exe'), 'desktop');
173
- }
174
- return [...found.values()];
175
- }
176
-
177
- export async function resolveCodexRuntime(stateRoot, { refresh = false, environment = process.env } = {}) {
178
- fs.mkdirSync(stateRoot, { recursive: true });
179
- const recordFile = path.join(stateRoot, 'codex-runtime.json');
180
- const saved = readRecord(recordFile);
181
- environment = { ...environment,
182
- CODEX_HOME: environment.CODEX_HOME || saved?.codexHome || path.join(os.homedir(), '.codex'),
183
- CODE_RELAX_CODEX_PATH: environment.CODE_RELAX_CODEX_PATH || (saved?.source === 'explicit' ? saved.executable : '') };
184
- if (!refresh && saved?.executable && runtimeFilesAvailable(saved.executable)
185
- && saved.fingerprint === fingerprint(saved.executable)) return saved;
186
- const candidates = [], rejected = [];
187
- let installed = null;
188
- try { installed = await installedDesktopRuntime(); }
189
- catch (error) { rejected.push(`无法读取当前 Codex Desktop 安装包:${error.message}`); }
190
- for (const candidate of discoverCodex(environment)) {
191
- try {
192
- if (!runtimeFilesAvailable(candidate.executable)) throw new Error('缺少同目录的 codex-code-mode-host.exe');
193
- if (candidate.source === 'desktop' && !await matchesInstalledDesktop(candidate.executable, installed)) {
194
- throw new Error('不是当前 Codex Desktop 安装包配套的 CLI');
195
- }
196
- const { stdout } = await exec(candidate.executable, ['--version'], { windowsHide: true, timeout: 10000 });
197
- const version = /^codex-cli (\d+\.\d+\.\d+(?:-[\w.]+)?)/m.exec(stdout)?.[1];
198
- if (!version) throw new Error('无法识别版本');
199
- candidates.push({ ...candidate, version });
200
- } catch (error) { rejected.push(`${candidate.executable}: ${error.message}`); }
201
- }
202
- candidates.sort((a, b) => Number(b.source === 'desktop') - Number(a.source === 'desktop') || compareVersions(b.version, a.version));
203
- for (const candidate of candidates) {
204
- try {
205
- const executable = candidate.source === 'desktop'
206
- ? await retainDesktopRuntime(candidate.executable, installed, stateRoot) : candidate.executable;
207
- await verifyProtocol(executable, stateRoot);
208
- const result = { ...candidate, executable, fingerprint: fingerprint(executable),
209
- codexHome: path.resolve(environment.CODEX_HOME || path.join(os.homedir(), '.codex')) };
210
- writeRecord(recordFile, result);
211
- return result;
212
- } catch (error) { rejected.push(`${candidate.executable}: ${error.message}`); }
213
- }
214
- throw new Error(`未找到兼容的 Codex CLI,请安装或更新 Codex CLI 后执行 relax setup。${rejected.length ? '\n' + rejected.join('\n') : ''}`);
215
- }
1
+ import fs from 'node:fs';
2
+ import { compareVersions, verifyProtocol } from '../codex-protocol.mjs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { createRequire } from 'node:module';
6
+ import { execFile } from 'node:child_process';
7
+ import { createHash, randomUUID } from 'node:crypto';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { promisify } from 'node:util';
10
+ import { readRecord, writeRecord } from '../../service-lifecycle.mjs';
11
+
12
+ const exec = promisify(execFile);
13
+ export const codeModeHostPath = executable => path.join(path.dirname(executable), 'codex-code-mode-host.exe');
14
+ export const runtimeFilesAvailable = executable => fs.existsSync(executable) && fs.existsSync(codeModeHostPath(executable));
15
+ const fingerprint = executable => [executable, codeModeHostPath(executable)]
16
+ .map(file => { const s = fs.statSync(file); return `${s.size}:${s.mtimeMs}`; }).join('|');
17
+ export const runtimeFingerprint = fingerprint;
18
+ const powershell = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
19
+ const desktopRuntimeScript = path.join(path.dirname(fileURLToPath(import.meta.url)), 'desktop-runtime.ps1');
20
+
21
+ async function fileHash(file) {
22
+ const hash = createHash('sha256');
23
+ for await (const chunk of fs.createReadStream(file)) hash.update(chunk);
24
+ return hash.digest('hex');
25
+ }
26
+
27
+ async function installedDesktopRuntime() {
28
+ const { stdout } = await exec(powershell, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
29
+ '-File', desktopRuntimeScript], { windowsHide: true, encoding: 'utf8', timeout: 30000 });
30
+ return JSON.parse(stdout.trim());
31
+ }
32
+
33
+ async function matchesInstalledDesktop(executable, installed) {
34
+ if (!installed || !runtimeFilesAvailable(executable) || !runtimeFilesAvailable(installed.executable)) return false;
35
+ const files = [[executable, installed.executable], [codeModeHostPath(executable), installed.codeModeHost]];
36
+ for (const [candidate, bundled] of files) {
37
+ if (fs.statSync(candidate).size !== fs.statSync(bundled).size || await fileHash(candidate) !== await fileHash(bundled)) return false;
38
+ }
39
+ return true;
40
+ }
41
+
42
+ function runtimeFiles(directory, relative = '') {
43
+ const files = [];
44
+ for (const entry of fs.readdirSync(path.join(directory, relative), { withFileTypes: true })) {
45
+ const name = path.join(relative, entry.name);
46
+ if (entry.isDirectory()) files.push(...runtimeFiles(directory, name));
47
+ else if (entry.isFile()) files.push(name);
48
+ else throw new Error(`Codex 内核目录包含不支持的文件类型:${name}`);
49
+ }
50
+ return files.sort();
51
+ }
52
+
53
+ function runtimeDirectories(files) {
54
+ const directories = new Set();
55
+ for (const file of files) {
56
+ let directory = path.dirname(file);
57
+ while (directory !== '.') { directories.add(directory); directory = path.dirname(directory); }
58
+ }
59
+ return [...directories].sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
60
+ }
61
+
62
+ async function retainDesktopRuntime(executable, installed, stateRoot) {
63
+ if (!/^[\w.-]+$/.test(installed.package)) throw new Error('Codex Desktop 安装包名称无效。');
64
+ const source = path.dirname(executable);
65
+ const files = runtimeFiles(source);
66
+ if (files.includes('runtime.json')) throw new Error('Codex 内核目录与保留清单文件名冲突。');
67
+ const directories = runtimeDirectories(files);
68
+ const root = path.join(stateRoot, 'desktop-runtimes');
69
+ const target = path.join(root, installed.package);
70
+ const retained = path.join(target, 'codex.exe');
71
+ if (fs.existsSync(target)) {
72
+ const manifest = readRecord(path.join(target, 'runtime.json'));
73
+ if (manifest?.package !== installed.package || JSON.stringify(manifest.files) !== JSON.stringify(files)
74
+ || JSON.stringify(runtimeFiles(target)) !== JSON.stringify([...files, 'runtime.json'].sort())
75
+ || !await matchesInstalledDesktop(retained, installed)) {
76
+ throw new Error(`已保留的 Codex 内核文件不完整:${target}`);
77
+ }
78
+ return retained;
79
+ }
80
+ fs.mkdirSync(root, { recursive: true });
81
+ const staging = path.join(root, `.${installed.package}.${randomUUID()}.tmp`);
82
+ fs.mkdirSync(staging);
83
+ const created = [];
84
+ try {
85
+ for (const directory of directories) fs.mkdirSync(path.join(staging, directory));
86
+ for (const name of files) {
87
+ const from = path.join(source, name), to = path.join(staging, name);
88
+ created.push(to);
89
+ try { fs.linkSync(from, to); }
90
+ catch (error) {
91
+ if (error.code !== 'EXDEV') throw error;
92
+ fs.copyFileSync(from, to);
93
+ }
94
+ }
95
+ if (!await matchesInstalledDesktop(path.join(staging, 'codex.exe'), installed)) {
96
+ throw new Error('保留的 Codex 内核与当前安装包不一致。');
97
+ }
98
+ const manifestPath = path.join(staging, 'runtime.json');
99
+ writeRecord(manifestPath, { package: installed.package, files });
100
+ created.push(manifestPath);
101
+ fs.renameSync(staging, target);
102
+ return retained;
103
+ } catch (error) {
104
+ for (const file of created) fs.rmSync(file, { force: true });
105
+ for (const directory of directories.reverse()) {
106
+ const nested = path.join(staging, directory);
107
+ if (fs.existsSync(nested)) fs.rmdirSync(nested);
108
+ }
109
+ fs.rmdirSync(staging);
110
+ throw error;
111
+ }
112
+ }
113
+
114
+ export function pruneRetainedDesktopRuntimes(stateRoot, keepExecutable) {
115
+ const root = path.join(stateRoot, 'desktop-runtimes');
116
+ if (!fs.existsSync(root)) return;
117
+ const keep = path.resolve(path.dirname(keepExecutable)).toLowerCase();
118
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
119
+ if (!entry.isDirectory() || !/^OpenAI\.Codex_[\w.-]+$/.test(entry.name)) continue;
120
+ const directory = path.join(root, entry.name);
121
+ if (path.resolve(directory).toLowerCase() === keep || fs.lstatSync(directory).isSymbolicLink()) continue;
122
+ const manifest = readRecord(path.join(directory, 'runtime.json'));
123
+ const files = manifest?.files;
124
+ if (manifest?.package !== entry.name || !Array.isArray(files) || !files.includes('codex.exe')
125
+ || !files.includes('codex-code-mode-host.exe') || new Set(files).size !== files.length
126
+ || !files.every(name => typeof name === 'string' && name !== 'runtime.json'
127
+ && path.resolve(directory, name).toLowerCase().startsWith(`${path.resolve(directory).toLowerCase()}${path.sep}`))) continue;
128
+ const expected = [...files, 'runtime.json'].sort();
129
+ if (JSON.stringify(runtimeFiles(directory)) !== JSON.stringify(expected)) continue;
130
+ if (!fs.lstatSync(path.join(directory, 'runtime.json')).isFile()
131
+ || !files.every(name => fs.lstatSync(path.join(directory, name)).isFile())) continue;
132
+ for (const name of files) fs.unlinkSync(path.join(directory, name));
133
+ fs.unlinkSync(path.join(directory, 'runtime.json'));
134
+ for (const name of runtimeDirectories(files).reverse()) fs.rmdirSync(path.join(directory, name));
135
+ fs.rmdirSync(directory);
136
+ }
137
+ }
138
+
139
+ // Resolve native binaries, never execute a shell shim from a background task.
140
+ export function discoverCodex(environment = process.env) {
141
+ const found = new Map();
142
+ const add = (file, source) => {
143
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return;
144
+ const executable = fs.realpathSync(file);
145
+ if (!found.has(executable.toLowerCase()) || source === 'desktop') found.set(executable.toLowerCase(), { executable, source });
146
+ };
147
+ if (environment.CODE_RELAX_CODEX_PATH) {
148
+ const file = environment.CODE_RELAX_CODEX_PATH;
149
+ if (!path.isAbsolute(file) || path.basename(file).toLowerCase() !== 'codex.exe') throw new Error('CODE_RELAX_CODEX_PATH 必须是 codex.exe 的绝对路径。');
150
+ add(file, 'explicit');
151
+ if (!found.size) throw new Error('CODE_RELAX_CODEX_PATH 指定的程序不存在。');
152
+ return [...found.values()];
153
+ }
154
+ const local = environment.LOCALAPPDATA || path.join(os.homedir(), 'AppData/Local');
155
+ const home = environment.CODEX_HOME || path.join(os.homedir(), '.codex');
156
+ const directories = [...String(environment.PATH || environment.Path || '').split(';').filter(Boolean),
157
+ environment.CODEX_INSTALL_DIR, path.join(local, 'Programs/OpenAI/Codex/bin'),
158
+ path.join(home, 'packages/standalone/current/bin')].filter(Boolean);
159
+ for (const directory of directories) {
160
+ add(path.join(directory, 'codex.exe'), 'cli');
161
+ const packageFile = path.join(directory, 'node_modules/@openai/codex/package.json');
162
+ if (!fs.existsSync(packageFile)) continue;
163
+ const root = path.dirname(fs.realpathSync(packageFile));
164
+ let vendor = path.join(root, 'vendor');
165
+ try { vendor = path.join(path.dirname(createRequire(packageFile).resolve('@openai/codex-win32-x64/package.json')), 'vendor'); }
166
+ catch (error) { if (error.code !== 'MODULE_NOT_FOUND') throw error; }
167
+ add(path.join(vendor, 'x86_64-pc-windows-msvc/bin/codex.exe'), 'npm');
168
+ }
169
+ const desktopRoot = path.join(local, 'OpenAI/Codex/bin');
170
+ add(path.join(desktopRoot, 'codex.exe'), 'desktop');
171
+ if (fs.existsSync(desktopRoot)) for (const entry of fs.readdirSync(desktopRoot, { withFileTypes: true })) {
172
+ if (entry.isDirectory()) add(path.join(desktopRoot, entry.name, 'codex.exe'), 'desktop');
173
+ }
174
+ return [...found.values()];
175
+ }
176
+
177
+ export async function resolveCodexRuntime(stateRoot, { refresh = false, environment = process.env } = {}) {
178
+ fs.mkdirSync(stateRoot, { recursive: true });
179
+ const recordFile = path.join(stateRoot, 'codex-runtime.json');
180
+ const saved = readRecord(recordFile);
181
+ environment = { ...environment,
182
+ CODEX_HOME: environment.CODEX_HOME || saved?.codexHome || path.join(os.homedir(), '.codex'),
183
+ CODE_RELAX_CODEX_PATH: environment.CODE_RELAX_CODEX_PATH || (saved?.source === 'explicit' ? saved.executable : '') };
184
+ if (!refresh && saved?.executable && runtimeFilesAvailable(saved.executable)
185
+ && saved.fingerprint === fingerprint(saved.executable)) return saved;
186
+ const candidates = [], rejected = [];
187
+ let installed = null;
188
+ try { installed = await installedDesktopRuntime(); }
189
+ catch (error) { rejected.push(`无法读取当前 Codex Desktop 安装包:${error.message}`); }
190
+ for (const candidate of discoverCodex(environment)) {
191
+ try {
192
+ if (!runtimeFilesAvailable(candidate.executable)) throw new Error('缺少同目录的 codex-code-mode-host.exe');
193
+ if (candidate.source === 'desktop' && !await matchesInstalledDesktop(candidate.executable, installed)) {
194
+ throw new Error('不是当前 Codex Desktop 安装包配套的 CLI');
195
+ }
196
+ const { stdout } = await exec(candidate.executable, ['--version'], { windowsHide: true, timeout: 10000 });
197
+ const version = /^codex-cli (\d+\.\d+\.\d+(?:-[\w.]+)?)/m.exec(stdout)?.[1];
198
+ if (!version) throw new Error('无法识别版本');
199
+ candidates.push({ ...candidate, version });
200
+ } catch (error) { rejected.push(`${candidate.executable}: ${error.message}`); }
201
+ }
202
+ candidates.sort((a, b) => Number(b.source === 'desktop') - Number(a.source === 'desktop') || compareVersions(b.version, a.version));
203
+ for (const candidate of candidates) {
204
+ try {
205
+ const executable = candidate.source === 'desktop'
206
+ ? await retainDesktopRuntime(candidate.executable, installed, stateRoot) : candidate.executable;
207
+ await verifyProtocol(executable, stateRoot);
208
+ const result = { ...candidate, executable, fingerprint: fingerprint(executable),
209
+ codexHome: path.resolve(environment.CODEX_HOME || path.join(os.homedir(), '.codex')) };
210
+ writeRecord(recordFile, result);
211
+ return result;
212
+ } catch (error) { rejected.push(`${candidate.executable}: ${error.message}`); }
213
+ }
214
+ throw new Error(`未找到兼容的 Codex CLI,请安装或更新 Codex CLI 后执行 relax setup。${rejected.length ? '\n' + rejected.join('\n') : ''}`);
215
+ }
@@ -1,27 +1,27 @@
1
- import { spawn } from 'node:child_process';
2
- import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { runtimePaths } from './runtime-paths.mjs';
5
-
6
- // Secrets travel over stdin/stdout only; never PowerShell arguments or errors.
7
- export function credentialStore(action, record, root = runtimePaths().credentialsRoot) {
8
- if (process.platform !== 'win32') throw new Error('Credential platform adapter unavailable');
9
- return new Promise((resolve, reject) => {
10
- const child = spawn(path.join(process.env.SystemRoot, 'System32/WindowsPowerShell/v1.0/powershell.exe'),
11
- ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File',
12
- fileURLToPath(new URL('./credential-store.ps1', import.meta.url))],
13
- { windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] });
14
- let output = '', diagnostic = '';
15
- const timer = setTimeout(() => child.kill(), 15000);
16
- child.stdout.setEncoding('utf8'); child.stdout.on('data', data => { output += data; });
17
- child.stderr.setEncoding('utf8'); child.stderr.on('data', data => { diagnostic += data; }); child.stdin.on('error', () => {});
18
- child.once('error', () => { clearTimeout(timer); reject(new Error('无法启动 Windows 凭据保护组件')); });
19
- child.once('close', code => {
20
- clearTimeout(timer);
21
- if (code !== 0) return reject(new Error('Windows 凭据保护失败,未使用明文替代;请检查目录权限或 Windows 用户是否一致;' + diagnostic.trim()));
22
- try { resolve(JSON.parse(output.replace(/^\uFEFF/, ''))); }
23
- catch { reject(new Error('Windows 凭据保护返回无效结果')); }
24
- });
25
- child.stdin.end(JSON.stringify({ action, root, record }));
26
- });
27
- }
1
+ import { spawn } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { runtimePaths } from './runtime-paths.mjs';
5
+
6
+ // Secrets travel over stdin/stdout only; never PowerShell arguments or errors.
7
+ export function credentialStore(action, record, root = runtimePaths().credentialsRoot) {
8
+ if (process.platform !== 'win32') throw new Error('Credential platform adapter unavailable');
9
+ return new Promise((resolve, reject) => {
10
+ const child = spawn(path.join(process.env.SystemRoot, 'System32/WindowsPowerShell/v1.0/powershell.exe'),
11
+ ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File',
12
+ fileURLToPath(new URL('./credential-store.ps1', import.meta.url))],
13
+ { windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] });
14
+ let output = '', diagnostic = '';
15
+ const timer = setTimeout(() => child.kill(), 15000);
16
+ child.stdout.setEncoding('utf8'); child.stdout.on('data', data => { output += data; });
17
+ child.stderr.setEncoding('utf8'); child.stderr.on('data', data => { diagnostic += data; }); child.stdin.on('error', () => {});
18
+ child.once('error', () => { clearTimeout(timer); reject(new Error('无法启动 Windows 凭据保护组件')); });
19
+ child.once('close', code => {
20
+ clearTimeout(timer);
21
+ if (code !== 0) return reject(new Error('Windows 凭据保护失败,未使用明文替代;请检查目录权限或 Windows 用户是否一致;' + diagnostic.trim()));
22
+ try { resolve(JSON.parse(output.replace(/^\uFEFF/, ''))); }
23
+ catch { reject(new Error('Windows 凭据保护返回无效结果')); }
24
+ });
25
+ child.stdin.end(JSON.stringify({ action, root, record }));
26
+ });
27
+ }
@@ -1,49 +1,49 @@
1
- $ErrorActionPreference = 'Stop'
2
- [Console]::InputEncoding = New-Object System.Text.UTF8Encoding($false)
3
- [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
4
- try {
5
- Add-Type -AssemblyName System.Security
6
- $request = [Console]::In.ReadToEnd() | ConvertFrom-Json
7
- $sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
8
- $directory = Join-Path ([System.IO.Path]::GetFullPath($request.root)) $sid.Value
9
- if (!(Test-Path -LiteralPath $directory)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null }
10
- if ((Get-Item -LiteralPath $directory).Attributes -band [System.IO.FileAttributes]::ReparsePoint) { throw 'Invalid credential directory' }
11
- $acl = New-Object System.Security.AccessControl.DirectorySecurity
12
- $acl.SetAccessRuleProtection($true, $false)
13
- foreach ($principal in @($sid.Value, 'S-1-5-18', 'S-1-5-32-544')) {
14
- $identity = New-Object System.Security.Principal.SecurityIdentifier($principal)
15
- $rule = New-Object System.Security.AccessControl.FileSystemAccessRule($identity, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
16
- $acl.AddAccessRule($rule)
17
- }
18
- [System.IO.Directory]::SetAccessControl($directory, $acl)
19
- $file = Join-Path $directory 'self-relay.json'
20
- if (Test-Path -LiteralPath $file) {
21
- if ((Get-Item -LiteralPath $file).Attributes -band [System.IO.FileAttributes]::ReparsePoint) { throw 'Invalid credential file' }
22
- }
23
- if ($request.action -eq 'save') {
24
- $plain = [System.Text.Encoding]::UTF8.GetBytes(($request.record | ConvertTo-Json -Depth 12 -Compress))
25
- $encrypted = [System.Security.Cryptography.ProtectedData]::Protect($plain, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)
26
- $json = @{ version = 1; protection = 'DPAPI-CurrentUser'; data = [Convert]::ToBase64String($encrypted) } | ConvertTo-Json -Compress
27
- $temporary = Join-Path $directory ([Guid]::NewGuid().ToString() + '.tmp')
28
- $backup = $temporary + '.bak'
29
- try {
30
- $stream = [System.IO.File]::Open($temporary, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write)
31
- try { $bytes = [System.Text.Encoding]::UTF8.GetBytes($json); $stream.Write($bytes, 0, $bytes.Length); $stream.Flush($true) } finally { $stream.Dispose() }
32
- if (Test-Path -LiteralPath $file) { [System.IO.File]::Replace($temporary, $file, $backup) } else { [System.IO.File]::Move($temporary, $file) }
33
- } finally {
34
- if (Test-Path -LiteralPath $temporary) { Remove-Item -LiteralPath $temporary -Force }
35
- if (Test-Path -LiteralPath $backup) { Remove-Item -LiteralPath $backup -Force }
36
- }
37
- } elseif ($request.action -ne 'load') { throw 'Invalid operation' }
38
- $record = $null
39
- if (Test-Path -LiteralPath $file) {
40
- $envelope = [System.IO.File]::ReadAllText($file, [System.Text.Encoding]::UTF8) | ConvertFrom-Json
41
- if ($envelope.version -ne 1 -or $envelope.protection -ne 'DPAPI-CurrentUser') { throw 'Invalid credential envelope' }
42
- $plain = [System.Security.Cryptography.ProtectedData]::Unprotect([Convert]::FromBase64String($envelope.data), $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)
43
- $record = [System.Text.Encoding]::UTF8.GetString($plain) | ConvertFrom-Json
44
- }
45
- [Console]::Write((@{ path = $file; record = $record } | ConvertTo-Json -Depth 12 -Compress))
46
- } catch {
47
- [Console]::Error.WriteLine('Credential protection failed: ' + $_.Exception.GetType().Name + ' at line ' + $_.InvocationInfo.ScriptLineNumber)
48
- exit 1
49
- }
1
+ $ErrorActionPreference = 'Stop'
2
+ [Console]::InputEncoding = New-Object System.Text.UTF8Encoding($false)
3
+ [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
4
+ try {
5
+ Add-Type -AssemblyName System.Security
6
+ $request = [Console]::In.ReadToEnd() | ConvertFrom-Json
7
+ $sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
8
+ $directory = Join-Path ([System.IO.Path]::GetFullPath($request.root)) $sid.Value
9
+ if (!(Test-Path -LiteralPath $directory)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null }
10
+ if ((Get-Item -LiteralPath $directory).Attributes -band [System.IO.FileAttributes]::ReparsePoint) { throw 'Invalid credential directory' }
11
+ $acl = New-Object System.Security.AccessControl.DirectorySecurity
12
+ $acl.SetAccessRuleProtection($true, $false)
13
+ foreach ($principal in @($sid.Value, 'S-1-5-18', 'S-1-5-32-544')) {
14
+ $identity = New-Object System.Security.Principal.SecurityIdentifier($principal)
15
+ $rule = New-Object System.Security.AccessControl.FileSystemAccessRule($identity, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
16
+ $acl.AddAccessRule($rule)
17
+ }
18
+ [System.IO.Directory]::SetAccessControl($directory, $acl)
19
+ $file = Join-Path $directory 'self-relay.json'
20
+ if (Test-Path -LiteralPath $file) {
21
+ if ((Get-Item -LiteralPath $file).Attributes -band [System.IO.FileAttributes]::ReparsePoint) { throw 'Invalid credential file' }
22
+ }
23
+ if ($request.action -eq 'save') {
24
+ $plain = [System.Text.Encoding]::UTF8.GetBytes(($request.record | ConvertTo-Json -Depth 12 -Compress))
25
+ $encrypted = [System.Security.Cryptography.ProtectedData]::Protect($plain, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)
26
+ $json = @{ version = 1; protection = 'DPAPI-CurrentUser'; data = [Convert]::ToBase64String($encrypted) } | ConvertTo-Json -Compress
27
+ $temporary = Join-Path $directory ([Guid]::NewGuid().ToString() + '.tmp')
28
+ $backup = $temporary + '.bak'
29
+ try {
30
+ $stream = [System.IO.File]::Open($temporary, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write)
31
+ try { $bytes = [System.Text.Encoding]::UTF8.GetBytes($json); $stream.Write($bytes, 0, $bytes.Length); $stream.Flush($true) } finally { $stream.Dispose() }
32
+ if (Test-Path -LiteralPath $file) { [System.IO.File]::Replace($temporary, $file, $backup) } else { [System.IO.File]::Move($temporary, $file) }
33
+ } finally {
34
+ if (Test-Path -LiteralPath $temporary) { Remove-Item -LiteralPath $temporary -Force }
35
+ if (Test-Path -LiteralPath $backup) { Remove-Item -LiteralPath $backup -Force }
36
+ }
37
+ } elseif ($request.action -ne 'load') { throw 'Invalid operation' }
38
+ $record = $null
39
+ if (Test-Path -LiteralPath $file) {
40
+ $envelope = [System.IO.File]::ReadAllText($file, [System.Text.Encoding]::UTF8) | ConvertFrom-Json
41
+ if ($envelope.version -ne 1 -or $envelope.protection -ne 'DPAPI-CurrentUser') { throw 'Invalid credential envelope' }
42
+ $plain = [System.Security.Cryptography.ProtectedData]::Unprotect([Convert]::FromBase64String($envelope.data), $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)
43
+ $record = [System.Text.Encoding]::UTF8.GetString($plain) | ConvertFrom-Json
44
+ }
45
+ [Console]::Write((@{ path = $file; record = $record } | ConvertTo-Json -Depth 12 -Compress))
46
+ } catch {
47
+ [Console]::Error.WriteLine('Credential protection failed: ' + $_.Exception.GetType().Name + ' at line ' + $_.InvocationInfo.ScriptLineNumber)
48
+ exit 1
49
+ }