tmex-cli 0.12.3 → 0.12.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 (62) hide show
  1. package/CHANGELOG.md +11 -3
  2. package/dist/cli-node.js +287 -98
  3. package/dist/runtime/server.js +27 -15
  4. package/package.json +1 -1
  5. package/resources/fe-dist/assets/{DevicePage-EoPdR0a8.js → DevicePage-C4JS6baR.js} +2 -2
  6. package/resources/fe-dist/assets/{DevicesPage-DaO7f0iC.js → DevicesPage-p0CfWP1T.js} +2 -2
  7. package/resources/fe-dist/assets/{FilePage-BQsLFpIe.js → FilePage-D1t8_zlX.js} +2 -2
  8. package/resources/fe-dist/assets/{SettingsPage-DNq70CL7.js → SettingsPage-Cq6n8Xjc.js} +2 -2
  9. package/resources/fe-dist/assets/{arc-Bw8pvFsh.js → arc-Cv0H1Phu.js} +2 -2
  10. package/resources/fe-dist/assets/{architectureDiagram-3BPJPVTR-ClXPEeos.js → architectureDiagram-3BPJPVTR-DG7npZeK.js} +2 -2
  11. package/resources/fe-dist/assets/{blockDiagram-GPEHLZMM-Sp24bCmA.js → blockDiagram-GPEHLZMM-weB7PKUK.js} +2 -2
  12. package/resources/fe-dist/assets/{c4Diagram-AAUBKEIU-DjyOwpgl.js → c4Diagram-AAUBKEIU-C0sEabtp.js} +2 -2
  13. package/resources/fe-dist/assets/{card-CMyk5OFt.js → card-Spfb5xRZ.js} +2 -2
  14. package/resources/fe-dist/assets/channel-DDiDKfY0.js +2 -0
  15. package/resources/fe-dist/assets/{chunk-2J33WTMH-_i-tgxvN.js → chunk-2J33WTMH-BxOp2sIC.js} +2 -2
  16. package/resources/fe-dist/assets/{chunk-4BX2VUAB-CYDjMAJT.js → chunk-4BX2VUAB-bhg4hxVT.js} +2 -2
  17. package/resources/fe-dist/assets/{chunk-55IACEB6-CGcgNWHA.js → chunk-55IACEB6-v6jayOak.js} +2 -2
  18. package/resources/fe-dist/assets/{chunk-727SXJPM-CVF1o51L.js → chunk-727SXJPM-IY0KVws8.js} +2 -2
  19. package/resources/fe-dist/assets/{chunk-AQP2D5EJ-BzXiGlmX.js → chunk-AQP2D5EJ-CgwymoWa.js} +2 -2
  20. package/resources/fe-dist/assets/{chunk-FMBD7UC4-DG7P62CK.js → chunk-FMBD7UC4-DN35690f.js} +2 -2
  21. package/resources/fe-dist/assets/{chunk-ND2GUHAM-C82kWDQB.js → chunk-ND2GUHAM-lJmtxdhS.js} +2 -2
  22. package/resources/fe-dist/assets/{chunk-QZHKN3VN-FB0r6Osl.js → chunk-QZHKN3VN-DFeaB8rn.js} +2 -2
  23. package/resources/fe-dist/assets/classDiagram-4FO5ZUOK-CXFjdzka.js +2 -0
  24. package/resources/fe-dist/assets/classDiagram-v2-Q7XG4LA2-CXFjdzka.js +2 -0
  25. package/resources/fe-dist/assets/{cose-bilkent-S5V4N54A--v8cfu0h.js → cose-bilkent-S5V4N54A-Dw2yYQz4.js} +2 -2
  26. package/resources/fe-dist/assets/{dagre-BM42HDAG-BTLOD-KX.js → dagre-BM42HDAG-BeMjJ7ln.js} +2 -2
  27. package/resources/fe-dist/assets/{diagram-2AECGRRQ-29BFdqPY.js → diagram-2AECGRRQ-Dur8_CuN.js} +2 -2
  28. package/resources/fe-dist/assets/{diagram-5GNKFQAL-t_wHTq5Q.js → diagram-5GNKFQAL-GJc7gVa_.js} +2 -2
  29. package/resources/fe-dist/assets/{diagram-KO2AKTUF-JNh9Pmt9.js → diagram-KO2AKTUF-pxckXfjw.js} +2 -2
  30. package/resources/fe-dist/assets/{diagram-LMA3HP47-DAifhNss.js → diagram-LMA3HP47-B9E8-X7C.js} +2 -2
  31. package/resources/fe-dist/assets/{diagram-OG6HWLK6-BOqtQL5W.js → diagram-OG6HWLK6-DrhHw5wy.js} +2 -2
  32. package/resources/fe-dist/assets/{erDiagram-TEJ5UH35-W-aJKAOK.js → erDiagram-TEJ5UH35-DY9Wg1A1.js} +2 -2
  33. package/resources/fe-dist/assets/{flowDiagram-I6XJVG4X-DEt1TjbJ.js → flowDiagram-I6XJVG4X-eq3WE9d6.js} +2 -2
  34. package/resources/fe-dist/assets/{ganttDiagram-6RSMTGT7-CrYu9aI_.js → ganttDiagram-6RSMTGT7-BlXISXe1.js} +2 -2
  35. package/resources/fe-dist/assets/{gitGraphDiagram-PVQCEYII-BlltYekf.js → gitGraphDiagram-PVQCEYII-Bg-xHeIY.js} +2 -2
  36. package/resources/fe-dist/assets/{index-FNpsxYPN.js → index-CYmVLCAD.js} +3 -3
  37. package/resources/fe-dist/assets/{infoDiagram-5YYISTIA-nhdfbNPF.js → infoDiagram-5YYISTIA-CW2drAHG.js} +2 -2
  38. package/resources/fe-dist/assets/{ishikawaDiagram-YF4QCWOH-CwluZjLD.js → ishikawaDiagram-YF4QCWOH-Qchz0nqR.js} +2 -2
  39. package/resources/fe-dist/assets/{journeyDiagram-JHISSGLW-BkvLX_wz.js → journeyDiagram-JHISSGLW-Dk-UsexE.js} +2 -2
  40. package/resources/fe-dist/assets/{kanban-definition-UN3LZRKU-DU_bbXWH.js → kanban-definition-UN3LZRKU-DKeGp7b5.js} +2 -2
  41. package/resources/fe-dist/assets/{linear-ByJrBbSQ.js → linear-Bt4f_qDW.js} +2 -2
  42. package/resources/fe-dist/assets/{markdown-preview-D9O-wO1U.js → markdown-preview-DXv9GzaA.js} +4 -4
  43. package/resources/fe-dist/assets/{mermaid.core-DR6r9S_a.js → mermaid.core-B-wSkmMd.js} +6 -6
  44. package/resources/fe-dist/assets/{mindmap-definition-RKZ34NQL-BYXsfrSb.js → mindmap-definition-RKZ34NQL-DXBwCMXu.js} +2 -2
  45. package/resources/fe-dist/assets/{pieDiagram-4H26LBE5-leRJNTgW.js → pieDiagram-4H26LBE5--KxRgbWG.js} +2 -2
  46. package/resources/fe-dist/assets/{quadrantDiagram-W4KKPZXB-BfxS6APJ.js → quadrantDiagram-W4KKPZXB-DqXsCI9E.js} +2 -2
  47. package/resources/fe-dist/assets/{requirementDiagram-4Y6WPE33-D1ViYHCD.js → requirementDiagram-4Y6WPE33-C2KeTWqB.js} +2 -2
  48. package/resources/fe-dist/assets/{sankeyDiagram-5OEKKPKP-CFjujjK9.js → sankeyDiagram-5OEKKPKP-CC5tcr7d.js} +2 -2
  49. package/resources/fe-dist/assets/{sequenceDiagram-3UESZ5HK-DYKl7nA9.js → sequenceDiagram-3UESZ5HK-CqlMDtOG.js} +2 -2
  50. package/resources/fe-dist/assets/{stateDiagram-AJRCARHV-C3e0GwoJ.js → stateDiagram-AJRCARHV-CVcrlBiw.js} +2 -2
  51. package/resources/fe-dist/assets/stateDiagram-v2-BHNVJYJU-B0BDuZBM.js +2 -0
  52. package/resources/fe-dist/assets/{terminal-settings-panel-DLuBkEEZ.js → terminal-settings-panel-CYV9svzz.js} +2 -2
  53. package/resources/fe-dist/assets/{timeline-definition-PNZ67QCA-DO38TAIg.js → timeline-definition-PNZ67QCA-CEBskfkL.js} +2 -2
  54. package/resources/fe-dist/assets/{vennDiagram-CIIHVFJN-Co9hBQx5.js → vennDiagram-CIIHVFJN-CrxyVTtu.js} +2 -2
  55. package/resources/fe-dist/assets/{wardley-L42UT6IY-lf2QB-Cd.js → wardley-L42UT6IY-CSmsRb8z.js} +2 -2
  56. package/resources/fe-dist/assets/{wardleyDiagram-YWT4CUSO-fJgWAjlp.js → wardleyDiagram-YWT4CUSO-DoDzNeOA.js} +2 -2
  57. package/resources/fe-dist/assets/{xychartDiagram-2RQKCTM6-DdxhqP-F.js → xychartDiagram-2RQKCTM6-Dbq_OKsv.js} +2 -2
  58. package/resources/fe-dist/index.html +1 -1
  59. package/resources/fe-dist/assets/channel-B9WrZCGJ.js +0 -2
  60. package/resources/fe-dist/assets/classDiagram-4FO5ZUOK-B1-CPF_4.js +0 -2
  61. package/resources/fe-dist/assets/classDiagram-v2-Q7XG4LA2-B1-CPF_4.js +0 -2
  62. package/resources/fe-dist/assets/stateDiagram-v2-BHNVJYJU-Bj3cAhsB.js +0 -2
