@walkhi/code-relax 0.1.0-beta.2 → 0.1.0-beta.20
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.
- package/README.md +55 -71
- package/dist/bin/codex-remote.mjs +150 -126
- package/dist/bin/manage-relay-admin.mjs +20 -0
- package/dist/bin/migrate-relay-db.mjs +107 -0
- package/dist/bin/self-relay-admin.mjs +8 -0
- package/dist/bin/self-relay-demo.mjs +2 -2
- package/dist/bin/self-relay-server.mjs +18 -10
- package/dist/shared/app-server-events.cjs +42 -15
- package/dist/shared/file-mentions.cjs +35 -0
- package/dist/shared/server-requests.cjs +86 -58
- package/dist/skills/code-relax/SKILL.md +7 -6
- package/dist/src/desktop-launcher.mjs +16 -21
- package/dist/src/file-upload-session.mjs +124 -0
- package/dist/src/lan-socket-server.mjs +52 -9
- package/dist/src/managed-app-server.mjs +44 -13
- package/dist/src/message-images.mjs +0 -18
- package/dist/src/onboarding.mjs +9 -11
- package/dist/src/platform/README.md +1 -1
- package/dist/src/platform/windows/background-process.mjs +1 -1
- package/dist/src/platform/windows/codex-runtime.mjs +251 -0
- package/dist/src/platform/windows/desktop-host.mjs +17 -3
- package/dist/src/platform/windows/desktop-info.ps1 +2 -4
- package/dist/src/platform/windows/desktop-runtime.ps1 +10 -0
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +7 -14
- package/dist/src/platform/windows/launch-worker.mjs +42 -0
- package/dist/src/platform/windows/resident-start.js +20 -0
- package/dist/src/platform/windows/resident-startup.ps1 +76 -20
- package/dist/src/platform/windows/service-host.mjs +4 -16
- package/dist/src/platform/windows/start-hidden-console.ps1 +10 -5
- package/dist/src/platform/windows/stop-desktop.ps1 +7 -4
- package/dist/src/platform/windows/user-environment.mjs +1 -1
- package/dist/src/runtime-discovery.mjs +43 -0
- package/dist/src/self-relay/admin-cloud.mjs +101 -0
- package/dist/src/self-relay/admin-page.mjs +79 -0
- package/dist/src/self-relay/admin-server.mjs +66 -0
- package/dist/src/self-relay/admin-state.mjs +82 -19
- package/dist/src/self-relay/admin-style.mjs +9 -0
- package/dist/src/self-relay/admin-usage-page.mjs +28 -0
- package/dist/src/self-relay/connector.mjs +243 -30
- package/dist/src/self-relay/demo.mjs +3 -4
- package/dist/src/self-relay/email-accounts.mjs +141 -0
- package/dist/src/self-relay/lifecycle.mjs +99 -142
- package/dist/src/self-relay/mysql-state.mjs +104 -0
- package/dist/src/self-relay/p2p-probe.mjs +1 -1
- package/dist/src/self-relay/server.mjs +335 -59
- package/dist/src/self-relay/usage-diagnostics.mjs +46 -0
- package/dist/src/self-relay/usage-meter.mjs +125 -0
- package/dist/src/server.mjs +731 -1439
- package/dist/src/service-control.mjs +4 -4
- package/dist/src/service-doctor.mjs +27 -29
- package/dist/src/service-health.mjs +2 -2
- package/dist/src/service-install.mjs +1 -4
- package/dist/src/service-lifecycle.mjs +19 -19
- package/dist/src/shared-app-server.mjs +277 -270
- package/dist/src/shared-catalog.mjs +1 -11
- package/dist/src/shared-recovery.mjs +11 -9
- package/dist/src/shared-thread-stream.mjs +1 -1
- package/dist/src/skill-install.mjs +0 -13
- package/package.json +17 -18
- package/tools/postinstall.mjs +69 -15
- package/dist/src/app-server-client.mjs +0 -468
- package/dist/src/app-server-tasks.mjs +0 -358
- package/dist/src/debug-ui.mjs +0 -131
- package/dist/src/platform/windows/desktop-monitor.mjs +0 -34
- package/dist/src/platform/windows/desktop-shortcut-run.ps1 +0 -5
- package/dist/src/platform/windows/desktop-tools.mjs +0 -48
- package/dist/src/platform/windows/launch-worker.ps1 +0 -20
- package/dist/src/thread-catalog.mjs +0 -47
- package/dist/tools/find-desktop-pipe.mjs +0 -10
- package/dist/web/activity-view.js +0 -283
- package/dist/web/approval-ui.js +0 -188
- package/dist/web/capabilities.js +0 -57
- package/dist/web/chat-transport.js +0 -202
- package/dist/web/chat.css +0 -782
- package/dist/web/chat.js +0 -3830
- package/dist/web/client-platform.js +0 -11
- package/dist/web/community-view.js +0 -20
- package/dist/web/community.css +0 -77
- package/dist/web/community.html +0 -37
- package/dist/web/composer-controller.js +0 -101
- package/dist/web/conversation-controller.js +0 -99
- package/dist/web/disclosure-state-controller.js +0 -95
- package/dist/web/draft-controller.js +0 -103
- package/dist/web/harmony-platform.js +0 -52
- package/dist/web/highlight-powershell.min.js +0 -39
- package/dist/web/highlight.min.js +0 -1262
- package/dist/web/history-cache.js +0 -240
- package/dist/web/history-controller.js +0 -167
- package/dist/web/index.html +0 -325
- package/dist/web/lan-session.js +0 -122
- package/dist/web/link-action-controller.js +0 -212
- package/dist/web/message-send-controller.js +0 -177
- package/dist/web/message-view.js +0 -98
- package/dist/web/p2p-data-channel.js +0 -50
- package/dist/web/p2p-diagnostics.js +0 -28
- package/dist/web/p2p-probe.js +0 -123
- package/dist/web/page-resume.js +0 -28
- package/dist/web/pending-message-store.js +0 -108
- package/dist/web/queue-controller.js +0 -82
- package/dist/web/resources.json +0 -1
- package/dist/web/self-relay-session.js +0 -129
- package/dist/web/server-requests.js +0 -67
- package/dist/web/station-connection-controller.js +0 -75
- package/dist/web/task-list-view.js +0 -296
- package/dist/web/thread-attention-controller.js +0 -124
- package/dist/web/thread-context-controller.js +0 -30
- package/dist/web/thread-list-controller.js +0 -61
- package/dist/web/thread-list-sync.js +0 -86
- package/dist/web/thread-title-controller.js +0 -57
- package/dist/web/timeline-formatters.js +0 -249
- package/dist/web/timeline-reducer.js +0 -81
- package/dist/web/timeline-renderer.js +0 -161
- package/dist/web/timeline-scroll-controller.js +0 -50
- package/dist/web/timeline-sync.js +0 -67
- package/dist/web/usage-controller.js +0 -258
- package/dist/web/vendor/README.md +0 -14
- package/dist/web/vendor/highlight-powershell.min.js +0 -39
- package/dist/web/vendor/highlight.LICENSE.txt +0 -29
- package/dist/web/vendor/highlight.min.js +0 -1262
- package/dist/web/vendor/lucide.LICENSE.txt +0 -17
package/README.md
CHANGED
|
@@ -1,83 +1,67 @@
|
|
|
1
|
-
# Code Relax
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## 安装
|
|
6
|
-
|
|
1
|
+
# Code Relax 桌面服务(Windows 私测包)
|
|
2
|
+
|
|
3
|
+
桌面服务让 HarmonyOS 客户端控制 Windows 上的Codex 内核。当前要求 Windows x64、Node >=22.12,以及已登录的 Codex Desktop 配套内核或独立 Codex CLI;npm 包不包含 Node,也不修改官方程序文件。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
7
|
公开 beta 已发布到 npm,安装或更新使用:
|
|
8
8
|
|
|
9
9
|
```powershell
|
|
10
|
-
npm i -g @walkhi/code-relax@beta --allow-scripts=@walkhi/code-relax
|
|
10
|
+
npm i -g @walkhi/code-relax@beta --allow-scripts=@walkhi/code-relax --foreground-scripts
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
首次安装前先保存工作,并从 Codex 菜单或 Windows 托盘完全退出 Codex
|
|
14
|
-
|
|
15
|
-
`setup`
|
|
16
|
-
|
|
17
|
-
##
|
|
18
|
-
|
|
19
|
-
```powershell
|
|
20
|
-
relax
|
|
21
|
-
relax shared-start # 完整控制、Desktop、Bridge
|
|
22
|
-
relax desktop-start # 基础控制、Desktop、Bridge
|
|
23
|
-
relax start # 启动/复用常驻 Bridge 和已配置中继
|
|
13
|
+
首次安装前先保存工作,并从 Codex 菜单或 Windows 托盘完全退出 Codex。更新无需先停止正在运行的桌面服务;安装器会短暂停止、替换并恢复服务。npm 全局安装会自动执行 `setup` 和 Skill 安装。`setup` 将完整服务部署到 `%LOCALAPPDATA%\Programs\CodexRemote\service`,记录安装所用 Node 的绝对路径,不复制 Node。配置、日志和二维码以 `installation.json.stateRoot` 为准,通常位于 `%LOCALAPPDATA%\CodexRemote`,MSIX 环境可能解析到包缓存目录;附件位于 `%ProgramData%\CodexRemote\attachments`。
|
|
14
|
+
|
|
15
|
+
`setup` 立即启动桌面服务和固定 45839 Codex 内核,并启用当前用户登录自启;自启不打开 Codex。Codex 内核健康后配置当前用户 `CODEX_APP_SERVER_WS_URL`,之后正常启动的 Desktop 自动接入。若安装时 Desktop 仍未接入,安装器会关闭并重新打开 Desktop;安装前应保存工作。普通更新无接入或启动链迁移需求时复用现有内核。
|
|
16
|
+
|
|
17
|
+
## Codex 内核
|
|
18
|
+
|
|
19
|
+
```powershell
|
|
20
|
+
relax start # 启动/复用桌面服务、Codex 内核和已配置的 Code Relax 中继
|
|
24
21
|
relax pair # 启动/复用服务并打开配对二维码
|
|
25
|
-
relax
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
relax relay-shared-start --json
|
|
43
|
-
relax relay-start --json
|
|
44
|
-
relax relay-status --json
|
|
22
|
+
relax doctor # 集中查看服务、登录自启和最近恢复结果
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Codex 内核由官方 `codex app-server --listen ws://127.0.0.1:45839` 提供执行服务,Desktop 和桌面服务均可连接;它只监听回环地址。桌面服务独立报告内核健康、Desktop 接入和手机可写状态;Desktop 退出后,手机仍可操作内核中的任务。同一 thread 已被另一个实例持有时,手机只读且可重新检测。
|
|
26
|
+
|
|
27
|
+
手机工作站详情可确认让 Desktop 接入Codex 内核,独立进程逐步执行并保留桌面服务/中继连接,见[启用流程](../docs/shared-recovery.md)。本机恢复命令为 `relax desktop-recover --confirm`,查看恢复结果用 `relax doctor`。桌面服务状态匹配且健康时直接复用;未知进程占用 45831/45839 时拒绝接管。
|
|
28
|
+
|
|
29
|
+
Codex 内核通过隐藏控制台启动,使后续控制台子进程继承隐藏窗口。手机的项目、任务列表和新建任务均使用Codex 内核,不依赖 Desktop 工具管道;项目和任务 ID 保持原生值。
|
|
30
|
+
|
|
31
|
+
Codex 内核未就绪时,手机停留在连接界面并显示“Codex 内核未就绪”;桌面服务保留健康和恢复接口,聊天请求返回错误。
|
|
32
|
+
|
|
33
|
+
## Code Relax 中继
|
|
34
|
+
|
|
35
|
+
私有中继仍需要本机桌面服务:
|
|
36
|
+
|
|
37
|
+
```powershell
|
|
38
|
+
relax start --json
|
|
45
39
|
relax pair --json
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
##
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
relax dev-status --json
|
|
57
|
-
relax dev-stop --json
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
调试页面与 Bridge 共用进程和端口(默认 45831),只额外提供白名单静态资源,用于基础控制和完整控制的局域网页面调试。`dev-stop` 只关闭静态页面,不影响 Bridge WebSocket。Code Relax 中继和 OpenAI 中继均使用 HAP 内置页面。
|
|
61
|
-
|
|
62
|
-
## 更新与卸载
|
|
63
|
-
|
|
64
|
-
更新固定服务:
|
|
65
|
-
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`start` 启动内核和桌面服务,并在桌面服务内恢复已配置中继。每台移动设备通过 `pair` 配对一次;新增设备不替换已有设备。桌面服务把 Windows hostname 和当前 Wi-Fi/有线网络候选经已认证 WebSocket 临时发送给手机,手机本地保存电脑名,只在设备身份验证通过后保存可达地址;服务器不持久化这些 Code Relax 中继 hostname、IP 或网络名。OpenAI 官方认证和设备配对仍直接访问 OpenAI;完成后,手机使用统一配对产生的设备凭据,通过独立元数据 WebSocket 把官方环境 ID、名称和 hostname 写入设备记录,不上传 OpenAI access token、配对码或登录态。
|
|
43
|
+
|
|
44
|
+
桌面服务的局域网端口仅提供鉴权 WebSocket,普通 HTTP 请求返回 404;手机聊天页面随 HAP 安装。
|
|
45
|
+
|
|
46
|
+
## 更新与卸载
|
|
47
|
+
|
|
48
|
+
更新固定服务:
|
|
49
|
+
|
|
66
50
|
```powershell
|
|
67
|
-
npm i -g @walkhi/code-relax@beta --allow-scripts=@walkhi/code-relax
|
|
51
|
+
npm i -g @walkhi/code-relax@beta --allow-scripts=@walkhi/code-relax --foreground-scripts
|
|
68
52
|
```
|
|
69
53
|
|
|
70
|
-
`setup`
|
|
71
|
-
|
|
72
|
-
卸载:
|
|
73
|
-
|
|
74
|
-
```powershell
|
|
54
|
+
`setup` 先暂存并校验新版本,再短暂停止桌面服务/中继完成切换;原服务此前正在运行时会自动恢复,新版本启动失败则回滚并启动旧版本。无需提前停止服务。Node 路径变化或原 Node 被删除后需重新 setup。升级可能因 Node 可执行文件路径变化而再次触发 Windows 防火墙授权。
|
|
55
|
+
|
|
56
|
+
卸载:
|
|
57
|
+
|
|
58
|
+
```powershell
|
|
75
59
|
relax uninstall
|
|
76
60
|
npm uninstall -g @walkhi/code-relax
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
`uninstall`
|
|
80
|
-
|
|
81
|
-
## 发布边界
|
|
82
|
-
|
|
83
|
-
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`uninstall` 停止桌面服务后先核对并清理 Code Relax 所有的当前用户 `CODEX_APP_SERVER_WS_URL`;用户后来修改或不归属 Code Relax 的值会保留。随后删除固定程序和本包管理且未修改的 Skill;保留其他配置、Token、日志、长期设备凭据、附件和自定义 Skill。它不卸载 Node、Codex Desktop,也不强制结束完整控制的执行进程。
|
|
64
|
+
|
|
65
|
+
## 发布边界
|
|
66
|
+
|
|
67
|
+
registry 测试包使用 `@walkhi/code-relax@beta`,主 CLI 为 `relax`,并保留 `code-relax` 兼容入口。安装摘要通过 `--foreground-scripts` 显示,首次安装或启动链迁移时干净重启 Desktop,分别报告已安装与实际运行版本;登录启动使用当前用户任务计划。通信协议仍为 7。详细规则见 [npm 分发](../docs/npm-distribution.md)。
|
|
@@ -1,113 +1,99 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { phonePairingSteps, desktopSetupSteps, pairingDisplayText } from '../src/onboarding.mjs';
|
|
3
|
-
import { lifecycle, readRecord } from '../src/service-lifecycle.mjs';
|
|
3
|
+
import { lifecycle, readRecord, writeRecord } from '../src/service-lifecycle.mjs';
|
|
4
4
|
import { runtimePaths } from '../src/platform/windows/runtime-paths.mjs';
|
|
5
5
|
import * as host from '../src/platform/windows/service-host.mjs';
|
|
6
6
|
import { installService, uninstallService, packageRoot } from '../src/service-install.mjs';
|
|
7
|
-
import { installSkill, removeSkill, inspectSkill } from '../src/skill-install.mjs';
|
|
7
|
+
import { installSkill, removeSkill, inspectSkill } from '../src/skill-install.mjs';
|
|
8
8
|
import { doctor } from '../src/service-doctor.mjs';
|
|
9
9
|
import { launchDesktop } from '../src/desktop-launcher.mjs';
|
|
10
|
-
import { desktopHost,
|
|
11
|
-
import { debugUi } from '../src/debug-ui.mjs';
|
|
10
|
+
import { desktopHost, removeDesktopShortcut, residentStartup, resolveStateRoot } from '../src/platform/windows/desktop-host.mjs';
|
|
12
11
|
import { selfRelay } from '../src/self-relay/lifecycle.mjs';
|
|
13
12
|
import { fileURLToPath } from 'node:url';
|
|
14
13
|
import path from 'node:path';
|
|
15
14
|
import fs from 'node:fs';
|
|
16
15
|
import { spawn } from 'node:child_process';
|
|
17
16
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
18
|
-
import { requestRecovery,
|
|
17
|
+
import { requestRecovery, recoveryWarning } from '../src/shared-recovery.mjs';
|
|
19
18
|
import { configureManagedEnvironment, clearManagedEnvironment } from '../src/platform/windows/user-environment.mjs';
|
|
19
|
+
import { resolveCodexRuntime } from '../src/platform/windows/codex-runtime.mjs';
|
|
20
|
+
import { controlRequest } from '../src/service-control.mjs';
|
|
21
|
+
import { processAlive } from '../src/background-process.mjs';
|
|
20
22
|
|
|
21
23
|
const help = `Code Relax(Windows)
|
|
22
|
-
用法:relax
|
|
23
|
-
|
|
24
|
-
start
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
desktop-
|
|
30
|
-
shortcuts 创建完整控制/基础控制桌面快捷方式
|
|
31
|
-
pair 启动或复用工作站服务,并打开一次性配对二维码
|
|
32
|
-
status 查看服务健康状态
|
|
33
|
-
stop 停止服务
|
|
34
|
-
dev 在 Bridge 端口开启调试页面(实时读取当前源码/包资源)
|
|
35
|
-
dev-status 查看 Bridge 调试页面状态
|
|
36
|
-
dev-stop 关闭调试页面,不停止 Bridge
|
|
37
|
-
relay-start 启动或复用私有中继,已有授权直接恢复,首次授权生成二维码
|
|
38
|
-
relay-shared-start 启动完整控制和 Bridge 后连接私有中继(需先退出当前 Codex)
|
|
39
|
-
relay-status 查看私有中继连接器及手机连接状态
|
|
40
|
-
relay-stop 停止私有中继连接器,不停止 Bridge 或 Codex
|
|
41
|
-
relay-revoke 撤销这台电脑的全部移动设备授权(停止连接器不会撤销授权)
|
|
42
|
-
doctor 检查安装、Node、Desktop、网络和 Skill,提示防火墙处理方式
|
|
43
|
-
skill-install 安装/更新当前用户的 Codex Skill,不覆盖自定义文件
|
|
44
|
-
skill-remove 移除本包安装且未修改的 Skill
|
|
24
|
+
用法:relax <命令> [--json];下列命令均可用
|
|
25
|
+
常用指令:
|
|
26
|
+
start 启动或复用桌面服务和 Codex 内核,并恢复已配置的 Code Relax 中继
|
|
27
|
+
pair 启动或复用桌面服务,并打开一次性配对二维码
|
|
28
|
+
维护指令:
|
|
29
|
+
setup 部署并启动桌面服务、Codex 内核和登录自启动;安全配置 Desktop 后续接入
|
|
30
|
+
doctor 检查服务、登录自启、最近恢复结果、网络和 Skill
|
|
31
|
+
desktop-recover --confirm 重新打开 Desktop 并接入 Codex 内核;不强制重启已有内核
|
|
45
32
|
uninstall 停止并移除固定目录服务和本包 Skill,清理自有 Desktop 环境变量,保留其他配置与附件
|
|
46
|
-
默认监听 0.0.0.0
|
|
47
|
-
--json 输出机器可读结果;统一二维码不含 LAN
|
|
33
|
+
默认监听 0.0.0.0,移动端与电脑需在同一受信任局域网。
|
|
34
|
+
--json 输出机器可读结果;统一二维码不含 LAN 地址、桌面服务凭据或中继地址。`;
|
|
35
|
+
|
|
36
|
+
function currentNodeExecutable() {
|
|
37
|
+
const link = process.env.NVM_SYMLINK && path.join(process.env.NVM_SYMLINK, 'node.exe');
|
|
38
|
+
if (link && fs.existsSync(link)) {
|
|
39
|
+
try {
|
|
40
|
+
if (fs.realpathSync(link).toLowerCase() === fs.realpathSync(process.execPath).toLowerCase()) return link;
|
|
41
|
+
} catch {}
|
|
42
|
+
}
|
|
43
|
+
return process.execPath;
|
|
44
|
+
}
|
|
48
45
|
|
|
49
|
-
async function main() {
|
|
46
|
+
async function main() {
|
|
50
47
|
const [command, ...flags] = process.argv.slice(2);
|
|
51
48
|
if (!command || command === '--help' || command === '-h') {
|
|
52
49
|
console.log(help);
|
|
53
50
|
return;
|
|
54
51
|
}
|
|
55
|
-
if (!['
|
|
56
|
-
|| flags.some(flag => flag !== '--json' && !(command === '
|
|
52
|
+
if (!['desktop-recover', 'setup', 'doctor', 'uninstall', 'start', 'pair'].includes(command)
|
|
53
|
+
|| flags.some(flag => flag !== '--json' && !(command === 'desktop-recover' && flag === '--confirm'))) {
|
|
57
54
|
throw new Error('无效命令或参数。使用 --help 查看用法。');
|
|
58
55
|
}
|
|
59
56
|
if (process.platform !== 'win32') throw new Error('当前被控端仅支持 Windows。');
|
|
60
57
|
const json = flags.includes('--json');
|
|
61
58
|
const paths = runtimePaths();
|
|
62
|
-
const { stateRoot, serviceRoot } = paths;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!result.bridgeReady) { console.log(json ? JSON.stringify(result) : result.displayText); return; }
|
|
75
|
-
}
|
|
76
|
-
const result = await selfRelay(command === 'relay-shared-start' ? 'relay-start' : command, stateRoot, command === 'relay-shared-start' ? 'lan-shared' : '');
|
|
77
|
-
console.log(json ? JSON.stringify(result) : `${result.displayText}${result.qrPath ? '\n二维码:' + result.qrPath : ''}`);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
if (['dev', 'dev-status', 'dev-stop'].includes(command)) {
|
|
81
|
-
const result = await debugUi(command, stateRoot);
|
|
82
|
-
console.log(json ? JSON.stringify(result) : result.message || JSON.stringify(result));
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
const installedEntry = path.join(serviceRoot, 'dist/bin/codex-remote.mjs');
|
|
86
|
-
if (['autostart-enable', 'autostart-disable', 'autostart-status', 'shared-recover', 'shared-recovery-status', 'shared-start', 'desktop-start', 'shortcuts', 'start', 'pair', 'status', 'stop'].includes(command) && path.resolve(fileURLToPath(import.meta.url)).toLowerCase() !== installedEntry.toLowerCase()) {
|
|
59
|
+
const { stateRoot, serviceRoot } = paths;
|
|
60
|
+
const installedEntry = path.join(serviceRoot, 'dist/bin/codex-remote.mjs');
|
|
61
|
+
const installationPath = path.join(serviceRoot, 'installation.json');
|
|
62
|
+
const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(Number);
|
|
63
|
+
const currentNodeSupported = nodeMajor > 22 || (nodeMajor === 22 && nodeMinor >= 12);
|
|
64
|
+
const installed = path.resolve(fileURLToPath(import.meta.url)).toLowerCase() === installedEntry.toLowerCase();
|
|
65
|
+
const installation = readRecord(installationPath);
|
|
66
|
+
if (installation?.nodeExecutable && !fs.existsSync(installation.nodeExecutable) && currentNodeSupported
|
|
67
|
+
&& ['start', 'pair', 'doctor'].includes(command)) {
|
|
68
|
+
writeRecord(installationPath, { ...installation, nodeExecutable: currentNodeExecutable() });
|
|
69
|
+
}
|
|
70
|
+
if (['desktop-recover', 'start', 'pair'].includes(command) && !installed) {
|
|
87
71
|
if (!fs.existsSync(installedEntry)) throw new Error('尚未部署服务,请先执行 relax setup。');
|
|
88
|
-
const executable = readRecord(
|
|
72
|
+
const executable = readRecord(installationPath)?.nodeExecutable;
|
|
89
73
|
if (!executable || !fs.existsSync(executable)) throw new Error('安装时选用的 Node 不存在,请重新执行 relax setup。');
|
|
90
74
|
const child = spawn(executable, [installedEntry, command, ...flags], { stdio: 'inherit', windowsHide: true });
|
|
91
75
|
const code = await new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', resolve); });
|
|
92
76
|
process.exitCode = code ?? 1;
|
|
93
77
|
return;
|
|
94
78
|
}
|
|
95
|
-
host.migrateState(stateRoot);
|
|
96
79
|
const service = lifecycle({ stateRoot, host });
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
if (!
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
80
|
+
const ensureAutostart = async () => {
|
|
81
|
+
const currentInstallation = readRecord(installationPath);
|
|
82
|
+
if (!currentInstallation?.nodeExecutable) throw new Error('请先执行 setup。');
|
|
83
|
+
const startup = { serviceRoot, nodeExecutable: currentInstallation.nodeExecutable };
|
|
84
|
+
const current = await residentStartup(startup, 'status');
|
|
85
|
+
if (current.enabled) return { ...current, repaired: false };
|
|
86
|
+
const result = await residentStartup(startup, 'enable');
|
|
87
|
+
return { ...result, repaired: true };
|
|
88
|
+
};
|
|
89
|
+
if (command === 'desktop-recover') {
|
|
90
|
+
if (!flags.includes('--confirm')) throw new Error(recoveryWarning + ' 使用 --confirm 确认。');
|
|
91
|
+
const result = await requestRecovery(stateRoot, { confirmed: true });
|
|
107
92
|
console.log(json ? JSON.stringify(result) : `${result.message || result.status}${result.error ? '\n' + result.error : ''}\n${result.operationId || ''}`.trim());
|
|
108
93
|
return;
|
|
109
94
|
}
|
|
110
95
|
if (command === 'pair') {
|
|
96
|
+
await ensureAutostart();
|
|
111
97
|
let bridge = await service.status();
|
|
112
98
|
if (!bridge.running) {
|
|
113
99
|
await service.start();
|
|
@@ -122,40 +108,26 @@ async function main() {
|
|
|
122
108
|
console.log(json ? JSON.stringify(result) : `${result.displayText}\n二维码:${result.qrPath}`);
|
|
123
109
|
return;
|
|
124
110
|
}
|
|
125
|
-
if (command === 'shortcuts') {
|
|
126
|
-
const installation = readRecord(path.join(serviceRoot, 'installation.json'));
|
|
127
|
-
if (!installation?.nodeExecutable) throw new Error('请先执行 relax setup。');
|
|
128
|
-
const shortcuts = await desktopShortcuts({ serviceRoot, nodeExecutable: installation.nodeExecutable });
|
|
129
|
-
console.log(json ? JSON.stringify({ shortcuts }) : `桌面快捷方式已创建:\n${shortcuts.join('\n')}`);
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
111
|
if (command === 'doctor') {
|
|
133
|
-
const result = await doctor({ paths, host, service });
|
|
112
|
+
const result = await doctor({ paths, host, service, ensureAutostart });
|
|
134
113
|
console.log(json ? JSON.stringify(result) : result.checks.map(item => `[${item.status}] ${item.message}`).join('\n'));
|
|
135
114
|
if (!result.ready) process.exitCode = 1;
|
|
136
115
|
return;
|
|
137
116
|
}
|
|
138
|
-
if (command === 'skill-install' || command === 'skill-remove') {
|
|
139
|
-
const result = command === 'skill-install'
|
|
140
|
-
? installSkill(path.join(packageRoot(path.dirname(fileURLToPath(import.meta.url))), 'dist/skills/code-relax/SKILL.md'))
|
|
141
|
-
: removeSkill();
|
|
142
|
-
console.log(json ? JSON.stringify(result) : `${result.message}\n${result.target}`);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
117
|
if (command === 'uninstall') {
|
|
146
118
|
const skill = inspectSkill();
|
|
147
119
|
let environment;
|
|
148
120
|
const result = await uninstallService(paths, service, async () => {
|
|
149
121
|
environment = await clearManagedEnvironment(stateRoot);
|
|
150
122
|
const installation = readRecord(path.join(serviceRoot, 'installation.json'));
|
|
151
|
-
if (installation?.nodeExecutable) await
|
|
123
|
+
if (installation?.nodeExecutable) await removeDesktopShortcut({ serviceRoot, nodeExecutable: installation.nodeExecutable });
|
|
152
124
|
if (installation?.nodeExecutable) await residentStartup({ serviceRoot, nodeExecutable: installation.nodeExecutable }, 'disable');
|
|
153
125
|
});
|
|
154
126
|
result.environment = environment;
|
|
155
127
|
result.skill = skill.managed ? removeSkill() : { removed: false, message: '未移除自定义或未安装的 Skill。' };
|
|
156
128
|
result.message = environment?.preserved
|
|
157
|
-
? '固定目录服务已卸载;检测到用户自定义的 CODEX_APP_SERVER_WS_URL
|
|
158
|
-
: '固定目录服务已卸载;Code Relax 设置的 app-server
|
|
129
|
+
? '固定目录服务已卸载;检测到用户自定义的 CODEX_APP_SERVER_WS_URL,已保留。配置、附件和可能仍在执行任务的 app-server 也已保留;重启 Windows可结束该进程。npm 命令入口可再用 npm uninstall -g @walkhi/code-relax 移除。'
|
|
130
|
+
: '固定目录服务已卸载;Code Relax 设置的 app-server 环境变量已清理,其他配置、附件和可能仍在执行任务的 app-server 已保留;重启 Windows可结束该进程。npm 命令入口可再用 npm uninstall -g @walkhi/code-relax 移除。';
|
|
159
131
|
console.log(json ? JSON.stringify(result) : `${result.message}\n${result.skill.message}`);
|
|
160
132
|
return;
|
|
161
133
|
}
|
|
@@ -164,16 +136,23 @@ async function main() {
|
|
|
164
136
|
if (major < 22 || (major === 22 && minor < 12)) throw new Error('需要 Node >=22.12。');
|
|
165
137
|
const physicalStateRoot = await resolveStateRoot(stateRoot);
|
|
166
138
|
// Preserve the effective package view; never replace a newer file already in it.
|
|
167
|
-
for (const name of ['bridge-config.json', 'bridge-session.json', 'bridge-startup-context.json',
|
|
168
|
-
'self-relay-config.json', 'self-relay-session.json', 'lan-device-authorizations.json',
|
|
169
|
-
'managed-app-server.json', 'managed-environment.json', 'shared-recovery.json', 'shared-recovery.lock'
|
|
139
|
+
for (const name of ['bridge-config.json', 'bridge-session.json', 'bridge-startup-context.json',
|
|
140
|
+
'self-relay-config.json', 'self-relay-session.json', 'lan-device-authorizations.json',
|
|
141
|
+
'managed-app-server.json', 'managed-environment.json', 'shared-recovery.json', 'shared-recovery.lock']) {
|
|
170
142
|
const source = path.join(stateRoot, name), target = path.join(physicalStateRoot, name);
|
|
171
143
|
if (fs.existsSync(source) && !fs.existsSync(target)) fs.copyFileSync(source, target, fs.constants.COPYFILE_EXCL);
|
|
172
144
|
}
|
|
173
|
-
const
|
|
174
|
-
const
|
|
145
|
+
const hadInstalledService = fs.existsSync(path.join(serviceRoot, 'installation.json'));
|
|
146
|
+
const previousInstallation = readRecord(path.join(serviceRoot, 'installation.json'));
|
|
147
|
+
if (previousInstallation?.nodeExecutable) await removeDesktopShortcut({ serviceRoot, nodeExecutable: previousInstallation.nodeExecutable });
|
|
148
|
+
const desktopBeforeSetup = await desktopHost.inspect();
|
|
149
|
+
const runtime = await resolveCodexRuntime(physicalStateRoot, { refresh: true });
|
|
150
|
+
const desktopNeedsAttach = desktopBeforeSetup.running && !await host.sharedServiceUrl();
|
|
151
|
+
const previous = hadInstalledService ? await service.status() : { processRunning: false };
|
|
152
|
+
const legacyRelaySession = readRecord(path.join(stateRoot, 'self-relay-session.json'));
|
|
153
|
+
const previousRelay = legacyRelaySession ? await controlRequest(legacyRelaySession) : await selfRelay('relay-status', stateRoot);
|
|
175
154
|
const resumeAfterUpdate = previous.processRunning || previousRelay.running;
|
|
176
|
-
const executable =
|
|
155
|
+
const executable = currentNodeExecutable();
|
|
177
156
|
const startInstalled = async () => {
|
|
178
157
|
const entry = path.join(serviceRoot, 'dist/bin/codex-remote.mjs');
|
|
179
158
|
const child = spawn(executable, [entry, 'start', '--json'], { windowsHide: true, stdio: ['ignore', 'ignore', 'pipe'] });
|
|
@@ -183,19 +162,28 @@ async function main() {
|
|
|
183
162
|
if (code) {
|
|
184
163
|
try { await selfRelay('relay-stop', stateRoot); } catch {}
|
|
185
164
|
try { await service.stop(); } catch {}
|
|
186
|
-
throw new Error(errors.trim().replace(/^(?:Code Relax:)+/u, '') || '
|
|
165
|
+
throw new Error(errors.trim().replace(/^(?:Code Relax:)+/u, '') || '桌面服务启动失败');
|
|
187
166
|
}
|
|
188
167
|
};
|
|
189
168
|
let stoppedForUpdate = false;
|
|
190
169
|
const installed = await installService(packageRoot(path.dirname(fileURLToPath(import.meta.url))), serviceRoot, executable, physicalStateRoot, {
|
|
191
170
|
beforeActivate: async () => {
|
|
171
|
+
if (desktopNeedsAttach) await desktopHost.closeDesktop(desktopBeforeSetup.desktop);
|
|
192
172
|
stoppedForUpdate = resumeAfterUpdate;
|
|
193
|
-
if (previousRelay.running
|
|
173
|
+
if (previousRelay.running && legacyRelaySession) {
|
|
174
|
+
await controlRequest(legacyRelaySession, 'POST');
|
|
175
|
+
const deadline = Date.now() + 10000;
|
|
176
|
+
while (processAlive(legacyRelaySession.pid)) {
|
|
177
|
+
if (Date.now() >= deadline) throw new Error('旧中继进程未在 10 秒内退出。');
|
|
178
|
+
await delay(100);
|
|
179
|
+
}
|
|
180
|
+
} else if (previousRelay.running) await selfRelay('relay-stop', stateRoot);
|
|
194
181
|
if (previous.processRunning) await service.stop();
|
|
195
182
|
},
|
|
196
183
|
verifyActivation: async () => { await startInstalled(); },
|
|
197
184
|
restoreActivation: async () => { if (stoppedForUpdate) await startInstalled(); },
|
|
198
185
|
});
|
|
186
|
+
const autostart = await residentStartup(installed, 'enable');
|
|
199
187
|
const deadline = Date.now() + 35_000;
|
|
200
188
|
let bridge = await service.status();
|
|
201
189
|
while (bridge.running && !['ready', 'failed'].includes(bridge.managedAppServer?.state) && Date.now() < deadline) {
|
|
@@ -203,53 +191,89 @@ async function main() {
|
|
|
203
191
|
bridge = await service.status();
|
|
204
192
|
}
|
|
205
193
|
if (!bridge.running || bridge.managedAppServer?.state !== 'ready') {
|
|
206
|
-
throw new Error(bridge.managedAppServer?.error || '
|
|
194
|
+
throw new Error(bridge.managedAppServer?.error || '桌面服务已安装,但 Codex 内核未就绪;未写入 Desktop 环境变量。');
|
|
195
|
+
}
|
|
196
|
+
const environment = desktopBeforeSetup.desktop
|
|
197
|
+
? await configureManagedEnvironment(physicalStateRoot) : { configured: false, reason: 'desktop-not-installed' };
|
|
198
|
+
let managedAppServerMigrated = false;
|
|
199
|
+
const managedAppServerMigration = {
|
|
200
|
+
required: bridge.managedAppServer.launcherRestartRequired === true,
|
|
201
|
+
restarted: false,
|
|
202
|
+
previousPid: bridge.managedAppServer.pid || null,
|
|
203
|
+
};
|
|
204
|
+
let desktopRestarted = false;
|
|
205
|
+
let desktopRestartReason = null;
|
|
206
|
+
if (managedAppServerMigration.required) {
|
|
207
|
+
const desktopInfo = await desktopHost.inspect();
|
|
208
|
+
const desktopAttached = desktopInfo.running
|
|
209
|
+
&& (bridge.desktop?.attachedToManagedServer === true || Boolean(await host.sharedServiceUrl()));
|
|
210
|
+
if (desktopAttached) await desktopHost.closeDesktop(desktopInfo.desktop);
|
|
211
|
+
await service.restartManagedAppServer();
|
|
212
|
+
const migrationDeadline = Date.now() + 40_000;
|
|
213
|
+
do {
|
|
214
|
+
await delay(250);
|
|
215
|
+
bridge = await service.status();
|
|
216
|
+
if (bridge.managedAppServer?.restart?.status === 'failed') {
|
|
217
|
+
throw new Error(bridge.managedAppServer.restart.error || 'Codex 内核启动链迁移失败。');
|
|
218
|
+
}
|
|
219
|
+
} while (Date.now() < migrationDeadline && (!bridge.running || bridge.managedAppServer?.state !== 'ready' || bridge.managedAppServer.launcherRestartRequired));
|
|
220
|
+
if (!bridge.running || bridge.managedAppServer?.state !== 'ready' || bridge.managedAppServer.launcherRestartRequired) {
|
|
221
|
+
throw new Error('Codex 内核未在 40 秒内完成启动链迁移。');
|
|
222
|
+
}
|
|
223
|
+
managedAppServerMigrated = true;
|
|
224
|
+
managedAppServerMigration.restarted = true;
|
|
225
|
+
if (desktopAttached) {
|
|
226
|
+
await launchDesktop({ stateRoot: physicalStateRoot, host: desktopHost });
|
|
227
|
+
desktopRestarted = true;
|
|
228
|
+
desktopRestartReason = 'app-server-migration';
|
|
229
|
+
bridge = await service.status();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const skill = installSkill(path.join(serviceRoot, 'dist/skills/code-relax/SKILL.md'));
|
|
233
|
+
if (desktopNeedsAttach && !desktopRestarted) {
|
|
234
|
+
const currentDesktop = await desktopHost.inspect();
|
|
235
|
+
if (currentDesktop.running) await desktopHost.closeDesktop(currentDesktop.desktop);
|
|
236
|
+
await launchDesktop({ stateRoot: physicalStateRoot, host: desktopHost });
|
|
237
|
+
desktopRestarted = true;
|
|
238
|
+
desktopRestartReason = 'attach-managed-server';
|
|
239
|
+
bridge = await service.status();
|
|
207
240
|
}
|
|
208
|
-
const autostart = await residentStartup(installed, 'enable');
|
|
209
|
-
const environment = await configureManagedEnvironment(physicalStateRoot);
|
|
210
241
|
installed.autostart = autostart;
|
|
242
|
+
installed.skill = skill;
|
|
211
243
|
installed.restarted = resumeAfterUpdate;
|
|
212
244
|
installed.environment = environment;
|
|
213
245
|
installed.managedAppServer = bridge.managedAppServer;
|
|
214
|
-
installed.
|
|
215
|
-
installed.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
let desktop;
|
|
228
|
-
if (command === 'shared-start' || command === 'desktop-start') {
|
|
229
|
-
if (!json) console.log('正在检查 Codex 的启动模式…');
|
|
230
|
-
desktop = await launchDesktop(command === 'shared-start' ? 'shared' : 'ordinary', { stateRoot, host: desktopHost });
|
|
231
|
-
delete process.env.CODEX_APP_TOOLS_PIPE_PATH;
|
|
232
|
-
}
|
|
233
|
-
if (!json && (desktop || command === 'start')) console.log('正在检查电脑连接并准备 Bridge…');
|
|
234
|
-
const result = await service[desktop ? 'start' : command]();
|
|
235
|
-
if (command === 'status') {
|
|
236
|
-
console.log(json ? JSON.stringify(result) : result.running
|
|
237
|
-
? `服务正常:${result.mode},端口 ${result.port},PID ${result.pid}` : '服务未就绪。');
|
|
246
|
+
installed.codexRuntime = runtime;
|
|
247
|
+
installed.managedAppServerMigrated = managedAppServerMigrated;
|
|
248
|
+
installed.managedAppServerMigration = { ...managedAppServerMigration,
|
|
249
|
+
currentPid: bridge.managedAppServer.pid || null,
|
|
250
|
+
launcherRevision: bridge.managedAppServer.launcherRevision ?? null };
|
|
251
|
+
installed.desktopRestarted = desktopRestarted;
|
|
252
|
+
installed.desktopRestartReason = desktopRestartReason;
|
|
253
|
+
const migrationText = managedAppServerMigrated ? '已自动重建旧启动链的 app-server;安装前尚未完成的任务可能已中断。\n' : '';
|
|
254
|
+
const restartText = desktopRestarted
|
|
255
|
+
? 'Codex Desktop 已干净重启并重新接入 Codex 内核。'
|
|
256
|
+
: desktopBeforeSetup.desktop ? 'Codex Desktop 使用同一 Codex 内核;移动端控制不依赖 Desktop 是否打开。'
|
|
257
|
+
: '使用独立 Codex CLI;无需安装 Codex Desktop。';
|
|
258
|
+
console.log(json ? JSON.stringify({ ...installed, nextSteps: desktopSetupSteps, displayText: `${migrationText}${desktopSetupSteps.join('\n')}\n${restartText}` }) : `桌面服务已安装:${installed.serviceRoot}\n使用已安装的 Node:${installed.nodeExecutable}\nCodex 内核已就绪;首次联网可能需要 Windows 防火墙授权。\n${migrationText}${desktopSetupSteps.join('\n')}\n${restartText}`);
|
|
238
259
|
return;
|
|
239
260
|
}
|
|
261
|
+
if (!json && command === 'start') console.log('正在检查电脑连接并准备桌面服务…');
|
|
262
|
+
await ensureAutostart();
|
|
263
|
+
const result = await service[command]();
|
|
240
264
|
const { accessUrl, qrPath: _legacyQrPath, ...metadata } = result;
|
|
241
|
-
const notice = '
|
|
265
|
+
const notice = '本机服务检查通过,移动端连通性尚未验证。若电脑弹出 Node 网络访问提示,请先处理 Windows 防火墙授权,再用移动端连接。';
|
|
242
266
|
let relay;
|
|
243
267
|
if (fs.existsSync(path.join(stateRoot, 'self-relay-config.json'))) relay = await selfRelay('relay-start', stateRoot);
|
|
244
268
|
const nextSteps = phonePairingSteps(metadata.transportMode, relay?.paired === true);
|
|
245
269
|
const displayText = pairingDisplayText(nextSteps, notice);
|
|
246
|
-
const output = { ...metadata, ...(
|
|
270
|
+
const output = { ...metadata, ...(relay ? { relayConnected: relay.relayConnected,
|
|
247
271
|
paired: relay.paired, clientCount: relay.clientCount, ...(relay.qrPath ? { qrPath: relay.qrPath, expiresAt: relay.expiresAt } : {}) } : {}),
|
|
248
272
|
nextSteps, displayText, mode: 'lan', listenAddress: '0.0.0.0', address: new URL(accessUrl).host,
|
|
249
273
|
lanAccess: 'unverified', notice };
|
|
250
274
|
if (json) console.log(JSON.stringify(output));
|
|
251
275
|
else {
|
|
252
|
-
console.log('电脑连接检查通过 ·
|
|
276
|
+
console.log('电脑连接检查通过 · 桌面服务已就绪');
|
|
253
277
|
console.log(`局域网模式:监听 0.0.0.0:${result.port},连接地址 ${output.address},PID ${result.pid}`);
|
|
254
278
|
if (relay?.qrPath) console.log(`配对二维码:${relay.qrPath}`);
|
|
255
279
|
console.log(displayText);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import mysql from 'mysql2/promise';
|
|
4
|
+
|
|
5
|
+
const path = process.env.RELAY_MYSQL_CONFIG_FILE;
|
|
6
|
+
const action = process.argv[2];
|
|
7
|
+
const email = (process.argv[3] || '').trim().toLowerCase();
|
|
8
|
+
if (!path || !['--grant', '--revoke', '--list'].includes(action) ||
|
|
9
|
+
(action !== '--list' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))) {
|
|
10
|
+
throw new Error('Use --grant EMAIL, --revoke EMAIL or --list with RELAY_MYSQL_CONFIG_FILE');
|
|
11
|
+
}
|
|
12
|
+
const db = await mysql.createConnection({ ...JSON.parse(fs.readFileSync(path, 'utf8')),
|
|
13
|
+
dateStrings: true, timezone: 'Z', multipleStatements: false });
|
|
14
|
+
try {
|
|
15
|
+
if (action === '--grant') await db.execute(`INSERT INTO relay_admin_emails (email,granted_at)
|
|
16
|
+
VALUES (?,UTC_TIMESTAMP(3)) ON DUPLICATE KEY UPDATE email=VALUES(email)`, [email]);
|
|
17
|
+
else if (action === '--revoke') await db.execute('DELETE FROM relay_admin_emails WHERE email=?', [email]);
|
|
18
|
+
const [rows] = await db.query('SELECT email FROM relay_admin_emails ORDER BY email');
|
|
19
|
+
console.log(JSON.stringify({ admins: rows.map(row => row.email) }));
|
|
20
|
+
} finally { await db.end(); }
|