@unlinearity/cli-wechat-bridge 1.1.0 → 1.1.1
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 +30 -0
- package/dist/bridge/bridge-adapters.shared.js +4 -1
- package/dist/bridge/wechat-bridge.js +5 -0
- package/dist/daemon/wechat-daemon.js +5 -0
- package/dist/i18n/messages-en.js +79 -2
- package/dist/i18n/messages-zh.js +79 -2
- package/dist/utils/doctor.js +540 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
- [运行配置](docs/configuration.md):数据目录、上传大小限制、调试开关等环境变量。
|
|
27
27
|
- [开发说明](docs/development.md):源码运行、测试、构建、打包和全局 smoke 验证。
|
|
28
28
|
- [发布说明](docs/releases/README.md):各版本变更与升级说明。
|
|
29
|
+
- [通信架构](docs/architecture.md):三种 CLI 适配器的通信机制、PTY 依赖分析和技术决策。
|
|
29
30
|
|
|
30
31
|
## 这个项目解决什么问题?
|
|
31
32
|
|
|
@@ -63,6 +64,35 @@ npm install -g cli-wechat-bridge@latest
|
|
|
63
64
|
|
|
64
65
|
兼容性说明:旧包名 `@unlinearity/cli-wechat-bridge` 会继续同步发布,已经安装旧包名的用户可以正常升级;新用户优先使用更短的 `cli-wechat-bridge`。
|
|
65
66
|
|
|
67
|
+
<details>
|
|
68
|
+
<summary><b>安装遇到问题?(node-pty 原生模块)</b></summary>
|
|
69
|
+
|
|
70
|
+
本项目使用 `node-pty` 为 CLI 适配器提供完整终端模拟。**Claude Code 适配器当前通过 PTY 交互模式工作**,node-pty 不可用时会回退到兼容模式,但 Claude Code 在此模式下可能无法正常桥接;Codex 适配器主要通过 WebSocket RPC 通信,通常不受影响;OpenCode 适配器完全不依赖 node-pty。
|
|
71
|
+
|
|
72
|
+
**Linux 用户**(最常见):需要原生模块编译工具:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Debian / Ubuntu
|
|
76
|
+
sudo apt install build-essential python3
|
|
77
|
+
# RHEL / Fedora
|
|
78
|
+
sudo dnf groupinstall "Development Tools" && sudo dnf install python3
|
|
79
|
+
# Alpine
|
|
80
|
+
apk add build-base python3
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
安装编译工具后重新安装:`npm install -g cli-wechat-bridge@latest`
|
|
84
|
+
|
|
85
|
+
**macOS 用户**:如遇编译问题,安装 Xcode 命令行工具:`xcode-select --install`
|
|
86
|
+
|
|
87
|
+
**Windows 用户**:
|
|
88
|
+
- 需要 Windows 10 1809(build 18309)或更高版本
|
|
89
|
+
- 如果 node-pty 加载失败,运行 `npm rebuild node-pty` 或重新安装
|
|
90
|
+
- 确保已安装 [Visual C++ Redistributable](https://aka.ms/vs/17/release/vc_redist.x64.exe)
|
|
91
|
+
|
|
92
|
+
运行 `wechat-daemon --doctor` 可快速检查环境状态。详见 [问题排查](docs/troubleshooting.md#pty-不可用--回退模式)。
|
|
93
|
+
|
|
94
|
+
</details>
|
|
95
|
+
|
|
66
96
|
### 3. 完成微信登录
|
|
67
97
|
|
|
68
98
|
全局安装后运行:
|
|
@@ -1301,6 +1301,9 @@ export function buildSpawnDiagnostic(error, spawnTarget, platform = process.plat
|
|
|
1301
1301
|
if (platform === "darwin") {
|
|
1302
1302
|
lines.push(t("spawn.diagnostic.xcode"));
|
|
1303
1303
|
}
|
|
1304
|
+
if (platform === "linux") {
|
|
1305
|
+
lines.push(t("spawn.diagnostic.linuxBuildTools"));
|
|
1306
|
+
}
|
|
1304
1307
|
}
|
|
1305
1308
|
else if (errorMessage.includes("ENOENT") || errorMessage.includes("spawn")) {
|
|
1306
1309
|
lines.push(t("spawn.diagnostic.notFound", { target }));
|
|
@@ -1310,7 +1313,7 @@ export function buildSpawnDiagnostic(error, spawnTarget, platform = process.plat
|
|
|
1310
1313
|
}
|
|
1311
1314
|
lines.push(t("spawn.diagnostic.nodeVersion"));
|
|
1312
1315
|
if (platform === "win32") {
|
|
1313
|
-
lines.push(t("spawn.diagnostic.
|
|
1316
|
+
lines.push(t("spawn.diagnostic.winFull"));
|
|
1314
1317
|
}
|
|
1315
1318
|
return lines.join("\n");
|
|
1316
1319
|
}
|
|
@@ -242,6 +242,11 @@ function printUsageAndExit() {
|
|
|
242
242
|
process.exit(0);
|
|
243
243
|
}
|
|
244
244
|
async function main() {
|
|
245
|
+
if (process.argv.includes("--doctor")) {
|
|
246
|
+
const { runDoctorCheck } = await import("../utils/doctor.js");
|
|
247
|
+
await runDoctorCheck(process.argv.slice(2), { mode: "bridge" });
|
|
248
|
+
process.exit(0);
|
|
249
|
+
}
|
|
245
250
|
const options = parseCliArgs(process.argv.slice(2));
|
|
246
251
|
const daemonEndpoint = readDaemonEndpoint();
|
|
247
252
|
if (daemonEndpoint && await isDaemonEndpointAlive(daemonEndpoint, { timeoutMs: 500 })) {
|
|
@@ -1580,6 +1580,11 @@ export async function runDaemon(options) {
|
|
|
1580
1580
|
await daemon.runPollLoop();
|
|
1581
1581
|
}
|
|
1582
1582
|
export async function main(argv = process.argv.slice(2)) {
|
|
1583
|
+
if (argv.includes("--doctor")) {
|
|
1584
|
+
const { runDoctorCheck } = await import("../utils/doctor.js");
|
|
1585
|
+
await runDoctorCheck(argv, { mode: "daemon" });
|
|
1586
|
+
process.exit(0);
|
|
1587
|
+
}
|
|
1583
1588
|
try {
|
|
1584
1589
|
await runDaemon(parseDaemonCliArgs(argv));
|
|
1585
1590
|
}
|
package/dist/i18n/messages-en.js
CHANGED
|
@@ -26,16 +26,17 @@ export const messages = {
|
|
|
26
26
|
"hook.healthCheck.logHint": "\nCheck: {logPath}",
|
|
27
27
|
"hook.healthCheck.fixes": "\nCommon fixes:\n- Ensure Node.js >= 22.6.0: node --version\n- Reinstall: npm install -g cli-wechat-bridge@latest\n- Check firewall: allow localhost TCP connections",
|
|
28
28
|
// === PTY fallback ===
|
|
29
|
-
"pty.fallback.warning": "[Warning] PTY unavailable
|
|
29
|
+
"pty.fallback.warning": "[Warning] PTY unavailable — using fallback mode (TERM=dumb).\nCore features (messaging, approvals, session management) work normally; terminal rendering (colors, progress bars) may be degraded.\nFix: Linux install build-essential python3 | Windows run npm rebuild node-pty | macOS run xcode-select --install\nRun --doctor for detailed environment info.\n",
|
|
30
30
|
// === Spawn diagnostic ===
|
|
31
31
|
"spawn.diagnostic.title": "Failed to start CLI process: {target}\nError: {error}",
|
|
32
32
|
"spawn.diagnostic.fixesHeader": "\nPossible fixes:",
|
|
33
33
|
"spawn.diagnostic.nodePty": "- The node-pty native module is incompatible with your Node.js version.\n- Run: npm rebuild node-pty\n- Or reinstall: npm install -g cli-wechat-bridge@latest",
|
|
34
34
|
"spawn.diagnostic.xcode": "- Ensure Xcode CLI tools are installed: xcode-select --install",
|
|
35
|
+
"spawn.diagnostic.linuxBuildTools": "- Linux requires build tools for node-pty: sudo apt install build-essential python3\n- Then reinstall: npm install -g cli-wechat-bridge@latest",
|
|
35
36
|
"spawn.diagnostic.notFound": "- The command \"{target}\" was not found on PATH.\n- Verify it is installed and accessible from your terminal.",
|
|
36
37
|
"spawn.diagnostic.generic": "- Reinstall: npm install -g cli-wechat-bridge@latest",
|
|
37
38
|
"spawn.diagnostic.nodeVersion": "- Ensure Node.js >= 22.6.0: node --version",
|
|
38
|
-
"spawn.diagnostic.
|
|
39
|
+
"spawn.diagnostic.winFull": "- Ensure Windows 10 build 18309+ (run 'winver' to check)\n- Install Visual C++ Redistributable: https://aka.ms/vs/17/release/vc_redist.x64.exe\n- Run: npm rebuild node-pty\n- Try running as Administrator",
|
|
39
40
|
// === Emoji bindings ===
|
|
40
41
|
"binding.usage": "Invalid format.\nUsage: /bind [emoji] command\nExample: /bind 🚀 deploy --prod",
|
|
41
42
|
"binding.bound": "Bound {emoji} → {command}",
|
|
@@ -45,4 +46,80 @@ export const messages = {
|
|
|
45
46
|
"binding.listHeader": "Emoji bindings:",
|
|
46
47
|
// === Version checker ===
|
|
47
48
|
"update.available": "[Update Available] Version {latest} is available (current: {current}).\nRun: npm install -g cli-wechat-bridge@latest",
|
|
49
|
+
// === Doctor ===
|
|
50
|
+
"doctor.title": "CLI WeChat Bridge Doctor",
|
|
51
|
+
"doctor.section.environment": "Environment",
|
|
52
|
+
"doctor.section.adapterCli": "Adapter commands",
|
|
53
|
+
"doctor.section.data": "Data",
|
|
54
|
+
"doctor.section.runtime": "Runtime",
|
|
55
|
+
"doctor.label.node": "Node.js",
|
|
56
|
+
"doctor.label.platform": "Platform",
|
|
57
|
+
"doctor.label.winBuild": "Windows build",
|
|
58
|
+
"doctor.label.nodePty": "node-pty",
|
|
59
|
+
"doctor.label.shell": "shell",
|
|
60
|
+
"doctor.label.dataDir": "Data dir",
|
|
61
|
+
"doctor.label.credentials": "Credentials",
|
|
62
|
+
"doctor.label.mode": "Mode",
|
|
63
|
+
"doctor.label.cwd": "Cwd",
|
|
64
|
+
"doctor.label.adapter": "Adapter",
|
|
65
|
+
"doctor.label.daemon": "Daemon",
|
|
66
|
+
"doctor.label.bridgeLock": "Bridge lock",
|
|
67
|
+
"doctor.label.workspaceEndpoint": "Workspace endpoint ({adapter})",
|
|
68
|
+
"doctor.label.legacyEndpoint": "Legacy endpoint",
|
|
69
|
+
"doctor.winBuildConpty": "(ConPTY requires build 18309+)",
|
|
70
|
+
"doctor.nodePty.loaded": "loaded",
|
|
71
|
+
"doctor.nodePty.fix": "Fix: {fix}",
|
|
72
|
+
"doctor.adapterCli.shellNative": "uses the local shell; no other adapter CLI is checked.",
|
|
73
|
+
"doctor.adapterCli.notFound": "not found{optional}",
|
|
74
|
+
"doctor.dataDirMissing": "(will be created on first run)",
|
|
75
|
+
"doctor.credentialsFound": "found",
|
|
76
|
+
"doctor.credentialsMissing": "not found (run wechat-setup first)",
|
|
77
|
+
"doctor.mode.bridge": "standalone bridge",
|
|
78
|
+
"doctor.mode.generic": "generic",
|
|
79
|
+
"doctor.notSpecified": "(not specified)",
|
|
80
|
+
"doctor.none": "none",
|
|
81
|
+
"doctor.unknown": "unknown",
|
|
82
|
+
"doctor.yes": "yes",
|
|
83
|
+
"doctor.no": "no",
|
|
84
|
+
"doctor.optional": "(optional)",
|
|
85
|
+
"doctor.invalidShape": "unexpected file shape",
|
|
86
|
+
"doctor.file": "File: {file}",
|
|
87
|
+
"doctor.issue": "Issue: {issue}.",
|
|
88
|
+
"doctor.daemon.invalid": "invalid endpoint file ({error})",
|
|
89
|
+
"doctor.daemon.stale": "stale endpoint pid={pid} cwd={cwd} port={port}",
|
|
90
|
+
"doctor.daemon.staleStartup": "Startup: bridge and daemon startup paths will clear stale daemon endpoint state before continuing.",
|
|
91
|
+
"doctor.daemon.live": "live pid={pid} cwd={cwd} port={port}",
|
|
92
|
+
"doctor.daemon.liveBridgeStartup": "Startup: standalone bridge will refuse to start while this daemon is live.",
|
|
93
|
+
"doctor.daemon.liveDaemonStartup": "Startup: daemon startup will request shutdown of this daemon before binding.",
|
|
94
|
+
"doctor.lock.invalid": "invalid lock file ({error})",
|
|
95
|
+
"doctor.lock.stale": "stale",
|
|
96
|
+
"doctor.lock.staleDaemonStartup": "daemon startup will clear this stale single-bridge lock.",
|
|
97
|
+
"doctor.lock.staleBridgeStartup": "the next standalone bridge start will replace this stale lock.",
|
|
98
|
+
"doctor.lock.reclaimable": "reclaimable",
|
|
99
|
+
"doctor.lock.parent": "pid={pid} alive={alive}",
|
|
100
|
+
"doctor.lock.detail.pid": "pid",
|
|
101
|
+
"doctor.lock.detail.adapter": "adapter",
|
|
102
|
+
"doctor.lock.detail.cwd": "cwd",
|
|
103
|
+
"doctor.lock.detail.lifecycle": "lifecycle",
|
|
104
|
+
"doctor.lock.detail.startedAt": "started at",
|
|
105
|
+
"doctor.lock.detail.parent": "parent",
|
|
106
|
+
"doctor.lock.detail.startup": "startup",
|
|
107
|
+
"doctor.lock.detail.note": "note",
|
|
108
|
+
"doctor.lock.reclaimableDaemonStartup": "daemon startup will stop or clear this orphaned single bridge before continuing.",
|
|
109
|
+
"doctor.lock.reclaimableBridgeStartup": "standalone bridge startup can reclaim this companion-bound or legacy lock because its parent is gone.",
|
|
110
|
+
"doctor.lock.live": "live",
|
|
111
|
+
"doctor.lock.liveDaemonStartup": "daemon startup will stop this single bridge before continuing.",
|
|
112
|
+
"doctor.lock.liveBridgeStartup": "standalone bridge will fail with a lock conflict until this process exits.",
|
|
113
|
+
"doctor.lock.cwdDifferent": "lock cwd differs from requested cwd ({cwd}); single-bridge lock is global.",
|
|
114
|
+
"doctor.lock.adapterDifferent": "lock adapter={lockAdapter} differs from requested adapter={requestedAdapter}.",
|
|
115
|
+
"doctor.endpoint.legacySame": "same as adapter endpoint",
|
|
116
|
+
"doctor.endpoint.summary": "instance={instanceId} kind={kind} port={port} reachable={reachable}",
|
|
117
|
+
"doctor.endpoint.owner": "ownerPid={ownerPid} ownerAlive={ownerAlive} companionPid={companionPid} companionAlive={companionAlive} status={status}",
|
|
118
|
+
"doctor.endpoint.issue.kind": "endpoint kind={kind} differs from requested adapter={adapter}",
|
|
119
|
+
"doctor.endpoint.issue.cwd": "endpoint cwd differs from requested cwd ({cwd})",
|
|
120
|
+
"doctor.endpoint.issue.port": "endpoint port {port} is not reachable",
|
|
121
|
+
"doctor.endpoint.issue.ownerDead": "bridge owner pid={pid} is dead",
|
|
122
|
+
"doctor.endpoint.issue.companionDead": "companion pid={pid} is dead",
|
|
123
|
+
"doctor.endpoint.issue.workerStatus": "visible worker status={status}",
|
|
124
|
+
"doctor.endpoint.issue.instanceMismatch": "endpoint instanceId does not match bridge lock instanceId",
|
|
48
125
|
};
|
package/dist/i18n/messages-zh.js
CHANGED
|
@@ -26,16 +26,17 @@ export const messages = {
|
|
|
26
26
|
"hook.healthCheck.logHint": "\n查看日志: {logPath}",
|
|
27
27
|
"hook.healthCheck.fixes": "\n常见修复方法:\n- 确保 Node.js >= 22.6.0: node --version\n- 重新安装: npm install -g cli-wechat-bridge@latest\n- 检查防火墙: 允许 localhost TCP 连接",
|
|
28
28
|
// === PTY 回退 ===
|
|
29
|
-
"pty.fallback.warning": "[警告] PTY
|
|
29
|
+
"pty.fallback.warning": "[警告] PTY 不可用 — 已切换到回退模式(TERM=dumb)。\n核心功能(消息收发、审批、会话管理)正常工作;终端渲染(颜色、进度条)可能受限。\n修复方法: Linux 安装 build-essential python3 | Windows 运行 npm rebuild node-pty | macOS 运行 xcode-select --install\n运行 --doctor 查看详细环境信息。\n",
|
|
30
30
|
// === 启动诊断 ===
|
|
31
31
|
"spawn.diagnostic.title": "无法启动 CLI 进程: {target}\n错误: {error}",
|
|
32
32
|
"spawn.diagnostic.fixesHeader": "\n可能的修复方法:",
|
|
33
33
|
"spawn.diagnostic.nodePty": "- node-pty 原生模块与当前 Node.js 版本不兼容。\n- 运行: npm rebuild node-pty\n- 或重新安装: npm install -g cli-wechat-bridge@latest",
|
|
34
34
|
"spawn.diagnostic.xcode": "- 确保已安装 Xcode 命令行工具: xcode-select --install",
|
|
35
|
+
"spawn.diagnostic.linuxBuildTools": "- Linux 需要编译工具来构建 node-pty: sudo apt install build-essential python3\n- 然后重新安装: npm install -g cli-wechat-bridge@latest",
|
|
35
36
|
"spawn.diagnostic.notFound": "- 命令 \"{target}\" 未在 PATH 中找到。\n- 请确认已安装并可从终端访问。",
|
|
36
37
|
"spawn.diagnostic.generic": "- 重新安装: npm install -g cli-wechat-bridge@latest",
|
|
37
38
|
"spawn.diagnostic.nodeVersion": "- 确保 Node.js >= 22.6.0: node --version",
|
|
38
|
-
"spawn.diagnostic.
|
|
39
|
+
"spawn.diagnostic.winFull": "- 确保 Windows 10 build 18309+(运行 winver 查看)\n- 安装 Visual C++ Redistributable: https://aka.ms/vs/17/release/vc_redist.x64.exe\n- 运行: npm rebuild node-pty\n- 尝试以管理员身份运行",
|
|
39
40
|
// === Emoji 绑定 ===
|
|
40
41
|
"binding.usage": "格式错误。\n用法: /bind [表情] 命令\n示例: /bind 🚀 deploy --prod",
|
|
41
42
|
"binding.bound": "已绑定 {emoji} → {command}",
|
|
@@ -45,4 +46,80 @@ export const messages = {
|
|
|
45
46
|
"binding.listHeader": "Emoji 绑定列表:",
|
|
46
47
|
// === 版本检查 ===
|
|
47
48
|
"update.available": "[有新版本] 版本 {latest} 可用(当前: {current})。\n运行: npm install -g cli-wechat-bridge@latest",
|
|
49
|
+
// === Doctor ===
|
|
50
|
+
"doctor.title": "CLI WeChat Bridge 诊断",
|
|
51
|
+
"doctor.section.environment": "环境",
|
|
52
|
+
"doctor.section.adapterCli": "适配器命令",
|
|
53
|
+
"doctor.section.data": "数据",
|
|
54
|
+
"doctor.section.runtime": "运行时",
|
|
55
|
+
"doctor.label.node": "Node.js",
|
|
56
|
+
"doctor.label.platform": "平台",
|
|
57
|
+
"doctor.label.winBuild": "Windows build",
|
|
58
|
+
"doctor.label.nodePty": "node-pty",
|
|
59
|
+
"doctor.label.shell": "shell",
|
|
60
|
+
"doctor.label.dataDir": "数据目录",
|
|
61
|
+
"doctor.label.credentials": "凭据",
|
|
62
|
+
"doctor.label.mode": "模式",
|
|
63
|
+
"doctor.label.cwd": "目录",
|
|
64
|
+
"doctor.label.adapter": "适配器",
|
|
65
|
+
"doctor.label.daemon": "守护进程",
|
|
66
|
+
"doctor.label.bridgeLock": "桥接锁",
|
|
67
|
+
"doctor.label.workspaceEndpoint": "工作区端点 ({adapter})",
|
|
68
|
+
"doctor.label.legacyEndpoint": "旧端点",
|
|
69
|
+
"doctor.winBuildConpty": "(ConPTY 需要 build 18309+)",
|
|
70
|
+
"doctor.nodePty.loaded": "已加载",
|
|
71
|
+
"doctor.nodePty.fix": "修复: {fix}",
|
|
72
|
+
"doctor.adapterCli.shellNative": "使用本机 shell,无需检查其它适配器 CLI。",
|
|
73
|
+
"doctor.adapterCli.notFound": "未找到{optional}",
|
|
74
|
+
"doctor.dataDirMissing": "(首次运行会创建)",
|
|
75
|
+
"doctor.credentialsFound": "已找到",
|
|
76
|
+
"doctor.credentialsMissing": "未找到 (先运行 wechat-setup)",
|
|
77
|
+
"doctor.mode.bridge": "独立桥接",
|
|
78
|
+
"doctor.mode.generic": "通用",
|
|
79
|
+
"doctor.notSpecified": "(未指定)",
|
|
80
|
+
"doctor.none": "无",
|
|
81
|
+
"doctor.unknown": "未知",
|
|
82
|
+
"doctor.yes": "是",
|
|
83
|
+
"doctor.no": "否",
|
|
84
|
+
"doctor.optional": "(可选)",
|
|
85
|
+
"doctor.invalidShape": "文件结构不符合预期",
|
|
86
|
+
"doctor.file": "文件: {file}",
|
|
87
|
+
"doctor.issue": "问题: {issue}。",
|
|
88
|
+
"doctor.daemon.invalid": "端点文件无效 ({error})",
|
|
89
|
+
"doctor.daemon.stale": "过期端点 pid={pid} cwd={cwd} port={port}",
|
|
90
|
+
"doctor.daemon.staleStartup": "启动行为: 桥接和守护进程启动路径会先清理过期守护进程端点。",
|
|
91
|
+
"doctor.daemon.live": "运行中 pid={pid} cwd={cwd} port={port}",
|
|
92
|
+
"doctor.daemon.liveBridgeStartup": "启动行为: 守护进程运行中时,独立桥接会拒绝启动。",
|
|
93
|
+
"doctor.daemon.liveDaemonStartup": "启动行为: 守护进程启动会先请求关闭这个旧守护进程再绑定。",
|
|
94
|
+
"doctor.lock.invalid": "锁文件无效 ({error})",
|
|
95
|
+
"doctor.lock.stale": "过期",
|
|
96
|
+
"doctor.lock.staleDaemonStartup": "守护进程启动会清理这个过期的单桥接锁。",
|
|
97
|
+
"doctor.lock.staleBridgeStartup": "下次独立桥接启动会替换这个过期锁。",
|
|
98
|
+
"doctor.lock.reclaimable": "可回收",
|
|
99
|
+
"doctor.lock.parent": "pid={pid} alive={alive}",
|
|
100
|
+
"doctor.lock.detail.pid": "pid",
|
|
101
|
+
"doctor.lock.detail.adapter": "适配器",
|
|
102
|
+
"doctor.lock.detail.cwd": "目录",
|
|
103
|
+
"doctor.lock.detail.lifecycle": "生命周期",
|
|
104
|
+
"doctor.lock.detail.startedAt": "启动时间",
|
|
105
|
+
"doctor.lock.detail.parent": "父进程",
|
|
106
|
+
"doctor.lock.detail.startup": "启动行为",
|
|
107
|
+
"doctor.lock.detail.note": "注意",
|
|
108
|
+
"doctor.lock.reclaimableDaemonStartup": "守护进程启动会先停止或清理这个孤儿单桥接。",
|
|
109
|
+
"doctor.lock.reclaimableBridgeStartup": "独立桥接可回收这个父进程已退出的锁。",
|
|
110
|
+
"doctor.lock.live": "运行中",
|
|
111
|
+
"doctor.lock.liveDaemonStartup": "守护进程启动会先停止这个单桥接。",
|
|
112
|
+
"doctor.lock.liveBridgeStartup": "该进程退出前,独立桥接会因锁冲突启动失败。",
|
|
113
|
+
"doctor.lock.cwdDifferent": "锁 cwd 与请求 cwd 不同 ({cwd});单桥接锁是全局的。",
|
|
114
|
+
"doctor.lock.adapterDifferent": "锁 adapter={lockAdapter} 与请求 adapter={requestedAdapter} 不同。",
|
|
115
|
+
"doctor.endpoint.legacySame": "与适配器端点相同",
|
|
116
|
+
"doctor.endpoint.summary": "实例={instanceId} 类型={kind} 端口={port} 可达={reachable}",
|
|
117
|
+
"doctor.endpoint.owner": "ownerPid={ownerPid} ownerAlive={ownerAlive} companionPid={companionPid} companionAlive={companionAlive} 状态={status}",
|
|
118
|
+
"doctor.endpoint.issue.kind": "端点类型 {kind} 与请求适配器 {adapter} 不同",
|
|
119
|
+
"doctor.endpoint.issue.cwd": "端点 cwd 与请求 cwd 不同 ({cwd})",
|
|
120
|
+
"doctor.endpoint.issue.port": "端点端口 {port} 不可达",
|
|
121
|
+
"doctor.endpoint.issue.ownerDead": "桥接 owner pid={pid} 已退出",
|
|
122
|
+
"doctor.endpoint.issue.companionDead": "companion pid={pid} 已退出",
|
|
123
|
+
"doctor.endpoint.issue.workerStatus": "可见端 worker 状态={status}",
|
|
124
|
+
"doctor.endpoint.issue.instanceMismatch": "端点 instanceId 与桥接锁 instanceId 不一致",
|
|
48
125
|
};
|
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { normalizeBridgeLockPayload, shouldAutoReclaimBridgeLock, } from "../bridge/bridge-state.js";
|
|
7
|
+
import { readLocalCompanionEndpoint as readWorkspaceEndpoint, } from "../companion/local-companion-link.js";
|
|
8
|
+
import { isDaemonEndpointAlive, } from "../daemon/daemon-link.js";
|
|
9
|
+
import { t } from "../i18n/index.js";
|
|
10
|
+
import { resolveChannelDataDir } from "../wechat/channel-config.js";
|
|
11
|
+
const STATE_OK = "[ok]";
|
|
12
|
+
const STATE_WARN = "[warn]";
|
|
13
|
+
const STATE_FAIL = "[fail]";
|
|
14
|
+
const RUNTIME_ENDPOINT_TIMEOUT_MS = 500;
|
|
15
|
+
const DAEMON_ENDPOINT_TIMEOUT_MS = 500;
|
|
16
|
+
function msg(key, params) {
|
|
17
|
+
return t(`doctor.${key}`, params);
|
|
18
|
+
}
|
|
19
|
+
function section(lines, title) {
|
|
20
|
+
if (lines.length > 0 && lines.at(-1) !== "") {
|
|
21
|
+
lines.push("");
|
|
22
|
+
}
|
|
23
|
+
lines.push(title);
|
|
24
|
+
}
|
|
25
|
+
function row(status, label, value) {
|
|
26
|
+
return ` ${status} ${label} ${value}`;
|
|
27
|
+
}
|
|
28
|
+
function fieldRow(label, value) {
|
|
29
|
+
return ` ${label} ${value}`;
|
|
30
|
+
}
|
|
31
|
+
function detail(value) {
|
|
32
|
+
return ` ${value}`;
|
|
33
|
+
}
|
|
34
|
+
function detailField(label, value) {
|
|
35
|
+
return detail(`${label} ${value}`);
|
|
36
|
+
}
|
|
37
|
+
function findExecutable(name) {
|
|
38
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
39
|
+
try {
|
|
40
|
+
return execFileSync(cmd, [name], {
|
|
41
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
42
|
+
encoding: "utf8",
|
|
43
|
+
}).trim().split(/\r?\n/)[0] ?? null;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function getPlatformPtyFix(platform) {
|
|
50
|
+
switch (platform) {
|
|
51
|
+
case "linux":
|
|
52
|
+
return "sudo apt install build-essential python3 && npm install -g cli-wechat-bridge@latest";
|
|
53
|
+
case "darwin":
|
|
54
|
+
return "xcode-select --install && npm rebuild node-pty";
|
|
55
|
+
case "win32":
|
|
56
|
+
return "npm rebuild node-pty (also ensure Visual C++ Redistributable is installed)";
|
|
57
|
+
default:
|
|
58
|
+
return "npm rebuild node-pty";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function defaultIsProcessAlive(pid) {
|
|
62
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
process.kill(pid, 0);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function defaultIsTcpPortReachable(port, timeoutMs) {
|
|
74
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return await new Promise((resolve) => {
|
|
78
|
+
const socket = net.connect({
|
|
79
|
+
host: "127.0.0.1",
|
|
80
|
+
port,
|
|
81
|
+
});
|
|
82
|
+
let done = false;
|
|
83
|
+
const finish = (result) => {
|
|
84
|
+
if (done) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
done = true;
|
|
88
|
+
socket.destroy();
|
|
89
|
+
resolve(result);
|
|
90
|
+
};
|
|
91
|
+
socket.setTimeout(timeoutMs);
|
|
92
|
+
socket.once("connect", () => finish(true));
|
|
93
|
+
socket.once("timeout", () => finish(false));
|
|
94
|
+
socket.once("error", () => finish(false));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function resolveDeps(deps = {}) {
|
|
98
|
+
return {
|
|
99
|
+
platform: deps.platform ?? process.platform,
|
|
100
|
+
arch: deps.arch ?? process.arch,
|
|
101
|
+
nodeVersion: deps.nodeVersion ?? process.version,
|
|
102
|
+
osRelease: deps.osRelease ?? os.release,
|
|
103
|
+
findExecutable: deps.findExecutable ?? findExecutable,
|
|
104
|
+
loadNodePty: deps.loadNodePty ??
|
|
105
|
+
(async () => {
|
|
106
|
+
await import("node-pty");
|
|
107
|
+
}),
|
|
108
|
+
resolveDataDir: deps.resolveDataDir ?? resolveChannelDataDir,
|
|
109
|
+
exists: deps.exists ?? fs.existsSync,
|
|
110
|
+
readTextFile: deps.readTextFile ??
|
|
111
|
+
((filePath) => fs.readFileSync(filePath, "utf8")),
|
|
112
|
+
isProcessAlive: deps.isProcessAlive ?? defaultIsProcessAlive,
|
|
113
|
+
isTcpPortReachable: deps.isTcpPortReachable ?? defaultIsTcpPortReachable,
|
|
114
|
+
isDaemonAlive: deps.isDaemonAlive ??
|
|
115
|
+
((endpoint) => isDaemonEndpointAlive(endpoint, { timeoutMs: DAEMON_ENDPOINT_TIMEOUT_MS })),
|
|
116
|
+
readLocalCompanionEndpoint: deps.readLocalCompanionEndpoint ??
|
|
117
|
+
((cwd, adapter) => readWorkspaceEndpoint(cwd, adapter ? { adapter } : {})),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function isBridgeAdapterKind(value) {
|
|
121
|
+
return (value === "codex" ||
|
|
122
|
+
value === "claude" ||
|
|
123
|
+
value === "opencode" ||
|
|
124
|
+
value === "shell");
|
|
125
|
+
}
|
|
126
|
+
export function parseDoctorCliArgs(argv, mode = "generic", fallbackCwd = process.cwd()) {
|
|
127
|
+
let cwd = fallbackCwd;
|
|
128
|
+
let adapter;
|
|
129
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
130
|
+
const arg = argv[i];
|
|
131
|
+
const next = argv[i + 1];
|
|
132
|
+
if (!arg) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (arg === "--cwd" && next) {
|
|
136
|
+
cwd = path.resolve(next);
|
|
137
|
+
i += 1;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (arg === "--adapter" && isBridgeAdapterKind(next)) {
|
|
141
|
+
adapter = next;
|
|
142
|
+
i += 1;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
mode,
|
|
148
|
+
cwd: path.resolve(cwd),
|
|
149
|
+
adapter,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function statusTag(status) {
|
|
153
|
+
if (status === "ok") {
|
|
154
|
+
return STATE_OK;
|
|
155
|
+
}
|
|
156
|
+
if (status === "warn") {
|
|
157
|
+
return STATE_WARN;
|
|
158
|
+
}
|
|
159
|
+
return STATE_FAIL;
|
|
160
|
+
}
|
|
161
|
+
function shortError(error) {
|
|
162
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
163
|
+
return message.length > 100 ? `${message.slice(0, 100)}...` : message;
|
|
164
|
+
}
|
|
165
|
+
function sameWorkspacePath(left, right) {
|
|
166
|
+
const normalizedLeft = path.resolve(left);
|
|
167
|
+
const normalizedRight = path.resolve(right);
|
|
168
|
+
return process.platform === "win32"
|
|
169
|
+
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
|
|
170
|
+
: normalizedLeft === normalizedRight;
|
|
171
|
+
}
|
|
172
|
+
function normalizeDaemonEndpoint(value) {
|
|
173
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const record = value;
|
|
177
|
+
if (typeof record.protocolVersion !== "number" ||
|
|
178
|
+
typeof record.pid !== "number" ||
|
|
179
|
+
typeof record.port !== "number" ||
|
|
180
|
+
typeof record.token !== "string" ||
|
|
181
|
+
typeof record.cwd !== "string" ||
|
|
182
|
+
typeof record.startedAt !== "string") {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
protocolVersion: record.protocolVersion,
|
|
187
|
+
pid: record.pid,
|
|
188
|
+
port: record.port,
|
|
189
|
+
token: record.token,
|
|
190
|
+
cwd: record.cwd,
|
|
191
|
+
startedAt: record.startedAt,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function readNormalizedJsonFile(filePath, deps, normalize) {
|
|
195
|
+
if (!deps.exists(filePath)) {
|
|
196
|
+
return { kind: "missing", filePath };
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const parsed = JSON.parse(deps.readTextFile(filePath));
|
|
200
|
+
const normalized = normalize(parsed);
|
|
201
|
+
if (!normalized) {
|
|
202
|
+
return {
|
|
203
|
+
kind: "invalid",
|
|
204
|
+
filePath,
|
|
205
|
+
error: msg("invalidShape"),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
kind: "ok",
|
|
210
|
+
filePath,
|
|
211
|
+
value: normalized,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
return {
|
|
216
|
+
kind: "invalid",
|
|
217
|
+
filePath,
|
|
218
|
+
error: shortError(error),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function formatMode(mode) {
|
|
223
|
+
if (mode === "bridge") {
|
|
224
|
+
return msg("mode.bridge");
|
|
225
|
+
}
|
|
226
|
+
if (mode === "daemon") {
|
|
227
|
+
return "wechat-daemon";
|
|
228
|
+
}
|
|
229
|
+
return msg("mode.generic");
|
|
230
|
+
}
|
|
231
|
+
function formatAlive(value) {
|
|
232
|
+
if (value === undefined) {
|
|
233
|
+
return msg("unknown");
|
|
234
|
+
}
|
|
235
|
+
return value ? msg("yes") : msg("no");
|
|
236
|
+
}
|
|
237
|
+
function appendLockDetailLines(lines, lock) {
|
|
238
|
+
lines.push(detailField(msg("lock.detail.pid"), String(lock.pid)));
|
|
239
|
+
lines.push(detailField(msg("lock.detail.adapter"), lock.adapter));
|
|
240
|
+
lines.push(detailField(msg("lock.detail.cwd"), lock.cwd));
|
|
241
|
+
lines.push(detailField(msg("lock.detail.lifecycle"), lock.lifecycle));
|
|
242
|
+
lines.push(detailField(msg("lock.detail.startedAt"), lock.startedAt));
|
|
243
|
+
}
|
|
244
|
+
async function appendDaemonLines(lines, options, dataDir, deps) {
|
|
245
|
+
const endpointFile = path.join(dataDir, "daemon-endpoint.json");
|
|
246
|
+
const endpointResult = readNormalizedJsonFile(endpointFile, deps, normalizeDaemonEndpoint);
|
|
247
|
+
const label = msg("label.daemon");
|
|
248
|
+
if (endpointResult.kind === "missing") {
|
|
249
|
+
lines.push(row(STATE_OK, label, msg("none")));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (endpointResult.kind === "invalid") {
|
|
253
|
+
lines.push(row(STATE_WARN, label, msg("daemon.invalid", {
|
|
254
|
+
error: endpointResult.error,
|
|
255
|
+
})));
|
|
256
|
+
lines.push(detail(msg("file", { file: endpointResult.filePath })));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const endpoint = endpointResult.value;
|
|
260
|
+
const pidAlive = deps.isProcessAlive(endpoint.pid);
|
|
261
|
+
const daemonAlive = pidAlive ? await deps.isDaemonAlive(endpoint) : false;
|
|
262
|
+
if (!daemonAlive) {
|
|
263
|
+
lines.push(row(STATE_WARN, label, msg("daemon.stale", {
|
|
264
|
+
pid: endpoint.pid,
|
|
265
|
+
cwd: endpoint.cwd,
|
|
266
|
+
port: endpoint.port,
|
|
267
|
+
})));
|
|
268
|
+
lines.push(detail(msg("daemon.staleStartup")));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const status = options.mode === "bridge" ? "fail" : "warn";
|
|
272
|
+
lines.push(row(statusTag(status), label, msg("daemon.live", {
|
|
273
|
+
pid: endpoint.pid,
|
|
274
|
+
cwd: endpoint.cwd,
|
|
275
|
+
port: endpoint.port,
|
|
276
|
+
})));
|
|
277
|
+
if (options.mode === "bridge") {
|
|
278
|
+
lines.push(detail(msg("daemon.liveBridgeStartup")));
|
|
279
|
+
}
|
|
280
|
+
else if (options.mode === "daemon") {
|
|
281
|
+
lines.push(detail(msg("daemon.liveDaemonStartup")));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function appendBridgeLockLines(lines, options, dataDir, deps) {
|
|
285
|
+
const lockFile = path.join(dataDir, "bridge.lock.json");
|
|
286
|
+
const lockResult = readNormalizedJsonFile(lockFile, deps, normalizeBridgeLockPayload);
|
|
287
|
+
const label = msg("label.bridgeLock");
|
|
288
|
+
if (lockResult.kind === "missing") {
|
|
289
|
+
lines.push(row(STATE_OK, label, msg("none")));
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
if (lockResult.kind === "invalid") {
|
|
293
|
+
lines.push(row(STATE_WARN, label, msg("lock.invalid", {
|
|
294
|
+
error: lockResult.error,
|
|
295
|
+
})));
|
|
296
|
+
lines.push(detail(msg("file", { file: lockResult.filePath })));
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
const lock = lockResult.value;
|
|
300
|
+
const lockAlive = deps.isProcessAlive(lock.pid);
|
|
301
|
+
const parentAlive = lock.parentPid > 1 ? deps.isProcessAlive(lock.parentPid) : undefined;
|
|
302
|
+
const reclaimable = lockAlive
|
|
303
|
+
? shouldAutoReclaimBridgeLock(lock, deps.isProcessAlive)
|
|
304
|
+
: false;
|
|
305
|
+
if (!lockAlive) {
|
|
306
|
+
lines.push(row(STATE_WARN, label, msg("lock.stale")));
|
|
307
|
+
appendLockDetailLines(lines, lock);
|
|
308
|
+
lines.push(detailField(msg("lock.detail.startup"), options.mode === "daemon"
|
|
309
|
+
? msg("lock.staleDaemonStartup")
|
|
310
|
+
: msg("lock.staleBridgeStartup")));
|
|
311
|
+
}
|
|
312
|
+
else if (reclaimable) {
|
|
313
|
+
lines.push(row(STATE_WARN, label, msg("lock.reclaimable")));
|
|
314
|
+
appendLockDetailLines(lines, lock);
|
|
315
|
+
lines.push(detailField(msg("lock.detail.parent"), msg("lock.parent", {
|
|
316
|
+
pid: lock.parentPid,
|
|
317
|
+
alive: formatAlive(parentAlive),
|
|
318
|
+
})));
|
|
319
|
+
lines.push(detailField(msg("lock.detail.startup"), options.mode === "daemon"
|
|
320
|
+
? msg("lock.reclaimableDaemonStartup")
|
|
321
|
+
: msg("lock.reclaimableBridgeStartup")));
|
|
322
|
+
}
|
|
323
|
+
else if (options.mode === "daemon") {
|
|
324
|
+
lines.push(row(STATE_WARN, label, msg("lock.live")));
|
|
325
|
+
appendLockDetailLines(lines, lock);
|
|
326
|
+
lines.push(detailField(msg("lock.detail.startup"), msg("lock.liveDaemonStartup")));
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
lines.push(row(STATE_FAIL, label, msg("lock.live")));
|
|
330
|
+
appendLockDetailLines(lines, lock);
|
|
331
|
+
lines.push(detailField(msg("lock.detail.startup"), msg("lock.liveBridgeStartup")));
|
|
332
|
+
}
|
|
333
|
+
if (!sameWorkspacePath(lock.cwd, options.cwd)) {
|
|
334
|
+
lines.push(detailField(msg("lock.detail.note"), msg("lock.cwdDifferent", { cwd: options.cwd })));
|
|
335
|
+
}
|
|
336
|
+
if (options.adapter && lock.adapter !== options.adapter) {
|
|
337
|
+
lines.push(detailField(msg("lock.detail.note"), msg("lock.adapterDifferent", {
|
|
338
|
+
lockAdapter: lock.adapter,
|
|
339
|
+
requestedAdapter: options.adapter,
|
|
340
|
+
})));
|
|
341
|
+
}
|
|
342
|
+
return lock;
|
|
343
|
+
}
|
|
344
|
+
function chooseEndpointAdapters(options, lock) {
|
|
345
|
+
if (options.adapter) {
|
|
346
|
+
return [options.adapter];
|
|
347
|
+
}
|
|
348
|
+
if (lock) {
|
|
349
|
+
return [lock.adapter];
|
|
350
|
+
}
|
|
351
|
+
return options.mode === "daemon"
|
|
352
|
+
? ["codex", "claude", "opencode"]
|
|
353
|
+
: ["codex"];
|
|
354
|
+
}
|
|
355
|
+
function chooseCliChecks(options) {
|
|
356
|
+
if (options.mode === "bridge") {
|
|
357
|
+
switch (options.adapter) {
|
|
358
|
+
case "codex":
|
|
359
|
+
return [{ name: "codex", label: "Codex CLI", optional: false }];
|
|
360
|
+
case "claude":
|
|
361
|
+
return [{ name: "claude", label: "Claude CLI", optional: false }];
|
|
362
|
+
case "opencode":
|
|
363
|
+
return [{ name: "opencode", label: "OpenCode", optional: false }];
|
|
364
|
+
case "shell":
|
|
365
|
+
return [];
|
|
366
|
+
default:
|
|
367
|
+
return [];
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return [
|
|
371
|
+
{ name: "codex", label: "Codex CLI", optional: true },
|
|
372
|
+
{ name: "claude", label: "Claude CLI", optional: true },
|
|
373
|
+
{ name: "opencode", label: "OpenCode", optional: true },
|
|
374
|
+
];
|
|
375
|
+
}
|
|
376
|
+
function appendCliLines(lines, options, deps) {
|
|
377
|
+
const clis = chooseCliChecks(options);
|
|
378
|
+
if (options.mode === "bridge" && options.adapter === "shell") {
|
|
379
|
+
section(lines, msg("section.adapterCli"));
|
|
380
|
+
lines.push(row(STATE_OK, msg("label.shell"), msg("adapterCli.shellNative")));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (clis.length === 0) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
section(lines, msg("section.adapterCli"));
|
|
387
|
+
for (const cli of clis) {
|
|
388
|
+
const loc = deps.findExecutable(cli.name);
|
|
389
|
+
if (loc) {
|
|
390
|
+
lines.push(row(STATE_OK, cli.label, loc));
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
lines.push(row(STATE_FAIL, cli.label, msg("adapterCli.notFound", {
|
|
394
|
+
optional: cli.optional ? ` ${msg("optional")}` : "",
|
|
395
|
+
})));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async function appendEndpointSummary(lines, label, endpoint, options, lock, deps) {
|
|
400
|
+
if (!endpoint) {
|
|
401
|
+
lines.push(row(STATE_OK, label, msg("none")));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const port = endpoint.serverPort ?? endpoint.port;
|
|
405
|
+
const reachable = await deps.isTcpPortReachable(port, RUNTIME_ENDPOINT_TIMEOUT_MS);
|
|
406
|
+
const bridgeOwnerAlive = endpoint.bridgeOwnerPid
|
|
407
|
+
? deps.isProcessAlive(endpoint.bridgeOwnerPid)
|
|
408
|
+
: undefined;
|
|
409
|
+
const companionAlive = endpoint.companionPid
|
|
410
|
+
? deps.isProcessAlive(endpoint.companionPid)
|
|
411
|
+
: undefined;
|
|
412
|
+
const issues = [];
|
|
413
|
+
if (options.adapter && endpoint.kind !== options.adapter) {
|
|
414
|
+
issues.push(msg("endpoint.issue.kind", {
|
|
415
|
+
kind: endpoint.kind,
|
|
416
|
+
adapter: options.adapter,
|
|
417
|
+
}));
|
|
418
|
+
}
|
|
419
|
+
if (!sameWorkspacePath(endpoint.cwd, options.cwd)) {
|
|
420
|
+
issues.push(msg("endpoint.issue.cwd", { cwd: options.cwd }));
|
|
421
|
+
}
|
|
422
|
+
if (!reachable) {
|
|
423
|
+
issues.push(msg("endpoint.issue.port", { port }));
|
|
424
|
+
}
|
|
425
|
+
if (bridgeOwnerAlive === false) {
|
|
426
|
+
issues.push(msg("endpoint.issue.ownerDead", {
|
|
427
|
+
pid: endpoint.bridgeOwnerPid ?? msg("none"),
|
|
428
|
+
}));
|
|
429
|
+
}
|
|
430
|
+
if (companionAlive === false) {
|
|
431
|
+
issues.push(msg("endpoint.issue.companionDead", {
|
|
432
|
+
pid: endpoint.companionPid ?? msg("none"),
|
|
433
|
+
}));
|
|
434
|
+
}
|
|
435
|
+
if (endpoint.companionStatus === "stopped" ||
|
|
436
|
+
endpoint.companionStatus === "error") {
|
|
437
|
+
issues.push(msg("endpoint.issue.workerStatus", {
|
|
438
|
+
status: endpoint.companionStatus,
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
if (lock &&
|
|
442
|
+
endpoint.kind === lock.adapter &&
|
|
443
|
+
sameWorkspacePath(endpoint.cwd, lock.cwd) &&
|
|
444
|
+
endpoint.instanceId !== lock.instanceId) {
|
|
445
|
+
issues.push(msg("endpoint.issue.instanceMismatch"));
|
|
446
|
+
}
|
|
447
|
+
const status = issues.length > 0 ? "warn" : "ok";
|
|
448
|
+
lines.push(row(statusTag(status), label, msg("endpoint.summary", {
|
|
449
|
+
instanceId: endpoint.instanceId,
|
|
450
|
+
kind: endpoint.kind,
|
|
451
|
+
port,
|
|
452
|
+
reachable: reachable ? msg("yes") : msg("no"),
|
|
453
|
+
})));
|
|
454
|
+
lines.push(detail(msg("endpoint.owner", {
|
|
455
|
+
ownerPid: endpoint.bridgeOwnerPid ?? msg("none"),
|
|
456
|
+
ownerAlive: formatAlive(bridgeOwnerAlive),
|
|
457
|
+
companionPid: endpoint.companionPid ?? msg("none"),
|
|
458
|
+
companionAlive: formatAlive(companionAlive),
|
|
459
|
+
status: endpoint.companionStatus ?? msg("unknown"),
|
|
460
|
+
})));
|
|
461
|
+
for (const issue of issues) {
|
|
462
|
+
lines.push(detail(msg("issue", { issue })));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async function appendWorkspaceEndpointLines(lines, options, lock, deps) {
|
|
466
|
+
const adapters = chooseEndpointAdapters(options, lock);
|
|
467
|
+
const seenEndpoints = new Set();
|
|
468
|
+
for (const adapter of adapters) {
|
|
469
|
+
const endpoint = deps.readLocalCompanionEndpoint(options.cwd, adapter);
|
|
470
|
+
if (endpoint) {
|
|
471
|
+
seenEndpoints.add(`${endpoint.kind}:${endpoint.instanceId}:${endpoint.port}`);
|
|
472
|
+
}
|
|
473
|
+
await appendEndpointSummary(lines, msg("label.workspaceEndpoint", { adapter }), endpoint, { ...options, adapter }, lock, deps);
|
|
474
|
+
}
|
|
475
|
+
const legacyEndpoint = deps.readLocalCompanionEndpoint(options.cwd);
|
|
476
|
+
const legacyLabel = msg("label.legacyEndpoint");
|
|
477
|
+
if (!legacyEndpoint) {
|
|
478
|
+
lines.push(row(STATE_OK, legacyLabel, msg("none")));
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const legacyKey = `${legacyEndpoint.kind}:${legacyEndpoint.instanceId}:${legacyEndpoint.port}`;
|
|
482
|
+
if (seenEndpoints.has(legacyKey)) {
|
|
483
|
+
lines.push(row(STATE_OK, legacyLabel, msg("endpoint.legacySame")));
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
await appendEndpointSummary(lines, legacyLabel, legacyEndpoint, options, lock, deps);
|
|
487
|
+
}
|
|
488
|
+
export async function buildDoctorReport(options = {}, rawDeps = {}) {
|
|
489
|
+
const deps = resolveDeps(rawDeps);
|
|
490
|
+
const parsed = parseDoctorCliArgs(options.argv ?? process.argv.slice(2), options.mode ?? "generic", options.cwd ?? process.cwd());
|
|
491
|
+
const doctorOptions = {
|
|
492
|
+
...parsed,
|
|
493
|
+
cwd: path.resolve(options.cwd ?? parsed.cwd),
|
|
494
|
+
adapter: options.adapter ?? parsed.adapter,
|
|
495
|
+
};
|
|
496
|
+
const lines = [];
|
|
497
|
+
lines.push(msg("title"));
|
|
498
|
+
section(lines, msg("section.environment"));
|
|
499
|
+
lines.push(row(STATE_OK, msg("label.node"), deps.nodeVersion));
|
|
500
|
+
lines.push(row(STATE_OK, msg("label.platform"), `${deps.platform}-${deps.arch}`));
|
|
501
|
+
if (deps.platform === "win32") {
|
|
502
|
+
const release = deps.osRelease();
|
|
503
|
+
const build = parseInt(release.split(".").pop() ?? "0", 10);
|
|
504
|
+
const ok = build >= 18309;
|
|
505
|
+
lines.push(row(ok ? STATE_OK : STATE_FAIL, msg("label.winBuild"), ok ? String(build) : `${build} ${msg("winBuildConpty")}`));
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
await deps.loadNodePty();
|
|
509
|
+
lines.push(row(STATE_OK, msg("label.nodePty"), msg("nodePty.loaded")));
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
lines.push(row(STATE_FAIL, msg("label.nodePty"), shortError(error)));
|
|
513
|
+
lines.push(detail(msg("nodePty.fix", { fix: getPlatformPtyFix(deps.platform) })));
|
|
514
|
+
}
|
|
515
|
+
appendCliLines(lines, doctorOptions, deps);
|
|
516
|
+
section(lines, msg("section.data"));
|
|
517
|
+
const dataDir = deps.resolveDataDir();
|
|
518
|
+
const dataDirExists = deps.exists(dataDir);
|
|
519
|
+
lines.push(row(dataDirExists ? STATE_OK : STATE_FAIL, msg("label.dataDir"), dataDirExists ? dataDir : `${dataDir} ${msg("dataDirMissing")}`));
|
|
520
|
+
const credFile = path.join(dataDir, "account.json");
|
|
521
|
+
const credExists = deps.exists(credFile);
|
|
522
|
+
lines.push(row(credExists ? STATE_OK : STATE_FAIL, msg("label.credentials"), credExists ? msg("credentialsFound") : msg("credentialsMissing")));
|
|
523
|
+
section(lines, msg("section.runtime"));
|
|
524
|
+
lines.push(fieldRow(msg("label.mode"), formatMode(doctorOptions.mode)));
|
|
525
|
+
lines.push(fieldRow(msg("label.cwd"), doctorOptions.cwd));
|
|
526
|
+
lines.push(fieldRow(msg("label.adapter"), doctorOptions.adapter ?? msg("notSpecified")));
|
|
527
|
+
lines.push("");
|
|
528
|
+
await appendDaemonLines(lines, doctorOptions, dataDir, deps);
|
|
529
|
+
const lock = appendBridgeLockLines(lines, doctorOptions, dataDir, deps);
|
|
530
|
+
await appendWorkspaceEndpointLines(lines, doctorOptions, lock, deps);
|
|
531
|
+
lines.push("");
|
|
532
|
+
return lines;
|
|
533
|
+
}
|
|
534
|
+
export async function runDoctorCheck(argv = process.argv.slice(2), options = {}) {
|
|
535
|
+
const lines = await buildDoctorReport({
|
|
536
|
+
argv,
|
|
537
|
+
mode: options.mode ?? "generic",
|
|
538
|
+
});
|
|
539
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
540
|
+
}
|