tmex-cli 0.13.0 → 0.15.0
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/CHANGELOG.md +13 -19
- package/dist/cli-node.js +492 -87
- package/dist/runtime/server.js +431 -44
- package/package.json +1 -1
- package/resources/fe-dist/assets/{DevicePage-Cj9jwadE.js → DevicePage-kndrXVHE.js} +1 -1
- package/resources/fe-dist/assets/DevicesPage-CZLzOS04.js +1 -0
- package/resources/fe-dist/assets/{FilePage-CbHZVJbn.js → FilePage-CxVqdlbt.js} +1 -1
- package/resources/fe-dist/assets/{SettingsPage-CCV0NSQF.js → SettingsPage-C50JRe91.js} +1 -1
- package/resources/fe-dist/assets/{agent-tab-DPwxh08T.js → agent-tab-BVWxou-d.js} +1 -1
- package/resources/fe-dist/assets/{api-DlcFE5in.js → api-CrHrQflz.js} +1 -1
- package/resources/fe-dist/assets/{arc-Bzmm6y8F.js → arc-fsY_kDTa.js} +1 -1
- package/resources/fe-dist/assets/{architectureDiagram-3BPJPVTR-BdsN6XRe.js → architectureDiagram-3BPJPVTR-Vsg8Cd3e.js} +1 -1
- package/resources/fe-dist/assets/{blockDiagram-GPEHLZMM-CXdQhkqo.js → blockDiagram-GPEHLZMM-G7sC3DZO.js} +1 -1
- package/resources/fe-dist/assets/{c4Diagram-AAUBKEIU-CE_W2_pV.js → c4Diagram-AAUBKEIU-C1AfLOva.js} +1 -1
- package/resources/fe-dist/assets/{card-B_sl9KIo.js → card-CXa-2u8i.js} +1 -1
- package/resources/fe-dist/assets/channel-DS4kWkSb.js +1 -0
- package/resources/fe-dist/assets/{chunk-2J33WTMH-BAdEIJQO.js → chunk-2J33WTMH-C1p02eGI.js} +1 -1
- package/resources/fe-dist/assets/{chunk-4BX2VUAB-BZ0yj21c.js → chunk-4BX2VUAB-C8n2FURp.js} +1 -1
- package/resources/fe-dist/assets/{chunk-55IACEB6-BknLtkSo.js → chunk-55IACEB6-Da0nuNCD.js} +1 -1
- package/resources/fe-dist/assets/{chunk-727SXJPM-CxVON9n5.js → chunk-727SXJPM-C1TXyDEt.js} +1 -1
- package/resources/fe-dist/assets/{chunk-AQP2D5EJ-CLqOK2pR.js → chunk-AQP2D5EJ-CnRKlbXo.js} +1 -1
- package/resources/fe-dist/assets/{chunk-FMBD7UC4-Dt5Vot-R.js → chunk-FMBD7UC4-B8HGrJ3t.js} +1 -1
- package/resources/fe-dist/assets/{chunk-ND2GUHAM-t9HYlb8C.js → chunk-ND2GUHAM-CcN2Ou-o.js} +1 -1
- package/resources/fe-dist/assets/{chunk-QZHKN3VN-DZReNtLB.js → chunk-QZHKN3VN-DfWJ_tl5.js} +1 -1
- package/resources/fe-dist/assets/classDiagram-4FO5ZUOK-BfsrnUi9.js +1 -0
- package/resources/fe-dist/assets/classDiagram-v2-Q7XG4LA2-BfsrnUi9.js +1 -0
- package/resources/fe-dist/assets/{copy-Z9Fsj1r-.js → copy-BveLgJe5.js} +1 -1
- package/resources/fe-dist/assets/{cose-bilkent-S5V4N54A-BCTtGF2n.js → cose-bilkent-S5V4N54A-BkmsTzwU.js} +1 -1
- package/resources/fe-dist/assets/{dagre-BM42HDAG-BQ48EqSj.js → dagre-BM42HDAG-pGgtc4EE.js} +1 -1
- package/resources/fe-dist/assets/{diagram-2AECGRRQ-BXjt2moR.js → diagram-2AECGRRQ-DJxixZvm.js} +1 -1
- package/resources/fe-dist/assets/{diagram-5GNKFQAL-B2bvNTQO.js → diagram-5GNKFQAL-2ITViev7.js} +1 -1
- package/resources/fe-dist/assets/{diagram-KO2AKTUF-DLHWfbBH.js → diagram-KO2AKTUF-EMcadaeo.js} +1 -1
- package/resources/fe-dist/assets/{diagram-LMA3HP47-BCFT3Bur.js → diagram-LMA3HP47-w3sLU7zY.js} +1 -1
- package/resources/fe-dist/assets/{diagram-OG6HWLK6-bNZtxUJ2.js → diagram-OG6HWLK6-B4KGMQr7.js} +1 -1
- package/resources/fe-dist/assets/en_US-Chxeay8F.js +1 -0
- package/resources/fe-dist/assets/{erDiagram-TEJ5UH35-KoAzrx4j.js → erDiagram-TEJ5UH35-CB0P-Wz7.js} +1 -1
- package/resources/fe-dist/assets/{files-tab-DdBDTjfs.js → files-tab-lNenDPFy.js} +1 -1
- package/resources/fe-dist/assets/{flowDiagram-I6XJVG4X-C6wGExWt.js → flowDiagram-I6XJVG4X-CmBc7GAX.js} +1 -1
- package/resources/fe-dist/assets/{ganttDiagram-6RSMTGT7-BIWybjes.js → ganttDiagram-6RSMTGT7-CHNf7S_K.js} +1 -1
- package/resources/fe-dist/assets/{gitGraphDiagram-PVQCEYII-CD6tz24l.js → gitGraphDiagram-PVQCEYII-CabspIF1.js} +1 -1
- package/resources/fe-dist/assets/{index-CUCrFB_J.js → index-BQbq6q_-.js} +1 -1
- package/resources/fe-dist/assets/{index-CwH-l50P.js → index-DOaZjm8K.js} +79 -79
- package/resources/fe-dist/assets/index-DaOR3c0u.css +1 -0
- package/resources/fe-dist/assets/{infoDiagram-5YYISTIA-CrI-qlYw.js → infoDiagram-5YYISTIA-ny6wqvHH.js} +1 -1
- package/resources/fe-dist/assets/{ishikawaDiagram-YF4QCWOH-D9v5anxN.js → ishikawaDiagram-YF4QCWOH-EFCdGVsV.js} +1 -1
- package/resources/fe-dist/assets/ja_JP-BI-C8I9X.js +1 -0
- package/resources/fe-dist/assets/{journeyDiagram-JHISSGLW-BH38CiJ0.js → journeyDiagram-JHISSGLW-Cvi8jO9a.js} +1 -1
- package/resources/fe-dist/assets/{kanban-definition-UN3LZRKU-CKZ3_oXD.js → kanban-definition-UN3LZRKU-nKej8tSo.js} +1 -1
- package/resources/fe-dist/assets/{linear-C_oE01xA.js → linear-leOPyu29.js} +1 -1
- package/resources/fe-dist/assets/{markdown-preview-BtNbNwlP.js → markdown-preview-rqUSs5w8.js} +3 -3
- package/resources/fe-dist/assets/{mermaid.core-4dbvlhht.js → mermaid.core-QvhJBi8f.js} +5 -5
- package/resources/fe-dist/assets/{mindmap-definition-RKZ34NQL-C5o4mnnX.js → mindmap-definition-RKZ34NQL-xWje0B_-.js} +1 -1
- package/resources/fe-dist/assets/{pieDiagram-4H26LBE5-2ss3HotB.js → pieDiagram-4H26LBE5-DT1i53ec.js} +1 -1
- package/resources/fe-dist/assets/{quadrantDiagram-W4KKPZXB-Bolcfx55.js → quadrantDiagram-W4KKPZXB-C1zbS4Tx.js} +1 -1
- package/resources/fe-dist/assets/{requirementDiagram-4Y6WPE33-B-atSHRR.js → requirementDiagram-4Y6WPE33-By_eNRll.js} +1 -1
- package/resources/fe-dist/assets/{sankeyDiagram-5OEKKPKP-ByynRcAV.js → sankeyDiagram-5OEKKPKP-CG7jC05N.js} +1 -1
- package/resources/fe-dist/assets/{send-D0P5wgcD.js → send-CcFyyX2G.js} +1 -1
- package/resources/fe-dist/assets/{sequenceDiagram-3UESZ5HK-CNC9IxV4.js → sequenceDiagram-3UESZ5HK-C9yK_EKN.js} +1 -1
- package/resources/fe-dist/assets/{stateDiagram-AJRCARHV-CqzYDImp.js → stateDiagram-AJRCARHV-B-ROUwGP.js} +1 -1
- package/resources/fe-dist/assets/stateDiagram-v2-BHNVJYJU-iopOcale.js +1 -0
- package/resources/fe-dist/assets/{terminal-settings-panel-cjw9P_wY.js → terminal-settings-panel-CNwWqOMV.js} +1 -1
- package/resources/fe-dist/assets/{timeline-definition-PNZ67QCA-C38KIECs.js → timeline-definition-PNZ67QCA-yMM8VB4U.js} +1 -1
- package/resources/fe-dist/assets/{transfer-toast-5tb-zYhi.js → transfer-toast-mQs0hgyg.js} +1 -1
- package/resources/fe-dist/assets/{triangle-alert-CQzql6mY.js → triangle-alert-DMp7kMmJ.js} +1 -1
- package/resources/fe-dist/assets/{vennDiagram-CIIHVFJN-BJudKfzJ.js → vennDiagram-CIIHVFJN-CE08llZ3.js} +1 -1
- package/resources/fe-dist/assets/{wardley-L42UT6IY-B6p_luMy.js → wardley-L42UT6IY-BKLGCmLJ.js} +1 -1
- package/resources/fe-dist/assets/{wardleyDiagram-YWT4CUSO-DZJeOiWQ.js → wardleyDiagram-YWT4CUSO-BsjwfO3P.js} +1 -1
- package/resources/fe-dist/assets/{xychartDiagram-2RQKCTM6-DOMCute2.js → xychartDiagram-2RQKCTM6-Bhz6AI7w.js} +1 -1
- package/resources/fe-dist/assets/{zap-DWNtdoic.js → zap-DPwn8lbv.js} +1 -1
- package/resources/fe-dist/assets/zh_CN-DE-BaQ3P.js +1 -0
- package/resources/fe-dist/index.html +2 -2
- package/resources/gateway-drizzle/0011_stormy_sauron.sql +1 -0
- package/resources/gateway-drizzle/meta/_journal.json +7 -0
- package/resources/fe-dist/assets/DevicesPage-BbypaGC7.js +0 -1
- package/resources/fe-dist/assets/channel-CuapxAWT.js +0 -1
- package/resources/fe-dist/assets/classDiagram-4FO5ZUOK-C0k136Ud.js +0 -1
- package/resources/fe-dist/assets/classDiagram-v2-Q7XG4LA2-C0k136Ud.js +0 -1
- package/resources/fe-dist/assets/en_US-B07wSHDj.js +0 -1
- package/resources/fe-dist/assets/index-D-q7dOhH.css +0 -1
- package/resources/fe-dist/assets/ja_JP-DjF6Lrmx.js +0 -1
- package/resources/fe-dist/assets/stateDiagram-v2-BHNVJYJU-C3azxyWE.js +0 -1
- package/resources/fe-dist/assets/zh_CN-D10j4J8Y.js +0 -1
package/dist/cli-node.js
CHANGED
|
@@ -6,6 +6,7 @@ import { resolve as resolve4 } from "node:path";
|
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { resolve } from "node:path";
|
|
8
8
|
var MIN_BUN_VERSION = "1.3.0";
|
|
9
|
+
var MIN_TMUX_VERSION = { major: 3, minor: 0 };
|
|
9
10
|
var DEFAULT_SERVICE_NAME = "tmex";
|
|
10
11
|
function defaultInstallDir(platform) {
|
|
11
12
|
if (platform === "darwin") {
|
|
@@ -29,8 +30,8 @@ var MESSAGES = {
|
|
|
29
30
|
"cli.help": `tmex CLI
|
|
30
31
|
|
|
31
32
|
Usage:
|
|
32
|
-
tmex init [--no-interactive --install-dir <path> --host <host> --port <port> --db-path <path> --autostart <true|false> --bun-path <path>]
|
|
33
|
-
tmex doctor [--install-dir <path>] [--json] [--bun-path <path>]
|
|
33
|
+
tmex init [--no-interactive --install-dir <path> --host <host> --port <port> --db-path <path> --autostart <true|false> --bun-path <path> --install-deps --skip-dep-check]
|
|
34
|
+
tmex doctor [--install-dir <path>] [--json] [--bun-path <path>] [--fix]
|
|
34
35
|
tmex upgrade [--version <version>] [--install-dir <path>] [--bun-path <path>]
|
|
35
36
|
tmex uninstall [--install-dir <path>] [--yes] [--purge]
|
|
36
37
|
|
|
@@ -74,6 +75,7 @@ Global flags:
|
|
|
74
75
|
"init.prompt.serviceName": "Service name (service-name)",
|
|
75
76
|
"init.prompt.dirExistsConfirm": "Directory {{installDir}} already exists. Continue (will not delete existing config/db)?",
|
|
76
77
|
"init.error.installDirNotEmpty": "Install directory is not empty: {{installDir}}. Use --force to overwrite.",
|
|
78
|
+
"init.error.noServiceManager": "No supported service manager found (platform: {{platform}}). tmex requires systemd (Linux) or launchd (macOS).",
|
|
77
79
|
"init.warning.noServiceManager": "Service manager is not supported on platform {{platform}}. Files are deployed but autostart is not configured.",
|
|
78
80
|
"init.done": "Initialization completed.",
|
|
79
81
|
"init.summary.installDir": "Install dir",
|
|
@@ -87,8 +89,12 @@ Global flags:
|
|
|
87
89
|
"doctor.platform.unsupported": "Platform {{platform}} is not officially supported (only macOS and common Linux distros are guaranteed).",
|
|
88
90
|
"doctor.bun.ok": "Bun installed: {{version}}",
|
|
89
91
|
"doctor.bun.fail": "Bun check failed: {{reason}}",
|
|
90
|
-
"doctor.tmux.ok": "tmux installed",
|
|
91
|
-
"doctor.tmux.fail": "tmux not found (tmex requires tmux).",
|
|
92
|
+
"doctor.tmux.ok": "tmux installed: {{version}}",
|
|
93
|
+
"doctor.tmux.fail": "tmux not found (tmex requires tmux >= 3.0).",
|
|
94
|
+
"doctor.tmux.versionLow": "tmux version too low: {{version}} (requires >= 3.0)",
|
|
95
|
+
"doctor.fix.header": "Attempting to fix issues...",
|
|
96
|
+
"doctor.fix.skip": "Skipping unfixable item: {{id}}",
|
|
97
|
+
"doctor.fix.hint": 'Run "tmex doctor --fix" to attempt automatic installation.',
|
|
92
98
|
"doctor.ssh.ok": "ssh installed",
|
|
93
99
|
"doctor.ssh.missing": "ssh not found; SSH devices will not work.",
|
|
94
100
|
"doctor.installDir.exists": "Install directory exists: {{installDir}}",
|
|
@@ -119,6 +125,19 @@ Global flags:
|
|
|
119
125
|
"uninstall.done": "Uninstall completed.",
|
|
120
126
|
"uninstall.summary.installDir": "Install dir",
|
|
121
127
|
"uninstall.summary.serviceName": "Service name",
|
|
128
|
+
"tmux.notFound": "tmux not found. tmex requires tmux >= 3.0 to operate.",
|
|
129
|
+
"tmux.versionTooLow": "tmux version too low: current {{version}}, required >= 3.0",
|
|
130
|
+
"deps.install.confirm": "Install {{dep}} now?",
|
|
131
|
+
"deps.install.running": "Installing {{dep}}...",
|
|
132
|
+
"deps.install.success": "{{dep}} installed successfully.",
|
|
133
|
+
"deps.install.failed": "Failed to install {{dep}}.",
|
|
134
|
+
"deps.install.manual": "Please install manually and retry.",
|
|
135
|
+
"deps.install.sudoRequired": "This operation requires sudo.",
|
|
136
|
+
"deps.install.sudoUnavailable": "sudo is not available. Please run as root or install sudo.",
|
|
137
|
+
"deps.install.nonInteractive": "Missing dependency: {{dep}}. Use --install-deps to install automatically.",
|
|
138
|
+
"deps.install.hint": "Suggested install command: {{command}}",
|
|
139
|
+
"deps.install.brewMissing": "Homebrew not found. Install Homebrew first: https://brew.sh",
|
|
140
|
+
"deps.install.unknownDistro": "Unable to detect Linux distribution. Please install {{dep}} manually.",
|
|
122
141
|
"runtime.restartRequested": "Restart requested; exiting for service manager restart.",
|
|
123
142
|
"runtime.started": "Service started on {{url}}",
|
|
124
143
|
"runtime.frontendMissing": "Frontend assets not found.",
|
|
@@ -130,8 +149,8 @@ Global flags:
|
|
|
130
149
|
"cli.help": `tmex CLI
|
|
131
150
|
|
|
132
151
|
用法:
|
|
133
|
-
tmex init [--no-interactive --install-dir <path> --host <host> --port <port> --db-path <path> --autostart <true|false> --bun-path <path>]
|
|
134
|
-
tmex doctor [--install-dir <path>] [--json] [--bun-path <path>]
|
|
152
|
+
tmex init [--no-interactive --install-dir <path> --host <host> --port <port> --db-path <path> --autostart <true|false> --bun-path <path> --install-deps --skip-dep-check]
|
|
153
|
+
tmex doctor [--install-dir <path>] [--json] [--bun-path <path>] [--fix]
|
|
135
154
|
tmex upgrade [--version <version>] [--install-dir <path>] [--bun-path <path>]
|
|
136
155
|
tmex uninstall [--install-dir <path>] [--yes] [--purge]
|
|
137
156
|
|
|
@@ -175,6 +194,7 @@ Global flags:
|
|
|
175
194
|
"init.prompt.serviceName": "服务名称(service-name)",
|
|
176
195
|
"init.prompt.dirExistsConfirm": "目录 {{installDir}} 已存在,是否继续(不会删除现有配置与数据库)?",
|
|
177
196
|
"init.error.installDirNotEmpty": "安装目录已存在且非空:{{installDir}}。如需覆盖请加 --force",
|
|
197
|
+
"init.error.noServiceManager": "未检测到可用的服务管理器(平台:{{platform}})。tmex 需要 systemd(Linux)或 launchd(macOS)。",
|
|
178
198
|
"init.warning.noServiceManager": "当前平台 {{platform}} 未实现自动服务安装,已完成文件部署。",
|
|
179
199
|
"init.done": "初始化完成。",
|
|
180
200
|
"init.summary.installDir": "安装目录",
|
|
@@ -188,8 +208,12 @@ Global flags:
|
|
|
188
208
|
"doctor.platform.unsupported": "当前平台 {{platform}} 非官方支持范围(仅保证 macOS 与常见 Linux 发行版)。",
|
|
189
209
|
"doctor.bun.ok": "Bun 已安装:{{version}}",
|
|
190
210
|
"doctor.bun.fail": "Bun 检查失败:{{reason}}",
|
|
191
|
-
"doctor.tmux.ok": "tmux
|
|
192
|
-
"doctor.tmux.fail": "未检测到 tmux(tmex 需要 tmux 才能工作)。",
|
|
211
|
+
"doctor.tmux.ok": "tmux 已安装:{{version}}",
|
|
212
|
+
"doctor.tmux.fail": "未检测到 tmux(tmex 需要 tmux >= 3.0 才能工作)。",
|
|
213
|
+
"doctor.tmux.versionLow": "tmux 版本过低:{{version}}(要求 >= 3.0)",
|
|
214
|
+
"doctor.fix.header": "正在尝试修复问题...",
|
|
215
|
+
"doctor.fix.skip": "跳过无法自动修复的项目:{{id}}",
|
|
216
|
+
"doctor.fix.hint": '运行 "tmex doctor --fix" 尝试自动安装缺失的依赖。',
|
|
193
217
|
"doctor.ssh.ok": "ssh 已安装",
|
|
194
218
|
"doctor.ssh.missing": "未检测到 ssh,远程设备将不可用。",
|
|
195
219
|
"doctor.installDir.exists": "安装目录存在:{{installDir}}",
|
|
@@ -220,6 +244,19 @@ Global flags:
|
|
|
220
244
|
"uninstall.done": "卸载完成。",
|
|
221
245
|
"uninstall.summary.installDir": "安装目录",
|
|
222
246
|
"uninstall.summary.serviceName": "服务名称",
|
|
247
|
+
"tmux.notFound": "未检测到 tmux。tmex 需要 tmux >= 3.0 才能工作。",
|
|
248
|
+
"tmux.versionTooLow": "tmux 版本过低:当前 {{version}},要求 >= 3.0",
|
|
249
|
+
"deps.install.confirm": "是否现在安装 {{dep}}?",
|
|
250
|
+
"deps.install.running": "正在安装 {{dep}}...",
|
|
251
|
+
"deps.install.success": "{{dep}} 安装成功。",
|
|
252
|
+
"deps.install.failed": "安装 {{dep}} 失败。",
|
|
253
|
+
"deps.install.manual": "请手动安装后重试。",
|
|
254
|
+
"deps.install.sudoRequired": "此操作需要 sudo 权限。",
|
|
255
|
+
"deps.install.sudoUnavailable": "sudo 不可用,请以 root 身份执行或安装 sudo。",
|
|
256
|
+
"deps.install.nonInteractive": "缺少依赖:{{dep}}。使用 --install-deps 自动安装。",
|
|
257
|
+
"deps.install.hint": "建议安装命令:{{command}}",
|
|
258
|
+
"deps.install.brewMissing": "未检测到 Homebrew,请先安装 Homebrew:https://brew.sh",
|
|
259
|
+
"deps.install.unknownDistro": "无法检测 Linux 发行版,请手动安装 {{dep}}。",
|
|
223
260
|
"runtime.restartRequested": "收到重启请求,退出并等待服务管理器拉起。",
|
|
224
261
|
"runtime.started": "服务已启动:{{url}}",
|
|
225
262
|
"runtime.frontendMissing": "未找到前端静态资源。",
|
|
@@ -574,8 +611,307 @@ async function checkBunVersion(minVersion = MIN_BUN_VERSION, opts = {}) {
|
|
|
574
611
|
return firstFailure ?? { ok: false, reason: t("bun.notFound") };
|
|
575
612
|
}
|
|
576
613
|
|
|
614
|
+
// src/lib/linux-distro.ts
|
|
615
|
+
import { readFile } from "node:fs/promises";
|
|
616
|
+
function parseOsRelease(content) {
|
|
617
|
+
const lines = content.split(`
|
|
618
|
+
`);
|
|
619
|
+
const fields = {};
|
|
620
|
+
for (const line of lines) {
|
|
621
|
+
const trimmed = line.trim();
|
|
622
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
623
|
+
continue;
|
|
624
|
+
const eqIndex = trimmed.indexOf("=");
|
|
625
|
+
if (eqIndex < 0)
|
|
626
|
+
continue;
|
|
627
|
+
const key = trimmed.slice(0, eqIndex);
|
|
628
|
+
let value = trimmed.slice(eqIndex + 1);
|
|
629
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
630
|
+
value = value.slice(1, -1);
|
|
631
|
+
}
|
|
632
|
+
fields[key] = value;
|
|
633
|
+
}
|
|
634
|
+
const id = fields.ID;
|
|
635
|
+
if (!id)
|
|
636
|
+
return null;
|
|
637
|
+
const idLikeRaw = fields.ID_LIKE;
|
|
638
|
+
const idLike = idLikeRaw ? idLikeRaw.split(/\s+/).filter(Boolean) : [];
|
|
639
|
+
return {
|
|
640
|
+
id,
|
|
641
|
+
idLike,
|
|
642
|
+
versionId: fields.VERSION_ID,
|
|
643
|
+
name: fields.NAME
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
async function detectLinuxDistro() {
|
|
647
|
+
try {
|
|
648
|
+
const content = await readFile("/etc/os-release", "utf-8");
|
|
649
|
+
return parseOsRelease(content);
|
|
650
|
+
} catch {
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
function detectPackageManager(distro, platform = process.platform) {
|
|
655
|
+
if (platform === "darwin")
|
|
656
|
+
return "brew";
|
|
657
|
+
if (platform !== "linux")
|
|
658
|
+
return "unknown";
|
|
659
|
+
if (!distro)
|
|
660
|
+
return "unknown";
|
|
661
|
+
const allIds = [distro.id, ...distro.idLike];
|
|
662
|
+
for (const id of allIds) {
|
|
663
|
+
const lower = id.toLowerCase();
|
|
664
|
+
if (lower === "debian" || lower === "ubuntu")
|
|
665
|
+
return "apt";
|
|
666
|
+
if (lower === "fedora" || lower === "rhel" || lower === "centos")
|
|
667
|
+
return "dnf";
|
|
668
|
+
if (lower === "arch" || lower === "manjaro")
|
|
669
|
+
return "pacman";
|
|
670
|
+
if (lower === "alpine")
|
|
671
|
+
return "apk";
|
|
672
|
+
if (lower.startsWith("opensuse") || lower === "suse")
|
|
673
|
+
return "zypper";
|
|
674
|
+
}
|
|
675
|
+
return "unknown";
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// src/lib/prompt.ts
|
|
679
|
+
import { stdin, stdout } from "node:process";
|
|
680
|
+
import { createInterface } from "node:readline/promises";
|
|
681
|
+
async function promptText(ctx, message, defaultValue) {
|
|
682
|
+
if (ctx.nonInteractive) {
|
|
683
|
+
return defaultValue ?? "";
|
|
684
|
+
}
|
|
685
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
686
|
+
try {
|
|
687
|
+
const suffix = defaultValue !== undefined ? ` (${defaultValue})` : "";
|
|
688
|
+
const answer = (await rl.question(`${message}${suffix}: `)).trim();
|
|
689
|
+
return answer || defaultValue || "";
|
|
690
|
+
} finally {
|
|
691
|
+
rl.close();
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
async function promptConfirm(ctx, message, defaultValue) {
|
|
695
|
+
if (ctx.nonInteractive) {
|
|
696
|
+
return defaultValue;
|
|
697
|
+
}
|
|
698
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
699
|
+
try {
|
|
700
|
+
const hint = defaultValue ? "Y/n" : "y/N";
|
|
701
|
+
const answer = (await rl.question(`${message} [${hint}]: `)).trim().toLowerCase();
|
|
702
|
+
if (!answer) {
|
|
703
|
+
return defaultValue;
|
|
704
|
+
}
|
|
705
|
+
if (answer === "y" || answer === "yes")
|
|
706
|
+
return true;
|
|
707
|
+
if (answer === "n" || answer === "no")
|
|
708
|
+
return false;
|
|
709
|
+
return defaultValue;
|
|
710
|
+
} finally {
|
|
711
|
+
rl.close();
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/lib/tmux.ts
|
|
716
|
+
function parseTmuxVersion(versionOutput) {
|
|
717
|
+
const match = versionOutput.match(/(\d+)\.(\d+)/);
|
|
718
|
+
if (!match)
|
|
719
|
+
return null;
|
|
720
|
+
return {
|
|
721
|
+
major: Number.parseInt(match[1], 10),
|
|
722
|
+
minor: Number.parseInt(match[2], 10)
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
function compareTmuxVersion(current, min) {
|
|
726
|
+
if (!current)
|
|
727
|
+
return true;
|
|
728
|
+
if (current.major !== min.major)
|
|
729
|
+
return current.major > min.major;
|
|
730
|
+
return current.minor >= min.minor;
|
|
731
|
+
}
|
|
732
|
+
async function checkTmuxVersion(minVersion = MIN_TMUX_VERSION) {
|
|
733
|
+
const result = await runCommand("tmux", ["-V"], {
|
|
734
|
+
stdio: "pipe",
|
|
735
|
+
timeoutMs: 5000
|
|
736
|
+
}).catch(() => null);
|
|
737
|
+
if (!result || result.code !== 0) {
|
|
738
|
+
return { ok: false, reason: "not-found" };
|
|
739
|
+
}
|
|
740
|
+
const raw = result.stdout.trim();
|
|
741
|
+
const version = parseTmuxVersion(raw);
|
|
742
|
+
if (!compareTmuxVersion(version, minVersion)) {
|
|
743
|
+
return {
|
|
744
|
+
ok: false,
|
|
745
|
+
version: version ?? undefined,
|
|
746
|
+
versionRaw: raw,
|
|
747
|
+
reason: "version-too-low"
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
ok: true,
|
|
752
|
+
version: version ?? undefined,
|
|
753
|
+
versionRaw: raw
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/lib/dep-install.ts
|
|
758
|
+
var TMUX_INSTALL_COMMANDS = {
|
|
759
|
+
brew: { label: "Homebrew", command: "brew install tmux", requiresSudo: false, packageManager: "brew" },
|
|
760
|
+
apt: { label: "apt", command: "apt install -y tmux", requiresSudo: true, packageManager: "apt" },
|
|
761
|
+
dnf: { label: "dnf", command: "dnf install -y tmux", requiresSudo: true, packageManager: "dnf" },
|
|
762
|
+
pacman: { label: "pacman", command: "pacman -S --noconfirm tmux", requiresSudo: true, packageManager: "pacman" },
|
|
763
|
+
apk: { label: "apk", command: "apk add tmux", requiresSudo: true, packageManager: "apk" },
|
|
764
|
+
zypper: { label: "zypper", command: "zypper install -y tmux", requiresSudo: true, packageManager: "zypper" },
|
|
765
|
+
unknown: null
|
|
766
|
+
};
|
|
767
|
+
function planBunInstall() {
|
|
768
|
+
return [
|
|
769
|
+
{
|
|
770
|
+
label: "Official installer",
|
|
771
|
+
command: "curl -fsSL https://bun.sh/install | bash",
|
|
772
|
+
requiresSudo: false,
|
|
773
|
+
packageManager: "curl"
|
|
774
|
+
}
|
|
775
|
+
];
|
|
776
|
+
}
|
|
777
|
+
async function planTmuxInstall(platform = process.platform) {
|
|
778
|
+
if (platform === "darwin") {
|
|
779
|
+
const brewAvailable = await isCommandAvailable("brew");
|
|
780
|
+
if (!brewAvailable)
|
|
781
|
+
return [];
|
|
782
|
+
return [TMUX_INSTALL_COMMANDS.brew];
|
|
783
|
+
}
|
|
784
|
+
if (platform === "linux") {
|
|
785
|
+
const distro = await detectLinuxDistro();
|
|
786
|
+
const pm = detectPackageManager(distro, platform);
|
|
787
|
+
const cmd = TMUX_INSTALL_COMMANDS[pm];
|
|
788
|
+
if (cmd)
|
|
789
|
+
return [cmd];
|
|
790
|
+
return [];
|
|
791
|
+
}
|
|
792
|
+
return [];
|
|
793
|
+
}
|
|
794
|
+
function getInstallHint(dep, platform = process.platform) {
|
|
795
|
+
if (dep === "bun") {
|
|
796
|
+
return "curl -fsSL https://bun.sh/install | bash";
|
|
797
|
+
}
|
|
798
|
+
if (platform === "darwin") {
|
|
799
|
+
return "brew install tmux";
|
|
800
|
+
}
|
|
801
|
+
return "apt/dnf/pacman/apk install tmux";
|
|
802
|
+
}
|
|
803
|
+
async function getInstallHintAsync(dep, platform = process.platform) {
|
|
804
|
+
if (dep === "bun") {
|
|
805
|
+
return "curl -fsSL https://bun.sh/install | bash";
|
|
806
|
+
}
|
|
807
|
+
if (platform === "darwin") {
|
|
808
|
+
return "brew install tmux";
|
|
809
|
+
}
|
|
810
|
+
if (platform === "linux") {
|
|
811
|
+
const distro = await detectLinuxDistro();
|
|
812
|
+
const pm = detectPackageManager(distro, platform);
|
|
813
|
+
const cmd = TMUX_INSTALL_COMMANDS[pm];
|
|
814
|
+
if (cmd) {
|
|
815
|
+
const prefix = cmd.requiresSudo ? "sudo " : "";
|
|
816
|
+
return `${prefix}${cmd.command}`;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return "apt/dnf/pacman/apk install tmux";
|
|
820
|
+
}
|
|
821
|
+
async function isCommandAvailable(command) {
|
|
822
|
+
const result = await runCommand(command, ["--version"], {
|
|
823
|
+
stdio: "pipe",
|
|
824
|
+
timeoutMs: 5000
|
|
825
|
+
}).catch(() => null);
|
|
826
|
+
return result !== null && result.code === 0;
|
|
827
|
+
}
|
|
828
|
+
function isRoot() {
|
|
829
|
+
return process.getuid?.() === 0;
|
|
830
|
+
}
|
|
831
|
+
async function isSudoAvailable() {
|
|
832
|
+
const result = await runCommand("sudo", ["-n", "true"], {
|
|
833
|
+
stdio: "pipe",
|
|
834
|
+
timeoutMs: 5000
|
|
835
|
+
}).catch(() => null);
|
|
836
|
+
return result !== null && result.code === 0;
|
|
837
|
+
}
|
|
838
|
+
function resolveCommand(cmd) {
|
|
839
|
+
if (!cmd.requiresSudo)
|
|
840
|
+
return cmd.command;
|
|
841
|
+
if (isRoot())
|
|
842
|
+
return cmd.command;
|
|
843
|
+
return `sudo ${cmd.command}`;
|
|
844
|
+
}
|
|
845
|
+
async function executeDependencyInstall(plan, options) {
|
|
846
|
+
if (plan.commands.length === 0) {
|
|
847
|
+
if (plan.dep === "tmux" && process.platform === "darwin") {
|
|
848
|
+
console.error(`[tmex] ${t("deps.install.brewMissing")}`);
|
|
849
|
+
} else {
|
|
850
|
+
console.error(`[tmex] ${t("deps.install.unknownDistro", { dep: plan.dep })}`);
|
|
851
|
+
}
|
|
852
|
+
console.error(`[tmex] ${t("deps.install.manual")}`);
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
const cmd = plan.commands[0];
|
|
856
|
+
const fullCommand = resolveCommand(cmd);
|
|
857
|
+
if (cmd.requiresSudo && !isRoot()) {
|
|
858
|
+
if (options.nonInteractive) {
|
|
859
|
+
const sudoOk = await isSudoAvailable();
|
|
860
|
+
if (!sudoOk) {
|
|
861
|
+
console.error(`[tmex] ${t("deps.install.sudoUnavailable")}`);
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
console.log(`[tmex] ${t("deps.install.hint", { command: fullCommand })}`);
|
|
867
|
+
if (!options.autoConfirm) {
|
|
868
|
+
if (options.nonInteractive) {
|
|
869
|
+
console.error(`[tmex] ${t("deps.install.nonInteractive", { dep: plan.dep })}`);
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
const confirmed = await promptConfirm({ nonInteractive: false }, t("deps.install.confirm", { dep: plan.dep }), true);
|
|
873
|
+
if (!confirmed)
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
console.log(`[tmex] ${t("deps.install.running", { dep: plan.dep })}`);
|
|
877
|
+
const parts = fullCommand.split(" ");
|
|
878
|
+
const bin = parts[0];
|
|
879
|
+
const args = parts.slice(1);
|
|
880
|
+
if (fullCommand.includes("|")) {
|
|
881
|
+
const result = await runCommand("sh", ["-c", fullCommand], { stdio: "inherit" }).catch(() => null);
|
|
882
|
+
if (!result || result.code !== 0) {
|
|
883
|
+
console.error(`[tmex] ${t("deps.install.failed", { dep: plan.dep })}`);
|
|
884
|
+
console.error(`[tmex] ${t("deps.install.manual")}`);
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
} else {
|
|
888
|
+
const result = await runCommand(bin, args, { stdio: "inherit" }).catch(() => null);
|
|
889
|
+
if (!result || result.code !== 0) {
|
|
890
|
+
console.error(`[tmex] ${t("deps.install.failed", { dep: plan.dep })}`);
|
|
891
|
+
console.error(`[tmex] ${t("deps.install.manual")}`);
|
|
892
|
+
return false;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (plan.dep === "bun") {
|
|
896
|
+
const check = await checkBunVersion();
|
|
897
|
+
if (check.ok) {
|
|
898
|
+
console.log(`[tmex] ${t("deps.install.success", { dep: plan.dep })}`);
|
|
899
|
+
return true;
|
|
900
|
+
}
|
|
901
|
+
} else {
|
|
902
|
+
const check = await checkTmuxVersion();
|
|
903
|
+
if (check.ok) {
|
|
904
|
+
console.log(`[tmex] ${t("deps.install.success", { dep: plan.dep })}`);
|
|
905
|
+
return true;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
console.error(`[tmex] ${t("deps.install.failed", { dep: plan.dep })}`);
|
|
909
|
+
console.error(`[tmex] ${t("deps.install.manual")}`);
|
|
910
|
+
return false;
|
|
911
|
+
}
|
|
912
|
+
|
|
577
913
|
// src/lib/env-file.ts
|
|
578
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
914
|
+
import { readFile as readFile2, writeFile } from "node:fs/promises";
|
|
579
915
|
function parseEnvContent(content) {
|
|
580
916
|
const result = {};
|
|
581
917
|
for (const rawLine of content.split(/\r?\n/)) {
|
|
@@ -598,7 +934,7 @@ function stringifyEnv(values) {
|
|
|
598
934
|
`;
|
|
599
935
|
}
|
|
600
936
|
async function readEnvFile(filePath) {
|
|
601
|
-
const content = await
|
|
937
|
+
const content = await readFile2(filePath, "utf8");
|
|
602
938
|
return parseEnvContent(content);
|
|
603
939
|
}
|
|
604
940
|
async function writeEnvFile(filePath, values) {
|
|
@@ -607,7 +943,7 @@ async function writeEnvFile(filePath, values) {
|
|
|
607
943
|
|
|
608
944
|
// src/lib/fs-utils.ts
|
|
609
945
|
import { constants } from "node:fs";
|
|
610
|
-
import { access, cp, mkdir, readFile as
|
|
946
|
+
import { access, cp, mkdir, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
611
947
|
import { dirname, resolve as resolve2 } from "node:path";
|
|
612
948
|
async function pathExists(path) {
|
|
613
949
|
try {
|
|
@@ -631,7 +967,7 @@ async function writeText(path, content, mode) {
|
|
|
631
967
|
});
|
|
632
968
|
}
|
|
633
969
|
async function readText(path) {
|
|
634
|
-
return await
|
|
970
|
+
return await readFile3(path, "utf8");
|
|
635
971
|
}
|
|
636
972
|
function resolvePath(...parts) {
|
|
637
973
|
return resolve2(...parts);
|
|
@@ -702,10 +1038,10 @@ function resolveInstallDir(input) {
|
|
|
702
1038
|
}
|
|
703
1039
|
|
|
704
1040
|
// src/lib/json-file.ts
|
|
705
|
-
import { readFile as
|
|
1041
|
+
import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
706
1042
|
import { dirname as dirname3 } from "node:path";
|
|
707
1043
|
async function readJsonFile(path) {
|
|
708
|
-
const content = await
|
|
1044
|
+
const content = await readFile4(path, "utf8");
|
|
709
1045
|
return JSON.parse(content);
|
|
710
1046
|
}
|
|
711
1047
|
async function writeJsonFile(path, value, mode) {
|
|
@@ -715,11 +1051,18 @@ async function writeJsonFile(path, value, mode) {
|
|
|
715
1051
|
}
|
|
716
1052
|
|
|
717
1053
|
// src/lib/platform.ts
|
|
718
|
-
function detectServiceManager(platform = process.platform) {
|
|
719
|
-
if (platform === "linux")
|
|
720
|
-
return "systemd-user";
|
|
1054
|
+
async function detectServiceManager(platform = process.platform) {
|
|
721
1055
|
if (platform === "darwin")
|
|
722
1056
|
return "launchd";
|
|
1057
|
+
if (platform === "linux") {
|
|
1058
|
+
const result = await runCommand("systemctl", ["--version"], {
|
|
1059
|
+
stdio: "pipe",
|
|
1060
|
+
timeoutMs: 5000
|
|
1061
|
+
}).catch(() => null);
|
|
1062
|
+
if (result && result.code === 0)
|
|
1063
|
+
return "systemd-user";
|
|
1064
|
+
return "none";
|
|
1065
|
+
}
|
|
723
1066
|
return "none";
|
|
724
1067
|
}
|
|
725
1068
|
function isSupportedPlatform(platform = process.platform) {
|
|
@@ -853,7 +1196,7 @@ async function installLaunchdService(options) {
|
|
|
853
1196
|
}
|
|
854
1197
|
}
|
|
855
1198
|
async function installService(options) {
|
|
856
|
-
const manager = detectServiceManager();
|
|
1199
|
+
const manager = await detectServiceManager();
|
|
857
1200
|
if (manager === "systemd-user") {
|
|
858
1201
|
await installSystemdService(options);
|
|
859
1202
|
return;
|
|
@@ -868,7 +1211,7 @@ async function stopSystemd(serviceName) {
|
|
|
868
1211
|
await runCommand("systemctl", ["--user", "stop", serviceName]).catch(() => null);
|
|
869
1212
|
}
|
|
870
1213
|
async function stopService(serviceName, installDir) {
|
|
871
|
-
const manager = detectServiceManager();
|
|
1214
|
+
const manager = await detectServiceManager();
|
|
872
1215
|
if (manager === "systemd-user") {
|
|
873
1216
|
await stopSystemd(serviceName);
|
|
874
1217
|
return;
|
|
@@ -892,7 +1235,7 @@ async function uninstallSystemdService(serviceName) {
|
|
|
892
1235
|
await runCommand("systemctl", ["--user", "daemon-reload"]).catch(() => null);
|
|
893
1236
|
}
|
|
894
1237
|
async function uninstallService(options) {
|
|
895
|
-
const manager = detectServiceManager();
|
|
1238
|
+
const manager = await detectServiceManager();
|
|
896
1239
|
if (manager === "systemd-user") {
|
|
897
1240
|
await uninstallSystemdService(options.serviceName);
|
|
898
1241
|
return;
|
|
@@ -941,7 +1284,7 @@ async function queryLaunchdStatus(serviceName, installDir) {
|
|
|
941
1284
|
};
|
|
942
1285
|
}
|
|
943
1286
|
async function getServiceStatus(serviceName, installDir) {
|
|
944
|
-
const manager = detectServiceManager();
|
|
1287
|
+
const manager = await detectServiceManager();
|
|
945
1288
|
if (manager === "systemd-user") {
|
|
946
1289
|
return await querySystemdStatus(serviceName);
|
|
947
1290
|
}
|
|
@@ -956,8 +1299,8 @@ async function getServiceStatus(serviceName, installDir) {
|
|
|
956
1299
|
detail: t("service.status.none", { platform: process.platform })
|
|
957
1300
|
};
|
|
958
1301
|
}
|
|
959
|
-
function serviceHint(serviceName) {
|
|
960
|
-
const manager = detectServiceManager();
|
|
1302
|
+
async function serviceHint(serviceName) {
|
|
1303
|
+
const manager = await detectServiceManager();
|
|
961
1304
|
if (manager === "systemd-user") {
|
|
962
1305
|
return t("service.hint.systemd", { serviceName });
|
|
963
1306
|
}
|
|
@@ -968,13 +1311,6 @@ function serviceHint(serviceName) {
|
|
|
968
1311
|
}
|
|
969
1312
|
|
|
970
1313
|
// src/commands/doctor.ts
|
|
971
|
-
async function checkCommandExists(bin, args, id, okMessage, failMessage) {
|
|
972
|
-
const result = await runCommand(bin, args, { stdio: "pipe" }).catch(() => null);
|
|
973
|
-
if (result?.code === 0) {
|
|
974
|
-
return { id, level: "pass", message: okMessage, detail: result.stdout.trim() };
|
|
975
|
-
}
|
|
976
|
-
return { id, level: "fail", message: failMessage, detail: result?.stderr || result?.stdout };
|
|
977
|
-
}
|
|
978
1314
|
function printChecks(checks, json) {
|
|
979
1315
|
if (json) {
|
|
980
1316
|
console.log(JSON.stringify({ checks }, null, 2));
|
|
@@ -986,10 +1322,14 @@ function printChecks(checks, json) {
|
|
|
986
1322
|
if (check.detail) {
|
|
987
1323
|
console.log(` ${check.detail.trim()}`);
|
|
988
1324
|
}
|
|
1325
|
+
if (check.hint && check.level !== "pass") {
|
|
1326
|
+
console.log(` ${t("deps.install.hint", { command: check.hint })}`);
|
|
1327
|
+
}
|
|
989
1328
|
}
|
|
990
1329
|
}
|
|
991
1330
|
async function runDoctor(parsed) {
|
|
992
1331
|
const json = asBoolean(parsed.flags.json) ?? false;
|
|
1332
|
+
const fix = asBoolean(parsed.flags.fix) ?? false;
|
|
993
1333
|
const installDirFlag = asString(parsed.flags["install-dir"]);
|
|
994
1334
|
const installDir = resolveInstallDir(installDirFlag || defaultInstallDir(process.platform));
|
|
995
1335
|
const installLayout = createInstallLayout(installDir);
|
|
@@ -1025,10 +1365,36 @@ async function runDoctor(parsed) {
|
|
|
1025
1365
|
id: "bun",
|
|
1026
1366
|
level: "fail",
|
|
1027
1367
|
message: t("doctor.bun.fail", { reason: bun.reason || t("bun.checkFailed") }),
|
|
1028
|
-
detail: bun.path
|
|
1368
|
+
detail: bun.path,
|
|
1369
|
+
hint: await getInstallHintAsync("bun"),
|
|
1370
|
+
fixable: true
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
const tmux = await checkTmuxVersion();
|
|
1374
|
+
if (tmux.ok) {
|
|
1375
|
+
checks.push({
|
|
1376
|
+
id: "tmux",
|
|
1377
|
+
level: "pass",
|
|
1378
|
+
message: t("doctor.tmux.ok", { version: tmux.versionRaw || "unknown" }),
|
|
1379
|
+
detail: tmux.versionRaw
|
|
1380
|
+
});
|
|
1381
|
+
} else if (tmux.reason === "version-too-low") {
|
|
1382
|
+
checks.push({
|
|
1383
|
+
id: "tmux",
|
|
1384
|
+
level: "fail",
|
|
1385
|
+
message: t("doctor.tmux.versionLow", { version: tmux.versionRaw || "" }),
|
|
1386
|
+
hint: await getInstallHintAsync("tmux"),
|
|
1387
|
+
fixable: true
|
|
1388
|
+
});
|
|
1389
|
+
} else {
|
|
1390
|
+
checks.push({
|
|
1391
|
+
id: "tmux",
|
|
1392
|
+
level: "fail",
|
|
1393
|
+
message: t("doctor.tmux.fail"),
|
|
1394
|
+
hint: await getInstallHintAsync("tmux"),
|
|
1395
|
+
fixable: true
|
|
1029
1396
|
});
|
|
1030
1397
|
}
|
|
1031
|
-
checks.push(await checkCommandExists("tmux", ["-V"], "tmux", t("doctor.tmux.ok"), t("doctor.tmux.fail")));
|
|
1032
1398
|
const ssh = await runCommand("ssh", ["-V"], { stdio: "pipe" }).catch(() => null);
|
|
1033
1399
|
if (ssh?.code === 0) {
|
|
1034
1400
|
checks.push({
|
|
@@ -1166,6 +1532,38 @@ async function runDoctor(parsed) {
|
|
|
1166
1532
|
});
|
|
1167
1533
|
}
|
|
1168
1534
|
printChecks(checks, json);
|
|
1535
|
+
const fixableFailures = checks.filter((c) => c.level === "fail" && c.fixable);
|
|
1536
|
+
if (fix && fixableFailures.length > 0) {
|
|
1537
|
+
console.log(`
|
|
1538
|
+
[tmex] ${t("doctor.fix.header")}`);
|
|
1539
|
+
for (const check of fixableFailures) {
|
|
1540
|
+
const dep = check.id;
|
|
1541
|
+
if (dep !== "bun" && dep !== "tmux") {
|
|
1542
|
+
console.log(`[tmex] ${t("doctor.fix.skip", { id: check.id })}`);
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
const commands = dep === "bun" ? planBunInstall() : await planTmuxInstall();
|
|
1546
|
+
const plan = {
|
|
1547
|
+
dep,
|
|
1548
|
+
commands,
|
|
1549
|
+
requiredVersion: dep === "tmux" ? ">= 3.0" : ">= 1.3.0",
|
|
1550
|
+
issue: check.id === "tmux" && check.message.includes("version") ? "version-too-low" : "missing"
|
|
1551
|
+
};
|
|
1552
|
+
const nonInteractive = asBoolean(parsed.flags["no-interactive"]) ?? false;
|
|
1553
|
+
await executeDependencyInstall(plan, {
|
|
1554
|
+
nonInteractive,
|
|
1555
|
+
autoConfirm: nonInteractive
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
console.log("");
|
|
1559
|
+
const rerunParsed = { ...parsed, flags: { ...parsed.flags, fix: false } };
|
|
1560
|
+
await runDoctor(rerunParsed);
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
if (!fix && fixableFailures.length > 0 && !json) {
|
|
1564
|
+
console.log(`
|
|
1565
|
+
[tmex] ${t("doctor.fix.hint")}`);
|
|
1566
|
+
}
|
|
1169
1567
|
const failed = checks.some((check) => check.level === "fail");
|
|
1170
1568
|
if (failed) {
|
|
1171
1569
|
process.exitCode = 1;
|
|
@@ -1298,43 +1696,6 @@ async function restoreInstallArtifacts(installLayout, backupDir) {
|
|
|
1298
1696
|
}
|
|
1299
1697
|
}
|
|
1300
1698
|
|
|
1301
|
-
// src/lib/prompt.ts
|
|
1302
|
-
import { stdin, stdout } from "node:process";
|
|
1303
|
-
import { createInterface } from "node:readline/promises";
|
|
1304
|
-
async function promptText(ctx, message, defaultValue) {
|
|
1305
|
-
if (ctx.nonInteractive) {
|
|
1306
|
-
return defaultValue ?? "";
|
|
1307
|
-
}
|
|
1308
|
-
const rl = createInterface({ input: stdin, output: stdout });
|
|
1309
|
-
try {
|
|
1310
|
-
const suffix = defaultValue !== undefined ? ` (${defaultValue})` : "";
|
|
1311
|
-
const answer = (await rl.question(`${message}${suffix}: `)).trim();
|
|
1312
|
-
return answer || defaultValue || "";
|
|
1313
|
-
} finally {
|
|
1314
|
-
rl.close();
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
async function promptConfirm(ctx, message, defaultValue) {
|
|
1318
|
-
if (ctx.nonInteractive) {
|
|
1319
|
-
return defaultValue;
|
|
1320
|
-
}
|
|
1321
|
-
const rl = createInterface({ input: stdin, output: stdout });
|
|
1322
|
-
try {
|
|
1323
|
-
const hint = defaultValue ? "Y/n" : "y/N";
|
|
1324
|
-
const answer = (await rl.question(`${message} [${hint}]: `)).trim().toLowerCase();
|
|
1325
|
-
if (!answer) {
|
|
1326
|
-
return defaultValue;
|
|
1327
|
-
}
|
|
1328
|
-
if (answer === "y" || answer === "yes")
|
|
1329
|
-
return true;
|
|
1330
|
-
if (answer === "n" || answer === "no")
|
|
1331
|
-
return false;
|
|
1332
|
-
return defaultValue;
|
|
1333
|
-
} finally {
|
|
1334
|
-
rl.close();
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
1699
|
// src/lib/version.ts
|
|
1339
1700
|
import { join as join5 } from "node:path";
|
|
1340
1701
|
async function readPackageVersion(packageRoot) {
|
|
@@ -1367,6 +1728,8 @@ async function directoryHasContent(path) {
|
|
|
1367
1728
|
async function buildInitConfig(parsed) {
|
|
1368
1729
|
const nonInteractive = parsed.flags["no-interactive"] === true;
|
|
1369
1730
|
const force = asBoolean(parsed.flags.force) ?? false;
|
|
1731
|
+
const installDeps = asBoolean(parsed.flags["install-deps"]) ?? false;
|
|
1732
|
+
const skipDepCheck = asBoolean(parsed.flags["skip-dep-check"]) ?? false;
|
|
1370
1733
|
if (nonInteractive) {
|
|
1371
1734
|
const installDir2 = resolveInstallDir(assertNonEmpty(mustGetStringFlag(parsed.flags, "install-dir"), "install-dir"));
|
|
1372
1735
|
const host2 = assertNonEmpty(mustGetStringFlag(parsed.flags, "host"), "host");
|
|
@@ -1382,7 +1745,9 @@ async function buildInitConfig(parsed) {
|
|
|
1382
1745
|
autostart: autostart2,
|
|
1383
1746
|
serviceName: serviceName2,
|
|
1384
1747
|
force,
|
|
1385
|
-
nonInteractive
|
|
1748
|
+
nonInteractive,
|
|
1749
|
+
installDeps,
|
|
1750
|
+
skipDepCheck
|
|
1386
1751
|
};
|
|
1387
1752
|
}
|
|
1388
1753
|
const fallbackInstallDir = defaultInstallDir(process.platform);
|
|
@@ -1405,15 +1770,59 @@ async function buildInitConfig(parsed) {
|
|
|
1405
1770
|
autostart,
|
|
1406
1771
|
serviceName,
|
|
1407
1772
|
force,
|
|
1408
|
-
nonInteractive
|
|
1773
|
+
nonInteractive,
|
|
1774
|
+
installDeps,
|
|
1775
|
+
skipDepCheck
|
|
1409
1776
|
};
|
|
1410
1777
|
}
|
|
1778
|
+
async function handleDepFailure(dep, config, errorMessage) {
|
|
1779
|
+
const hint = await getInstallHintAsync(dep);
|
|
1780
|
+
const commands = dep === "bun" ? planBunInstall() : await planTmuxInstall();
|
|
1781
|
+
const plan = {
|
|
1782
|
+
dep,
|
|
1783
|
+
commands,
|
|
1784
|
+
requiredVersion: dep === "tmux" ? ">= 3.0" : ">= 1.3.0",
|
|
1785
|
+
issue: "missing"
|
|
1786
|
+
};
|
|
1787
|
+
if (config.installDeps || !config.nonInteractive) {
|
|
1788
|
+
const installed = await executeDependencyInstall(plan, {
|
|
1789
|
+
nonInteractive: config.nonInteractive,
|
|
1790
|
+
autoConfirm: config.installDeps && config.nonInteractive
|
|
1791
|
+
});
|
|
1792
|
+
if (installed)
|
|
1793
|
+
return;
|
|
1794
|
+
throw new Error(errorMessage);
|
|
1795
|
+
}
|
|
1796
|
+
throw new Error(`${errorMessage}
|
|
1797
|
+
${t("deps.install.hint", { command: hint })}`);
|
|
1798
|
+
}
|
|
1411
1799
|
async function runInit(parsed) {
|
|
1800
|
+
const manager = await detectServiceManager();
|
|
1801
|
+
if (manager === "none") {
|
|
1802
|
+
throw new Error(t("init.error.noServiceManager", { platform: process.platform }));
|
|
1803
|
+
}
|
|
1412
1804
|
const config = await buildInitConfig(parsed);
|
|
1805
|
+
if (!config.skipDepCheck) {
|
|
1806
|
+
const tmux = await checkTmuxVersion();
|
|
1807
|
+
if (!tmux.ok) {
|
|
1808
|
+
const reason = tmux.reason === "version-too-low" ? t("tmux.versionTooLow", { version: tmux.versionRaw || "" }) : t("tmux.notFound");
|
|
1809
|
+
await handleDepFailure("tmux", config, reason);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1413
1812
|
const explicitBunPath = readExplicitBunPath(parsed.flags);
|
|
1414
1813
|
const bun = await checkBunVersion(undefined, { explicitPath: explicitBunPath });
|
|
1415
1814
|
if (!bun.ok || !bun.path) {
|
|
1416
|
-
|
|
1815
|
+
const reason = bun.reason || t("bun.checkFailed");
|
|
1816
|
+
if (!config.skipDepCheck) {
|
|
1817
|
+
await handleDepFailure("bun", config, reason);
|
|
1818
|
+
const bunRetry = await checkBunVersion(undefined, { explicitPath: explicitBunPath });
|
|
1819
|
+
if (!bunRetry.ok || !bunRetry.path) {
|
|
1820
|
+
throw new Error(bunRetry.reason || t("bun.checkFailed"));
|
|
1821
|
+
}
|
|
1822
|
+
Object.assign(bun, bunRetry);
|
|
1823
|
+
} else {
|
|
1824
|
+
throw new Error(reason);
|
|
1825
|
+
}
|
|
1417
1826
|
}
|
|
1418
1827
|
if (!config.force && await directoryHasContent(config.installDir)) {
|
|
1419
1828
|
if (config.nonInteractive) {
|
|
@@ -1438,17 +1847,12 @@ async function runInit(parsed) {
|
|
|
1438
1847
|
});
|
|
1439
1848
|
await writeEnvFile(installLayout.envPath, envValues);
|
|
1440
1849
|
await writeRunScript(installLayout, bun.path);
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
installDir: config.installDir,
|
|
1448
|
-
runScriptPath: installLayout.runScriptPath,
|
|
1449
|
-
autostart: config.autostart
|
|
1450
|
-
});
|
|
1451
|
-
}
|
|
1850
|
+
await installService({
|
|
1851
|
+
serviceName: config.serviceName,
|
|
1852
|
+
installDir: config.installDir,
|
|
1853
|
+
runScriptPath: installLayout.runScriptPath,
|
|
1854
|
+
autostart: config.autostart
|
|
1855
|
+
});
|
|
1452
1856
|
const cliVersion = await readPackageVersion(packageLayout.packageRoot);
|
|
1453
1857
|
const meta = {
|
|
1454
1858
|
serviceName: config.serviceName,
|
|
@@ -1465,9 +1869,7 @@ async function runInit(parsed) {
|
|
|
1465
1869
|
console.log(`- ${t("init.summary.serviceName")}: ${config.serviceName}`);
|
|
1466
1870
|
console.log(`- ${t("init.summary.bun")}: ${bun.version} (${bun.path})`);
|
|
1467
1871
|
console.log(`- ${t("init.summary.autostart")}: ${config.autostart ? t("init.summary.autostart.on") : t("init.summary.autostart.off")}`);
|
|
1468
|
-
|
|
1469
|
-
console.log(`- ${t("init.summary.serviceHint")}: ${serviceHint(config.serviceName)}`);
|
|
1470
|
-
}
|
|
1872
|
+
console.log(`- ${t("init.summary.serviceHint")}: ${await serviceHint(config.serviceName)}`);
|
|
1471
1873
|
}
|
|
1472
1874
|
|
|
1473
1875
|
// src/commands/uninstall.ts
|
|
@@ -1592,7 +1994,10 @@ async function runUpgrade(parsed) {
|
|
|
1592
1994
|
metaBunPath: meta.bunPath
|
|
1593
1995
|
});
|
|
1594
1996
|
if (!bun.ok || !bun.path) {
|
|
1595
|
-
|
|
1997
|
+
const hint = getInstallHint("bun");
|
|
1998
|
+
const reason = bun.reason || t("bun.checkFailed");
|
|
1999
|
+
throw new Error(`${reason}
|
|
2000
|
+
${t("deps.install.hint", { command: hint })}`);
|
|
1596
2001
|
}
|
|
1597
2002
|
const packageLayout = await resolvePackageLayout(import.meta.url);
|
|
1598
2003
|
const backupDir = await mkdtemp(join6(tmpdir(), "tmex-upgrade-"));
|