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.
Files changed (82) hide show
  1. package/CHANGELOG.md +13 -19
  2. package/dist/cli-node.js +492 -87
  3. package/dist/runtime/server.js +431 -44
  4. package/package.json +1 -1
  5. package/resources/fe-dist/assets/{DevicePage-Cj9jwadE.js → DevicePage-kndrXVHE.js} +1 -1
  6. package/resources/fe-dist/assets/DevicesPage-CZLzOS04.js +1 -0
  7. package/resources/fe-dist/assets/{FilePage-CbHZVJbn.js → FilePage-CxVqdlbt.js} +1 -1
  8. package/resources/fe-dist/assets/{SettingsPage-CCV0NSQF.js → SettingsPage-C50JRe91.js} +1 -1
  9. package/resources/fe-dist/assets/{agent-tab-DPwxh08T.js → agent-tab-BVWxou-d.js} +1 -1
  10. package/resources/fe-dist/assets/{api-DlcFE5in.js → api-CrHrQflz.js} +1 -1
  11. package/resources/fe-dist/assets/{arc-Bzmm6y8F.js → arc-fsY_kDTa.js} +1 -1
  12. package/resources/fe-dist/assets/{architectureDiagram-3BPJPVTR-BdsN6XRe.js → architectureDiagram-3BPJPVTR-Vsg8Cd3e.js} +1 -1
  13. package/resources/fe-dist/assets/{blockDiagram-GPEHLZMM-CXdQhkqo.js → blockDiagram-GPEHLZMM-G7sC3DZO.js} +1 -1
  14. package/resources/fe-dist/assets/{c4Diagram-AAUBKEIU-CE_W2_pV.js → c4Diagram-AAUBKEIU-C1AfLOva.js} +1 -1
  15. package/resources/fe-dist/assets/{card-B_sl9KIo.js → card-CXa-2u8i.js} +1 -1
  16. package/resources/fe-dist/assets/channel-DS4kWkSb.js +1 -0
  17. package/resources/fe-dist/assets/{chunk-2J33WTMH-BAdEIJQO.js → chunk-2J33WTMH-C1p02eGI.js} +1 -1
  18. package/resources/fe-dist/assets/{chunk-4BX2VUAB-BZ0yj21c.js → chunk-4BX2VUAB-C8n2FURp.js} +1 -1
  19. package/resources/fe-dist/assets/{chunk-55IACEB6-BknLtkSo.js → chunk-55IACEB6-Da0nuNCD.js} +1 -1
  20. package/resources/fe-dist/assets/{chunk-727SXJPM-CxVON9n5.js → chunk-727SXJPM-C1TXyDEt.js} +1 -1
  21. package/resources/fe-dist/assets/{chunk-AQP2D5EJ-CLqOK2pR.js → chunk-AQP2D5EJ-CnRKlbXo.js} +1 -1
  22. package/resources/fe-dist/assets/{chunk-FMBD7UC4-Dt5Vot-R.js → chunk-FMBD7UC4-B8HGrJ3t.js} +1 -1
  23. package/resources/fe-dist/assets/{chunk-ND2GUHAM-t9HYlb8C.js → chunk-ND2GUHAM-CcN2Ou-o.js} +1 -1
  24. package/resources/fe-dist/assets/{chunk-QZHKN3VN-DZReNtLB.js → chunk-QZHKN3VN-DfWJ_tl5.js} +1 -1
  25. package/resources/fe-dist/assets/classDiagram-4FO5ZUOK-BfsrnUi9.js +1 -0
  26. package/resources/fe-dist/assets/classDiagram-v2-Q7XG4LA2-BfsrnUi9.js +1 -0
  27. package/resources/fe-dist/assets/{copy-Z9Fsj1r-.js → copy-BveLgJe5.js} +1 -1
  28. package/resources/fe-dist/assets/{cose-bilkent-S5V4N54A-BCTtGF2n.js → cose-bilkent-S5V4N54A-BkmsTzwU.js} +1 -1
  29. package/resources/fe-dist/assets/{dagre-BM42HDAG-BQ48EqSj.js → dagre-BM42HDAG-pGgtc4EE.js} +1 -1
  30. package/resources/fe-dist/assets/{diagram-2AECGRRQ-BXjt2moR.js → diagram-2AECGRRQ-DJxixZvm.js} +1 -1
  31. package/resources/fe-dist/assets/{diagram-5GNKFQAL-B2bvNTQO.js → diagram-5GNKFQAL-2ITViev7.js} +1 -1
  32. package/resources/fe-dist/assets/{diagram-KO2AKTUF-DLHWfbBH.js → diagram-KO2AKTUF-EMcadaeo.js} +1 -1
  33. package/resources/fe-dist/assets/{diagram-LMA3HP47-BCFT3Bur.js → diagram-LMA3HP47-w3sLU7zY.js} +1 -1
  34. package/resources/fe-dist/assets/{diagram-OG6HWLK6-bNZtxUJ2.js → diagram-OG6HWLK6-B4KGMQr7.js} +1 -1
  35. package/resources/fe-dist/assets/en_US-Chxeay8F.js +1 -0
  36. package/resources/fe-dist/assets/{erDiagram-TEJ5UH35-KoAzrx4j.js → erDiagram-TEJ5UH35-CB0P-Wz7.js} +1 -1
  37. package/resources/fe-dist/assets/{files-tab-DdBDTjfs.js → files-tab-lNenDPFy.js} +1 -1
  38. package/resources/fe-dist/assets/{flowDiagram-I6XJVG4X-C6wGExWt.js → flowDiagram-I6XJVG4X-CmBc7GAX.js} +1 -1
  39. package/resources/fe-dist/assets/{ganttDiagram-6RSMTGT7-BIWybjes.js → ganttDiagram-6RSMTGT7-CHNf7S_K.js} +1 -1
  40. package/resources/fe-dist/assets/{gitGraphDiagram-PVQCEYII-CD6tz24l.js → gitGraphDiagram-PVQCEYII-CabspIF1.js} +1 -1
  41. package/resources/fe-dist/assets/{index-CUCrFB_J.js → index-BQbq6q_-.js} +1 -1
  42. package/resources/fe-dist/assets/{index-CwH-l50P.js → index-DOaZjm8K.js} +79 -79
  43. package/resources/fe-dist/assets/index-DaOR3c0u.css +1 -0
  44. package/resources/fe-dist/assets/{infoDiagram-5YYISTIA-CrI-qlYw.js → infoDiagram-5YYISTIA-ny6wqvHH.js} +1 -1
  45. package/resources/fe-dist/assets/{ishikawaDiagram-YF4QCWOH-D9v5anxN.js → ishikawaDiagram-YF4QCWOH-EFCdGVsV.js} +1 -1
  46. package/resources/fe-dist/assets/ja_JP-BI-C8I9X.js +1 -0
  47. package/resources/fe-dist/assets/{journeyDiagram-JHISSGLW-BH38CiJ0.js → journeyDiagram-JHISSGLW-Cvi8jO9a.js} +1 -1
  48. package/resources/fe-dist/assets/{kanban-definition-UN3LZRKU-CKZ3_oXD.js → kanban-definition-UN3LZRKU-nKej8tSo.js} +1 -1
  49. package/resources/fe-dist/assets/{linear-C_oE01xA.js → linear-leOPyu29.js} +1 -1
  50. package/resources/fe-dist/assets/{markdown-preview-BtNbNwlP.js → markdown-preview-rqUSs5w8.js} +3 -3
  51. package/resources/fe-dist/assets/{mermaid.core-4dbvlhht.js → mermaid.core-QvhJBi8f.js} +5 -5
  52. package/resources/fe-dist/assets/{mindmap-definition-RKZ34NQL-C5o4mnnX.js → mindmap-definition-RKZ34NQL-xWje0B_-.js} +1 -1
  53. package/resources/fe-dist/assets/{pieDiagram-4H26LBE5-2ss3HotB.js → pieDiagram-4H26LBE5-DT1i53ec.js} +1 -1
  54. package/resources/fe-dist/assets/{quadrantDiagram-W4KKPZXB-Bolcfx55.js → quadrantDiagram-W4KKPZXB-C1zbS4Tx.js} +1 -1
  55. package/resources/fe-dist/assets/{requirementDiagram-4Y6WPE33-B-atSHRR.js → requirementDiagram-4Y6WPE33-By_eNRll.js} +1 -1
  56. package/resources/fe-dist/assets/{sankeyDiagram-5OEKKPKP-ByynRcAV.js → sankeyDiagram-5OEKKPKP-CG7jC05N.js} +1 -1
  57. package/resources/fe-dist/assets/{send-D0P5wgcD.js → send-CcFyyX2G.js} +1 -1
  58. package/resources/fe-dist/assets/{sequenceDiagram-3UESZ5HK-CNC9IxV4.js → sequenceDiagram-3UESZ5HK-C9yK_EKN.js} +1 -1
  59. package/resources/fe-dist/assets/{stateDiagram-AJRCARHV-CqzYDImp.js → stateDiagram-AJRCARHV-B-ROUwGP.js} +1 -1
  60. package/resources/fe-dist/assets/stateDiagram-v2-BHNVJYJU-iopOcale.js +1 -0
  61. package/resources/fe-dist/assets/{terminal-settings-panel-cjw9P_wY.js → terminal-settings-panel-CNwWqOMV.js} +1 -1
  62. package/resources/fe-dist/assets/{timeline-definition-PNZ67QCA-C38KIECs.js → timeline-definition-PNZ67QCA-yMM8VB4U.js} +1 -1
  63. package/resources/fe-dist/assets/{transfer-toast-5tb-zYhi.js → transfer-toast-mQs0hgyg.js} +1 -1
  64. package/resources/fe-dist/assets/{triangle-alert-CQzql6mY.js → triangle-alert-DMp7kMmJ.js} +1 -1
  65. package/resources/fe-dist/assets/{vennDiagram-CIIHVFJN-BJudKfzJ.js → vennDiagram-CIIHVFJN-CE08llZ3.js} +1 -1
  66. package/resources/fe-dist/assets/{wardley-L42UT6IY-B6p_luMy.js → wardley-L42UT6IY-BKLGCmLJ.js} +1 -1
  67. package/resources/fe-dist/assets/{wardleyDiagram-YWT4CUSO-DZJeOiWQ.js → wardleyDiagram-YWT4CUSO-BsjwfO3P.js} +1 -1
  68. package/resources/fe-dist/assets/{xychartDiagram-2RQKCTM6-DOMCute2.js → xychartDiagram-2RQKCTM6-Bhz6AI7w.js} +1 -1
  69. package/resources/fe-dist/assets/{zap-DWNtdoic.js → zap-DPwn8lbv.js} +1 -1
  70. package/resources/fe-dist/assets/zh_CN-DE-BaQ3P.js +1 -0
  71. package/resources/fe-dist/index.html +2 -2
  72. package/resources/gateway-drizzle/0011_stormy_sauron.sql +1 -0
  73. package/resources/gateway-drizzle/meta/_journal.json +7 -0
  74. package/resources/fe-dist/assets/DevicesPage-BbypaGC7.js +0 -1
  75. package/resources/fe-dist/assets/channel-CuapxAWT.js +0 -1
  76. package/resources/fe-dist/assets/classDiagram-4FO5ZUOK-C0k136Ud.js +0 -1
  77. package/resources/fe-dist/assets/classDiagram-v2-Q7XG4LA2-C0k136Ud.js +0 -1
  78. package/resources/fe-dist/assets/en_US-B07wSHDj.js +0 -1
  79. package/resources/fe-dist/assets/index-D-q7dOhH.css +0 -1
  80. package/resources/fe-dist/assets/ja_JP-DjF6Lrmx.js +0 -1
  81. package/resources/fe-dist/assets/stateDiagram-v2-BHNVJYJU-C3azxyWE.js +0 -1
  82. 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 readFile(filePath, "utf8");
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 readFile2, writeFile as writeFile2 } from "node:fs/promises";
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 readFile2(path, "utf8");
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 readFile3, writeFile as writeFile3 } from "node:fs/promises";
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 readFile3(path, "utf8");
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
- throw new Error(bun.reason || t("bun.checkFailed"));
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
- const manager = detectServiceManager();
1442
- if (manager === "none") {
1443
- console.warn(`[tmex] ${t("init.warning.noServiceManager", { platform: process.platform })}`);
1444
- } else {
1445
- await installService({
1446
- serviceName: config.serviceName,
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
- if (manager !== "none") {
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
- throw new Error(bun.reason || t("bun.checkFailed"));
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-"));