termdock 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/client/assets/{abnfDiagram-VRR7QNED-BjNCcQvm.js → abnfDiagram-VRR7QNED-DfSPmfvk.js} +1 -1
  2. package/dist/client/assets/{arc-xcCAKMnT.js → arc-Dkic2Ys2.js} +1 -1
  3. package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-ijtAKPsW.js → architectureDiagram-ZJ3FMSHR-OIeaN9c0.js} +1 -1
  4. package/dist/client/assets/{blockDiagram-677ZJIJ3-GVIbuo6R.js → blockDiagram-677ZJIJ3-B1Li-8o4.js} +1 -1
  5. package/dist/client/assets/{c4Diagram-LMCZKHZV-B3IbU0ga.js → c4Diagram-LMCZKHZV-DxZP1HhQ.js} +1 -1
  6. package/dist/client/assets/channel-hUX0TNF1.js +1 -0
  7. package/dist/client/assets/{chunk-2Q5K7J3B-DEwVqhvo.js → chunk-2Q5K7J3B-siB88l2n.js} +1 -1
  8. package/dist/client/assets/{chunk-32BRIVSS-CGNuIJuG.js → chunk-32BRIVSS-DOl2RLC0.js} +1 -1
  9. package/dist/client/assets/{chunk-5VM5RSS4-DliA2ku3.js → chunk-5VM5RSS4-X7x0ygAw.js} +1 -1
  10. package/dist/client/assets/{chunk-EX3LRPZG-DzFXj1M3.js → chunk-EX3LRPZG-CildsCwC.js} +1 -1
  11. package/dist/client/assets/{chunk-JWPE2WC7-h3s-pxcn.js → chunk-JWPE2WC7-B4OIalg-.js} +1 -1
  12. package/dist/client/assets/{chunk-MOJQB5TN-CQST9jsX.js → chunk-MOJQB5TN-kVtTpE1Z.js} +1 -1
  13. package/dist/client/assets/{chunk-RYQCIY6F-CXjnUh8k.js → chunk-RYQCIY6F-j0Rf1WIb.js} +1 -1
  14. package/dist/client/assets/{chunk-V7JOEXUC-CI7QyRAs.js → chunk-V7JOEXUC-DvKHUUY0.js} +1 -1
  15. package/dist/client/assets/{chunk-VR4S4FIN-DYQ8EokS.js → chunk-VR4S4FIN-C2SkkG6S.js} +1 -1
  16. package/dist/client/assets/{chunk-XXDRQBXY-BM9jxTTb.js → chunk-XXDRQBXY-Bhx4eEPh.js} +1 -1
  17. package/dist/client/assets/classDiagram-OUVF2IWQ-BGrRzpPf.js +1 -0
  18. package/dist/client/assets/classDiagram-v2-EOCWNBFH-BGrRzpPf.js +1 -0
  19. package/dist/client/assets/{cose-bilkent-JH36ORCC-C4nYmp4I.js → cose-bilkent-JH36ORCC-B9__o5mM.js} +1 -1
  20. package/dist/client/assets/{cynefin-VYW2F7L2-DnlhMeVt.js → cynefin-VYW2F7L2-Cgq3d5IK.js} +1 -1
  21. package/dist/client/assets/{cynefinDiagram-TSTJHNR4-DJuHDI6A.js → cynefinDiagram-TSTJHNR4-CW_8isSR.js} +1 -1
  22. package/dist/client/assets/{dagre-VKFMJZFB-CsBEz_S1.js → dagre-VKFMJZFB-CiSHPpPo.js} +1 -1
  23. package/dist/client/assets/{diagram-FQU43EPY-BDNTGROP.js → diagram-FQU43EPY-DQLYZFuO.js} +1 -1
  24. package/dist/client/assets/{diagram-G47NLZAW-CJJwi6PP.js → diagram-G47NLZAW-CmBlOweN.js} +1 -1
  25. package/dist/client/assets/{diagram-NH7WQ7WH-CXJWffic.js → diagram-NH7WQ7WH-CI-QXvQ_.js} +1 -1
  26. package/dist/client/assets/{diagram-OA4YK3LP-wOtL7NBA.js → diagram-OA4YK3LP-HNHm_DIG.js} +1 -1
  27. package/dist/client/assets/{diagram-WEI45ONY-ClOt2G7I.js → diagram-WEI45ONY-B4ZZJeAH.js} +1 -1
  28. package/dist/client/assets/{ebnfDiagram-CCIWWBDH-DoXhiNjK.js → ebnfDiagram-CCIWWBDH-CRX39RKm.js} +1 -1
  29. package/dist/client/assets/{erDiagram-Q63AITRT-DB5a-Aw4.js → erDiagram-Q63AITRT-CHPKoSH_.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-23GEKE2U-Cz9w-F5K.js → flowDiagram-23GEKE2U-BMJ4Jvkp.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-NO4QXBWP-CDykkj3o.js → ganttDiagram-NO4QXBWP-BICNG2H3.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-CNkfDkzA.js → gitGraphDiagram-IHSO6WYX-DfgV98Zm.js} +1 -1
  33. package/dist/client/assets/index-B_QcfsiC.css +1 -0
  34. package/dist/client/assets/{index-0d_16SRD.js → index-DgBC14IE.js} +84 -84
  35. package/dist/client/assets/{infoDiagram-FWYZ7A6U-CfrwrocT.js → infoDiagram-FWYZ7A6U-D-cG7V-w.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-C2AD_Ae2.js → ishikawaDiagram-FXEZZL3T-CxrdbW_4.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-5HDEW3XC-DzQvMG3N.js → journeyDiagram-5HDEW3XC-EqbAzJQn.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-HUTT4EX6-CUVhWmNF.js → kanban-definition-HUTT4EX6-DZCCEBAF.js} +1 -1
  39. package/dist/client/assets/{linear-D5qZ3q5c.js → linear-DDB46PlE.js} +1 -1
  40. package/dist/client/assets/{mermaid.core-D-e_wdPJ.js → mermaid.core-DDyxC1tS.js} +4 -4
  41. package/dist/client/assets/{mindmap-definition-LN4V7U3C-CDHLWnzb.js → mindmap-definition-LN4V7U3C-CLwUoR1i.js} +1 -1
  42. package/dist/client/assets/{pegDiagram-2B236MQR-9_bnKT8u.js → pegDiagram-2B236MQR-Cim4emaE.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-ENE6RG2P-gw-xIG1b.js → pieDiagram-ENE6RG2P-CN3i-enb.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-Cn_lstJf.js → quadrantDiagram-ABIIQ3AL-D2Vw3b23.js} +1 -1
  45. package/dist/client/assets/{railroadDiagram-RFXS5EU6-yZM8qXUp.js → railroadDiagram-RFXS5EU6-BDdYpxbY.js} +1 -1
  46. package/dist/client/assets/{requirementDiagram-TGXJPOKE-WQ1iDS_X.js → requirementDiagram-TGXJPOKE-cG9vRYjk.js} +1 -1
  47. package/dist/client/assets/{sankeyDiagram-HTMAVEWB-UVz3wsFF.js → sankeyDiagram-HTMAVEWB-Bc_96xns.js} +1 -1
  48. package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-BuJBBGcB.js → sequenceDiagram-DBY2YBRQ-BQ_sobMy.js} +1 -1
  49. package/dist/client/assets/{sizeCapture-X5ZJPWSS-CysR04La.js → sizeCapture-X5ZJPWSS-D1KJ3lWK.js} +1 -1
  50. package/dist/client/assets/{stateDiagram-2N3HPSRC-DYC8TYxP.js → stateDiagram-2N3HPSRC-UJo0IFk7.js} +1 -1
  51. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-DHtRqGqm.js +1 -0
  52. package/dist/client/assets/{swimlanes-5IMT3BWC-FT83sDlI.js → swimlanes-5IMT3BWC-Bics4VQN.js} +2 -2
  53. package/dist/client/assets/swimlanesDiagram-G3AALYLV-6KvsNCHz.js +8 -0
  54. package/dist/client/assets/{timeline-definition-FHXFAJF6-C-o8uRSU.js → timeline-definition-FHXFAJF6-DTui5o00.js} +1 -1
  55. package/dist/client/assets/{vennDiagram-L72KCM5P-Cw-g3-Li.js → vennDiagram-L72KCM5P-BdGmxwgP.js} +1 -1
  56. package/dist/client/assets/{wardleyDiagram-EHGQE667-B_JEirkb.js → wardleyDiagram-EHGQE667-_cnbAkx4.js} +1 -1
  57. package/dist/client/assets/{xychartDiagram-FW5EYKEG-B1LkJSxz.js → xychartDiagram-FW5EYKEG-Bc1koTvH.js} +1 -1
  58. package/dist/client/index.html +3 -2
  59. package/dist/client/sw.js +1 -1
  60. package/dist/client/workbox-9c191d2f.js +1 -0
  61. package/dist/server/cli.js +15 -6
  62. package/dist/server/config.js +48 -8
  63. package/dist/server/routes/onboarding.js +4 -4
  64. package/dist/server/routes/terminal.js +34 -23
  65. package/dist/server/utils/bootCheck.js +299 -0
  66. package/package.json +1 -1
  67. package/scripts/postinstall.mjs +102 -34
  68. package/dist/client/assets/channel-Fzeh-Sl6.js +0 -1
  69. package/dist/client/assets/classDiagram-OUVF2IWQ-DzF9pacK.js +0 -1
  70. package/dist/client/assets/classDiagram-v2-EOCWNBFH-DzF9pacK.js +0 -1
  71. package/dist/client/assets/index-Cv7q4nZR.css +0 -1
  72. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-Cru4nzfa.js +0 -1
  73. package/dist/client/assets/swimlanesDiagram-G3AALYLV-DUvRCWFX.js +0 -8
  74. package/dist/client/workbox-abeb32eb.js +0 -1
