@walkhi/code-relax 0.1.0-beta.65 → 0.1.0-beta.68

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 (121) hide show
  1. package/dist/bin/codex-remote.mjs +4 -2
  2. package/dist/bin/manage-relay-admin.mjs +20 -20
  3. package/dist/bin/migrate-relay-db.mjs +109 -109
  4. package/dist/bin/publish-harmony-release.mjs +46 -46
  5. package/dist/bin/self-relay-admin.mjs +8 -8
  6. package/dist/bin/self-relay-demo.mjs +33 -33
  7. package/dist/bin/self-relay-server.mjs +66 -66
  8. package/dist/bin/set-relay-admin-password.mjs +11 -11
  9. package/dist/build-platform.json +1 -1
  10. package/dist/get-lan-networks.ps1 +40 -40
  11. package/dist/shared/app-server-events.cjs +511 -511
  12. package/dist/shared/p2p-data-channel.cjs +72 -72
  13. package/dist/shared/p2p-diagnostics.cjs +28 -28
  14. package/dist/shared/p2p-ice-policy.cjs +22 -22
  15. package/dist/shared/plan-pricing.cjs +32 -32
  16. package/dist/shared/question-attention.cjs +69 -69
  17. package/dist/shared/release-version.cjs +31 -31
  18. package/dist/src/archived-threads.mjs +22 -22
  19. package/dist/src/background-process.mjs +18 -18
  20. package/dist/src/code-browser.mjs +100 -100
  21. package/dist/src/desktop-launcher.mjs +62 -62
  22. package/dist/src/dots-task-files.mjs +52 -52
  23. package/dist/src/execution-health.mjs +26 -26
  24. package/dist/src/message-images.mjs +9 -9
  25. package/dist/src/plan-pricing.mjs +47 -47
  26. package/dist/src/platform/codex-runtime.mjs +3 -3
  27. package/dist/src/platform/credential-store.mjs +19 -19
  28. package/dist/src/platform/desktop-host.mjs +3 -3
  29. package/dist/src/platform/desktop-permission-mode.mjs +17 -17
  30. package/dist/src/platform/desktop-project-registration.mjs +3 -3
  31. package/dist/src/platform/file-opener.mjs +9 -9
  32. package/dist/src/platform/linux/README.md +3 -3
  33. package/dist/src/platform/linux/codex-runtime.mjs +56 -56
  34. package/dist/src/platform/linux/desktop-installation.mjs +36 -36
  35. package/dist/src/platform/linux/desktop-project-registration.mjs +11 -11
  36. package/dist/src/platform/linux/network-info.mjs +11 -11
  37. package/dist/src/platform/linux/project-locations.mjs +17 -17
  38. package/dist/src/platform/linux/resident-startup.mjs +52 -52
  39. package/dist/src/platform/linux/retained-runtime.mjs +76 -76
  40. package/dist/src/platform/linux/service-host.mjs +18 -18
  41. package/dist/src/platform/macos/README.md +3 -3
  42. package/dist/src/platform/macos/codex-runtime.mjs +0 -4
  43. package/dist/src/platform/macos/desktop-project-registration.mjs +11 -11
  44. package/dist/src/platform/macos/network-info.mjs +8 -8
  45. package/dist/src/platform/macos/project-locations.mjs +8 -8
  46. package/dist/src/platform/macos/resident-startup.mjs +84 -84
  47. package/dist/src/platform/macos/retained-runtime.mjs +127 -127
  48. package/dist/src/platform/macos/service-host.mjs +18 -18
  49. package/dist/src/platform/macos/system.mjs +29 -29
  50. package/dist/src/platform/network-info.mjs +3 -3
  51. package/dist/src/platform/path-identity.mjs +5 -5
  52. package/dist/src/platform/posix/network-candidates.mjs +23 -23
  53. package/dist/src/platform/posix/runtime-paths.mjs +25 -25
  54. package/dist/src/platform/power-inhibitor.mjs +22 -22
  55. package/dist/src/platform/project-locations.mjs +3 -3
  56. package/dist/src/platform/runtime-paths.mjs +8 -8
  57. package/dist/src/platform/select.mjs +5 -5
  58. package/dist/src/platform/service-host.mjs +3 -3
  59. package/dist/src/platform/user-environment.mjs +3 -3
  60. package/dist/src/platform/windows/IsolatedProcess.cs +79 -79
  61. package/dist/src/platform/windows/background-process.mjs +30 -30
  62. package/dist/src/platform/windows/desktop-helper.mjs +179 -179
  63. package/dist/src/platform/windows/desktop-runtime.ps1 +10 -10
  64. package/dist/src/platform/windows/desktop-shortcuts.ps1 +7 -7
  65. package/dist/src/platform/windows/keep-awake.ps1 +20 -20
  66. package/dist/src/platform/windows/network-info.mjs +34 -34
  67. package/dist/src/platform/windows/resident-start.js +20 -20
  68. package/dist/src/platform/windows/resident-start.ps1 +7 -7
  69. package/dist/src/platform/windows/resident-startup.ps1 +83 -83
  70. package/dist/src/platform/windows/resolve-state-root.ps1 +14 -14
  71. package/dist/src/platform/windows/runtime-paths.mjs +20 -20
  72. package/dist/src/platform/windows/rustdesk-video-probe.mjs +167 -167
  73. package/dist/src/platform/windows/start-hidden-console.ps1 +58 -58
  74. package/dist/src/platform/windows/stop-desktop.ps1 +22 -22
  75. package/dist/src/platform/windows/user-environment.ps1 +41 -41
  76. package/dist/src/project-create.mjs +121 -121
  77. package/dist/src/project-management.mjs +1 -1
  78. package/dist/src/project-remove.mjs +55 -55
  79. package/dist/src/project-update.mjs +59 -59
  80. package/dist/src/relay-config-import.mjs +45 -45
  81. package/dist/src/self-relay/account-connections.mjs +52 -52
  82. package/dist/src/self-relay/admin-auth.mjs +59 -59
  83. package/dist/src/self-relay/admin-cloud.mjs +101 -101
  84. package/dist/src/self-relay/admin-login-page.mjs +7 -7
  85. package/dist/src/self-relay/admin-server.mjs +105 -105
  86. package/dist/src/self-relay/admin-state.mjs +127 -127
  87. package/dist/src/self-relay/admin-workstations-page.mjs +95 -95
  88. package/dist/src/self-relay/admin-workstations.mjs +143 -143
  89. package/dist/src/self-relay/android-releases.mjs +61 -61
  90. package/dist/src/self-relay/client.mjs +61 -61
  91. package/dist/src/self-relay/connection-reporting.mjs +38 -38
  92. package/dist/src/self-relay/debug-allowlist.mjs +33 -33
  93. package/dist/src/self-relay/demo.mjs +6 -6
  94. package/dist/src/self-relay/device-info.mjs +27 -27
  95. package/dist/src/self-relay/device-store.mjs +92 -92
  96. package/dist/src/self-relay/lan-authorizations.mjs +71 -71
  97. package/dist/src/self-relay/lifecycle.mjs +278 -278
  98. package/dist/src/self-relay/long-pairing.mjs +40 -40
  99. package/dist/src/self-relay/mysql-state.mjs +118 -118
  100. package/dist/src/self-relay/phone-connections.mjs +55 -55
  101. package/dist/src/self-relay/server.mjs +1066 -1066
  102. package/dist/src/self-relay/usage-daily.mjs +55 -55
  103. package/dist/src/self-relay/windows-route-helper.mjs +75 -75
  104. package/dist/src/self-relay/wire.mjs +36 -36
  105. package/dist/src/server.mjs +1947 -1947
  106. package/dist/src/service-control.mjs +82 -82
  107. package/dist/src/service-health.mjs +33 -33
  108. package/dist/src/service-power.mjs +58 -58
  109. package/dist/src/service-restart.mjs +81 -81
  110. package/dist/src/service-update-worker.mjs +42 -42
  111. package/dist/src/service-update.mjs +80 -80
  112. package/dist/src/shared-recovery.mjs +152 -152
  113. package/dist/src/skill-install.mjs +51 -51
  114. package/dist/src/thread-title-generation.mjs +150 -150
  115. package/dist/src/turn-model-history.mjs +47 -47
  116. package/dist/tools/rtc-route-helper.ps1 +99 -99
  117. package/dist/tools/start-rtc-route-helper.ps1 +15 -15
  118. package/dist/web/vendor/markdown-it.LICENSE.txt +22 -22
  119. package/dist/web/vendor/markdown-it.umd.min.js +13 -13
  120. package/package.json +4 -4
  121. package/tools/postinstall.mjs +1 -1
