termdock 1.0.1 → 1.0.3
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/dist/client/assets/{abnfDiagram-VRR7QNED-Cf0p7Eu-.js → abnfDiagram-VRR7QNED-BVuFNukd.js} +1 -1
- package/dist/client/assets/{arc-yhaVxFBi.js → arc-JFOslCNs.js} +1 -1
- package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-DV-OSXY6.js → architectureDiagram-ZJ3FMSHR-B-TyEAOJ.js} +1 -1
- package/dist/client/assets/{blockDiagram-677ZJIJ3-ZFSHeVvK.js → blockDiagram-677ZJIJ3-5ycSMMTY.js} +1 -1
- package/dist/client/assets/{c4Diagram-LMCZKHZV-Z7p1zkZ5.js → c4Diagram-LMCZKHZV-BvXzrvb_.js} +1 -1
- package/dist/client/assets/channel-Cnn-8i2b.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-D03kywS-.js → chunk-2Q5K7J3B-00s3llQG.js} +1 -1
- package/dist/client/assets/{chunk-32BRIVSS-BPMnoBke.js → chunk-32BRIVSS-BJMhtTym.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-Ca--vT5Q.js → chunk-5VM5RSS4-DedB9Vaa.js} +1 -1
- package/dist/client/assets/{chunk-EX3LRPZG-Bc42nIJ5.js → chunk-EX3LRPZG-DWGG3NoJ.js} +1 -1
- package/dist/client/assets/{chunk-JWPE2WC7-N1WKBcc5.js → chunk-JWPE2WC7-D34JiLTf.js} +1 -1
- package/dist/client/assets/{chunk-MOJQB5TN-BDc-GV5_.js → chunk-MOJQB5TN-D6Ywmh0z.js} +1 -1
- package/dist/client/assets/{chunk-RYQCIY6F-BpXNrVrz.js → chunk-RYQCIY6F-BH6yURdq.js} +1 -1
- package/dist/client/assets/{chunk-V7JOEXUC-BTlki_tC.js → chunk-V7JOEXUC-DBSqOhi7.js} +1 -1
- package/dist/client/assets/{chunk-VR4S4FIN-BXnHZWrK.js → chunk-VR4S4FIN-DzIaqkfm.js} +1 -1
- package/dist/client/assets/{chunk-XXDRQBXY-zy3DIq5F.js → chunk-XXDRQBXY-9iVmQCNp.js} +1 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-DiAxD67S.js +1 -0
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-DiAxD67S.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-DcE2Tqcn.js → cose-bilkent-JH36ORCC-1Yqlhf0s.js} +1 -1
- package/dist/client/assets/{cynefin-VYW2F7L2-CirPmVbw.js → cynefin-VYW2F7L2-DIvwKU3S.js} +1 -1
- package/dist/client/assets/{cynefinDiagram-TSTJHNR4-742hkNFA.js → cynefinDiagram-TSTJHNR4-C_tvEhnO.js} +1 -1
- package/dist/client/assets/{dagre-VKFMJZFB-Zmky1sDU.js → dagre-VKFMJZFB-BW6zPwqv.js} +1 -1
- package/dist/client/assets/{diagram-FQU43EPY-BAVYDJIF.js → diagram-FQU43EPY-BPdiUHXh.js} +1 -1
- package/dist/client/assets/{diagram-G47NLZAW--BZg7BJ2.js → diagram-G47NLZAW-DcSplQyb.js} +1 -1
- package/dist/client/assets/{diagram-NH7WQ7WH-7QMiGNb8.js → diagram-NH7WQ7WH-DJfc5gP9.js} +1 -1
- package/dist/client/assets/{diagram-OA4YK3LP-DTz1wt7W.js → diagram-OA4YK3LP-BMhJGhOv.js} +1 -1
- package/dist/client/assets/{diagram-WEI45ONY-IhWlLIuv.js → diagram-WEI45ONY-BfvX5Q8Q.js} +1 -1
- package/dist/client/assets/{ebnfDiagram-CCIWWBDH-CvudDgv1.js → ebnfDiagram-CCIWWBDH-CHXVkELN.js} +1 -1
- package/dist/client/assets/{erDiagram-Q63AITRT-BxxNWFA6.js → erDiagram-Q63AITRT-BsY8w4KG.js} +1 -1
- package/dist/client/assets/{flowDiagram-23GEKE2U-bDu_tMe8.js → flowDiagram-23GEKE2U-BkzZO16s.js} +1 -1
- package/dist/client/assets/{ganttDiagram-NO4QXBWP-B-EI33mG.js → ganttDiagram-NO4QXBWP-CYc_FSmb.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-DBjjTP9W.js → gitGraphDiagram-IHSO6WYX-DZVYJm6Q.js} +1 -1
- package/dist/client/assets/{index-Cv7q4nZR.css → index-B1I3x8rE.css} +1 -1
- package/dist/client/assets/{index-Bi0L0Km4.js → index-DIbF-DRk.js} +40 -40
- package/dist/client/assets/{infoDiagram-FWYZ7A6U-DGjFR5qD.js → infoDiagram-FWYZ7A6U-Dnv8kWO8.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-BHYp5v41.js → ishikawaDiagram-FXEZZL3T-2olMVSmL.js} +1 -1
- package/dist/client/assets/{journeyDiagram-5HDEW3XC-DKhoytdF.js → journeyDiagram-5HDEW3XC-B_GL15RA.js} +1 -1
- package/dist/client/assets/{kanban-definition-HUTT4EX6-ascD0XVf.js → kanban-definition-HUTT4EX6-B6_36kLK.js} +1 -1
- package/dist/client/assets/{linear-bkyU-Rlp.js → linear-CE3AYySp.js} +1 -1
- package/dist/client/assets/{mermaid.core-DAp1jSn7.js → mermaid.core-mNpsdXrC.js} +4 -4
- package/dist/client/assets/{mindmap-definition-LN4V7U3C-C2GC8yQl.js → mindmap-definition-LN4V7U3C-BRH7DD07.js} +1 -1
- package/dist/client/assets/{pegDiagram-2B236MQR-Z2ysn4Ju.js → pegDiagram-2B236MQR-CPWaVwbR.js} +1 -1
- package/dist/client/assets/{pieDiagram-ENE6RG2P-B1ae8akt.js → pieDiagram-ENE6RG2P-Dg488vOr.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-CsjSqwMk.js → quadrantDiagram-ABIIQ3AL-X9TiKVp4.js} +1 -1
- package/dist/client/assets/{railroadDiagram-RFXS5EU6-MFxHHceB.js → railroadDiagram-RFXS5EU6-Dm_BeOAW.js} +1 -1
- package/dist/client/assets/{requirementDiagram-TGXJPOKE-2uwCBtHV.js → requirementDiagram-TGXJPOKE-lkU41AKk.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-HTMAVEWB-D9kfU1GN.js → sankeyDiagram-HTMAVEWB-CltVgAob.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-DpVoVgLp.js → sequenceDiagram-DBY2YBRQ-D65aOt87.js} +1 -1
- package/dist/client/assets/{sizeCapture-X5ZJPWSS-DaOWqq1e.js → sizeCapture-X5ZJPWSS-B_zcJ4qw.js} +1 -1
- package/dist/client/assets/{stateDiagram-2N3HPSRC-D2VX0Qzz.js → stateDiagram-2N3HPSRC-DMF21ttG.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-CUgSOIEq.js +1 -0
- package/dist/client/assets/{swimlanes-5IMT3BWC-DSaEdpIw.js → swimlanes-5IMT3BWC-t01s2riN.js} +2 -2
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-DHbd0hST.js +8 -0
- package/dist/client/assets/{timeline-definition-FHXFAJF6-DPNbaxGI.js → timeline-definition-FHXFAJF6-DTvM_hB8.js} +1 -1
- package/dist/client/assets/{vennDiagram-L72KCM5P-UKbbBFUs.js → vennDiagram-L72KCM5P-BTXCVT1w.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-EHGQE667-JUcWelHZ.js → wardleyDiagram-EHGQE667-DASx33Ug.js} +1 -1
- package/dist/client/assets/{xychartDiagram-FW5EYKEG-BCEj_jKY.js → xychartDiagram-FW5EYKEG-C6ZvW88N.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/sw.js +1 -1
- package/dist/server/cli.js +14 -5
- package/dist/server/config.js +50 -4
- package/dist/server/routes/terminal.js +34 -15
- package/dist/server/utils/bootCheck.js +299 -0
- package/package.json +5 -3
- package/scripts/inspect-diff-trace.mjs +148 -0
- package/scripts/inspect-fs-io.mjs +107 -0
- package/scripts/patch-termdock-font.py +134 -0
- package/scripts/postinstall.mjs +148 -0
- package/dist/client/assets/channel-OEjXE69P.js +0 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-BqnMVXbr.js +0 -1
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-BqnMVXbr.js +0 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-Dm3WkZpy.js +0 -1
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-DrnizR1A.js +0 -8
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Termdock 启动自检 — 确保原生模块和系统依赖在启动时可用
|
|
3
|
+
*
|
|
4
|
+
* 设计目标:覆盖 npm 全局安装不跑 postinstall 的缺口。
|
|
5
|
+
* 每次 CLI 启动时检查,通过后写 marker 避免重复检查。
|
|
6
|
+
*
|
|
7
|
+
* 检查项目:
|
|
8
|
+
* 1. node-pty : 实际 spawn 测试 → 失败则从源码重编译
|
|
9
|
+
* 2. tmux : 检测是否安装 → 缺失则尝试自动安装
|
|
10
|
+
*
|
|
11
|
+
* —— 如何新增检查项 ——
|
|
12
|
+
* 在 BootChecks 接口加字段,在 runBootChecks() 加 step,
|
|
13
|
+
* 在 formatBootCheckReport() 加对应的输出行。
|
|
14
|
+
*/
|
|
15
|
+
import { execSync, spawnSync } from 'child_process';
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import os from 'os';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import { createRequire } from 'module';
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Helpers
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
const MARKER_FILE = path.join(os.homedir(), '.termdock', '.boot-check-ok');
|
|
24
|
+
function which(bin) {
|
|
25
|
+
// 先查 PATH
|
|
26
|
+
try {
|
|
27
|
+
const result = execSync(`command -v ${bin} 2>/dev/null`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
28
|
+
if (result)
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
catch { /* not in PATH */ }
|
|
32
|
+
// macOS non-interactive SSH 的 PATH 不包含 Homebrew 路径,手动检查
|
|
33
|
+
const extraPaths = [
|
|
34
|
+
'/opt/homebrew/bin',
|
|
35
|
+
'/usr/local/bin',
|
|
36
|
+
'/opt/local/bin', // MacPorts
|
|
37
|
+
];
|
|
38
|
+
for (const dir of extraPaths) {
|
|
39
|
+
const fullPath = path.join(dir, bin);
|
|
40
|
+
try {
|
|
41
|
+
fs.accessSync(fullPath, fs.constants.X_OK);
|
|
42
|
+
return fullPath;
|
|
43
|
+
}
|
|
44
|
+
catch { /* not here */ }
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function runQuiet(cmd, timeoutMs = 30_000) {
|
|
49
|
+
try {
|
|
50
|
+
const r = spawnSync(cmd, [], {
|
|
51
|
+
shell: true,
|
|
52
|
+
timeout: timeoutMs,
|
|
53
|
+
encoding: 'utf-8',
|
|
54
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
55
|
+
});
|
|
56
|
+
return { ok: r.status === 0, stdout: r.stdout, stderr: r.stderr };
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return { ok: false, stdout: '', stderr: '' };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// 1. node-pty 验证 + 重建
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
function checkNodePty() {
|
|
66
|
+
const result = { ok: false, rebuilt: false };
|
|
67
|
+
try {
|
|
68
|
+
const require_ = createRequire(import.meta.url);
|
|
69
|
+
let ptyPath;
|
|
70
|
+
try {
|
|
71
|
+
ptyPath = require_.resolve('node-pty/package.json').replace('/package.json', '');
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
result.error = 'node-pty not found in module resolution path';
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
const pty = require_(ptyPath);
|
|
78
|
+
// 实际 spawn 测试(不仅仅是 require,macOS ARM 预编译包可能 require 成功但 spawn 失败)
|
|
79
|
+
const proc = pty.spawn(process.env.SHELL || '/bin/bash', [], { name: 'xterm', cols: 80, rows: 24 });
|
|
80
|
+
proc.kill();
|
|
81
|
+
result.ok = true;
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
// require 失败或 spawn 失败 → 尝试重编译
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const require_ = createRequire(import.meta.url);
|
|
89
|
+
let ptyPath;
|
|
90
|
+
try {
|
|
91
|
+
ptyPath = require_.resolve('node-pty/package.json').replace('/package.json', '');
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
result.error = 'node-pty not found in module resolution path';
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
// 清除预编译二进制缓存
|
|
98
|
+
const prebuildsDir = path.join(ptyPath, 'prebuilds');
|
|
99
|
+
if (fs.existsSync(prebuildsDir)) {
|
|
100
|
+
fs.rmSync(prebuildsDir, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
const rebuild = runQuiet(`cd ${ptyPath} && npx --yes node-gyp rebuild`, 120_000);
|
|
103
|
+
if (rebuild.ok) {
|
|
104
|
+
// 重试 spawn
|
|
105
|
+
try {
|
|
106
|
+
const pty = require_(ptyPath);
|
|
107
|
+
const proc = pty.spawn(process.env.SHELL || '/bin/bash', [], { name: 'xterm', cols: 80, rows: 24 });
|
|
108
|
+
proc.kill();
|
|
109
|
+
result.ok = true;
|
|
110
|
+
result.rebuilt = true;
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
result.error = 'node-pty 重编译成功但 spawn 仍然失败';
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
result.error = `node-pty 重编译失败: ${rebuild.stderr.slice(0, 200)}`;
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
result.error = `node-pty 未找到或无法初始化: ${String(e).slice(0, 200)}`;
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// 2. tmux 检测 + 自动安装
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
function checkTmux() {
|
|
130
|
+
const result = { ok: false, installed: false };
|
|
131
|
+
// 先检查是否已安装
|
|
132
|
+
const tmuxPath = which('tmux');
|
|
133
|
+
if (tmuxPath) {
|
|
134
|
+
try {
|
|
135
|
+
result.version = execSync('tmux -V', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
136
|
+
}
|
|
137
|
+
catch { /* ignore */ }
|
|
138
|
+
result.ok = true;
|
|
139
|
+
result.installed = false; // 没有新安装
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
// —— 尝试自动安装 ——
|
|
143
|
+
const platform = process.platform;
|
|
144
|
+
if (platform === 'darwin') {
|
|
145
|
+
// macOS: 尝试多个包管理器
|
|
146
|
+
const brew = which('brew');
|
|
147
|
+
if (brew) {
|
|
148
|
+
const r = runQuiet(`${brew} install tmux 2>&1`, 120_000);
|
|
149
|
+
if (r.ok || which('tmux')) {
|
|
150
|
+
try {
|
|
151
|
+
result.version = execSync('tmux -V', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
152
|
+
}
|
|
153
|
+
catch { /* ignore */ }
|
|
154
|
+
result.ok = true;
|
|
155
|
+
result.installed = true;
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const port = which('port'); // MacPorts
|
|
160
|
+
if (port) {
|
|
161
|
+
const r = runQuiet(`${port} install tmux 2>&1`, 120_000);
|
|
162
|
+
if (r.ok || which('tmux')) {
|
|
163
|
+
try {
|
|
164
|
+
result.version = execSync('tmux -V', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
165
|
+
}
|
|
166
|
+
catch { /* ignore */ }
|
|
167
|
+
result.ok = true;
|
|
168
|
+
result.installed = true;
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const nixEnv = which('nix-env');
|
|
173
|
+
if (nixEnv) {
|
|
174
|
+
const r = runQuiet(`${nixEnv} -iA nixpkgs.tmux 2>&1`, 120_000);
|
|
175
|
+
if (r.ok || which('tmux')) {
|
|
176
|
+
try {
|
|
177
|
+
result.version = execSync('tmux -V', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
178
|
+
}
|
|
179
|
+
catch { /* ignore */ }
|
|
180
|
+
result.ok = true;
|
|
181
|
+
result.installed = true;
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
result.error = 'tmux 未安装且未找到包管理器 (brew/port/nix)。请安装 Homebrew: https://brew.sh';
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
if (platform === 'linux') {
|
|
189
|
+
// 尝试多种 Linux 包管理器
|
|
190
|
+
const managers = [
|
|
191
|
+
{ bin: 'apt-get', cmd: 'apt-get install -y tmux' },
|
|
192
|
+
{ bin: 'dnf', cmd: 'dnf install -y tmux' },
|
|
193
|
+
{ bin: 'yum', cmd: 'yum install -y tmux' },
|
|
194
|
+
{ bin: 'apk', cmd: 'apk add tmux' },
|
|
195
|
+
{ bin: 'pacman', cmd: 'pacman -S --noconfirm tmux' },
|
|
196
|
+
{ bin: 'zypper', cmd: 'zypper install -y tmux' },
|
|
197
|
+
];
|
|
198
|
+
for (const m of managers) {
|
|
199
|
+
if (!which(m.bin))
|
|
200
|
+
continue;
|
|
201
|
+
// 需要 root 权限
|
|
202
|
+
const r = runQuiet(`sudo ${m.cmd} 2>&1`, 120_000);
|
|
203
|
+
if (r.ok || which('tmux')) {
|
|
204
|
+
try {
|
|
205
|
+
result.version = execSync('tmux -V', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
206
|
+
}
|
|
207
|
+
catch { /* ignore */ }
|
|
208
|
+
result.ok = true;
|
|
209
|
+
result.installed = true;
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
// 也试一下不用 sudo
|
|
213
|
+
const r2 = runQuiet(`${m.cmd} 2>&1`, 120_000);
|
|
214
|
+
if (r2.ok || which('tmux')) {
|
|
215
|
+
try {
|
|
216
|
+
result.version = execSync('tmux -V', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
217
|
+
}
|
|
218
|
+
catch { /* ignore */ }
|
|
219
|
+
result.ok = true;
|
|
220
|
+
result.installed = true;
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
result.error = 'tmux 未安装。请用系统包管理器安装,例如: sudo apt install tmux';
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
// 其他平台
|
|
228
|
+
result.error = `不支持在 ${platform} 上自动安装 tmux。请手动安装。`;
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// 主入口
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
let _cachedResult = null;
|
|
235
|
+
export function runBootChecks() {
|
|
236
|
+
if (_cachedResult)
|
|
237
|
+
return _cachedResult;
|
|
238
|
+
// 如果 marker 存在且有效,读取缓存的检查结果
|
|
239
|
+
if (fs.existsSync(MARKER_FILE)) {
|
|
240
|
+
try {
|
|
241
|
+
const cached = JSON.parse(fs.readFileSync(MARKER_FILE, 'utf-8'));
|
|
242
|
+
if (cached.nodePty?.ok && cached.tmux?.ok) {
|
|
243
|
+
_cachedResult = {
|
|
244
|
+
nodePty: cached.nodePty,
|
|
245
|
+
tmux: cached.tmux,
|
|
246
|
+
};
|
|
247
|
+
return _cachedResult;
|
|
248
|
+
}
|
|
249
|
+
// marker 存在但状态不完整(例如 postinstall 部分失败)→ 重新检查
|
|
250
|
+
}
|
|
251
|
+
catch { /* marker 损坏 → 重新检查 */ }
|
|
252
|
+
}
|
|
253
|
+
const nodePty = checkNodePty();
|
|
254
|
+
const tmux = checkTmux();
|
|
255
|
+
_cachedResult = { nodePty, tmux };
|
|
256
|
+
// 全部通过则写 marker
|
|
257
|
+
if (nodePty.ok && tmux.ok) {
|
|
258
|
+
try {
|
|
259
|
+
fs.mkdirSync(path.dirname(MARKER_FILE), { recursive: true });
|
|
260
|
+
fs.writeFileSync(MARKER_FILE, JSON.stringify({ checkedAt: new Date().toISOString(), nodePty, tmux }), 'utf-8');
|
|
261
|
+
}
|
|
262
|
+
catch { /* 非关键 */ }
|
|
263
|
+
}
|
|
264
|
+
return _cachedResult;
|
|
265
|
+
}
|
|
266
|
+
export function getBootChecks() {
|
|
267
|
+
return _cachedResult ?? runBootChecks();
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* 格式化启动检查报告,用于 CLI 启动时显示给用户。
|
|
271
|
+
*/
|
|
272
|
+
export function formatBootCheckReport(checks, enableColor) {
|
|
273
|
+
const lines = [];
|
|
274
|
+
const g = (s) => enableColor ? `\x1b[32m${s}\x1b[0m` : s;
|
|
275
|
+
const y = (s) => enableColor ? `\x1b[33m${s}\x1b[0m` : s;
|
|
276
|
+
const r = (s) => enableColor ? `\x1b[31m${s}\x1b[0m` : s;
|
|
277
|
+
const dim = (s) => enableColor ? `\x1b[2m${s}\x1b[0m` : s;
|
|
278
|
+
// node-pty
|
|
279
|
+
if (checks.nodePty.ok) {
|
|
280
|
+
const tag = checks.nodePty.rebuilt ? ' (rebuilt)' : '';
|
|
281
|
+
lines.push(` ${g('✓')} node-pty${dim(tag)}`);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
lines.push(` ${r('✗')} node-pty — ${checks.nodePty.error || 'unknown'}`);
|
|
285
|
+
}
|
|
286
|
+
// tmux
|
|
287
|
+
if (checks.tmux.ok) {
|
|
288
|
+
const detail = checks.tmux.installed
|
|
289
|
+
? ' (just installed)'
|
|
290
|
+
: checks.tmux.version
|
|
291
|
+
? ` ${dim(checks.tmux.version)}`
|
|
292
|
+
: '';
|
|
293
|
+
lines.push(` ${g('✓')} tmux${detail}`);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
lines.push(` ${y('!')} tmux — ${checks.tmux.error || 'not found'} (shell mode will be used)`);
|
|
297
|
+
}
|
|
298
|
+
return lines;
|
|
299
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "termdock",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "A complete web-based terminal application with modern UI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
"dist",
|
|
15
15
|
"README.md",
|
|
16
|
-
"LICENSE"
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"scripts"
|
|
17
18
|
],
|
|
18
19
|
"scripts": {
|
|
19
20
|
"clean": "rimraf dist",
|
|
@@ -31,7 +32,8 @@
|
|
|
31
32
|
"trace:fsio": "node scripts/inspect-fs-io.mjs",
|
|
32
33
|
"test": "vitest --passWithNoTests",
|
|
33
34
|
"test:ui": "vitest --ui",
|
|
34
|
-
"prepublishOnly": "npm run build"
|
|
35
|
+
"prepublishOnly": "npm run build",
|
|
36
|
+
"postinstall": "node scripts/postinstall.mjs"
|
|
35
37
|
},
|
|
36
38
|
"dependencies": {
|
|
37
39
|
"@dnd-kit/core": "^6.3.1",
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
const logPath = path.join(os.homedir(), '.termdock', 'diff-trace.log');
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const query = args.find((arg) => !arg.startsWith('--')) ?? '';
|
|
9
|
+
const sinceMinutesArg = args.find((arg) => arg.startsWith('--since-min='));
|
|
10
|
+
const sinceMinutes = sinceMinutesArg ? Number(sinceMinutesArg.split('=')[1]) : 30;
|
|
11
|
+
const sinceTs = Number.isFinite(sinceMinutes) && sinceMinutes > 0
|
|
12
|
+
? Date.now() - sinceMinutes * 60_000
|
|
13
|
+
: 0;
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(logPath)) {
|
|
16
|
+
console.error(`No diff trace log found at ${logPath}`);
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const rawLines = fs.readFileSync(logPath, 'utf8').trimEnd().split('\n').filter(Boolean);
|
|
21
|
+
const records = [];
|
|
22
|
+
for (const line of rawLines) {
|
|
23
|
+
try {
|
|
24
|
+
const item = JSON.parse(line);
|
|
25
|
+
const ts = Date.parse(item.ts);
|
|
26
|
+
if (sinceTs && Number.isFinite(ts) && ts < sinceTs) continue;
|
|
27
|
+
const haystack = [
|
|
28
|
+
item.traceId,
|
|
29
|
+
item.interactionId,
|
|
30
|
+
item.filePath,
|
|
31
|
+
item.requestPath,
|
|
32
|
+
item.cwd,
|
|
33
|
+
item.gitRoot,
|
|
34
|
+
item.requestSlotId,
|
|
35
|
+
item.previousRequestId,
|
|
36
|
+
item.code,
|
|
37
|
+
item.error,
|
|
38
|
+
item.event,
|
|
39
|
+
item.source,
|
|
40
|
+
JSON.stringify(item.data ?? {}),
|
|
41
|
+
].filter(Boolean).join(' ');
|
|
42
|
+
if (query && !haystack.includes(query)) continue;
|
|
43
|
+
records.push({ ...item, _time: Number.isFinite(ts) ? ts : 0 });
|
|
44
|
+
} catch {
|
|
45
|
+
// Ignore malformed old lines.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (records.length === 0) {
|
|
50
|
+
console.log(`No diff trace records matched "${query}" in the last ${sinceMinutes} minute(s).`);
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
records.sort((a, b) => a._time - b._time);
|
|
55
|
+
|
|
56
|
+
const groups = new Map();
|
|
57
|
+
for (const record of records) {
|
|
58
|
+
const key = record.interactionId || record.traceId || `unlinked:${record.filePath ?? record.requestPath ?? 'unknown'}`;
|
|
59
|
+
const group = groups.get(key) ?? [];
|
|
60
|
+
group.push(record);
|
|
61
|
+
groups.set(key, group);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const expectedOrder = [
|
|
65
|
+
'global_change_row_event',
|
|
66
|
+
'change_row_pointer_down',
|
|
67
|
+
'change_row_click_handler_enter',
|
|
68
|
+
'change_row_click',
|
|
69
|
+
'select_diff_file',
|
|
70
|
+
'select_diff_file_after',
|
|
71
|
+
'selected_file_state',
|
|
72
|
+
'start',
|
|
73
|
+
'effect_start',
|
|
74
|
+
'visible_start',
|
|
75
|
+
'request_start',
|
|
76
|
+
'slot-cancel-previous',
|
|
77
|
+
'request-start',
|
|
78
|
+
'git-root-resolved',
|
|
79
|
+
'response-ok',
|
|
80
|
+
'response-exception',
|
|
81
|
+
'response_headers',
|
|
82
|
+
'response_body_start',
|
|
83
|
+
'response_body',
|
|
84
|
+
'visible_result',
|
|
85
|
+
'state_set_result',
|
|
86
|
+
'end',
|
|
87
|
+
'load_text_finally',
|
|
88
|
+
'parse_done',
|
|
89
|
+
'tokenize_done',
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
function hasEvent(group, event) {
|
|
93
|
+
return group.some((item) => item.event === event);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isCancelledByNewerRequest(group) {
|
|
97
|
+
return group.some((item) => (
|
|
98
|
+
item.code === 'GIT_DIFF_CANCELLED'
|
|
99
|
+
|| item.data?.code === 'GIT_DIFF_CANCELLED'
|
|
100
|
+
|| item.error === 'Git diff request was cancelled because a newer file was selected.'
|
|
101
|
+
));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function inferStatus(group) {
|
|
105
|
+
const last = group[group.length - 1];
|
|
106
|
+
if (hasEvent(group, 'state_set_result') && hasEvent(group, 'end')) return 'OK';
|
|
107
|
+
if (isCancelledByNewerRequest(group)) return 'CANCELLED_BY_NEWER_REQUEST';
|
|
108
|
+
if (hasEvent(group, 'client_watchdog_timeout')) return 'CLIENT_WATCHDOG_TIMEOUT';
|
|
109
|
+
if (hasEvent(group, 'response_body_timeout')) return 'BODY_TIMEOUT';
|
|
110
|
+
if (hasEvent(group, 'response-exception') || hasEvent(group, 'response-error')) return 'SERVER_ERROR';
|
|
111
|
+
if (hasEvent(group, 'response-ok') && !hasEvent(group, 'response_headers')) return 'CLIENT_DID_NOT_OBSERVE_RESPONSE';
|
|
112
|
+
if (hasEvent(group, 'response_headers') && !hasEvent(group, 'response_body')) return 'CLIENT_BODY_NOT_FINISHED';
|
|
113
|
+
if (hasEvent(group, 'request_start') && !hasEvent(group, 'request-start')) return 'REQUEST_NOT_REACHED_SERVER';
|
|
114
|
+
if (hasEvent(group, 'select_diff_file') && !hasEvent(group, 'effect_start')) return 'VIEWER_NOT_STARTED';
|
|
115
|
+
return `INCOMPLETE_AFTER_${last?.source ?? 'unknown'}:${last?.event ?? 'unknown'}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function shortPath(item) {
|
|
119
|
+
const value = item.filePath ?? item.requestPath ?? item.data?.filePath ?? item.data?.requestedPath ?? '';
|
|
120
|
+
if (!value || typeof value !== 'string') return '';
|
|
121
|
+
const parts = value.split('/');
|
|
122
|
+
return parts.length > 4 ? `.../${parts.slice(-4).join('/')}` : value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const [key, group] of groups) {
|
|
126
|
+
const first = group[0];
|
|
127
|
+
const last = group[group.length - 1];
|
|
128
|
+
const status = inferStatus(group);
|
|
129
|
+
console.log(`\n=== ${status} ${key} ===`);
|
|
130
|
+
console.log(`time: ${first.ts} -> ${last.ts}`);
|
|
131
|
+
console.log(`file: ${shortPath(group.find((item) => item.filePath || item.requestPath) ?? first)}`);
|
|
132
|
+
const present = expectedOrder.filter((event) => hasEvent(group, event));
|
|
133
|
+
const missingAfterFirstGap = expectedOrder.filter((event) => !hasEvent(group, event));
|
|
134
|
+
console.log(`seen: ${present.join(' -> ') || '(none)'}`);
|
|
135
|
+
if (status !== 'OK' && status !== 'CANCELLED_BY_NEWER_REQUEST') console.log(`missing: ${missingAfterFirstGap.slice(0, 8).join(', ')}`);
|
|
136
|
+
for (const item of group) {
|
|
137
|
+
const rel = `${String(item.source ?? '').padEnd(20)} ${String(item.event ?? '').padEnd(26)}`;
|
|
138
|
+
const duration = item.durationMs !== undefined ? ` duration=${item.durationMs}ms` : '';
|
|
139
|
+
const bytes = item.bytes !== undefined ? ` bytes=${item.bytes}` : '';
|
|
140
|
+
const trace = item.traceId ? ` trace=${item.traceId}` : '';
|
|
141
|
+
const interaction = item.interactionId ? ` interaction=${item.interactionId}` : '';
|
|
142
|
+
const slot = item.requestSlotId ? ` slot=${item.requestSlotId}` : '';
|
|
143
|
+
const previous = item.previousRequestId !== undefined ? ` previous=${item.previousRequestId}` : '';
|
|
144
|
+
const code = item.code ? ` code=${item.code}` : '';
|
|
145
|
+
const err = item.error ? ` error=${JSON.stringify(item.error)}` : '';
|
|
146
|
+
console.log(` ${item.ts} ${rel}${duration}${bytes}${trace}${interaction}${slot}${previous}${code}${err}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
const logPath = path.join(os.homedir(), '.termdock', 'fs-io.log');
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const query = args.find((arg) => !arg.startsWith('--')) ?? '';
|
|
9
|
+
const sinceMinutesArg = args.find((arg) => arg.startsWith('--since-min='));
|
|
10
|
+
const slowMsArg = args.find((arg) => arg.startsWith('--slow-ms='));
|
|
11
|
+
const sinceMinutes = sinceMinutesArg ? Number(sinceMinutesArg.split('=')[1]) : 30;
|
|
12
|
+
const slowMs = slowMsArg ? Number(slowMsArg.split('=')[1]) : 500;
|
|
13
|
+
const sinceTs = Number.isFinite(sinceMinutes) && sinceMinutes > 0
|
|
14
|
+
? Date.now() - sinceMinutes * 60_000
|
|
15
|
+
: 0;
|
|
16
|
+
|
|
17
|
+
if (!fs.existsSync(logPath)) {
|
|
18
|
+
console.error(`No fs-io log found at ${logPath}`);
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const records = [];
|
|
23
|
+
for (const line of fs.readFileSync(logPath, 'utf8').trimEnd().split('\n').filter(Boolean)) {
|
|
24
|
+
try {
|
|
25
|
+
const item = JSON.parse(line);
|
|
26
|
+
const ts = Date.parse(item.ts);
|
|
27
|
+
if (sinceTs && Number.isFinite(ts) && ts < sinceTs) continue;
|
|
28
|
+
const haystack = [
|
|
29
|
+
item.op,
|
|
30
|
+
item.action,
|
|
31
|
+
item.event,
|
|
32
|
+
item.status,
|
|
33
|
+
item.path,
|
|
34
|
+
item.cwd,
|
|
35
|
+
item.repoRoot,
|
|
36
|
+
item.code,
|
|
37
|
+
item.error,
|
|
38
|
+
item.requestSlotId,
|
|
39
|
+
item.previousRequestId,
|
|
40
|
+
].filter(Boolean).join(' ');
|
|
41
|
+
if (query && !haystack.includes(query)) continue;
|
|
42
|
+
records.push({ ...item, _time: Number.isFinite(ts) ? ts : 0 });
|
|
43
|
+
} catch {
|
|
44
|
+
// Ignore malformed lines.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (records.length === 0) {
|
|
49
|
+
console.log(`No fs-io records matched "${query}" in the last ${sinceMinutes} minute(s).`);
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
records.sort((a, b) => a._time - b._time);
|
|
54
|
+
|
|
55
|
+
function short(value) {
|
|
56
|
+
if (!value || typeof value !== 'string') return '';
|
|
57
|
+
const home = os.homedir();
|
|
58
|
+
const normalized = value.startsWith(home) ? `~${value.slice(home.length)}` : value;
|
|
59
|
+
return normalized.length > 120 ? `...${normalized.slice(-117)}` : normalized;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function important(item) {
|
|
63
|
+
const duration = typeof item.durationMs === 'number' ? item.durationMs : 0;
|
|
64
|
+
return (
|
|
65
|
+
item.event === 'slot-cancel-previous'
|
|
66
|
+
|| item.event === 'git-child-abort-kill'
|
|
67
|
+
|| item.event === 'git-child-timeout-kill'
|
|
68
|
+
|| item.event === 'git-child-byte-limit-kill'
|
|
69
|
+
|| item.status === 'error'
|
|
70
|
+
|| item.code
|
|
71
|
+
|| item.error
|
|
72
|
+
|| duration >= slowMs
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const byOp = new Map();
|
|
77
|
+
for (const item of records) {
|
|
78
|
+
if (!item.op || item.event) continue;
|
|
79
|
+
const stat = byOp.get(item.op) ?? { count: 0, errors: 0, slow: 0, maxMs: 0 };
|
|
80
|
+
stat.count += 1;
|
|
81
|
+
if (item.status === 'error' || item.code || item.error) stat.errors += 1;
|
|
82
|
+
if ((item.durationMs ?? 0) >= slowMs) stat.slow += 1;
|
|
83
|
+
stat.maxMs = Math.max(stat.maxMs, item.durationMs ?? 0);
|
|
84
|
+
byOp.set(item.op, stat);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(`fs-io records: ${records.length}; since=${sinceMinutes}m; slow>=${slowMs}ms`);
|
|
88
|
+
if (byOp.size > 0) {
|
|
89
|
+
console.log('\nSummary by op:');
|
|
90
|
+
for (const [op, stat] of [...byOp.entries()].sort((a, b) => b[1].maxMs - a[1].maxMs)) {
|
|
91
|
+
console.log(` ${op.padEnd(14)} count=${String(stat.count).padStart(3)} slow=${String(stat.slow).padStart(3)} errors=${String(stat.errors).padStart(3)} max=${Math.round(stat.maxMs)}ms`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const notable = records.filter(important);
|
|
96
|
+
console.log(`\nNotable events: ${notable.length}`);
|
|
97
|
+
for (const item of notable.slice(-200)) {
|
|
98
|
+
const duration = item.durationMs !== undefined ? ` duration=${Math.round(item.durationMs)}ms` : '';
|
|
99
|
+
const count = item.count !== undefined ? ` count=${item.count}` : '';
|
|
100
|
+
const total = item.total !== undefined ? ` total=${item.total}` : '';
|
|
101
|
+
const slot = item.requestSlotId ? ` slot=${item.requestSlotId}` : '';
|
|
102
|
+
const previous = item.previousRequestId !== undefined ? ` previous=${item.previousRequestId}` : '';
|
|
103
|
+
const code = item.code ? ` code=${item.code}` : '';
|
|
104
|
+
const error = item.error ? ` error=${JSON.stringify(item.error)}` : '';
|
|
105
|
+
const target = item.path ? ` path=${short(item.path)}` : item.cwd ? ` cwd=${short(item.cwd)}` : '';
|
|
106
|
+
console.log(`${item.ts} ${String(item.op ?? '').padEnd(14)} ${String(item.event ?? item.status ?? '').padEnd(24)}${duration}${count}${total}${slot}${previous}${code}${error}${target}`);
|
|
107
|
+
}
|