@@ -10,6 +10,7 @@ import { randomUUID } from 'crypto';
10
10
  import { promisify } from 'util';
11
11
  import { caffeinateManager } from '../utils/caffeinate.js';
12
12
  import { pathValidator } from '../utils/pathValidator.js';
13
+ import { TERMINAL, TMUX } from '../config.js';
13
14
  import { localAccessManager } from '../utils/localAccess.js';
14
15
  import { normalizeLocalAccessName } from '../utils/settings.js';
15
16
  import { getOnboardingServerUrl } from '../onboardingServer.js';
@@ -504,22 +505,16 @@ function schedulePersistToolbarPresets() {
504
505
  }
505
506
  void loadToolbarPresetsFromDisk();
506
507
  // ── end toolbar presets persistence ──
507
- // 开发模式下使用更激进的清理策略
508
- const isDevelopment = process.env.NODE_ENV === 'development';
509
- // idle 超时:手机锁屏后客户端 JS 被 OS 暂停 → 无法发心跳维持活动。
510
- // DEV 原本是 5 分钟,太短,手机后台一会儿就被清掉触发 auto-recreate;
511
- // 调到 30 分钟覆盖大多数日常使用。生产 6 小时保持不变。
512
- const TERMINAL_IDLE_TIMEOUT = parseInt(process.env.TERMINAL_IDLE_TIMEOUT || (isDevelopment ? '1800000' : '21600000'), 10);
513
- const CLEANUP_INTERVAL = isDevelopment ? 60 * 1000 : 5 * 60 * 1000;
514
- const RECONNECT_SCROLLBACK = parseInt(process.env.TERMINAL_RECONNECT_SCROLLBACK || '200', 10);
508
+ // 终端配置(从 config.ts 读环境变量,getter 保证 dotenv 加载后仍然正确)
509
+ // 以下常量从 config.ts getter 读取,不再直接读 process.env
510
+ const TERMINAL_IDLE_TIMEOUT = TERMINAL.idleTimeout;
511
+ const CLEANUP_INTERVAL = TERMINAL.cleanupInterval;
512
+ const RECONNECT_SCROLLBACK = TERMINAL.reconnectScrollback;
515
513
  const TMUX_POLL_INTERVAL = parseInt(process.env.TMUX_POLL_INTERVAL || '500', 10);