package/CHANGELOG.md CHANGED
@@ -1,4 +1,4 @@
1
- # 0.12.3
1
+ # 0.12.4
2
2
 
3
3
  _2026-06-15_
4
4
 
@@ -6,7 +6,11 @@ _2026-06-15_
6
6
 
7
7
  ### New
8
8
 
9
- - Custom terminal shortcuts: build your own quick-action bar for the terminal — add, remove, drag to reorder, rename, and capture key combinations by pressing them. Your shortcuts are saved on the server and shared across all your browsers. Built-in special actions (paste, toggle the text-input keyboard, start a new Agent session, scroll the terminal to the bottom) can be added too, and an optional icon mode shows keys as Apple-style symbols (⌃⇧⏎). A live preview lets you see the bar before you save.
9
+ - The Agent can now recognize and work with a coding agent running inside a terminal pane when an AI coding assistant is active in a pane, tmex's Agent understands it and can act on it.
10
+
11
+ ### Fixes
12
+
13
+ - More reliable Bun detection during install and startup (issue #28). You can now point tmex at a specific Bun with `--bun-path`, and that choice is remembered for later runs.
10
14
 
11
15
  ---
12
16
 
@@ -14,4 +18,8 @@ _2026-06-15_
14
18
 
15
19
  ### 新增
16
20
 
17
- - 自定义终端快捷键:可以自己定制终端的快捷按钮栏——增删、拖拽排序、重命名,还能直接按下组合键来录入。快捷键保存在服务器、多端共享。内置特殊动作(粘贴、切换文本框键盘、新建 Agent 会话、终端回到最下方)也能一键加入;并可选「图标模式」用苹果风格符号(⌃⇧⏎)显示按键。编辑时提供实时预览,保存后生效。
21
+ - Agent 现在能识别并配合在终端 pane 内运行的编码助手——当某个 pane 里有 AI 编码助手在运行时,tmex 的 Agent 能感知它并对其进行操作。
22
+
23
+ ### 修复
24
+
25
+ - 更可靠地检测 Bun(安装与启动时,issue #28);现在可以用 `--bun-path` 指定使用哪个 Bun,并会记住该选择供后续复用。
package/dist/cli-node.js CHANGED
@@ -29,9 +29,9 @@ var MESSAGES = {
29
29
  "cli.help": `tmex CLI
30
30
 
31
31
  Usage:
32
- tmex init [--no-interactive --install-dir <path> --host <host> --port <port> --db-path <path> --autostart <true|false>]
33
- tmex doctor [--install-dir <path>] [--json]
34
- tmex upgrade [--version <version>] [--install-dir <path>]
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>]
34
+ tmex upgrade [--version <version>] [--install-dir <path>] [--bun-path <path>]
35
35
  tmex uninstall [--install-dir <path>] [--yes] [--purge]
36
36
 
37
37
  Global flags:
@@ -52,6 +52,8 @@ Global flags:
52
52
  "bun.versionExecFailed": "Failed to execute bun --version. Please verify Bun installation.",
53
53
  "bun.versionTooLow": "Bun version too low: current {{version}}, required >= {{minVersion}}",
54
54
  "bun.checkFailed": "Bun check failed.",
55
+ "bun.explicitInvalid": "Specified bun path is invalid or not executable: {{path}}",
56
+ "bun.unsafePath": "Unsafe bun path (contains shell metacharacters): {{path}}",
55
57
  "service.install.unsupportedPlatform": "Automatic service installation is not supported on this platform: {{platform}}",
56
58
  "service.systemd.daemonReloadFailed": "systemctl daemon-reload failed: {{detail}}",
57
59
  "service.systemd.enableFailed": "systemctl enable failed: {{detail}}",
@@ -128,9 +130,9 @@ Global flags:
128
130
  "cli.help": `tmex CLI
129
131
 
130
132
  用法:
131
- tmex init [--no-interactive --install-dir <path> --host <host> --port <port> --db-path <path> --autostart <true|false>]
132
- tmex doctor [--install-dir <path>] [--json]
133
- tmex upgrade [--version <version>] [--install-dir <path>]
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>]
135
+ tmex upgrade [--version <version>] [--install-dir <path>] [--bun-path <path>]
134
136
  tmex uninstall [--install-dir <path>] [--yes] [--purge]
135
137
 
136
138
  全局参数:
@@ -151,6 +153,8 @@ Global flags:
151
153
  "bun.versionExecFailed": "无法执行 bun --version,请检查 Bun 安装是否完整。",
152
154
  "bun.versionTooLow": "Bun 版本过低:当前 {{version}},要求 >= {{minVersion}}",
153
155
  "bun.checkFailed": "Bun 检查失败。",
156
+ "bun.explicitInvalid": "指定的 bun 路径无效或不可执行:{{path}}",
157
+ "bun.unsafePath": "bun 路径包含 shell 特殊字符,不安全:{{path}}",
154
158
  "service.install.unsupportedPlatform": "当前平台不支持自动安装服务:{{platform}}",
155
159
  "service.systemd.daemonReloadFailed": "systemctl daemon-reload 失败:{{detail}}",
156
160
  "service.systemd.enableFailed": "systemctl enable 失败:{{detail}}",
@@ -259,7 +263,7 @@ function t(key, vars) {
259
263
  // src/lib/bun.ts
260
264
  import { existsSync } from "node:fs";
261
265
  import { homedir as homedir2 } from "node:os";
262
- import { join } from "node:path";
266
+ import { isAbsolute, join } from "node:path";
263
267
 
264
268
  // src/lib/process.ts
265
269
  import { spawn } from "node:child_process";
@@ -269,10 +273,21 @@ async function runCommand(command, args, options = {}) {
269
273
  const child = spawn(command, args, {
270
274
  cwd: options.cwd,
271
275
  env: options.env,
272
- stdio
276
+ stdio: stdio === "pipe" ? ["ignore", "pipe", "pipe"] : stdio
273
277
  });
274
278
  let stdout = "";
275
279
  let stderr = "";
280
+ let settled = false;
281
+ let timer = null;
282
+ const finish = (fn) => {
283
+ if (settled)
284
+ return;
285
+ settled = true;
286
+ if (timer) {
287
+ clearTimeout(timer);
288
+ }
289
+ fn();
290
+ };
276
291
  if (stdio === "pipe") {
277
292
  child.stdout?.on("data", (chunk) => {
278
293
  stdout += chunk.toString();
@@ -281,13 +296,21 @@ async function runCommand(command, args, options = {}) {
281
296
  stderr += chunk.toString();
282
297
  });
283
298
  }
284
- child.on("error", reject);
299
+ if (options.timeoutMs && options.timeoutMs > 0) {
300
+ timer = setTimeout(() => {
301
+ child.kill("SIGKILL");
302
+ finish(() => reject(new Error(`Command timed out after ${options.timeoutMs}ms: ${command}`)));
303
+ }, options.timeoutMs);
304
+ }
305
+ child.on("error", (error) => {
306
+ finish(() => reject(error));
307
+ });
285
308
  child.on("close", (code) => {
286
- resolve2({
309
+ finish(() => resolve2({
287
310
  code: code ?? 1,
288
311
  stdout,
289
312
  stderr
290
- });
313
+ }));
291
314
  });
292
315
  });
293
316
  }
@@ -316,63 +339,239 @@ function compareSemver(left, right) {
316
339
  return 0;
317
340
  }
318
341
 
342
+ // src/lib/validate.ts
343
+ function asString(value) {
344
+ if (typeof value === "string")
345
+ return value;
346
+ return;
347
+ }
348
+ function asBoolean(value) {
349
+ if (typeof value === "boolean")
350
+ return value;
351
+ if (typeof value !== "string")
352
+ return;
353
+ const normalized = value.trim().toLowerCase();
354
+ if (normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "y") {
355
+ return true;
356
+ }
357
+ if (normalized === "false" || normalized === "0" || normalized === "no" || normalized === "n") {
358
+ return false;
359
+ }
360
+ return;
361
+ }
362
+ function parsePort(value) {
363
+ const port = Number(value);
364
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
365
+ throw new Error(t("errors.validate.invalidPort", { value }));
366
+ }
367
+ return port;
368
+ }
369
+ function assertNonEmpty(value, fieldName) {
370
+ const trimmed = value.trim();
371
+ if (!trimmed) {
372
+ throw new Error(t("errors.validate.emptyField", { field: fieldName }));
373
+ }
374
+ return trimmed;
375
+ }
376
+
319
377
  // src/lib/bun.ts
320
- async function locateBunFromShell() {
321
- const result = await runCommand("zsh", ["-lic", "command -v bun"], { stdio: "pipe" }).catch(() => null);
378
+ var ESC = 27;
379
+ var CSI_OPEN = 91;
380
+ var OSC_OPEN = 93;
381
+ var BEL = 7;
382
+ var ST_TAIL = 92;
383
+ var LF = 10;
384
+ var CR = 13;
385
+ var DEL = 127;
386
+ var SLASH = 47;
387
+ var PROBE_TIMEOUT_MS = 5000;
388
+ function sanitizeBunPath(raw) {
389
+ let stripped = "";
390
+ for (let i = 0;i < raw.length; i += 1) {
391
+ const code = raw.charCodeAt(i);
392
+ if (code === ESC) {
393
+ const next = raw.charCodeAt(i + 1);
394
+ if (next === CSI_OPEN) {
395
+ i += 1;
396
+ while (i + 1 < raw.length) {
397
+ const c = raw.charCodeAt(i + 1);
398
+ i += 1;
399
+ if (c >= 64 && c <= 126) {
400
+ break;
401
+ }
402
+ }
403
+ } else if (next === OSC_OPEN) {
404
+ i += 1;
405
+ while (i + 1 < raw.length) {
406
+ const c = raw.charCodeAt(i + 1);
407
+ if (c === BEL) {
408
+ i += 1;
409
+ break;
410
+ }
411
+ if (c === ESC && raw.charCodeAt(i + 2) === ST_TAIL) {
412
+ i += 2;
413
+ break;
414
+ }
415
+ i += 1;
416
+ }
417
+ }
418
+ continue;
419
+ }
420
+ stripped += raw[i];
421
+ }
422
+ const lines = [];
423
+ let current = "";
424
+ const flush = () => {
425
+ let cleaned = "";
426
+ for (let i = 0;i < current.length; i += 1) {
427
+ const code = current.charCodeAt(i);
428
+ if (code <= 31 || code === DEL) {
429
+ continue;
430
+ }
431
+ cleaned += current[i];
432
+ }
433
+ cleaned = cleaned.trim();
434
+ if (cleaned.length > 0) {
435
+ lines.push(cleaned);
436
+ }
437
+ current = "";
438
+ };
439
+ for (let i = 0;i < stripped.length; i += 1) {
440
+ const code = stripped.charCodeAt(i);
441
+ if (code === LF || code === CR) {
442
+ flush();
443
+ continue;
444
+ }
445
+ current += stripped[i];
446
+ }
447
+ flush();
448
+ if (lines.length === 0) {
449
+ return "";
450
+ }
451
+ for (let i = lines.length - 1;i >= 0; i -= 1) {
452
+ if (lines[i].charCodeAt(0) === SLASH) {
453
+ return lines[i];
454
+ }
455
+ }
456
+ return lines[lines.length - 1];
457
+ }
458
+ function readExplicitBunPath(flags) {
459
+ return asString(flags["bun-path"]) || process.env.TMEX_BUN_PATH;
460
+ }
461
+ function isBunRuntime() {
462
+ const bunVersion = process.versions.bun;
463
+ return typeof bunVersion === "string" && bunVersion.length > 0;
464
+ }
465
+ function hardCandidatePaths() {
466
+ return [
467
+ join(homedir2(), ".bun", "bin", "bun"),
468
+ "/opt/homebrew/bin/bun",
469
+ "/usr/local/bin/bun",
470
+ "/home/linuxbrew/.linuxbrew/bin/bun"
471
+ ];
472
+ }
473
+ async function locateBunFromShellWith(shell) {
474
+ const result = await runCommand(shell, ["-lic", "command -v bun"], {
475
+ stdio: "pipe",
476
+ timeoutMs: PROBE_TIMEOUT_MS
477
+ }).catch(() => null);
322
478
  if (!result || result.code !== 0) {
323
479
  return null;
324
480
  }
325
- const bin = result.stdout.trim();
326
- if (!bin) {
481
+ const bin = sanitizeBunPath(result.stdout);
482
+ if (!bin || !isAbsolute(bin) || !existsSync(bin)) {
327
483
  return null;
328
484
  }
329
485
  return bin;
330
486
  }
331
- async function findBunBinary() {
332
- const zshBin = await locateBunFromShell();
333
- if (zshBin) {
334
- return zshBin;
335
- }
336
- const fallback = join(homedir2(), ".bun", "bin", "bun");
337
- if (existsSync(fallback)) {
338
- return fallback;
487
+ async function locateBunFromShell() {
488
+ const shells = [];
489
+ const envShell = process.env.SHELL?.trim();
490
+ if (envShell) {
491
+ shells.push(envShell);
492
+ }
493
+ for (const fallbackShell of ["zsh", "bash"]) {
494
+ if (!shells.includes(fallbackShell)) {
495
+ shells.push(fallbackShell);
496
+ }
339
497
  }
340
- const direct = await runCommand("bun", ["--version"], { stdio: "pipe" }).catch(() => null);
341
- if (direct?.code === 0) {
342
- return "bun";
498
+ for (const shell of shells) {
499
+ const found = await locateBunFromShellWith(shell);
500
+ if (found) {
501
+ return found;
502
+ }
343
503
  }
344
504
  return null;
345
505
  }
346
- async function checkBunVersion(minVersion = MIN_BUN_VERSION) {
347
- const bunPath = await findBunBinary();
348
- if (!bunPath) {
349
- return {
350
- ok: false,
351
- reason: t("bun.notFound")
352
- };
506
+ async function probeBunCandidates(metaBunPath) {
507
+ const candidates = [];
508
+ const add = (value) => {
509
+ if (!value) {
510
+ return;
511
+ }
512
+ const sanitized = sanitizeBunPath(value);
513
+ if (sanitized && !candidates.includes(sanitized)) {
514
+ candidates.push(sanitized);
515
+ }
516
+ };
517
+ if (isBunRuntime()) {
518
+ add(process.execPath);
353
519
  }
354
- const versionResult = await runCommand(bunPath, ["--version"], { stdio: "pipe" }).catch(() => null);
520
+ add(metaBunPath);
521
+ add(await locateBunFromShell());
522
+ candidates.push("bun");
523
+ for (const candidate of hardCandidatePaths()) {
524
+ add(candidate);
525
+ }
526
+ return candidates;
527
+ }
528
+ async function validateBunAt(candidate, minVersion) {
529
+ const versionResult = await runCommand(candidate, ["--version"], {
530
+ stdio: "pipe",
531
+ timeoutMs: PROBE_TIMEOUT_MS
532
+ }).catch(() => null);
355
533
  if (!versionResult || versionResult.code !== 0) {
356
- return {
357
- ok: false,
358
- reason: t("bun.versionExecFailed"),
359
- path: bunPath
360
- };
534
+ return { ok: false, path: candidate, reason: t("bun.versionExecFailed") };
361
535
  }
362
- const version = versionResult.stdout.trim();
536
+ const version = sanitizeBunPath(versionResult.stdout);
363
537
  if (compareSemver(version, minVersion) < 0) {
364
538
  return {
365
539
  ok: false,
366
- path: bunPath,
540
+ path: candidate,
367
541
  version,
368
542
  reason: t("bun.versionTooLow", { version, minVersion })
369
543
  };
370
544
  }
371
- return {
372
- ok: true,
373
- path: bunPath,
374
- version
375
- };
545
+ return { ok: true, path: candidate, version };
546
+ }
547
+ async function checkBunVersion(minVersion = MIN_BUN_VERSION, opts = {}) {
548
+ if (opts.explicitPath !== undefined && opts.explicitPath !== "") {
549
+ const explicit = sanitizeBunPath(opts.explicitPath);
550
+ if (!explicit || !isAbsolute(explicit) || !existsSync(explicit)) {
551
+ const reported = explicit || opts.explicitPath;
552
+ return {
553
+ ok: false,
554
+ path: reported,
555
+ reason: t("bun.explicitInvalid", { path: reported })
556
+ };
557
+ }
558
+ return await validateBunAt(explicit, minVersion);
559
+ }
560
+ const candidates = await probeBunCandidates(opts.metaBunPath);
561
+ let firstFailure = null;
562
+ for (const candidate of candidates) {
563
+ if (candidate !== "bun" && isAbsolute(candidate) && !existsSync(candidate)) {
564
+ continue;
565
+ }
566
+ const result = await validateBunAt(candidate, minVersion);
567
+ if (result.ok) {
568
+ return result;
569
+ }
570
+ if (!firstFailure) {
571
+ firstFailure = result;
572
+ }
573
+ }
574
+ return firstFailure ?? { ok: false, reason: t("bun.notFound") };
376
575
  }
377
576
 
378
577
  // src/lib/env-file.ts
@@ -768,41 +967,6 @@ function serviceHint(serviceName) {
768
967
  return t("service.hint.none");
769
968
  }
770
969
 
771
- // src/lib/validate.ts
772
- function asString(value) {
773
- if (typeof value === "string")
774
- return value;
775
- return;
776
- }
777
- function asBoolean(value) {
778
- if (typeof value === "boolean")
779
- return value;
780
- if (typeof value !== "string")
781
- return;
782
- const normalized = value.trim().toLowerCase();
783
- if (normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "y") {
784
- return true;
785
- }
786
- if (normalized === "false" || normalized === "0" || normalized === "no" || normalized === "n") {
787
- return false;
788
- }
789
- return;
790
- }
791
- function parsePort(value) {
792
- const port = Number(value);
793
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
794
- throw new Error(t("errors.validate.invalidPort", { value }));
795
- }
796
- return port;
797
- }
798
- function assertNonEmpty(value, fieldName) {
799
- const trimmed = value.trim();
800
- if (!trimmed) {
801
- throw new Error(t("errors.validate.emptyField", { field: fieldName }));
802
- }
803
- return trimmed;
804
- }
805
-
806
970
  // src/commands/doctor.ts
807
971
  async function checkCommandExists(bin, args, id, okMessage, failMessage) {
808
972
  const result = await runCommand(bin, args, { stdio: "pipe" }).catch(() => null);
@@ -843,7 +1007,12 @@ async function runDoctor(parsed) {
843
1007
  message: t("doctor.platform.supported", { platform: process.platform })
844
1008
  });
845
1009
  }
846
- const bun = await checkBunVersion();
1010
+ const explicitBunPath = readExplicitBunPath(parsed.flags);
1011
+ const meta = await pathExists(installLayout.metaPath) ? await readJsonFile(installLayout.metaPath).catch(() => null) : null;
1012
+ const bun = await checkBunVersion(undefined, {
1013
+ explicitPath: explicitBunPath,
1014
+ metaBunPath: meta?.bunPath
1015
+ });
847
1016
  if (bun.ok) {
848
1017
  checks.push({
849
1018
  id: "bun",
@@ -946,11 +1115,8 @@ async function runDoctor(parsed) {
946
1115
  });
947
1116
  }
948
1117
  let serviceName = asString(parsed.flags["service-name"]) || "tmex";
949
- if (await pathExists(installLayout.metaPath)) {
950
- const meta = await readJsonFile(installLayout.metaPath).catch(() => null);
951
- if (meta?.serviceName) {
952
- serviceName = meta.serviceName;
953
- }
1118
+ if (meta?.serviceName) {
1119
+ serviceName = meta.serviceName;
954
1120
  }
955
1121
  const status = await getServiceStatus(serviceName, installDir);
956
1122
  if (status.manager === "none") {
@@ -1008,12 +1174,13 @@ async function runDoctor(parsed) {
1008
1174
 
1009
1175
  // src/commands/init.ts
1010
1176
  import { readdir } from "node:fs/promises";
1011
- import { dirname as dirname4, resolve as resolve6 } from "node:path";
1177
+ import { dirname as dirname5, resolve as resolve6 } from "node:path";
1012
1178
 
1013
1179
  // src/lib/install.ts
1014
1180
  import { randomBytes } from "node:crypto";
1015
1181
  import { chmod, copyFile, rm as rm2 } from "node:fs/promises";
1016
- import { resolve as resolve5 } from "node:path";
1182
+ import { homedir as homedir4 } from "node:os";
1183
+ import { dirname as dirname4, isAbsolute as isAbsolute2, join as join4, resolve as resolve5 } from "node:path";
1017
1184
  function generateMasterKey() {
1018
1185
  return randomBytes(32).toString("base64");
1019
1186
  }
@@ -1050,6 +1217,20 @@ async function deployRuntimeFiles(packageLayout, installLayout) {
1050
1217
  await copyDirectory(packageLayout.resourceDrizzlePath, installLayout.drizzleDir);
1051
1218
  }
1052
1219
  async function writeRunScript(installLayout, bunPath) {
1220
+ for (let i = 0;i < bunPath.length; i += 1) {
1221
+ const code = bunPath.charCodeAt(i);
1222
+ if (code === 34 || code === 96 || code === 36 || code === 92 || code === 10 || code === 13) {
1223
+ throw new Error(t("bun.unsafePath", { path: bunPath }));
1224
+ }
1225
+ }
1226
+ const homeBunBin = join4(homedir4(), ".bun", "bin");
1227
+ const bunDir = isAbsolute2(bunPath) ? dirname4(bunPath) : "";
1228
+ const extraPathDirs = [
1229
+ bunDir,
1230
+ "/opt/homebrew/bin",
1231
+ "/usr/local/bin",
1232
+ "/home/linuxbrew/.linuxbrew/bin"
1233
+ ].filter((dir, index, arr) => dir.length > 0 && dir !== homeBunBin && arr.indexOf(dir) === index);
1053
1234
  const lines = [
1054
1235
  "#!/usr/bin/env bash",
1055
1236
  "set -euo pipefail",
@@ -1065,6 +1246,7 @@ async function writeRunScript(installLayout, bunPath) {
1065
1246
  'if [[ -n "${HOME:-}" ]] && [[ -d "${HOME}/.bun/bin" ]]; then',
1066
1247
  ' export PATH="${HOME}/.bun/bin:${PATH:-}"',
1067
1248
  "fi",
1249
+ `export PATH="${[...extraPathDirs, "${PATH:-}"].join(":")}"`,
1068
1250
  "",
1069
1251
  `export TMEX_FE_DIST_DIR="${installLayout.feDir}"`,
1070
1252
  `export TMEX_MIGRATIONS_DIR="${installLayout.drizzleDir}"`,
@@ -1154,9 +1336,9 @@ async function promptConfirm(ctx, message, defaultValue) {
1154
1336
  }
1155
1337
 
1156
1338
  // src/lib/version.ts
1157
- import { join as join4 } from "node:path";
1339
+ import { join as join5 } from "node:path";
1158
1340
  async function readPackageVersion(packageRoot) {
1159
- const pkg = await readJsonFile(join4(packageRoot, "package.json"));
1341
+ const pkg = await readJsonFile(join5(packageRoot, "package.json"));
1160
1342
  return pkg.version || "0.0.0";
1161
1343
  }
1162
1344
 
@@ -1228,7 +1410,8 @@ async function buildInitConfig(parsed) {
1228
1410
  }
1229
1411
  async function runInit(parsed) {
1230
1412
  const config = await buildInitConfig(parsed);
1231
- const bun = await checkBunVersion();
1413
+ const explicitBunPath = readExplicitBunPath(parsed.flags);
1414
+ const bun = await checkBunVersion(undefined, { explicitPath: explicitBunPath });
1232
1415
  if (!bun.ok || !bun.path) {
1233
1416
  throw new Error(bun.reason || t("bun.checkFailed"));
1234
1417
  }
@@ -1244,7 +1427,7 @@ async function runInit(parsed) {
1244
1427
  const packageLayout = await resolvePackageLayout(import.meta.url);
1245
1428
  const installLayout = createInstallLayout(config.installDir);
1246
1429
  await ensureInstallDir(config.installDir, config.force);
1247
- await ensureDir(dirname4(config.databasePath));
1430
+ await ensureDir(dirname5(config.databasePath));
1248
1431
  await deployRuntimeFiles(packageLayout, installLayout);
1249
1432
  const masterKey = generateMasterKey();
1250
1433
  const envValues = buildAppEnvValues({
@@ -1273,7 +1456,8 @@ async function runInit(parsed) {
1273
1456
  autostart: config.autostart,
1274
1457
  installDir: config.installDir,
1275
1458
  updatedAt: new Date().toISOString(),
1276
- cliVersion
1459
+ cliVersion,
1460
+ bunPath: bun.path
1277
1461
  };
1278
1462
  await writeInstallMeta(installLayout, meta);
1279
1463
  console.log(`[tmex] ${t("init.done")}`);
@@ -1345,7 +1529,7 @@ async function runUninstall(parsed) {
1345
1529
  // src/commands/upgrade.ts
1346
1530
  import { mkdtemp, rm as rm4 } from "node:fs/promises";
1347
1531
  import { tmpdir } from "node:os";
1348
- import { join as join5 } from "node:path";
1532
+ import { join as join6 } from "node:path";
1349
1533
  async function delegateUpgrade(parsed, targetVersion) {
1350
1534
  const args = ["--yes", `tmex-cli@${targetVersion}`, "upgrade", "--apply-current-package"];
1351
1535
  const passthrough = ["install-dir", "service-name", "yes", "lang"];
@@ -1401,13 +1585,17 @@ async function runUpgrade(parsed) {
1401
1585
  if (!await pathExists(installLayout.metaPath)) {
1402
1586
  throw new Error(t("upgrade.missingMeta", { path: installLayout.metaPath }));
1403
1587
  }
1404
- const bun = await checkBunVersion();
1588
+ const meta = await readJsonFile(installLayout.metaPath);
1589
+ const explicitBunPath = readExplicitBunPath(parsed.flags);
1590
+ const bun = await checkBunVersion(undefined, {
1591
+ explicitPath: explicitBunPath,
1592
+ metaBunPath: meta.bunPath
1593
+ });
1405
1594
  if (!bun.ok || !bun.path) {
1406
1595
  throw new Error(bun.reason || t("bun.checkFailed"));
1407
1596
  }
1408
- const meta = await readJsonFile(installLayout.metaPath);
1409
1597
  const packageLayout = await resolvePackageLayout(import.meta.url);
1410
- const backupDir = await mkdtemp(join5(tmpdir(), "tmex-upgrade-"));
1598
+ const backupDir = await mkdtemp(join6(tmpdir(), "tmex-upgrade-"));
1411
1599
  try {
1412
1600
  await stopService(meta.serviceName, installDir);
1413
1601
  await backupInstallArtifacts(installLayout, backupDir);
@@ -1416,6 +1604,7 @@ async function runUpgrade(parsed) {
1416
1604
  const cliVersion = await readPackageVersion(packageLayout.packageRoot);
1417
1605
  meta.updatedAt = new Date().toISOString();
1418
1606
  meta.cliVersion = cliVersion;
1607
+ meta.bunPath = bun.path;
1419
1608
  await writeInstallMeta(installLayout, meta);
1420
1609
  await installService({
1421
1610
  serviceName: meta.serviceName,