@@ -1,76 +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
- }
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
+ }
@@ -1,18 +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
- }
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
+ }
@@ -1,3 +1,3 @@
1
- # macos 被控主机(预留)
2
-
3
- 仅规划 Codex 的系统路径、进程/IPC 和运行环境适配,尚未实现。接入排在控制端扩展之后;不预设支持 Desktop 共享任务。
1
+ # macos 被控主机(预留)
2
+
3
+ 仅规划 Codex 的系统路径、进程/IPC 和运行环境适配,尚未实现。接入排在控制端扩展之后;不预设支持 Desktop 共享任务。
@@ -100,10 +100,6 @@ export async function resolveCodexRuntime(stateRoot, { refresh = false, environm
100
100
  const candidates = [], rejected = [];
101
101
  for (const candidate of discoverCodex({ ...environment, CODEX_HOME: codexHome, CODE_RELAX_CODEX_PATH: explicit })) {
102
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
103
  if (!runtimeFilesAvailable(candidate.executable)) throw new Error('缺少配套原生 codex-code-mode-host');
108
104
  const { stdout } = await exec(candidate.executable, ['--version'], { timeout: 10000, env: { ...environment, CODEX_HOME: codexHome } });
109
105
  const version = /^codex-cli (\d+\.\d+\.\d+(?:-[\w.]+)?)/m.exec(stdout)?.[1];
@@ -1,11 +1,11 @@
1
- import { desktopHost } from './desktop-host.mjs';
2
- import { withDesktopProjectRegistration as register } from '../desktop-project-store.mjs';
3
-
4
- // Verified against the Mac 26.1007.71326 local-projects/project-order store.
5
- export async function withDesktopProjectRegistration(options, action) {
6
- const host = options.host || desktopHost;
7
- if (!(await host.inspect()).desktop)
8
- return action({ register: async () => ({ status: 'not-applicable', reason: 'desktop-not-installed' }),
9
- remove: async (_project, _operation, _save, removeNative) => { await removeNative(); return { status: 'not-applicable', reason: 'desktop-not-installed' }; } });
10
- return register({ ...options, host }, action);
11
- }
1
+ import { desktopHost } from './desktop-host.mjs';
2
+ import { withDesktopProjectRegistration as register } from '../desktop-project-store.mjs';
3
+
4
+ // Verified against the Mac 26.1007.71326 local-projects/project-order store.
5
+ export async function withDesktopProjectRegistration(options, action) {
6
+ const host = options.host || desktopHost;
7
+ if (!(await host.inspect()).desktop)
8
+ return action({ register: async () => ({ status: 'not-applicable', reason: 'desktop-not-installed' }),
9
+ remove: async (_project, _operation, _save, removeNative) => { await removeNative(); return { status: 'not-applicable', reason: 'desktop-not-installed' }; } });
10
+ return register({ ...options, host }, action);
11
+ }
@@ -1,8 +1,8 @@
1
- import os from 'node:os';
2
- import { systemRequest } from './system.mjs';
3
- import { networkCandidates } from '../posix/network-candidates.mjs';
4
- export { networkCandidates } from '../posix/network-candidates.mjs';
5
-
6
- export async function lanNetworks() {
7
- return networkCandidates(await systemRequest({ operation: 'network-interfaces' }), os.networkInterfaces());
8
- }
1
+ import os from 'node:os';
2
+ import { systemRequest } from './system.mjs';
3
+ import { networkCandidates } from '../posix/network-candidates.mjs';
4
+ export { networkCandidates } from '../posix/network-candidates.mjs';
5
+
6
+ export async function lanNetworks() {
7
+ return networkCandidates(await systemRequest({ operation: 'network-interfaces' }), os.networkInterfaces());
8
+ }
@@ -1,8 +1,8 @@
1
- import path from 'node:path';
2
- import { systemRequest } from './system.mjs';
3
-
4
- export async function projectLocations() {
5
- const { home, documents, downloads } = await systemRequest({ operation: 'locations' });
6
- if (![home, documents, downloads].every(value => typeof value === 'string' && path.isAbsolute(value))) throw new Error('Mac 用户目录无效。');
7
- return { home, downloads, defaultParent: path.join(documents, 'ChatGPT'), roots: ['/'] };
8
- }
1
+ import path from 'node:path';
2
+ import { systemRequest } from './system.mjs';
3
+
4
+ export async function projectLocations() {
5
+ const { home, documents, downloads } = await systemRequest({ operation: 'locations' });
6
+ if (![home, documents, downloads].every(value => typeof value === 'string' && path.isAbsolute(value))) throw new Error('Mac 用户目录无效。');
7
+ return { home, downloads, defaultParent: path.join(documents, 'ChatGPT'), roots: ['/'] };
8
+ }
@@ -1,84 +1,84 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { execFile } from 'node:child_process';
5
- import { promisify } from 'node:util';
6
- import { readRecord } from '../../service-lifecycle.mjs';
7
- import { assertPlainPath } from '../../skill-install.mjs';
8
- import { runtimePaths } from '../runtime-paths.mjs';
9
-
10
- const exec = promisify(execFile);
11
- export const launchAgentLabel = 'cn.walkhi.code-relax';
12
- const xml = value => String(value).replace(/[&<>"']/g, character => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;' }[character]));
13
- export function launchAgentPlist({ serviceRoot, nodeExecutable, stateRoot, home, environment }) {
14
- const entry = path.join(serviceRoot, 'dist/bin/codex-remote.mjs');
15
- // npm adds temporary .bin directories during postinstall. Login must use the
16
- // user's stable tool paths after the source archive has been removed.
17
- const stablePath = String(environment.PATH || '').split(':')
18
- .filter(value => value.startsWith('/') && !value.endsWith('/node_modules/.bin')).join(':');
19
- const variables = { HOME: home, PATH: stablePath || '/usr/bin:/bin:/usr/sbin:/sbin' };
20
- for (const name of ['CODEX_HOME', 'CODEX_INSTALL_DIR', 'CODE_RELAX_CODEX_PATH']) if (environment[name]) variables[name] = environment[name];
21
- const strings = [nodeExecutable, entry, ...Object.values(variables), stateRoot];
22
- if (strings.some(value => /[\u0000-\u0008\u000b\u000c\u000e-\u001f]/.test(value))) throw new Error('登录启动参数含无效控制字符。');
23
- return `<?xml version="1.0" encoding="UTF-8"?>
24
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
25
- <plist version="1.0"><dict>
26
- <key>Label</key><string>${launchAgentLabel}</string>
27
- <key>ProgramArguments</key><array>${[nodeExecutable, entry, 'start', '--json'].map(value => `<string>${xml(value)}</string>`).join('')}</array>
28
- <key>RunAtLoad</key><true/>
29
- <key>AbandonProcessGroup</key><true/>
30
- <key>EnvironmentVariables</key><dict>${Object.entries(variables).map(([key, value]) => `<key>${key}</key><string>${xml(value)}</string>`).join('')}</dict>
31
- <key>StandardOutPath</key><string>${xml(path.join(stateRoot, 'login.stdout.log'))}</string>
32
- <key>StandardErrorPath</key><string>${xml(path.join(stateRoot, 'login.stderr.log'))}</string>
33
- </dict></plist>
34
- `;
35
- }
36
-
37
- export async function residentStartup({ serviceRoot, nodeExecutable }, action = 'status') {
38
- if (!['status', 'enable', 'disable'].includes(action)) throw new Error('无效的登录启动操作。');
39
- const uid = process.getuid();
40
- if (uid === 0) throw new Error('请以普通 Mac 用户安装 Code Relax,不使用 sudo。');
41
- const home = os.homedir();
42
- const file = path.join(home, 'Library/LaunchAgents', `${launchAgentLabel}.plist`);
43
- const domain = `gui/${uid}`, target = `${domain}/${launchAgentLabel}`;
44
- assertPlainPath(file);
45
- let current;
46
- if (fs.existsSync(file)) {
47
- const { stdout } = await exec('/usr/bin/plutil', ['-convert', 'json', '-o', '-', file], { timeout: 5000 });
48
- current = JSON.parse(stdout);
49
- if (current.Label !== launchAgentLabel || current.ProgramArguments?.[1] !== path.join(serviceRoot, 'dist/bin/codex-remote.mjs'))
50
- throw new Error('同名 LaunchAgent 不属于此安装,未覆盖或移除。');
51
- }
52
- const { stdout: disabled } = await exec('/bin/launchctl', ['print-disabled', domain], { timeout: 5000 });
53
- const isDisabled = /"cn\.walkhi\.code-relax"\s*=>\s*true/.test(disabled);
54
- const expectedArguments = [nodeExecutable, path.join(serviceRoot, 'dist/bin/codex-remote.mjs'), 'start', '--json'];
55
- const enabled = JSON.stringify(current?.ProgramArguments) === JSON.stringify(expectedArguments)
56
- && current.RunAtLoad === true && current.AbandonProcessGroup === true
57
- && current.EnvironmentVariables?.HOME === home && !isDisabled;
58
- if (action === 'status') return { enabled, path: file, mechanism: 'LaunchAgent' };
59
- if (action === 'disable') {
60
- if (!current) return { enabled: false, removed: false, path: file };
61
- let loaded = false;
62
- try { await exec('/bin/launchctl', ['print', target], { timeout: 5000 }); loaded = true; }
63
- catch (error) { if (error.code !== 113) throw error; }
64
- if (loaded) await exec('/bin/launchctl', ['bootout', target], { timeout: 5000 });
65
- await exec('/bin/launchctl', ['disable', target], { timeout: 5000 });
66
- fs.unlinkSync(file);
67
- return { enabled: false, removed: true, path: file };
68
- }
69
- const { stateRoot } = runtimePaths();
70
- const runtime = readRecord(path.join(stateRoot, 'codex-runtime.json'));
71
- const environment = { ...process.env, ...(runtime?.codexHome ? { CODEX_HOME: runtime.codexHome } : {}) };
72
- fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
73
- fs.mkdirSync(stateRoot, { recursive: true, mode: 0o700 });
74
- const temporary = `${file}.${process.pid}.tmp`;
75
- try {
76
- fs.writeFileSync(temporary, launchAgentPlist({ serviceRoot, nodeExecutable, stateRoot, home, environment }), { flag: 'wx', mode: 0o600 });
77
- await exec('/usr/bin/plutil', ['-lint', temporary], { timeout: 5000 });
78
- fs.renameSync(temporary, file);
79
- } finally { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); }
80
- await exec('/bin/launchctl', ['enable', target], { timeout: 5000 });
81
- // setup/start already launches the service. Bootstrap would run a second start
82
- // concurrently; launchd will load this user agent at the next GUI login.
83
- return { enabled: true, path: file, mechanism: 'LaunchAgent', appliesAtNextLogin: true };
84
- }
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { execFile } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import { readRecord } from '../../service-lifecycle.mjs';
7
+ import { assertPlainPath } from '../../skill-install.mjs';
8
+ import { runtimePaths } from '../runtime-paths.mjs';
9
+
10
+ const exec = promisify(execFile);
11
+ export const launchAgentLabel = 'cn.walkhi.code-relax';
12
+ const xml = value => String(value).replace(/[&<>"']/g, character => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;' }[character]));
13
+ export function launchAgentPlist({ serviceRoot, nodeExecutable, stateRoot, home, environment }) {
14
+ const entry = path.join(serviceRoot, 'dist/bin/codex-remote.mjs');
15
+ // npm adds temporary .bin directories during postinstall. Login must use the
16
+ // user's stable tool paths after the source archive has been removed.
17
+ const stablePath = String(environment.PATH || '').split(':')
18
+ .filter(value => value.startsWith('/') && !value.endsWith('/node_modules/.bin')).join(':');
19
+ const variables = { HOME: home, PATH: stablePath || '/usr/bin:/bin:/usr/sbin:/sbin' };
20
+ for (const name of ['CODEX_HOME', 'CODEX_INSTALL_DIR', 'CODE_RELAX_CODEX_PATH']) if (environment[name]) variables[name] = environment[name];
21
+ const strings = [nodeExecutable, entry, ...Object.values(variables), stateRoot];
22
+ if (strings.some(value => /[\u0000-\u0008\u000b\u000c\u000e-\u001f]/.test(value))) throw new Error('登录启动参数含无效控制字符。');
23
+ return `<?xml version="1.0" encoding="UTF-8"?>
24
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
25
+ <plist version="1.0"><dict>
26
+ <key>Label</key><string>${launchAgentLabel}</string>
27
+ <key>ProgramArguments</key><array>${[nodeExecutable, entry, 'start', '--json'].map(value => `<string>${xml(value)}</string>`).join('')}</array>
28
+ <key>RunAtLoad</key><true/>
29
+ <key>AbandonProcessGroup</key><true/>
30
+ <key>EnvironmentVariables</key><dict>${Object.entries(variables).map(([key, value]) => `<key>${key}</key><string>${xml(value)}</string>`).join('')}</dict>
31
+ <key>StandardOutPath</key><string>${xml(path.join(stateRoot, 'login.stdout.log'))}</string>
32
+ <key>StandardErrorPath</key><string>${xml(path.join(stateRoot, 'login.stderr.log'))}</string>
33
+ </dict></plist>
34
+ `;
35
+ }
36
+
37
+ export async function residentStartup({ serviceRoot, nodeExecutable }, action = 'status') {
38
+ if (!['status', 'enable', 'disable'].includes(action)) throw new Error('无效的登录启动操作。');
39
+ const uid = process.getuid();
40
+ if (uid === 0) throw new Error('请以普通 Mac 用户安装 Code Relax,不使用 sudo。');
41
+ const home = os.homedir();
42
+ const file = path.join(home, 'Library/LaunchAgents', `${launchAgentLabel}.plist`);
43
+ const domain = `gui/${uid}`, target = `${domain}/${launchAgentLabel}`;
44
+ assertPlainPath(file);
45
+ let current;
46
+ if (fs.existsSync(file)) {
47
+ const { stdout } = await exec('/usr/bin/plutil', ['-convert', 'json', '-o', '-', file], { timeout: 5000 });
48
+ current = JSON.parse(stdout);
49
+ if (current.Label !== launchAgentLabel || current.ProgramArguments?.[1] !== path.join(serviceRoot, 'dist/bin/codex-remote.mjs'))
50
+ throw new Error('同名 LaunchAgent 不属于此安装,未覆盖或移除。');
51
+ }
52
+ const { stdout: disabled } = await exec('/bin/launchctl', ['print-disabled', domain], { timeout: 5000 });
53
+ const isDisabled = /"cn\.walkhi\.code-relax"\s*=>\s*true/.test(disabled);
54
+ const expectedArguments = [nodeExecutable, path.join(serviceRoot, 'dist/bin/codex-remote.mjs'), 'start', '--json'];
55
+ const enabled = JSON.stringify(current?.ProgramArguments) === JSON.stringify(expectedArguments)
56
+ && current.RunAtLoad === true && current.AbandonProcessGroup === true
57
+ && current.EnvironmentVariables?.HOME === home && !isDisabled;
58
+ if (action === 'status') return { enabled, path: file, mechanism: 'LaunchAgent' };
59
+ if (action === 'disable') {
60
+ if (!current) return { enabled: false, removed: false, path: file };
61
+ let loaded = false;
62
+ try { await exec('/bin/launchctl', ['print', target], { timeout: 5000 }); loaded = true; }
63
+ catch (error) { if (error.code !== 113) throw error; }
64
+ if (loaded) await exec('/bin/launchctl', ['bootout', target], { timeout: 5000 });
65
+ await exec('/bin/launchctl', ['disable', target], { timeout: 5000 });
66
+ fs.unlinkSync(file);
67
+ return { enabled: false, removed: true, path: file };
68
+ }
69
+ const { stateRoot } = runtimePaths();
70
+ const runtime = readRecord(path.join(stateRoot, 'codex-runtime.json'));
71
+ const environment = { ...process.env, ...(runtime?.codexHome ? { CODEX_HOME: runtime.codexHome } : {}) };
72
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
73
+ fs.mkdirSync(stateRoot, { recursive: true, mode: 0o700 });
74
+ const temporary = `${file}.${process.pid}.tmp`;
75
+ try {
76
+ fs.writeFileSync(temporary, launchAgentPlist({ serviceRoot, nodeExecutable, stateRoot, home, environment }), { flag: 'wx', mode: 0o600 });
77
+ await exec('/usr/bin/plutil', ['-lint', temporary], { timeout: 5000 });
78
+ fs.renameSync(temporary, file);
79
+ } finally { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); }
80
+ await exec('/bin/launchctl', ['enable', target], { timeout: 5000 });
81
+ // setup/start already launches the service. Bootstrap would run a second start
82
+ // concurrently; launchd will load this user agent at the next GUI login.
83
+ return { enabled: true, path: file, mechanism: 'LaunchAgent', appliesAtNextLogin: true };
84
+ }