516
514
  const ACTIVE_PROGRAM_POLL_INTERVAL = parseInt(process.env.TERMINAL_ACTIVE_PROGRAM_POLL_INTERVAL || '1200', 10);
517
- const FLOW_CONTROL_PAUSE_LEASE_MS = parseInt(process.env.TERMINAL_FLOW_CONTROL_PAUSE_LEASE_MS || '15000', 10);
515
+ const FLOW_CONTROL_PAUSE_LEASE_MS = TERMINAL.flowControlPauseLeaseMs;
518
516
  const TMUX_DELIMITER = '\x1f';
519
- const parsedTmuxHistoryLimit = Number.parseInt(process.env.TERMDOCK_TMUX_HISTORY_LIMIT || '10000', 10);
520
- const TERMDOCK_TMUX_HISTORY_LIMIT = Number.isFinite(parsedTmuxHistoryLimit) && parsedTmuxHistoryLimit > 0
521
- ? parsedTmuxHistoryLimit
522
- : 10000;
517
+ const TERMDOCK_TMUX_HISTORY_LIMIT = TMUX.historyLimit;
523
518
  // 输出历史缓冲区(限制大小)
524
519
  const MAX_HISTORY_SIZE = 100 * 1024; // 100KB per session
525
520
  const sessionHistory = new Map();
@@ -999,7 +994,23 @@ async function openInventorySession(req, input) {
999
994
  };
1000
995
  }
1001
996
  function getTmuxBinary() {
1002
- return process.env.TMUX_BIN || 'tmux';
997
+ if (process.env.TMUX_BIN)
998
+ return process.env.TMUX_BIN;
999
+ // macOS 非交互式 SSH 的 PATH 不含 Homebrew,手动探测常见路径
1000
+ const candidates = [
1001
+ 'tmux', // PATH lookup
1002
+ '/opt/homebrew/bin/tmux',
1003
+ '/usr/local/bin/tmux',
1004
+ '/opt/local/bin/tmux', // MacPorts
1005
+ ];
1006
+ for (const candidate of candidates) {
1007
+ try {
1008
+ fs.accessSync(candidate, fs.constants.X_OK);
1009
+ return candidate;
1010
+ }
1011
+ catch { /* not here */ }
1012
+ }
1013
+ return 'tmux'; // fallback to PATH
1003
1014
  }
1004
1015
  async function runTmux(args) {
1005
1016
  const { stdout } = await execFileAsync(getTmuxBinary(), args, {
@@ -1846,21 +1857,21 @@ const BUILTIN_AGENT_RULES = [
1846
1857
  rules: [
1847
1858
  // 雪花符号(spinner) + 1-25 个中英文字母 + 3 个点(英文 ... 或中文 …)
1848
1859
  // 严格按"结构"匹配,不依赖具体词,排除 token 数字/输入回显
1849
- { pattern: '[✢✶✻✽][A-Za-z\\u4E00-\\u9FA5]{1,25}(?:\\.{3}|…)', status: 'running', color: '#4ade80', indicator: 'spinner', clearDelayMs: 700 },
1860
+ { pattern: '[✢✶✻✽][A-Za-z\\u4E00-\\u9FA5]{1,25}(?:\\.{3}|…)', status: 'running', color: '#879A39', indicator: 'spinner', clearDelayMs: 700 },
1850
1861
  ],
1851
1862
  },
1852
1863
  {
1853
1864
  program: 'opencode',
1854
1865
  rules: [
1855
- { pattern: 'thinking|working|generating', status: 'running', color: '#4ade80', indicator: 'pulse', clearDelayMs: 900 },
1856
- { pattern: '~ Asking questions', status: 'waiting', color: '#facc15', indicator: 'question', clearDelayMs: 10000 },
1866
+ { pattern: 'thinking|working|generating', status: 'running', color: '#879A39', indicator: 'pulse', clearDelayMs: 900 },
1867
+ { pattern: '~ Asking questions', status: 'waiting', color: '#D0A215', indicator: 'question', clearDelayMs: 10000 },
1857
1868
  ],
1858
1869
  },
1859
1870
  {
1860
1871
  program: 'coco',
1861
1872
  rules: [
1862
- { pattern: COCO_WAITING_RULE_PATTERN, status: 'waiting', color: '#facc15', indicator: 'question', clearDelayMs: 10000 },
1863
- { pattern: '[·✢❋❇✽] (thinking|working|generating)', status: 'running', color: '#4ade80', indicator: 'spinner', clearDelayMs: 700 },
1873
+ { pattern: COCO_WAITING_RULE_PATTERN, status: 'waiting', color: '#D0A215', indicator: 'question', clearDelayMs: 10000 },
1874
+ { pattern: '[·✢❋❇✽] (thinking|working|generating)', status: 'running', color: '#879A39', indicator: 'spinner', clearDelayMs: 700 },
1864
1875
  ],
1865
1876
  },
1866
1877
  ];
@@ -1882,21 +1893,21 @@ function migrateAgentRules(rules) {
1882
1893
  const deprecatedCocoConfirmRule = {
1883
1894
  pattern: 'confirm|approve|permission|continue\\?',
1884
1895
  status: 'waiting',
1885
- color: '#facc15',
1896
+ color: '#D0A215',
1886
1897
  indicator: 'question',
1887
1898
  clearDelayMs: 10000,
1888
1899
  };
1889
1900
  const deprecatedCocoBroadWaitingRule = {
1890
1901
  pattern: 'Tab/Arrow keys to navigate|Esc to|select ·|Coco 等待态采样|AskUserQuestion|User\'s answers',
1891
1902
  status: 'waiting',
1892
- color: '#facc15',
1903
+ color: '#D0A215',
1893
1904
  indicator: 'question',
1894
1905
  clearDelayMs: 10000,
1895
1906
  };
1896
1907
  const cocoWaitingRule = {
1897
1908
  pattern: COCO_WAITING_RULE_PATTERN,
1898
1909
  status: 'waiting',
1899
- color: '#facc15',
1910
+ color: '#D0A215',
1900
1911
  indicator: 'question',
1901
1912
  clearDelayMs: 10000,
1902
1913
  };
@@ -1907,7 +1918,7 @@ function migrateAgentRules(rules) {
1907
1918
  isSameAgentRule(config.rules[0], {
1908
1919
  pattern: 'Thinking|Generating|Working',
1909
1920
  status: 'running',
1910
- color: '#4ade80',
1921
+ color: '#879A39',
1911
1922
  indicator: 'pulse',
1912
1923
  clearDelayMs: 900,
1913
1924
  });
@@ -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.2",
3
+ "version": "1.0.4",
4
4
  "description": "A complete web-based terminal application with modern UI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,45 +1,73 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Termdock postinstall — 确保原生模块编译 + tmux 可用
4
+ * Termdock postinstall — 确保原生模块可用 + 自动安装 tmux
5
5
  *
6
- * 执行时机:每次 `npm install -g termdock`(含首次安装和更新)
6
+ * 对于 npm 全局安装 (npm i -g termdock) 的场景,npm 10+ 默认不运行
7
+ * lifecycle scripts。此时 postinstall 不会执行,但 cli.ts 启动时的
8
+ * bootCheck 会做同样的检查并写入 marker,确保首次启动时一切就绪。
7
9
  */
8
10
 
9
- import { execSync } from 'child_process';
10
- import { existsSync } from 'fs';
11
+ import { execSync, spawnSync } from 'child_process';
11
12
  import { createRequire } from 'module';
13
+ import fs from 'fs';
14
+ import os from 'os';
15
+ import path from 'path';
12
16
 
13
17
  const require_ = createRequire(import.meta.url);
18
+ const pkgRoot = new URL('..', import.meta.url).pathname;
14
19
 
15
20
  function run(cmd, opts = {}) {
16
21
  console.log(` $ ${cmd}`);
17
22
  return execSync(cmd, { stdio: 'inherit', ...opts });
18
23
  }
19
24
 
20
- function runOk(cmd) {
21
- try { run(cmd); return true; } catch { return false; }
25
+ function runOk(cmd, opts = {}) {
26
+ try { run(cmd, opts); return true; } catch { return false; }
27
+ }
28
+
29
+ function runQuiet(cmd, timeoutMs = 30_000) {
30
+ try {
31
+ const r = spawnSync(cmd, [], { shell: true, timeout: timeoutMs, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
32
+ return { ok: r.status === 0, stdout: r.stdout, stderr: r.stderr };
33
+ } catch {
34
+ return { ok: false, stdout: '', stderr: '' };
35
+ }
22
36
  }
23
37
 
24
38
  function which(bin) {
25
- try { execSync(`which ${bin}`, { stdio: 'pipe' }); return true; } catch { return false; }
39
+ try { execSync(`command -v ${bin} 2>/dev/null`, { stdio: 'pipe' }); return true; } catch { /* not in PATH */ }
40
+ // macOS non-interactive SSH 的 PATH 不含 Homebrew,手动探测
41
+ const extraPaths = ['/opt/homebrew/bin', '/usr/local/bin', '/opt/local/bin'];
42
+ for (const dir of extraPaths) {
43
+ try { fs.accessSync(path.join(dir, bin), fs.constants.X_OK); return true; } catch { /* not here */ }
44
+ }
45
+ return false;
26
46
  }
27
47
 
28
- // —— native modules ——
48
+ // —— native modules: 实际 spawn 测试,不只是 require ——
29
49
  console.log('\n=== Rebuilding native modules ===');
30
- const pkgRoot = new URL('..', import.meta.url).pathname;
31
50
 
32
- for (const mod of ['node-pty', '@parcel/watcher']) {
51
+ const PTY_PATH = require_.resolve('node-pty/package.json', { paths: [pkgRoot] }).replace('/package.json', '');
52
+ function testPty() {
33
53
  try {
34
- require_(`${mod}`);
35
- console.log(` ${mod} already usable, skip rebuild`);
36
- } catch {
37
- console.log(` ${mod} needs rebuild...`);
38
- const modDir = require_.resolve(`${mod}/package.json`, { paths: [pkgRoot] });
39
- const modRoot = modDir.replace('/package.json', '');
40
- // 删掉预编译二进制,强制从源码构建
41
- runOk(`rm -rf ${modRoot}/prebuilds/*`);
42
- run(`cd ${modRoot} && npx --yes node-gyp rebuild`, { timeout: 120_000 });
54
+ const pty = require_(PTY_PATH);
55
+ const proc = pty.spawn(process.env.SHELL || '/bin/bash', [], { name: 'xterm', cols: 80, rows: 24 });
56
+ proc.kill();
57
+ return true;
58
+ } catch { return false; }
59
+ }
60
+
61
+ if (testPty()) {
62
+ console.log(' node-pty OK');
63
+ } else {
64
+ console.log(' node-pty needs rebuild (prebuilt binary incompatible with this OS/Node version)...');
65
+ runOk(`rm -rf ${PTY_PATH}/prebuilds/*`);
66
+ run(`cd ${PTY_PATH} && npx --yes node-gyp rebuild`, { timeout: 120_000 });
67
+ if (testPty()) {
68
+ console.log(' ✓ node-pty rebuilt successfully');
69
+ } else {
70
+ console.log(' ⚠ node-pty rebuild failed — terminal shell spawn may not work.');
43
71
  }
44
72
  }
45
73
 
@@ -51,30 +79,70 @@ if (!which('tmux')) {
51
79
  let installed = false;
52
80
 
53
81
  if (platform === 'darwin') {
54
- // macOS: try brew first, then fallback message
55
- if (which('brew')) {
56
- installed = runOk('brew install tmux');
82
+ // macOS: try brew → macports → nix, in that order
83
+ const brew = which('brew');
84
+ if (brew) {
85
+ installed = runOk('brew install tmux 2>&1', { timeout: 120_000 });
86
+ } else if (which('port')) {
87
+ installed = runOk('port install tmux 2>&1', { timeout: 120_000 });
88
+ } else if (which('nix-env')) {
89
+ installed = runOk('nix-env -iA nixpkgs.tmux 2>&1', { timeout: 120_000 });
57
90
  } else {
58
- console.log(' Homebrew not found. Install it: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"');
91
+ console.log(' No package manager found (brew/port/nix).');
92
+ console.log(' Install Homebrew: https://brew.sh — then reinstall termdock.');
59
93
  }
60
94
  } else if (platform === 'linux') {
61
- if (runOk('apt-get install -y tmux 2>/dev/null')) {
62
- installed = true;
63
- } else if (runOk('yum install -y tmux 2>/dev/null')) {
64
- installed = true;
65
- } else if (runOk('apk add tmux 2>/dev/null')) {
66
- installed = true;
95
+ const managers = [
96
+ { bin: 'apt-get', cmd: 'apt-get install -y tmux', sudo: true },
97
+ { bin: 'dnf', cmd: 'dnf install -y tmux', sudo: true },
98
+ { bin: 'yum', cmd: 'yum install -y tmux', sudo: true },
99
+ { bin: 'apk', cmd: 'apk add tmux', sudo: false },
100
+ { bin: 'pacman', cmd: 'pacman -S --noconfirm tmux', sudo: true },
101
+ { bin: 'zypper', cmd: 'zypper install -y tmux', sudo: true },
102
+ ];
103
+ for (const m of managers) {
104
+ if (!which(m.bin)) continue;
105
+ const cmd = m.sudo ? `sudo ${m.cmd}` : m.cmd;
106
+ if (runOk(`${cmd} 2>&1`, { timeout: 120_000 })) {
107
+ installed = true;
108
+ break;
109
+ }
110
+ // retry without sudo
111
+ if (m.sudo && runOk(`${m.cmd} 2>&1`, { timeout: 120_000 })) {
112
+ installed = true;
113
+ break;
114
+ }
67
115
  }
68
116
  }
69
117
 
70
118
  if (installed) {
71
- console.log(' ✓ tmux installed successfully');
72
- } else if (!which('tmux')) {
73
- console.log(' ⚠ Could not auto-install tmux. Termdock works without it (shell mode will be default).');
74
- console.log(' To use tmux features: install tmux then restart termdock.');
119
+ console.log(' ✓ tmux installed');
120
+ } else {
121
+ console.log(' ⚠ tmux not available shell mode will be used.');
122
+ console.log(' Termdock will check again on next start and try to fix automatically.');
75
123
  }
76
124
  } else {
77
- console.log(` ✓ tmux already available: ${execSync('tmux -V', { encoding: 'utf-8' }).trim()}`);
125
+ const ver = execSync('tmux -V', { encoding: 'utf-8' }).trim();
126
+ console.log(` ✓ tmux ${ver}`);
127
+ }
128
+
129
+ // —— 写入 boot-check marker,避免 CLI 启动时重复检查 ——
130
+ // 只有 node-pty 和 tmux 都就绪才写 marker;否则 CLI 启动时 bootCheck 会补做
131
+ const markerDir = path.join(os.homedir(), '.termdock');
132
+ const markerFile = path.join(markerDir, '.boot-check-ok');
133
+ if (testPty() && which('tmux')) {
134
+ try {
135
+ fs.mkdirSync(markerDir, { recursive: true });
136
+ fs.writeFileSync(markerFile, JSON.stringify({
137
+ checkedAt: new Date().toISOString(),
138
+ source: 'postinstall',
139
+ nodePty: { ok: true, rebuilt: false },
140
+ tmux: { ok: true, version: execSync('tmux -V', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim() },
141
+ }), 'utf-8');
142
+ } catch { /* non-critical */ }
143
+ } else {
144
+ console.log(' ℹ Boot check marker not written (some checks failed).');
145
+ console.log(' Termdock will re-check on first CLI start.');
78
146
  }
79
147
 
80
148
  console.log('\n✓ Termdock postinstall complete\n');
@@ -1 +0,0 @@
1
- import{U as a,C as n}from"./mermaid.core-D-e_wdPJ.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,a as s,c as t,C as e}from"./chunk-V7JOEXUC-CI7QyRAs.js";import{b as i}from"./mermaid.core-D-e_wdPJ.js";import"./chunk-5VM5RSS4-DliA2ku3.js";import"./chunk-XXDRQBXY-BM9jxTTb.js";import"./chunk-VR4S4FIN-DYQ8EokS.js";import"./chunk-32BRIVSS-CGNuIJuG.js";import"./purify.es-CC4Brkr_.js";import"./index-0d_16SRD.js";import"./react-XDCjal2T.js";import"./dnd-BCFAPC0L.js";import"./terminal-C7X98Amf.js";var g={parser:t,get db(){return new e},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{g as diagram};
@@ -1 +0,0 @@
1
- import{s as a,a as s,c as t,C as e}from"./chunk-V7JOEXUC-CI7QyRAs.js";import{b as i}from"./mermaid.core-D-e_wdPJ.js";import"./chunk-5VM5RSS4-DliA2ku3.js";import"./chunk-XXDRQBXY-BM9jxTTb.js";import"./chunk-VR4S4FIN-DYQ8EokS.js";import"./chunk-32BRIVSS-CGNuIJuG.js";import"./purify.es-CC4Brkr_.js";import"./index-0d_16SRD.js";import"./react-XDCjal2T.js";import"./dnd-BCFAPC0L.js";import"./terminal-C7X98Amf.js";var g={parser:t,get db(){return new e},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{g as diagram};