tmex-cli 0.2.5 → 0.3.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 (32) hide show
  1. package/dist/runtime/{cpufeatures-jxwx4v5j.node → cpufeatures-9rk1k79p.node} +0 -0
  2. package/dist/runtime/server.js +1821 -1303
  3. package/dist/runtime/{sshcrypto-sx6755rw.node → sshcrypto-z1hq11fy.node} +0 -0
  4. package/package.json +1 -1
  5. package/resources/fe-dist/assets/DevicePage-BE5Hlzny.js +26 -0
  6. package/resources/fe-dist/assets/DevicePage-BE5Hlzny.js.map +1 -0
  7. package/resources/fe-dist/assets/DevicesPage-BTB6mlBW.js +17 -0
  8. package/resources/fe-dist/assets/{DevicesPage-BrvmKNIQ.js.map → DevicesPage-BTB6mlBW.js.map} +1 -1
  9. package/resources/fe-dist/assets/SettingsPage-BDVq_Akt.js +17 -0
  10. package/resources/fe-dist/assets/{SettingsPage-XFRzVSzI.js.map → SettingsPage-BDVq_Akt.js.map} +1 -1
  11. package/resources/fe-dist/assets/ghostty-vt-BKQMf-5X.wasm +0 -0
  12. package/resources/fe-dist/assets/index-Chps0Pui.js +448 -0
  13. package/resources/fe-dist/assets/index-Chps0Pui.js.map +1 -0
  14. package/resources/fe-dist/assets/index-CyKyNcdz.css +1 -0
  15. package/resources/fe-dist/assets/select-7tfuDWw3.js +17 -0
  16. package/resources/fe-dist/assets/{select-CRYPgkuV.js.map → select-7tfuDWw3.js.map} +1 -1
  17. package/resources/fe-dist/assets/switch-CNHRGb7-.js +12 -0
  18. package/resources/fe-dist/assets/{switch-D216fzW5.js.map → switch-CNHRGb7-.js.map} +1 -1
  19. package/resources/fe-dist/assets/useValueChanged-Bi_NzPTr.js +7 -0
  20. package/resources/fe-dist/assets/{useValueChanged-CJav2OS1.js.map → useValueChanged-Bi_NzPTr.js.map} +1 -1
  21. package/resources/fe-dist/index.html +2 -2
  22. package/resources/fe-dist/assets/DevicePage-6GBZ9nXN.css +0 -32
  23. package/resources/fe-dist/assets/DevicePage-BOtxQsop.js +0 -267
  24. package/resources/fe-dist/assets/DevicePage-BOtxQsop.js.map +0 -1
  25. package/resources/fe-dist/assets/DevicesPage-BrvmKNIQ.js +0 -17
  26. package/resources/fe-dist/assets/SettingsPage-XFRzVSzI.js +0 -17
  27. package/resources/fe-dist/assets/index-CxzyWqMn.css +0 -1
  28. package/resources/fe-dist/assets/index-Vweko0vT.js +0 -200
  29. package/resources/fe-dist/assets/index-Vweko0vT.js.map +0 -1
  30. package/resources/fe-dist/assets/select-CRYPgkuV.js +0 -17
  31. package/resources/fe-dist/assets/switch-D216fzW5.js +0 -12
  32. package/resources/fe-dist/assets/useValueChanged-CJav2OS1.js +0 -7
@@ -5579,7 +5579,7 @@ var require_bcrypt_pbkdf = __commonJS((exports, module) => {
5579
5579
 
5580
5580
  // ../../node_modules/.bun/cpu-features@0.0.10/node_modules/cpu-features/build/Release/cpufeatures.node
5581
5581
  var require_cpufeatures = __commonJS((exports, module) => {
5582
- module.exports = __require("./cpufeatures-jxwx4v5j.node");
5582
+ module.exports = __require("./cpufeatures-9rk1k79p.node");
5583
5583
  });
5584
5584
 
5585
5585
  // ../../node_modules/.bun/cpu-features@0.0.10/node_modules/cpu-features/lib/index.js
@@ -6192,7 +6192,7 @@ var require_utils = __commonJS((exports, module) => {
6192
6192
 
6193
6193
  // ../../node_modules/.bun/ssh2@1.17.0/node_modules/ssh2/lib/protocol/crypto/build/Release/sshcrypto.node
6194
6194
  var require_sshcrypto = __commonJS((exports, module) => {
6195
- module.exports = __require("./sshcrypto-sx6755rw.node");
6195
+ module.exports = __require("./sshcrypto-z1hq11fy.node");
6196
6196
  });
6197
6197
 
6198
6198
  // ../../node_modules/.bun/ssh2@1.17.0/node_modules/ssh2/lib/protocol/crypto/poly1305.js
@@ -20282,7 +20282,7 @@ var require_lib3 = __commonJS((exports, module) => {
20282
20282
 
20283
20283
  // src/runtime/server.ts
20284
20284
  import { existsSync as existsSync3 } from "fs";
20285
- import { extname, join as join3, normalize, resolve as resolve2, sep } from "path";
20285
+ import { extname, join as join4, normalize, resolve as resolve2, sep } from "path";
20286
20286
 
20287
20287
  // ../../apps/gateway/src/crypto/errors.ts
20288
20288
  function contextLabel(context) {
@@ -20554,7 +20554,7 @@ Time: {{time}}`,
20554
20554
  connectionTimeout: "Connection timeout: Unable to connect to device. Please check network or firewall settings.",
20555
20555
  hostNotFound: "Host not found: Unable to resolve hostname. Please check DNS or hostname configuration.",
20556
20556
  handshakeFailed: "Handshake failed: Unable to establish secure connection. Possibly incompatible key exchange algorithm.",
20557
- tmuxUnavailable: "Remote tmux unavailable or failed to start. Please ensure tmux is installed and supports -CC mode.",
20557
+ tmuxUnavailable: "Remote tmux unavailable or failed to start. Please ensure tmux is installed and available in the remote shell PATH.",
20558
20558
  unknown: "Connection failed: {{message}}",
20559
20559
  reconnecting: "Connection interrupted, reconnecting in {{delay}} seconds ({{attempt}}/{{maxRetries}})",
20560
20560
  reconnectFailed: "Auto-reconnect failed, please retry manually",
@@ -20881,7 +20881,7 @@ Bot\uFF1A{{botName}}
20881
20881
  connectionTimeout: "\u8FDE\u63A5\u8D85\u65F6\uFF1A\u65E0\u6CD5\u8FDE\u63A5\u5230\u8BBE\u5907\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u6216\u9632\u706B\u5899\u8BBE\u7F6E",
20882
20882
  hostNotFound: "\u4E3B\u673A\u672A\u627E\u5230\uFF1A\u65E0\u6CD5\u89E3\u6790\u4E3B\u673A\u5730\u5740\uFF0C\u8BF7\u68C0\u67E5 DNS \u6216\u4E3B\u673A\u540D\u662F\u5426\u6B63\u786E",
20883
20883
  handshakeFailed: "\u63E1\u624B\u5931\u8D25\uFF1A\u65E0\u6CD5\u5EFA\u7ACB\u5B89\u5168\u8FDE\u63A5\uFF0C\u53EF\u80FD\u662F\u5BC6\u94A5\u4EA4\u6362\u7B97\u6CD5\u4E0D\u517C\u5BB9",
20884
- tmuxUnavailable: "\u8FDC\u7AEF tmux \u4E0D\u53EF\u7528\u6216\u542F\u52A8\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u8FDC\u7AEF\u662F\u5426\u5DF2\u5B89\u88C5 tmux \u4E14\u652F\u6301 -CC",
20884
+ tmuxUnavailable: "\u8FDC\u7AEF tmux \u4E0D\u53EF\u7528\u6216\u542F\u52A8\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u8FDC\u7AEF\u662F\u5426\u5DF2\u5B89\u88C5 tmux\uFF0C\u4E14\u8FDC\u7AEF shell PATH \u53EF\u627E\u5230 tmux",
20885
20885
  unknown: "\u8FDE\u63A5\u5931\u8D25\uFF1A{{message}}",
20886
20886
  reconnecting: "\u8FDE\u63A5\u4E2D\u65AD\uFF0C{{delay}} \u79D2\u540E\u81EA\u52A8\u91CD\u8FDE\uFF08{{attempt}}/{{maxRetries}}\uFF09",
20887
20887
  reconnectFailed: "\u81EA\u52A8\u91CD\u8FDE\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u91CD\u8BD5",
@@ -21208,7 +21208,7 @@ Bot\uFF1A{{botName}}
21208
21208
  connectionTimeout: "\u63A5\u7D9A\u30BF\u30A4\u30E0\u30A2\u30A6\u30C8\uFF1A\u30C7\u30D0\u30A4\u30B9\u306B\u63A5\u7D9A\u3067\u304D\u307E\u305B\u3093\u3002\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u307E\u305F\u306F\u30D5\u30A1\u30A4\u30A2\u30A6\u30A9\u30FC\u30EB\u8A2D\u5B9A\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
21209
21209
  hostNotFound: "\u30DB\u30B9\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\uFF1A\u30DB\u30B9\u30C8\u540D\u3092\u89E3\u6C7A\u3067\u304D\u307E\u305B\u3093\u3002DNS \u307E\u305F\u306F\u30DB\u30B9\u30C8\u540D\u8A2D\u5B9A\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
21210
21210
  handshakeFailed: "\u30CF\u30F3\u30C9\u30B7\u30A7\u30A4\u30AF\u306B\u5931\u6557\u3057\u307E\u3057\u305F\uFF1A\u5B89\u5168\u306A\u63A5\u7D9A\u3092\u78BA\u7ACB\u3067\u304D\u307E\u305B\u3093\u3002\u30AD\u30FC\u4EA4\u63DB\u30A2\u30EB\u30B4\u30EA\u30BA\u30E0\u304C\u4E92\u63DB\u6027\u304C\u306A\u3044\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002",
21211
- tmuxUnavailable: "\u30EA\u30E2\u30FC\u30C8 tmux \u304C\u5229\u7528\u3067\u304D\u306A\u3044\u304B\u8D77\u52D5\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002tmux \u304C\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3055\u308C -CC \u30E2\u30FC\u30C9\u3092\u30B5\u30DD\u30FC\u30C8\u3057\u3066\u3044\u308B\u3053\u3068\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
21211
+ tmuxUnavailable: "\u30EA\u30E2\u30FC\u30C8 tmux \u304C\u5229\u7528\u3067\u304D\u306A\u3044\u304B\u8D77\u52D5\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002tmux \u304C\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3055\u308C\u3001\u30EA\u30E2\u30FC\u30C8 shell \u306E PATH \u304B\u3089\u53C2\u7167\u3067\u304D\u308B\u3053\u3068\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
21212
21212
  unknown: "\u63A5\u7D9A\u306B\u5931\u6557\u3057\u307E\u3057\u305F\uFF1A{{message}}",
21213
21213
  reconnecting: "\u63A5\u7D9A\u304C\u4E2D\u65AD\u3055\u308C\u307E\u3057\u305F\u3002{{delay}} \u79D2\u5F8C\u306B\u518D\u63A5\u7D9A\u3057\u307E\u3059\uFF08{{attempt}}/{{maxRetries}}\uFF09",
21214
21214
  reconnectFailed: "\u81EA\u52D5\u518D\u63A5\u7D9A\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002\u624B\u52D5\u3067\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044",
@@ -22367,7 +22367,7 @@ async function decryptWithContext(ciphertext, context) {
22367
22367
  }
22368
22368
  }
22369
22369
 
22370
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/entity.js
22370
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/entity.js
22371
22371
  var entityKind = Symbol.for("drizzle:entityKind");
22372
22372
  var hasOwnEntityKind = Symbol.for("drizzle:hasOwnEntityKind");
22373
22373
  function is(value, type) {
@@ -22392,7 +22392,7 @@ function is(value, type) {
22392
22392
  return false;
22393
22393
  }
22394
22394
 
22395
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/column.js
22395
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/column.js
22396
22396
  class Column {
22397
22397
  constructor(table, config2) {
22398
22398
  this.table = table;
@@ -22442,7 +22442,7 @@ class Column {
22442
22442
  }
22443
22443
  }
22444
22444
 
22445
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/column-builder.js
22445
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/column-builder.js
22446
22446
  class ColumnBuilder {
22447
22447
  static [entityKind] = "ColumnBuilder";
22448
22448
  config;
@@ -22498,20 +22498,20 @@ class ColumnBuilder {
22498
22498
  }
22499
22499
  }
22500
22500
 
22501
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/table.utils.js
22501
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/table.utils.js
22502
22502
  var TableName = Symbol.for("drizzle:Name");
22503
22503
 
22504
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/tracing-utils.js
22504
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/tracing-utils.js
22505
22505
  function iife(fn, ...args) {
22506
22506
  return fn(...args);
22507
22507
  }
22508
22508
 
22509
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/pg-core/unique-constraint.js
22509
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/pg-core/unique-constraint.js
22510
22510
  function uniqueKeyName(table, columns) {
22511
22511
  return `${table[TableName]}_${columns.join("_")}_unique`;
22512
22512
  }
22513
22513
 
22514
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/pg-core/columns/common.js
22514
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/pg-core/columns/common.js
22515
22515
  class PgColumn extends Column {
22516
22516
  constructor(table, config2) {
22517
22517
  if (!config2.uniqueName) {
@@ -22560,7 +22560,7 @@ class ExtraConfigColumn extends PgColumn {
22560
22560
  }
22561
22561
  }
22562
22562
 
22563
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/pg-core/columns/enum.js
22563
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/pg-core/columns/enum.js
22564
22564
  class PgEnumObjectColumn extends PgColumn {
22565
22565
  static [entityKind] = "PgEnumObjectColumn";
22566
22566
  enum;
@@ -22590,7 +22590,7 @@ class PgEnumColumn extends PgColumn {
22590
22590
  }
22591
22591
  }
22592
22592
 
22593
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/subquery.js
22593
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/subquery.js
22594
22594
  class Subquery {
22595
22595
  static [entityKind] = "Subquery";
22596
22596
  constructor(sql, fields, alias, isWith = false, usedTables = []) {
@@ -22609,10 +22609,10 @@ class WithSubquery extends Subquery {
22609
22609
  static [entityKind] = "WithSubquery";
22610
22610
  }
22611
22611
 
22612
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/version.js
22612
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/version.js
22613
22613
  var version = "0.45.1";
22614
22614
 
22615
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/tracing.js
22615
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/tracing.js
22616
22616
  var otel;
22617
22617
  var rawTracer;
22618
22618
  var tracer = {
@@ -22639,10 +22639,10 @@ var tracer = {
22639
22639
  }
22640
22640
  };
22641
22641
 
22642
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/view-common.js
22642
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/view-common.js
22643
22643
  var ViewBaseConfig = Symbol.for("drizzle:ViewBaseConfig");
22644
22644
 
22645
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/table.js
22645
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/table.js
22646
22646
  var Schema = Symbol.for("drizzle:Schema");
22647
22647
  var Columns = Symbol.for("drizzle:Columns");
22648
22648
  var ExtraConfigColumns = Symbol.for("drizzle:ExtraConfigColumns");
@@ -22686,7 +22686,7 @@ function getTableUniqueName(table) {
22686
22686
  return `${table[Schema] ?? "public"}.${table[TableName]}`;
22687
22687
  }
22688
22688
 
22689
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sql/sql.js
22689
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sql/sql.js
22690
22690
  function isSQLWrapper(value) {
22691
22691
  return value !== null && value !== undefined && typeof value.getSQL === "function";
22692
22692
  }
@@ -23066,7 +23066,7 @@ Subquery.prototype.getSQL = function() {
23066
23066
  return new SQL([this]);
23067
23067
  };
23068
23068
 
23069
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/alias.js
23069
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/alias.js
23070
23070
  class ColumnAliasProxyHandler {
23071
23071
  constructor(table) {
23072
23072
  this.table = table;
@@ -23145,7 +23145,7 @@ function mapColumnsInSQLToAlias(query, alias) {
23145
23145
  }));
23146
23146
  }
23147
23147
 
23148
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/errors.js
23148
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/errors.js
23149
23149
  class DrizzleError extends Error {
23150
23150
  static [entityKind] = "DrizzleError";
23151
23151
  constructor({ message, cause }) {
@@ -23175,7 +23175,7 @@ class TransactionRollbackError extends DrizzleError {
23175
23175
  }
23176
23176
  }
23177
23177
 
23178
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/logger.js
23178
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/logger.js
23179
23179
  class ConsoleLogWriter {
23180
23180
  static [entityKind] = "ConsoleLogWriter";
23181
23181
  write(message) {
@@ -23207,7 +23207,7 @@ class NoopLogger {
23207
23207
  logQuery() {}
23208
23208
  }
23209
23209
 
23210
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/query-promise.js
23210
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/query-promise.js
23211
23211
  class QueryPromise {
23212
23212
  static [entityKind] = "QueryPromise";
23213
23213
  [Symbol.toStringTag] = "QueryPromise";
@@ -23228,7 +23228,7 @@ class QueryPromise {
23228
23228
  }
23229
23229
  }
23230
23230
 
23231
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/utils.js
23231
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/utils.js
23232
23232
  function mapResultRow(columns, row, joinsNotNullableMap) {
23233
23233
  const nullifyMap = {};
23234
23234
  const result = columns.reduce((result2, { path, field }, columnIndex) => {
@@ -23382,7 +23382,7 @@ function isConfig(data) {
23382
23382
  }
23383
23383
  var textDecoder = typeof TextDecoder === "undefined" ? null : new TextDecoder;
23384
23384
 
23385
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/pg-core/table.js
23385
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/pg-core/table.js
23386
23386
  var InlineForeignKeys = Symbol.for("drizzle:PgInlineForeignKeys");
23387
23387
  var EnableRLS = Symbol.for("drizzle:EnableRLS");
23388
23388
 
@@ -23398,7 +23398,7 @@ class PgTable extends Table {
23398
23398
  [Table.Symbol.ExtraConfigColumns] = {};
23399
23399
  }
23400
23400
 
23401
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/pg-core/primary-keys.js
23401
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/pg-core/primary-keys.js
23402
23402
  class PrimaryKeyBuilder {
23403
23403
  static [entityKind] = "PgPrimaryKeyBuilder";
23404
23404
  columns;
@@ -23426,7 +23426,7 @@ class PrimaryKey {
23426
23426
  }
23427
23427
  }
23428
23428
 
23429
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sql/expressions/conditions.js
23429
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sql/expressions/conditions.js
23430
23430
  function bindIfParam(value, column) {
23431
23431
  if (isDriverValueEncoder(column) && !isSQLWrapper(value) && !is(value, Param) && !is(value, Placeholder) && !is(value, Column) && !is(value, Table) && !is(value, View)) {
23432
23432
  return new Param(value, column);
@@ -23531,7 +23531,7 @@ function notIlike(column, value) {
23531
23531
  return sql`${column} not ilike ${value}`;
23532
23532
  }
23533
23533
 
23534
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sql/expressions/select.js
23534
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sql/expressions/select.js
23535
23535
  function asc(column) {
23536
23536
  return sql`${column} asc`;
23537
23537
  }
@@ -23539,7 +23539,7 @@ function desc(column) {
23539
23539
  return sql`${column} desc`;
23540
23540
  }
23541
23541
 
23542
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/relations.js
23542
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/relations.js
23543
23543
  class Relation {
23544
23544
  constructor(sourceTable, referencedTable, relationName) {
23545
23545
  this.sourceTable = sourceTable;
@@ -23759,7 +23759,7 @@ function mapRelationalRow(tablesConfig, tableConfig, row, buildQueryResultSelect
23759
23759
  return result;
23760
23760
  }
23761
23761
 
23762
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sql/functions/aggregate.js
23762
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sql/functions/aggregate.js
23763
23763
  function count(expression) {
23764
23764
  return sql`count(${expression || sql.raw("*")})`.mapWith(Number);
23765
23765
  }
@@ -26183,10 +26183,10 @@ var t2 = instance.t.bind(instance);
26183
26183
  // ../../apps/gateway/src/db/client.ts
26184
26184
  import { Database as Database2 } from "bun:sqlite";
26185
26185
 
26186
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/bun-sqlite/driver.js
26186
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/bun-sqlite/driver.js
26187
26187
  import { Database } from "bun:sqlite";
26188
26188
 
26189
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/selection-proxy.js
26189
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/selection-proxy.js
26190
26190
  class SelectionProxyHandler {
26191
26191
  static [entityKind] = "SelectionProxyHandler";
26192
26192
  config;
@@ -26238,7 +26238,7 @@ class SelectionProxyHandler {
26238
26238
  }
26239
26239
  }
26240
26240
 
26241
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/foreign-keys.js
26241
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/foreign-keys.js
26242
26242
  class ForeignKeyBuilder {
26243
26243
  static [entityKind] = "SQLiteForeignKeyBuilder";
26244
26244
  reference;
@@ -26292,7 +26292,7 @@ class ForeignKey {
26292
26292
  }
26293
26293
  }
26294
26294
 
26295
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/unique-constraint.js
26295
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/unique-constraint.js
26296
26296
  function uniqueKeyName2(table, columns) {
26297
26297
  return `${table[TableName]}_${columns.join("_")}_unique`;
26298
26298
  }
@@ -26337,7 +26337,7 @@ class UniqueConstraint {
26337
26337
  }
26338
26338
  }
26339
26339
 
26340
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/columns/common.js
26340
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/columns/common.js
26341
26341
  class SQLiteColumnBuilder extends ColumnBuilder {
26342
26342
  static [entityKind] = "SQLiteColumnBuilder";
26343
26343
  foreignKeyConfigs = [];
@@ -26388,7 +26388,7 @@ class SQLiteColumn extends Column {
26388
26388
  static [entityKind] = "SQLiteColumn";
26389
26389
  }
26390
26390
 
26391
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/columns/blob.js
26391
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/columns/blob.js
26392
26392
  class SQLiteBigIntBuilder extends SQLiteColumnBuilder {
26393
26393
  static [entityKind] = "SQLiteBigIntBuilder";
26394
26394
  constructor(name) {
@@ -26476,7 +26476,7 @@ function blob(a, b3) {
26476
26476
  return new SQLiteBlobBufferBuilder(name);
26477
26477
  }
26478
26478
 
26479
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/columns/custom.js
26479
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/columns/custom.js
26480
26480
  class SQLiteCustomColumnBuilder extends SQLiteColumnBuilder {
26481
26481
  static [entityKind] = "SQLiteCustomColumnBuilder";
26482
26482
  constructor(name, fieldConfig, customTypeParams) {
@@ -26517,7 +26517,7 @@ function customType(customTypeParams) {
26517
26517
  };
26518
26518
  }
26519
26519
 
26520
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/columns/integer.js
26520
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/columns/integer.js
26521
26521
  class SQLiteBaseIntegerBuilder extends SQLiteColumnBuilder {
26522
26522
  static [entityKind] = "SQLiteBaseIntegerBuilder";
26523
26523
  constructor(name, dataType, columnType) {
@@ -26619,7 +26619,7 @@ function integer(a, b3) {
26619
26619
  return new SQLiteIntegerBuilder(name);
26620
26620
  }
26621
26621
 
26622
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/columns/numeric.js
26622
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/columns/numeric.js
26623
26623
  class SQLiteNumericBuilder extends SQLiteColumnBuilder {
26624
26624
  static [entityKind] = "SQLiteNumericBuilder";
26625
26625
  constructor(name) {
@@ -26689,7 +26689,7 @@ function numeric(a, b3) {
26689
26689
  return mode === "number" ? new SQLiteNumericNumberBuilder(name) : mode === "bigint" ? new SQLiteNumericBigIntBuilder(name) : new SQLiteNumericBuilder(name);
26690
26690
  }
26691
26691
 
26692
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/columns/real.js
26692
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/columns/real.js
26693
26693
  class SQLiteRealBuilder extends SQLiteColumnBuilder {
26694
26694
  static [entityKind] = "SQLiteRealBuilder";
26695
26695
  constructor(name) {
@@ -26710,7 +26710,7 @@ function real(name) {
26710
26710
  return new SQLiteRealBuilder(name ?? "");
26711
26711
  }
26712
26712
 
26713
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/columns/text.js
26713
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/columns/text.js
26714
26714
  class SQLiteTextBuilder extends SQLiteColumnBuilder {
26715
26715
  static [entityKind] = "SQLiteTextBuilder";
26716
26716
  constructor(name, config2) {
@@ -26765,7 +26765,7 @@ function text(a, b3 = {}) {
26765
26765
  return new SQLiteTextBuilder(name, config2);
26766
26766
  }
26767
26767
 
26768
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/columns/all.js
26768
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/columns/all.js
26769
26769
  function getSQLiteColumnBuilders() {
26770
26770
  return {
26771
26771
  blob,
@@ -26777,7 +26777,7 @@ function getSQLiteColumnBuilders() {
26777
26777
  };
26778
26778
  }
26779
26779
 
26780
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/table.js
26780
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/table.js
26781
26781
  var InlineForeignKeys2 = Symbol.for("drizzle:SQLiteInlineForeignKeys");
26782
26782
 
26783
26783
  class SQLiteTable extends Table {
@@ -26811,7 +26811,7 @@ var sqliteTable = (name, columns, extraConfig) => {
26811
26811
  return sqliteTableBase(name, columns, extraConfig);
26812
26812
  };
26813
26813
 
26814
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/checks.js
26814
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/checks.js
26815
26815
  class CheckBuilder {
26816
26816
  constructor(name, value) {
26817
26817
  this.name = name;
@@ -26838,7 +26838,7 @@ function check(name, value) {
26838
26838
  return new CheckBuilder(name, value);
26839
26839
  }
26840
26840
 
26841
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/utils.js
26841
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/utils.js
26842
26842
  function extractUsedTable(table) {
26843
26843
  if (is(table, SQLiteTable)) {
26844
26844
  return [`${table[Table.Symbol.BaseName]}`];
@@ -26852,7 +26852,7 @@ function extractUsedTable(table) {
26852
26852
  return [];
26853
26853
  }
26854
26854
 
26855
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/query-builders/delete.js
26855
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/query-builders/delete.js
26856
26856
  class SQLiteDeleteBase extends QueryPromise {
26857
26857
  constructor(table, session, dialect, withList) {
26858
26858
  super();
@@ -26922,7 +26922,7 @@ class SQLiteDeleteBase extends QueryPromise {
26922
26922
  }
26923
26923
  }
26924
26924
 
26925
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/casing.js
26925
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/casing.js
26926
26926
  function toSnakeCase(input) {
26927
26927
  const words = input.replace(/['\u2019]/g, "").match(/[\da-z]+|[A-Z]+(?![a-z])|[A-Z][\da-z]+/g) ?? [];
26928
26928
  return words.map((word) => word.toLowerCase()).join("_");
@@ -26975,12 +26975,12 @@ class CasingCache {
26975
26975
  }
26976
26976
  }
26977
26977
 
26978
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/view-base.js
26978
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/view-base.js
26979
26979
  class SQLiteViewBase extends View {
26980
26980
  static [entityKind] = "SQLiteViewBase";
26981
26981
  }
26982
26982
 
26983
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/dialect.js
26983
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/dialect.js
26984
26984
  class SQLiteDialect {
26985
26985
  static [entityKind] = "SQLiteDialect";
26986
26986
  casing;
@@ -27530,7 +27530,7 @@ class SQLiteSyncDialect extends SQLiteDialect {
27530
27530
  }
27531
27531
  }
27532
27532
 
27533
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/query-builders/query-builder.js
27533
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/query-builders/query-builder.js
27534
27534
  class TypedQueryBuilder {
27535
27535
  static [entityKind] = "TypedQueryBuilder";
27536
27536
  getSelectedFields() {
@@ -27538,7 +27538,7 @@ class TypedQueryBuilder {
27538
27538
  }
27539
27539
  }
27540
27540
 
27541
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/query-builders/select.js
27541
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/query-builders/select.js
27542
27542
  class SQLiteSelectBuilder {
27543
27543
  static [entityKind] = "SQLiteSelectBuilder";
27544
27544
  fields;
@@ -27836,7 +27836,7 @@ var unionAll = createSetOperator("union", true);
27836
27836
  var intersect = createSetOperator("intersect", false);
27837
27837
  var except = createSetOperator("except", false);
27838
27838
 
27839
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/query-builders/query-builder.js
27839
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/query-builders/query-builder.js
27840
27840
  class QueryBuilder {
27841
27841
  static [entityKind] = "SQLiteQueryBuilder";
27842
27842
  dialect;
@@ -27895,7 +27895,7 @@ class QueryBuilder {
27895
27895
  }
27896
27896
  }
27897
27897
 
27898
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/query-builders/insert.js
27898
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/query-builders/insert.js
27899
27899
  class SQLiteInsertBuilder {
27900
27900
  constructor(table, session, dialect, withList) {
27901
27901
  this.table = table;
@@ -28004,7 +28004,7 @@ class SQLiteInsertBase extends QueryPromise {
28004
28004
  }
28005
28005
  }
28006
28006
 
28007
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/query-builders/update.js
28007
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/query-builders/update.js
28008
28008
  class SQLiteUpdateBuilder {
28009
28009
  constructor(table, session, dialect, withList) {
28010
28010
  this.table = table;
@@ -28108,7 +28108,7 @@ class SQLiteUpdateBase extends QueryPromise {
28108
28108
  }
28109
28109
  }
28110
28110
 
28111
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/query-builders/count.js
28111
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/query-builders/count.js
28112
28112
  class SQLiteCountBuilder extends SQL {
28113
28113
  constructor(params) {
28114
28114
  super(SQLiteCountBuilder.buildEmbeddedCount(params.source, params.filters).queryChunks);
@@ -28143,7 +28143,7 @@ class SQLiteCountBuilder extends SQL {
28143
28143
  }
28144
28144
  }
28145
28145
 
28146
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/query-builders/query.js
28146
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/query-builders/query.js
28147
28147
  class RelationalQueryBuilder {
28148
28148
  constructor(mode, fullSchema, schema, tableNamesMap, table, tableConfig, dialect, session) {
28149
28149
  this.mode = mode;
@@ -28237,7 +28237,7 @@ class SQLiteSyncRelationalQuery extends SQLiteRelationalQuery {
28237
28237
  }
28238
28238
  }
28239
28239
 
28240
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/query-builders/raw.js
28240
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/query-builders/raw.js
28241
28241
  class SQLiteRaw extends QueryPromise {
28242
28242
  constructor(execute, getSQL, action, dialect, mapBatchResult) {
28243
28243
  super();
@@ -28263,7 +28263,7 @@ class SQLiteRaw extends QueryPromise {
28263
28263
  }
28264
28264
  }
28265
28265
 
28266
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/db.js
28266
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/db.js
28267
28267
  class BaseSQLiteDatabase {
28268
28268
  constructor(resultKind, dialect, session, schema) {
28269
28269
  this.resultKind = resultKind;
@@ -28386,7 +28386,7 @@ class BaseSQLiteDatabase {
28386
28386
  }
28387
28387
  }
28388
28388
 
28389
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/cache/core/cache.js
28389
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/cache/core/cache.js
28390
28390
  class Cache {
28391
28391
  static [entityKind] = "Cache";
28392
28392
  }
@@ -28412,7 +28412,7 @@ async function hashQuery(sql2, params) {
28412
28412
  return hashHex;
28413
28413
  }
28414
28414
 
28415
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/session.js
28415
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/session.js
28416
28416
  class ExecuteResultSync extends QueryPromise {
28417
28417
  constructor(resultCb) {
28418
28418
  super();
@@ -28585,7 +28585,7 @@ class SQLiteTransaction extends BaseSQLiteDatabase {
28585
28585
  }
28586
28586
  }
28587
28587
 
28588
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/bun-sqlite/session.js
28588
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/bun-sqlite/session.js
28589
28589
  class SQLiteBunSession extends SQLiteSession {
28590
28590
  constructor(client, dialect, schema, options = {}) {
28591
28591
  super(dialect);
@@ -28684,7 +28684,7 @@ class PreparedQuery extends SQLitePreparedQuery {
28684
28684
  }
28685
28685
  }
28686
28686
 
28687
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/bun-sqlite/driver.js
28687
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/bun-sqlite/driver.js
28688
28688
  class BunSQLiteDatabase extends BaseSQLiteDatabase {
28689
28689
  static [entityKind] = "BunSQLiteDatabase";
28690
28690
  }
@@ -51863,8 +51863,8 @@ class EventNotifier {
51863
51863
  }
51864
51864
  var eventNotifier = new EventNotifier;
51865
51865
 
51866
- // ../../apps/gateway/src/tmux/connection.ts
51867
- var import_ssh2 = __toESM(require_lib3(), 1);
51866
+ // ../../apps/gateway/src/tmux-client/local-external-connection.ts
51867
+ import { mkdirSync, rmSync } from "fs";
51868
51868
  import { homedir } from "os";
51869
51869
 
51870
51870
  // ../../apps/gateway/src/tmux/local-shell-path.ts
@@ -52009,678 +52009,965 @@ function getLocalShellPath() {
52009
52009
  return defaultLocalShellPathCache.get();
52010
52010
  }
52011
52011
  function buildLocalTmuxEnv(resolvedPath, baseEnv = process.env) {
52012
- if (!resolvedPath) {
52013
- return { ...baseEnv };
52012
+ const nextEnv = {
52013
+ ...baseEnv
52014
+ };
52015
+ if (resolvedPath) {
52016
+ nextEnv.PATH = resolvedPath;
52017
+ }
52018
+ if (!nextEnv.LC_CTYPE && !nextEnv.LC_ALL && !nextEnv.LANG) {
52019
+ nextEnv.LC_CTYPE = "en_US.UTF-8";
52014
52020
  }
52021
+ return nextEnv;
52022
+ }
52023
+
52024
+ // ../../apps/gateway/src/tmux-client/command-builder.ts
52025
+ function quoteShellArg(value) {
52026
+ return `'${value.replaceAll("'", "'\\''")}'`;
52027
+ }
52028
+ function joinShellArgs(argv) {
52029
+ return argv.map((arg) => quoteShellArg(arg)).join(" ");
52030
+ }
52031
+
52032
+ // ../../apps/gateway/src/tmux-client/fs-paths.ts
52033
+ import { randomUUID as randomUUID2 } from "crypto";
52034
+ import { join as join3 } from "path";
52035
+ var DEFAULT_ROOT_DIR = "/tmp/tmex";
52036
+ var DEFAULT_GATEWAY_RUNTIME_ID = randomUUID2();
52037
+ function toSafePathSegment(value) {
52038
+ return Array.from(value).map((char) => {
52039
+ if (/^[A-Za-z0-9._-]$/.test(char)) {
52040
+ return char;
52041
+ }
52042
+ return `_${char.codePointAt(0)?.toString(16) ?? "00"}_`;
52043
+ }).join("");
52044
+ }
52045
+ function createRuntimeFsPaths(options) {
52046
+ const baseRootDir = options.rootDir ?? DEFAULT_ROOT_DIR;
52047
+ const safeSessionName = toSafePathSegment(options.sessionName?.trim() || "tmex");
52048
+ const safeGatewayRuntimeId = toSafePathSegment(options.gatewayRuntimeId?.trim() || DEFAULT_GATEWAY_RUNTIME_ID);
52049
+ const runtimeDirName = `${toSafePathSegment(options.deviceId)}-${safeGatewayRuntimeId}-${options.gatewayPid}`;
52050
+ const runtimeRootDir = join3(baseRootDir, runtimeDirName);
52051
+ const panesDir = join3(runtimeRootDir, "panes");
52052
+ const hooksDir = join3(runtimeRootDir, "hooks");
52015
52053
  return {
52016
- ...baseEnv,
52017
- PATH: resolvedPath
52054
+ rootDir: runtimeRootDir,
52055
+ panesDir,
52056
+ hooksDir,
52057
+ hookFifoPath: join3(hooksDir, "events.fifo"),
52058
+ paneFifoPath(paneId) {
52059
+ return join3(panesDir, `${safeSessionName}-${toSafePathSegment(paneId)}.fifo`);
52060
+ }
52018
52061
  };
52019
52062
  }
52020
52063
 
52021
- // ../../apps/gateway/src/tmux/ssh-auth.ts
52022
- function normalizeEnvValue(value) {
52023
- const trimmed = value?.trim();
52024
- return trimmed ? trimmed : undefined;
52025
- }
52026
- function resolveSshUsername(configuredUsername, authMode, env = process.env) {
52027
- const explicitUsername = normalizeEnvValue(configuredUsername);
52028
- if (explicitUsername) {
52029
- return explicitUsername;
52064
+ // ../../apps/gateway/src/tmux-client/input-encoder.ts
52065
+ var encoder = new TextEncoder;
52066
+ var SEND_KEYS_HEX_CHUNK_BYTES = 256;
52067
+ function encodeInputToHexChunks(input, chunkBytes = SEND_KEYS_HEX_CHUNK_BYTES) {
52068
+ const bytes = encoder.encode(input);
52069
+ const chunks = [];
52070
+ for (let offset = 0;offset < bytes.length; offset += chunkBytes) {
52071
+ const chunk2 = bytes.slice(offset, offset + chunkBytes);
52072
+ chunks.push(Array.from(chunk2, (byte) => byte.toString(16).padStart(2, "0")));
52030
52073
  }
52031
- if (authMode === "agent" || authMode === "auto") {
52032
- const currentUser = normalizeEnvValue(env.USER) ?? normalizeEnvValue(env.LOGNAME);
52033
- if (currentUser) {
52034
- return currentUser;
52074
+ return chunks;
52075
+ }
52076
+
52077
+ // ../../apps/gateway/src/tmux-client/pane-title-parser.ts
52078
+ var decoder = new TextDecoder;
52079
+ function createPaneTitleParser(options) {
52080
+ let phase = "normal";
52081
+ let oscKind = "";
52082
+ let titleBytes = [];
52083
+ return {
52084
+ push(data) {
52085
+ const output = [];
52086
+ for (const byte of data) {
52087
+ if (phase === "normal") {
52088
+ if (byte === 27) {
52089
+ phase = "esc";
52090
+ } else {
52091
+ output.push(byte);
52092
+ }
52093
+ continue;
52094
+ }
52095
+ if (phase === "esc") {
52096
+ if (byte === 93) {
52097
+ phase = "osc";
52098
+ oscKind = "";
52099
+ titleBytes = [];
52100
+ continue;
52101
+ }
52102
+ output.push(27, byte);
52103
+ phase = "normal";
52104
+ continue;
52105
+ }
52106
+ if (phase === "osc") {
52107
+ if (byte === 59) {
52108
+ phase = oscKind === "0" || oscKind === "2" ? "osc-data" : "normal";
52109
+ if (phase === "normal") {
52110
+ output.push(27, 93, ...encoderFromString(oscKind), 59);
52111
+ }
52112
+ continue;
52113
+ }
52114
+ oscKind += String.fromCharCode(byte);
52115
+ continue;
52116
+ }
52117
+ if (phase === "osc-data") {
52118
+ if (byte === 7) {
52119
+ emitTitle(options.onTitle, titleBytes);
52120
+ phase = "normal";
52121
+ continue;
52122
+ }
52123
+ if (byte === 27) {
52124
+ phase = "osc-st";
52125
+ continue;
52126
+ }
52127
+ titleBytes.push(byte);
52128
+ continue;
52129
+ }
52130
+ if (byte === 92) {
52131
+ emitTitle(options.onTitle, titleBytes);
52132
+ phase = "normal";
52133
+ continue;
52134
+ }
52135
+ titleBytes.push(27, byte);
52136
+ phase = "osc-data";
52137
+ }
52138
+ return new Uint8Array(output);
52035
52139
  }
52036
- }
52037
- return "root";
52140
+ };
52038
52141
  }
52039
- function resolveSshAgentSocket(authMode, env = process.env) {
52040
- if (authMode !== "agent" && authMode !== "auto") {
52142
+ function emitTitle(onTitle, titleBytes) {
52143
+ const title = decoder.decode(new Uint8Array(titleBytes)).trim();
52144
+ if (!title) {
52041
52145
  return;
52042
52146
  }
52043
- const socket = normalizeEnvValue(env.SSH_AUTH_SOCK);
52044
- if (socket) {
52045
- return socket;
52046
- }
52047
- if (authMode === "agent") {
52048
- throw new Error("SSH_AUTH_SOCK \u672A\u8BBE\u7F6E\uFF0C\u65E0\u6CD5\u4F7F\u7528 SSH Agent \u8BA4\u8BC1");
52049
- }
52050
- return;
52147
+ onTitle(title);
52148
+ }
52149
+ function encoderFromString(value) {
52150
+ return new TextEncoder().encode(value);
52051
52151
  }
52052
52152
 
52053
- // ../../apps/gateway/src/tmux/parser.ts
52054
- function isSameBytes(left, right) {
52055
- if (left.length !== right.length) {
52153
+ // ../../apps/gateway/src/tmux-client/local-external-connection.ts
52154
+ var DEFAULT_CHUNK_HISTORY_LINES = "-1000";
52155
+ var BELL_DEDUP_WINDOW_MS = 200;
52156
+ function shouldIgnoreReaderAbortError(error) {
52157
+ if (!error || typeof error !== "object") {
52056
52158
  return false;
52057
52159
  }
52058
- for (let i = 0;i < left.length; i++) {
52059
- if (left[i] !== right[i]) {
52060
- return false;
52160
+ const maybeError = error;
52161
+ return maybeError.name === "AbortError" && maybeError.code === "ERR_STREAM_RELEASE_LOCK" && typeof maybeError.message === "string" && maybeError.message.includes("releaseLock");
52162
+ }
52163
+ function defaultRun(argv) {
52164
+ return new Promise((resolve, reject) => {
52165
+ const subprocess = Bun.spawn(argv, {
52166
+ env: buildLocalTmuxEnv(getLocalShellPath()),
52167
+ stdout: "pipe",
52168
+ stderr: "pipe"
52169
+ });
52170
+ Promise.all([
52171
+ new Response(subprocess.stdout).text(),
52172
+ new Response(subprocess.stderr).text(),
52173
+ subprocess.exited
52174
+ ]).then(([stdout, stderr, exitCode]) => {
52175
+ resolve({ stdout, stderr, exitCode });
52176
+ }).catch(reject);
52177
+ });
52178
+ }
52179
+
52180
+ class LocalExternalTmuxConnection {
52181
+ deviceId;
52182
+ deps;
52183
+ callbacks;
52184
+ device = null;
52185
+ sessionName = "tmex";
52186
+ connected = false;
52187
+ manualDisconnect = false;
52188
+ activeWindowId = null;
52189
+ activePaneId = null;
52190
+ pendingPaneTitles = new Map;
52191
+ snapshotSession = null;
52192
+ snapshotWindows = new Map;
52193
+ currentPipePaneId = null;
52194
+ pipeReadAbort = null;
52195
+ pipeTransition = Promise.resolve();
52196
+ inputTransition = Promise.resolve();
52197
+ hookReadAbort = null;
52198
+ hookBuffer = "";
52199
+ bellDedup = new Map;
52200
+ fsPaths = createRuntimeFsPaths({
52201
+ deviceId: "pending",
52202
+ sessionName: "pending",
52203
+ gatewayPid: process.pid
52204
+ });
52205
+ constructor(options, inputDeps = {}) {
52206
+ this.deviceId = options.deviceId;
52207
+ this.callbacks = options;
52208
+ this.deps = {
52209
+ enableHooks: inputDeps.enableHooks ?? true,
52210
+ getDevice: inputDeps.getDevice ?? ((deviceId) => getDeviceById(deviceId)),
52211
+ run: inputDeps.run ?? defaultRun
52212
+ };
52213
+ }
52214
+ async connect() {
52215
+ this.manualDisconnect = false;
52216
+ this.device = this.deps.getDevice(this.deviceId);
52217
+ if (!this.device) {
52218
+ throw new Error(`Device not found: ${this.deviceId}`);
52219
+ }
52220
+ if (this.device.type !== "local") {
52221
+ throw new Error(`LocalExternalTmuxConnection only supports local device: ${this.deviceId}`);
52222
+ }
52223
+ this.sessionName = this.device.session?.trim() || "tmex";
52224
+ this.fsPaths = createRuntimeFsPaths({
52225
+ deviceId: this.deviceId,
52226
+ sessionName: this.sessionName,
52227
+ gatewayPid: process.pid
52228
+ });
52229
+ this.ensureRuntimeDirs();
52230
+ await this.ensureSession();
52231
+ if (this.deps.enableHooks) {
52232
+ await this.startHooks();
52061
52233
  }
52234
+ this.connected = true;
52235
+ updateDeviceRuntimeStatus(this.deviceId, {
52236
+ lastSeenAt: new Date().toISOString(),
52237
+ tmuxAvailable: true,
52238
+ lastError: null
52239
+ });
52240
+ await this.requestSnapshotInternal();
52062
52241
  }
52063
- return true;
52064
- }
52065
- function decodeTmuxEscapedValue(value) {
52066
- const bytes = [];
52067
- const encoder = new TextEncoder;
52068
- let cursor = 0;
52069
- for (let i = 0;i < value.length; i++) {
52070
- if (value[i] !== "\\") {
52071
- continue;
52242
+ disconnect() {
52243
+ if (!this.connected && this.manualDisconnect) {
52244
+ return;
52072
52245
  }
52073
- const octal = value.slice(i + 1, i + 4);
52074
- if (!/^[0-7]{3}$/.test(octal)) {
52075
- continue;
52246
+ this.manualDisconnect = true;
52247
+ this.connected = false;
52248
+ this.stopPipe();
52249
+ if (this.deps.enableHooks) {
52250
+ this.stopHooks();
52076
52251
  }
52077
- if (cursor < i) {
52078
- bytes.push(...encoder.encode(value.slice(cursor, i)));
52079
- }
52080
- bytes.push(Number.parseInt(octal, 8));
52081
- i += 3;
52082
- cursor = i + 1;
52083
- }
52084
- if (cursor < value.length) {
52085
- bytes.push(...encoder.encode(value.slice(cursor)));
52086
- }
52087
- return new Uint8Array(bytes);
52088
- }
52089
- function stripTmuxDcsWrapper(line) {
52090
- let cleanLine = line;
52091
- cleanLine = cleanLine.replace(/^\u001bP\d+p/, "");
52092
- if (cleanLine.endsWith("\x1B\\")) {
52093
- cleanLine = cleanLine.slice(0, -2);
52094
- } else if (cleanLine.endsWith("\x9C")) {
52095
- cleanLine = cleanLine.slice(0, -1);
52096
- }
52097
- return cleanLine;
52098
- }
52099
-
52100
- class TmuxControlParser {
52101
- buffer = "";
52102
- onEvent;
52103
- onTerminalOutput;
52104
- onPaneTitle;
52105
- onOutputBlockBegin;
52106
- onOutputBlock;
52107
- onNonControlOutput;
52108
- onExit;
52109
- onReady;
52110
- inOutputBlock = false;
52111
- outputBlockMeta = null;
52112
- outputBlockLines = [];
52113
- readyNotified = false;
52114
- lastOutputEndedWithCR = false;
52115
- lastOutputFrame = null;
52116
- outputTitleStates = new Map;
52117
- constructor(options) {
52118
- this.onEvent = options.onEvent;
52119
- this.onTerminalOutput = options.onTerminalOutput;
52120
- this.onPaneTitle = options.onPaneTitle;
52121
- this.onOutputBlockBegin = options.onOutputBlockBegin;
52122
- this.onOutputBlock = options.onOutputBlock;
52123
- this.onNonControlOutput = options.onNonControlOutput;
52124
- this.onExit = options.onExit;
52125
- this.onReady = options.onReady;
52126
- }
52127
- processData(data) {
52128
- const text2 = typeof data === "string" ? data : new TextDecoder().decode(data);
52129
- this.buffer += text2;
52130
- this.parseBuffer();
52131
- }
52132
- parseBuffer() {
52133
- while (true) {
52134
- const nlIndex = this.buffer.indexOf(`
52135
- `);
52136
- if (nlIndex === -1)
52137
- break;
52138
- let line = this.buffer.slice(0, nlIndex);
52139
- this.buffer = this.buffer.slice(nlIndex + 1);
52140
- if (line.endsWith("\r")) {
52141
- line = line.slice(0, -1);
52142
- }
52143
- if (line) {
52144
- this.parseLine(line);
52252
+ rmSync(this.fsPaths.rootDir, { recursive: true, force: true });
52253
+ }
52254
+ requestSnapshot() {
52255
+ this.requestSnapshotInternal();
52256
+ }
52257
+ sendInput(paneId, data) {
52258
+ if (!this.connected) {
52259
+ return;
52260
+ }
52261
+ const task = async () => {
52262
+ for (const chunk2 of encodeInputToHexChunks(data)) {
52263
+ await this.runTmux(["send-keys", "-H", "-t", paneId, ...chunk2]);
52145
52264
  }
52265
+ };
52266
+ const next = this.inputTransition.catch(() => {
52267
+ return;
52268
+ }).then(task);
52269
+ this.inputTransition = next;
52270
+ next.catch((error) => {
52271
+ this.callbacks.onError(error);
52272
+ });
52273
+ }
52274
+ resizePane(paneId, cols, rows) {
52275
+ if (!this.connected) {
52276
+ return;
52146
52277
  }
52278
+ this.resizePaneInternal(paneId, cols, rows).catch((error) => {
52279
+ this.callbacks.onError(error);
52280
+ });
52147
52281
  }
52148
- parseLine(line) {
52149
- const cleanLine = stripTmuxDcsWrapper(line);
52150
- if (this.inOutputBlock) {
52151
- if (cleanLine.startsWith("%end") || cleanLine.startsWith("%error")) {
52152
- this.finishOutputBlock(cleanLine);
52153
- return;
52154
- }
52155
- this.outputBlockLines.push(cleanLine);
52282
+ selectPane(windowId, paneId) {
52283
+ if (!this.connected) {
52156
52284
  return;
52157
52285
  }
52158
- if (!cleanLine.trim())
52286
+ this.selectPaneInternal(windowId, paneId, null).catch((error) => {
52287
+ this.callbacks.onError(error);
52288
+ });
52289
+ }
52290
+ selectPaneWithSize(windowId, paneId, cols, rows) {
52291
+ if (!this.connected) {
52159
52292
  return;
52160
- if (cleanLine.startsWith("%begin")) {
52161
- this.startOutputBlock(cleanLine);
52293
+ }
52294
+ this.selectPaneInternal(windowId, paneId, { cols, rows }).catch((error) => {
52295
+ this.callbacks.onError(error);
52296
+ });
52297
+ }
52298
+ selectWindow(windowId) {
52299
+ if (!this.connected) {
52162
52300
  return;
52163
52301
  }
52164
- if (cleanLine.startsWith("%")) {
52165
- this.parseControlLine(cleanLine);
52166
- this.notifyReady();
52302
+ this.runAndRefresh(["select-window", "-t", windowId]).catch((error) => {
52303
+ this.callbacks.onError(error);
52304
+ });
52305
+ }
52306
+ createWindow(name) {
52307
+ if (!this.connected) {
52167
52308
  return;
52168
52309
  }
52169
- this.onNonControlOutput?.(cleanLine);
52170
- console.log("[tmux] non-control output:", cleanLine);
52310
+ const argv = name ? ["new-window", "-n", name] : ["new-window"];
52311
+ this.runAndRefresh(argv).catch((error) => {
52312
+ this.callbacks.onError(error);
52313
+ });
52171
52314
  }
52172
- notifyReady() {
52173
- if (this.readyNotified)
52315
+ closeWindow(windowId) {
52316
+ if (!this.connected) {
52174
52317
  return;
52175
- this.readyNotified = true;
52176
- this.onReady?.();
52318
+ }
52319
+ this.closeWindowInternal(windowId).catch((error) => {
52320
+ this.callbacks.onError(error);
52321
+ });
52177
52322
  }
52178
- startOutputBlock(line) {
52179
- const meta = this.parseOutputBlockMeta(line);
52180
- if (!meta) {
52323
+ closePane(paneId) {
52324
+ if (!this.connected) {
52181
52325
  return;
52182
52326
  }
52183
- this.inOutputBlock = true;
52184
- this.outputBlockMeta = meta;
52185
- this.outputBlockLines = [];
52186
- this.onOutputBlockBegin?.(meta);
52327
+ this.runAndRefresh(["kill-pane", "-t", paneId], true).catch((error) => {
52328
+ this.callbacks.onError(error);
52329
+ });
52187
52330
  }
52188
- finishOutputBlock(line) {
52189
- const meta = this.parseOutputBlockMeta(line);
52190
- const currentMeta = this.outputBlockMeta;
52191
- this.inOutputBlock = false;
52192
- this.outputBlockMeta = null;
52193
- if (currentMeta && meta) {
52194
- this.onOutputBlock?.({
52195
- time: currentMeta.time,
52196
- commandNo: currentMeta.commandNo,
52197
- flags: currentMeta.flags,
52198
- lines: this.outputBlockLines,
52199
- isError: line.startsWith("%error")
52200
- });
52331
+ renameWindow(windowId, name) {
52332
+ if (!this.connected) {
52333
+ return;
52201
52334
  }
52202
- this.outputBlockLines = [];
52203
- this.notifyReady();
52335
+ this.runAndRefresh(["rename-window", "-t", windowId, name]).catch((error) => {
52336
+ this.callbacks.onError(error);
52337
+ });
52204
52338
  }
52205
- parseOutputBlockMeta(line) {
52206
- const spaceIndex = line.indexOf(" ");
52207
- if (spaceIndex === -1)
52208
- return null;
52209
- const args = line.slice(spaceIndex + 1).trim();
52210
- const parts = args.split(/\s+/);
52211
- if (parts.length < 3)
52212
- return null;
52213
- const time = Number(parts[0]);
52214
- const commandNo = Number(parts[1]);
52215
- const flags = Number(parts[2]);
52216
- if (Number.isNaN(time) || Number.isNaN(commandNo) || Number.isNaN(flags))
52217
- return null;
52218
- return { time, commandNo, flags };
52339
+ async ensureSession() {
52340
+ const exists3 = await this.runTmuxAllowFailure(["has-session", "-t", this.sessionName]);
52341
+ if (exists3.exitCode === 0) {
52342
+ return;
52343
+ }
52344
+ await this.runTmux(["new-session", "-d", "-c", homedir(), "-s", this.sessionName]);
52345
+ }
52346
+ ensureRuntimeDirs() {
52347
+ mkdirSync(this.fsPaths.rootDir, { recursive: true, mode: 448 });
52348
+ mkdirSync(this.fsPaths.panesDir, { recursive: true, mode: 448 });
52349
+ mkdirSync(this.fsPaths.hooksDir, { recursive: true, mode: 448 });
52350
+ }
52351
+ async startHooks() {
52352
+ this.ensureRuntimeDirs();
52353
+ const fifoPath = this.fsPaths.hookFifoPath;
52354
+ rmSync(fifoPath, { force: true });
52355
+ await this.runShell(`mkfifo ${quoteShellArg(fifoPath)}`);
52356
+ const readerProcess = Bun.spawn(["/bin/sh", "-lc", `tail -n +1 -f ${quoteShellArg(fifoPath)}`], {
52357
+ stdout: "pipe",
52358
+ stderr: "pipe"
52359
+ });
52360
+ const reader = readerProcess.stdout.getReader();
52361
+ (async () => {
52362
+ try {
52363
+ while (true) {
52364
+ const chunk2 = await reader.read();
52365
+ if (chunk2.done) {
52366
+ break;
52367
+ }
52368
+ this.handleHookChunk(new TextDecoder().decode(chunk2.value));
52369
+ }
52370
+ } catch (error) {
52371
+ if (!this.manualDisconnect && !shouldIgnoreReaderAbortError(error)) {
52372
+ this.callbacks.onError(error instanceof Error ? error : new Error(String(error)));
52373
+ }
52374
+ }
52375
+ })();
52376
+ this.hookReadAbort = () => {
52377
+ reader.releaseLock();
52378
+ readerProcess.kill();
52379
+ rmSync(fifoPath, { force: true });
52380
+ };
52381
+ await this.installHook("alert-bell", ["bell", "#{window_id}", "#{pane_id}"]);
52382
+ await this.installHook("pane-exited", ["pane-exited", "#{window_id}", "#{pane_id}"]);
52383
+ await this.installHook("pane-died", ["pane-died", "#{window_id}", "#{pane_id}"]);
52384
+ }
52385
+ async stopHooks() {
52386
+ await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "alert-bell"]);
52387
+ await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "pane-exited"]);
52388
+ await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "pane-died"]);
52389
+ this.hookReadAbort?.();
52390
+ this.hookReadAbort = null;
52391
+ this.hookBuffer = "";
52392
+ }
52393
+ async installHook(hookName, fields) {
52394
+ const fifoPath = this.fsPaths.hookFifoPath;
52395
+ const innerScript = `printf '%s\\t%s\\t%s\\n' ${fields.map((field) => quoteShellArg(field)).join(" ")} >> ${quoteShellArg(fifoPath)}`;
52396
+ await this.runTmux([
52397
+ "set-hook",
52398
+ "-t",
52399
+ this.sessionName,
52400
+ hookName,
52401
+ `run-shell -b ${quoteShellArg(innerScript)}`
52402
+ ]);
52219
52403
  }
52220
- normalizeTerminalOutputNewline(data) {
52221
- const startWithCR = this.lastOutputEndedWithCR;
52222
- let previousWasCR = startWithCR;
52223
- let extraCRCount = 0;
52224
- for (const byte of data) {
52225
- if (byte === 10 && !previousWasCR) {
52226
- extraCRCount += 1;
52404
+ handleHookChunk(text2) {
52405
+ this.hookBuffer += text2;
52406
+ while (true) {
52407
+ const newlineIndex = this.hookBuffer.indexOf(`
52408
+ `);
52409
+ if (newlineIndex < 0) {
52410
+ return;
52411
+ }
52412
+ const line = this.hookBuffer.slice(0, newlineIndex).trim();
52413
+ this.hookBuffer = this.hookBuffer.slice(newlineIndex + 1);
52414
+ if (!line) {
52415
+ continue;
52416
+ }
52417
+ const [type, windowId, paneId] = line.split("\t");
52418
+ if (type === "bell") {
52419
+ const key = paneId || windowId || "-";
52420
+ const previous = this.bellDedup.get(key) ?? 0;
52421
+ const now = Date.now();
52422
+ if (now - previous >= BELL_DEDUP_WINDOW_MS) {
52423
+ this.bellDedup.set(key, now);
52424
+ this.callbacks.onEvent({
52425
+ type: "bell",
52426
+ data: {
52427
+ windowId: windowId || undefined,
52428
+ paneId: paneId || this.activePaneId || undefined
52429
+ }
52430
+ });
52431
+ }
52432
+ continue;
52433
+ }
52434
+ if (type === "pane-exited" || type === "pane-died") {
52435
+ this.requestSnapshot();
52227
52436
  }
52228
- previousWasCR = byte === 13;
52229
52437
  }
52230
- this.lastOutputEndedWithCR = previousWasCR;
52231
- if (extraCRCount === 0) {
52232
- return data;
52438
+ }
52439
+ async runAndRefresh(argv, allowTargetMissing = false) {
52440
+ await this.runTmux(argv, allowTargetMissing);
52441
+ await this.requestSnapshotInternal();
52442
+ }
52443
+ async closeWindowInternal(windowId) {
52444
+ const count2 = Number.parseInt((await this.runTmux(["display-message", "-p", "-t", this.sessionName, "#{session_windows}"])).stdout.trim() || "0", 10);
52445
+ if (count2 <= 1) {
52446
+ await this.runTmux(["new-window", "-d", "-t", this.sessionName]);
52233
52447
  }
52234
- const normalized = new Uint8Array(data.length + extraCRCount);
52235
- let writeIndex = 0;
52236
- previousWasCR = startWithCR;
52237
- for (const byte of data) {
52238
- if (byte === 10 && !previousWasCR) {
52239
- normalized[writeIndex] = 13;
52240
- writeIndex += 1;
52241
- }
52242
- normalized[writeIndex] = byte;
52243
- writeIndex += 1;
52244
- previousWasCR = byte === 13;
52448
+ await this.runAndRefresh(["kill-window", "-t", windowId], true);
52449
+ }
52450
+ async resizePaneInternal(paneId, cols, rows) {
52451
+ const safeCols = Math.max(2, Math.floor(cols));
52452
+ const safeRows = Math.max(2, Math.floor(rows));
52453
+ const windowId = this.findPaneWindowId(paneId) ?? (await this.runTmux(["display-message", "-p", "-t", paneId, "#{window_id}"], true)).stdout.trim();
52454
+ if (!windowId) {
52455
+ return;
52245
52456
  }
52246
- return normalized;
52457
+ await this.runTmux(["resize-window", "-t", windowId, "-x", String(safeCols), "-y", String(safeRows)], true);
52458
+ await this.requestSnapshotInternal();
52247
52459
  }
52248
- getTitleParseState(paneId) {
52249
- const existing = this.outputTitleStates.get(paneId);
52250
- if (existing) {
52251
- return existing;
52460
+ async selectPaneInternal(windowId, paneId, size) {
52461
+ this.activeWindowId = windowId;
52462
+ this.activePaneId = paneId;
52463
+ await this.runTmux(["select-window", "-t", windowId], true);
52464
+ await this.runTmux(["select-pane", "-t", paneId], true);
52465
+ await this.startPipeForPane(paneId);
52466
+ if (size) {
52467
+ await this.resizePaneInternal(paneId, size.cols, size.rows);
52252
52468
  }
52253
- const created = {
52254
- phase: "normal",
52255
- titleBytes: []
52256
- };
52257
- this.outputTitleStates.set(paneId, created);
52258
- return created;
52469
+ this.callbacks.onEvent({
52470
+ type: "pane-active",
52471
+ data: { windowId, paneId }
52472
+ });
52473
+ await this.capturePaneHistory(paneId);
52474
+ await this.requestSnapshotInternal();
52259
52475
  }
52260
- emitPaneTitleIfNeeded(paneId, titleBytes) {
52261
- if (titleBytes.length === 0) {
52476
+ async capturePaneHistory(paneId) {
52477
+ const mode = (await this.runTmux(["display-message", "-p", "-t", paneId, "#{alternate_on}"], true)).stdout.trim();
52478
+ const normal = (await this.runTmux(["capture-pane", "-t", paneId, "-S", DEFAULT_CHUNK_HISTORY_LINES, "-e", "-p"], true)).stdout;
52479
+ const alternate = (await this.runTmux(["capture-pane", "-t", paneId, "-a", "-S", DEFAULT_CHUNK_HISTORY_LINES, "-e", "-p", "-q"], true)).stdout;
52480
+ const history = normal || alternate;
52481
+ if (history) {
52482
+ this.callbacks.onTerminalHistory(paneId, history);
52483
+ }
52484
+ }
52485
+ async requestSnapshotInternal() {
52486
+ if (!this.connected) {
52262
52487
  return;
52263
52488
  }
52264
- const title = new TextDecoder().decode(new Uint8Array(titleBytes)).trim();
52265
- if (!title) {
52489
+ const [sessionRes, windowsRes, panesRes] = await Promise.all([
52490
+ this.runTmuxAllowFailure([
52491
+ "display-message",
52492
+ "-p",
52493
+ "-t",
52494
+ this.sessionName,
52495
+ "#{session_id}\t#{session_name}"
52496
+ ]),
52497
+ this.runTmuxAllowFailure([
52498
+ "list-windows",
52499
+ "-t",
52500
+ this.sessionName,
52501
+ "-F",
52502
+ "#{window_id}\t#{window_index}\t#{window_name}\t#{window_active}"
52503
+ ]),
52504
+ this.runTmuxAllowFailure([
52505
+ "list-panes",
52506
+ "-t",
52507
+ this.sessionName,
52508
+ "-F",
52509
+ "#{pane_id}\t#{window_id}\t#{pane_index}\t#{pane_title}\t#{pane_active}\t#{pane_width}\t#{pane_height}"
52510
+ ])
52511
+ ]);
52512
+ if (sessionRes.exitCode !== 0 || windowsRes.exitCode !== 0 || panesRes.exitCode !== 0) {
52513
+ this.callbacks.onSnapshot({ deviceId: this.deviceId, session: null });
52266
52514
  return;
52267
52515
  }
52268
- this.onPaneTitle?.(paneId, title);
52516
+ this.parseSnapshotSession(sessionRes.stdout.split(/\r?\n/));
52517
+ this.parseSnapshotWindows(windowsRes.stdout.split(/\r?\n/));
52518
+ this.parseSnapshotPanes(panesRes.stdout.split(/\r?\n/));
52519
+ this.emitSnapshot();
52269
52520
  }
52270
- stripScreenTitleSequence(paneId, data) {
52271
- if (data.length === 0) {
52272
- return data;
52273
- }
52274
- const parseState = this.getTitleParseState(paneId);
52275
- const output = [];
52276
- let phase = parseState.phase;
52277
- const titleBytes = parseState.titleBytes;
52278
- for (const byte of data) {
52279
- if (phase === "normal") {
52280
- if (byte === 27) {
52281
- phase = "esc";
52282
- } else {
52283
- output.push(byte);
52284
- }
52521
+ parseSnapshotSession(lines) {
52522
+ this.snapshotSession = null;
52523
+ for (const line of lines) {
52524
+ if (!line.trim()) {
52285
52525
  continue;
52286
52526
  }
52287
- if (phase === "esc") {
52288
- if (byte === 107) {
52289
- phase = "title";
52290
- titleBytes.length = 0;
52291
- continue;
52292
- }
52293
- output.push(27);
52294
- if (byte === 27) {
52295
- phase = "esc";
52296
- } else {
52297
- output.push(byte);
52298
- phase = "normal";
52299
- }
52527
+ const [id, name] = line.split("\t");
52528
+ if (id) {
52529
+ this.snapshotSession = { id, name: name ?? "" };
52530
+ }
52531
+ return;
52532
+ }
52533
+ }
52534
+ parseSnapshotWindows(lines) {
52535
+ this.snapshotWindows.clear();
52536
+ for (const line of lines) {
52537
+ if (!line.trim()) {
52300
52538
  continue;
52301
52539
  }
52302
- if (phase === "title") {
52303
- if (byte === 27) {
52304
- phase = "titleEsc";
52305
- } else {
52306
- titleBytes.push(byte);
52307
- }
52540
+ const [id, indexRaw, name, activeRaw] = line.split("\t");
52541
+ if (!id) {
52308
52542
  continue;
52309
52543
  }
52310
- if (byte === 92) {
52311
- this.emitPaneTitleIfNeeded(paneId, titleBytes);
52312
- titleBytes.length = 0;
52313
- phase = "normal";
52314
- } else if (byte === 27) {
52315
- phase = "titleEsc";
52316
- } else {
52317
- titleBytes.push(27, byte);
52318
- phase = "title";
52544
+ const index = Number.parseInt(indexRaw ?? "", 10);
52545
+ const active = activeRaw === "1";
52546
+ if (active) {
52547
+ this.activeWindowId = id;
52319
52548
  }
52549
+ this.snapshotWindows.set(id, {
52550
+ id,
52551
+ index: Number.isNaN(index) ? 0 : index,
52552
+ name: name ?? "",
52553
+ active,
52554
+ panes: []
52555
+ });
52320
52556
  }
52321
- parseState.phase = phase;
52322
- return new Uint8Array(output);
52323
52557
  }
52324
- emitTerminalOutput(mode, paneId, data) {
52325
- const last = this.lastOutputFrame;
52326
- const isCrossModeDuplicate = last !== null && last.mode !== mode && last.paneId === paneId && isSameBytes(last.data, data);
52327
- if (!isCrossModeDuplicate) {
52328
- this.onTerminalOutput(paneId, data);
52558
+ parseSnapshotPanes(lines) {
52559
+ for (const window2 of this.snapshotWindows.values()) {
52560
+ window2.panes = [];
52329
52561
  }
52330
- this.lastOutputFrame = {
52331
- mode,
52332
- paneId,
52333
- data: data.slice()
52334
- };
52335
- }
52336
- parseControlLine(line) {
52337
- const spaceIndex = line.indexOf(" ");
52338
- const command = spaceIndex === -1 ? line : line.slice(0, spaceIndex);
52339
- const args = spaceIndex === -1 ? "" : line.slice(spaceIndex + 1);
52340
- switch (command) {
52341
- case "%window-add":
52342
- case "%unlinked-window-add":
52343
- this.onEvent({ type: "window-add", data: { windowId: args } });
52344
- break;
52345
- case "%window-close":
52346
- case "%unlinked-window-close":
52347
- this.onEvent({ type: "window-close", data: { windowId: args } });
52348
- break;
52349
- case "%window-renamed":
52350
- case "%unlinked-window-renamed": {
52351
- const parts = this.parseArgs(args);
52352
- this.onEvent({
52353
- type: "window-renamed",
52354
- data: { windowId: parts[0], name: parts[1] }
52355
- });
52356
- break;
52562
+ for (const line of lines) {
52563
+ if (!line.trim()) {
52564
+ continue;
52357
52565
  }
52358
- case "%window-pane-changed": {
52359
- const parts = this.parseArgs(args);
52360
- this.onEvent({
52361
- type: "pane-active",
52362
- data: { windowId: parts[0], paneId: parts[1] }
52363
- });
52364
- break;
52566
+ const [paneId, windowId, indexRaw, titleRaw, activeRaw, widthRaw, heightRaw] = line.split("\t");
52567
+ if (!paneId || !windowId) {
52568
+ continue;
52365
52569
  }
52366
- case "%pane-close":
52367
- this.onEvent({ type: "pane-close", data: { paneId: args } });
52368
- break;
52369
- case "%pane-add": {
52370
- const parts = this.parseArgs(args);
52371
- this.onEvent({
52372
- type: "pane-add",
52373
- data: {
52374
- paneId: parts[0] ?? args,
52375
- windowId: parts[1]
52376
- }
52377
- });
52378
- break;
52570
+ const index = Number.parseInt(indexRaw ?? "", 10);
52571
+ const width = Number.parseInt(widthRaw ?? "", 10);
52572
+ const height = Number.parseInt(heightRaw ?? "", 10);
52573
+ const pane = {
52574
+ id: paneId,
52575
+ windowId,
52576
+ index: Number.isNaN(index) ? 0 : index,
52577
+ title: this.pendingPaneTitles.get(paneId) ?? (titleRaw?.trim() ? titleRaw : undefined),
52578
+ active: activeRaw === "1",
52579
+ width: Number.isNaN(width) ? 0 : width,
52580
+ height: Number.isNaN(height) ? 0 : height
52581
+ };
52582
+ if (pane.active) {
52583
+ this.activePaneId = paneId;
52584
+ this.activeWindowId = windowId;
52379
52585
  }
52380
- case "%pane-mode-changed":
52381
- break;
52382
- case "%session-changed": {
52383
- const parts = this.parseArgs(args);
52384
- this.onEvent({
52385
- type: "window-add",
52386
- data: { sessionId: parts[0], name: parts[1] }
52387
- });
52388
- break;
52586
+ const window2 = this.snapshotWindows.get(windowId);
52587
+ if (!window2) {
52588
+ continue;
52389
52589
  }
52390
- case "%sessions-changed":
52391
- break;
52392
- case "%session-window-changed": {
52393
- const parts = this.parseArgs(args);
52394
- this.onEvent({
52395
- type: "window-active",
52396
- data: { sessionId: parts[0], windowId: parts[1] }
52397
- });
52398
- break;
52590
+ window2.panes.push(pane);
52591
+ this.pendingPaneTitles.delete(paneId);
52592
+ }
52593
+ for (const window2 of this.snapshotWindows.values()) {
52594
+ window2.panes.sort((left, right) => left.index - right.index);
52595
+ }
52596
+ }
52597
+ emitSnapshot() {
52598
+ const session = this.snapshotSession ? {
52599
+ id: this.snapshotSession.id,
52600
+ name: this.snapshotSession.name,
52601
+ windows: Array.from(this.snapshotWindows.values()).sort((left, right) => left.index - right.index)
52602
+ } : null;
52603
+ this.callbacks.onSnapshot({
52604
+ deviceId: this.deviceId,
52605
+ session
52606
+ });
52607
+ }
52608
+ findPaneWindowId(paneId) {
52609
+ for (const window2 of this.snapshotWindows.values()) {
52610
+ if (window2.panes.some((pane) => pane.id === paneId)) {
52611
+ return window2.id;
52399
52612
  }
52400
- case "%layout-change": {
52401
- const parts = this.parseArgs(args);
52402
- this.onEvent({
52403
- type: "layout-change",
52404
- data: { windowId: parts[0], layout: parts[1] }
52405
- });
52406
- break;
52613
+ }
52614
+ return null;
52615
+ }
52616
+ async startPipeForPane(paneId) {
52617
+ await this.queuePipeTransition(async () => {
52618
+ if (this.currentPipePaneId === paneId) {
52619
+ return;
52407
52620
  }
52408
- case "%output": {
52409
- const firstSpace = args.indexOf(" ");
52410
- if (firstSpace !== -1) {
52411
- const paneId = args.slice(0, firstSpace);
52412
- const value = args.slice(firstSpace + 1);
52413
- const decoded = decodeTmuxEscapedValue(value);
52414
- const stripped = this.stripScreenTitleSequence(paneId, decoded);
52415
- const normalized = this.normalizeTerminalOutputNewline(stripped);
52416
- this.emitTerminalOutput("output", paneId, normalized);
52621
+ await this.stopPipeNow();
52622
+ const fifoPath = this.fsPaths.paneFifoPath(paneId);
52623
+ this.ensureRuntimeDirs();
52624
+ rmSync(fifoPath, { force: true });
52625
+ await this.runShell(`mkfifo ${quoteShellArg(fifoPath)}`);
52626
+ const parser = createPaneTitleParser({
52627
+ onTitle: (title) => {
52628
+ this.pendingPaneTitles.set(paneId, title);
52629
+ this.requestSnapshot();
52417
52630
  }
52418
- break;
52419
- }
52420
- case "%extended-output": {
52421
- const firstSpace = args.indexOf(" ");
52422
- if (firstSpace === -1)
52423
- break;
52424
- const paneId = args.slice(0, firstSpace);
52425
- const colonIndex = args.indexOf(" : ");
52426
- if (colonIndex === -1)
52427
- break;
52428
- const value = args.slice(colonIndex + 3);
52429
- const decoded = decodeTmuxEscapedValue(value);
52430
- const stripped = this.stripScreenTitleSequence(paneId, decoded);
52431
- const normalized = this.normalizeTerminalOutputNewline(stripped);
52432
- this.emitTerminalOutput("extended", paneId, normalized);
52433
- break;
52434
- }
52435
- case "%exit":
52436
- this.onExit?.(args.trim() ? args : null);
52437
- break;
52438
- case "%bell":
52439
- {
52440
- const parts = this.parseArgs(args);
52441
- const windowId = parts[0] ?? args;
52442
- const paneId = parts[1];
52443
- const data = {};
52444
- if (windowId)
52445
- data.windowId = windowId;
52446
- if (paneId)
52447
- data.paneId = paneId;
52448
- this.onEvent({ type: "bell", data });
52631
+ });
52632
+ const readerProcess = Bun.spawn(["/bin/sh", "-lc", `cat ${quoteShellArg(fifoPath)}`], {
52633
+ stdout: "pipe",
52634
+ stderr: "pipe"
52635
+ });
52636
+ const reader = readerProcess.stdout.getReader();
52637
+ (async () => {
52638
+ try {
52639
+ while (true) {
52640
+ const chunk2 = await reader.read();
52641
+ if (chunk2.done) {
52642
+ break;
52643
+ }
52644
+ const raw = chunk2.value;
52645
+ const output = parser.push(raw);
52646
+ if (Array.from(raw).includes(7)) {
52647
+ this.callbacks.onEvent({ type: "bell", data: { paneId } });
52648
+ }
52649
+ if (output.length > 0) {
52650
+ this.callbacks.onTerminalOutput(paneId, output);
52651
+ }
52652
+ }
52653
+ } catch (error) {
52654
+ if (!this.manualDisconnect && !shouldIgnoreReaderAbortError(error)) {
52655
+ this.callbacks.onError(error instanceof Error ? error : new Error(String(error)));
52656
+ }
52449
52657
  }
52450
- break;
52451
- case "%pause":
52452
- case "%resume":
52453
- break;
52454
- case "%client-session-changed":
52455
- case "%client-detached":
52456
- case "lient-session-changed":
52457
- case "lient-detached":
52458
- break;
52459
- default:
52460
- console.log("[tmux] unknown control sequence:", command, args);
52658
+ })();
52659
+ this.pipeReadAbort = () => {
52660
+ reader.releaseLock();
52661
+ readerProcess.kill();
52662
+ rmSync(fifoPath, { force: true });
52663
+ };
52664
+ await this.runTmux(["pipe-pane", "-O", "-t", paneId, `cat >${fifoPath}`]);
52665
+ this.currentPipePaneId = paneId;
52666
+ });
52667
+ }
52668
+ async stopPipe() {
52669
+ await this.queuePipeTransition(() => this.stopPipeNow());
52670
+ }
52671
+ async stopPipeNow() {
52672
+ const paneId = this.currentPipePaneId;
52673
+ this.currentPipePaneId = null;
52674
+ if (paneId) {
52675
+ await this.runTmuxAllowFailure(["pipe-pane", "-t", paneId]);
52461
52676
  }
52677
+ this.pipeReadAbort?.();
52678
+ this.pipeReadAbort = null;
52462
52679
  }
52463
- parseArgs(args) {
52464
- const result = [];
52465
- let current = "";
52466
- let inQuotes = false;
52467
- for (let i = 0;i < args.length; i++) {
52468
- const char = args[i];
52469
- if (char === '"' && args[i - 1] !== "\\") {
52470
- inQuotes = !inQuotes;
52471
- } else if (char === " " && !inQuotes) {
52472
- if (current) {
52473
- result.push(current);
52474
- current = "";
52475
- }
52476
- } else {
52477
- current += char;
52478
- }
52680
+ queuePipeTransition(task) {
52681
+ const next = this.pipeTransition.catch(() => {
52682
+ return;
52683
+ }).then(task);
52684
+ this.pipeTransition = next;
52685
+ return next;
52686
+ }
52687
+ async runShell(command) {
52688
+ const result = await this.deps.run(["/bin/sh", "-lc", command]);
52689
+ if (result.exitCode !== 0) {
52690
+ throw new Error(result.stderr.trim() || `shell command failed: ${command}`);
52691
+ }
52692
+ }
52693
+ async runTmux(argv, allowTargetMissing = false) {
52694
+ const result = await this.runTmuxAllowFailure(argv);
52695
+ if (result.exitCode === 0) {
52696
+ return result;
52479
52697
  }
52480
- if (current) {
52481
- result.push(current);
52698
+ const message = (result.stderr.trim() || result.stdout.trim() || `tmux command failed: ${argv.join(" ")}`).trim();
52699
+ if (allowTargetMissing && this.isRecoverableTargetMissingError(message)) {
52700
+ this.recoverFromTargetMissingError(message);
52701
+ return result;
52482
52702
  }
52483
- return result;
52703
+ updateDeviceRuntimeStatus(this.deviceId, {
52704
+ lastSeenAt: new Date().toISOString(),
52705
+ tmuxAvailable: false,
52706
+ lastError: message
52707
+ });
52708
+ throw new Error(message);
52709
+ }
52710
+ async runTmuxAllowFailure(argv) {
52711
+ return this.deps.run(["tmux", ...argv]);
52484
52712
  }
52485
- flush() {
52486
- this.buffer = "";
52487
- this.lastOutputEndedWithCR = false;
52488
- this.lastOutputFrame = null;
52489
- this.outputTitleStates.clear();
52713
+ isRecoverableTargetMissingError(message) {
52714
+ const normalized = message.toLowerCase();
52715
+ return normalized.includes("can't find window") || normalized.includes("can't find pane") || normalized.includes("no such window") || normalized.includes("no such pane");
52716
+ }
52717
+ recoverFromTargetMissingError(message) {
52718
+ const normalized = message.toLowerCase();
52719
+ if (normalized.includes("window")) {
52720
+ this.activeWindowId = null;
52721
+ }
52722
+ if (normalized.includes("pane")) {
52723
+ this.activePaneId = null;
52724
+ }
52725
+ this.requestSnapshot();
52490
52726
  }
52491
52727
  }
52492
52728
 
52493
- // ../../apps/gateway/src/tmux/connection.ts
52494
- var BELL_DEDUP_WINDOW_MS = 200;
52495
- function buildLocalTmuxCommand(sessionName, startDirectory) {
52496
- return ["tmux", "-CC", "new-session", "-A", "-c", startDirectory, "-s", sessionName];
52729
+ // ../../apps/gateway/src/tmux-client/ssh-external-connection.ts
52730
+ var import_ssh2 = __toESM(require_lib3(), 1);
52731
+
52732
+ // ../../apps/gateway/src/tmux/ssh-auth.ts
52733
+ function normalizeEnvValue(value) {
52734
+ const trimmed = value?.trim();
52735
+ return trimmed ? trimmed : undefined;
52736
+ }
52737
+ function resolveSshUsername(configuredUsername, authMode, env = process.env) {
52738
+ const explicitUsername = normalizeEnvValue(configuredUsername);
52739
+ if (explicitUsername) {
52740
+ return explicitUsername;
52741
+ }
52742
+ if (authMode === "agent" || authMode === "auto") {
52743
+ const currentUser = normalizeEnvValue(env.USER) ?? normalizeEnvValue(env.LOGNAME);
52744
+ if (currentUser) {
52745
+ return currentUser;
52746
+ }
52747
+ }
52748
+ return "root";
52749
+ }
52750
+ function resolveSshAgentSocket(authMode, env = process.env) {
52751
+ if (authMode !== "agent" && authMode !== "auto") {
52752
+ return;
52753
+ }
52754
+ const socket = normalizeEnvValue(env.SSH_AUTH_SOCK);
52755
+ if (socket) {
52756
+ return socket;
52757
+ }
52758
+ if (authMode === "agent") {
52759
+ throw new Error("SSH_AUTH_SOCK \u672A\u8BBE\u7F6E\uFF0C\u65E0\u6CD5\u4F7F\u7528 SSH Agent \u8BA4\u8BC1");
52760
+ }
52761
+ return;
52762
+ }
52763
+
52764
+ // ../../apps/gateway/src/tmux-client/ssh-bootstrap.ts
52765
+ function buildSshBootstrapScript() {
52766
+ return [
52767
+ ". /etc/profile 2>/dev/null || true",
52768
+ '[ -f "$HOME/.profile" ] && . "$HOME/.profile" 2>/dev/null || true',
52769
+ '[ -f "$HOME/.bash_profile" ] && . "$HOME/.bash_profile" 2>/dev/null || true',
52770
+ 'TMUX_BIN="$(command -v tmux 2>/dev/null || true)"',
52771
+ 'if [ -z "$TMUX_BIN" ]; then',
52772
+ " for p in /usr/local/bin/tmux /opt/homebrew/bin/tmux /usr/bin/tmux /bin/tmux; do",
52773
+ ' [ -x "$p" ] && TMUX_BIN="$p" && break',
52774
+ " done",
52775
+ "fi",
52776
+ 'HOME_DIR="${HOME:-$(pwd)}"',
52777
+ 'if [ -z "$TMUX_BIN" ]; then',
52778
+ " printf 'TMEX_BOOT_FAIL\\ttmux_not_found\\n'",
52779
+ "else",
52780
+ ` printf 'TMEX_BOOT_OK\\t%s\\t%s\\t%s\\n' "$TMUX_BIN" "$("$TMUX_BIN" -V 2>/dev/null)" "$HOME_DIR"`,
52781
+ "fi"
52782
+ ].join(`
52783
+ `);
52784
+ }
52785
+ function parseSshBootstrapOutput(output) {
52786
+ const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
52787
+ for (const line of lines) {
52788
+ if (line.startsWith("TMEX_BOOT_OK\t")) {
52789
+ const [, tmuxBin = "", tmuxVersion = "", homeDir = ""] = line.split("\t");
52790
+ if (!tmuxBin || !homeDir) {
52791
+ return { ok: false, reason: "invalid_bootstrap_payload" };
52792
+ }
52793
+ return { ok: true, tmuxBin, tmuxVersion, homeDir };
52794
+ }
52795
+ if (line.startsWith("TMEX_BOOT_FAIL\t")) {
52796
+ const [, reason = "tmux_bootstrap_failed"] = line.split("\t");
52797
+ return { ok: false, reason };
52798
+ }
52799
+ }
52800
+ return { ok: false, reason: "missing_bootstrap_marker" };
52497
52801
  }
52498
52802
 
52499
- class TmuxConnection {
52803
+ // ../../apps/gateway/src/tmux-client/ssh-external-connection.ts
52804
+ var DEFAULT_HISTORY_LINES = "-1000";
52805
+ var BELL_DEDUP_WINDOW_MS2 = 200;
52806
+ var COMMAND_SENTINEL = "\x1ETMEX_END ";
52807
+
52808
+ class SshExternalTmuxConnection {
52500
52809
  deviceId;
52810
+ callbacks;
52811
+ deps;
52501
52812
  device = null;
52502
- subprocess = null;
52503
- terminal = null;
52504
- sshClient = null;
52505
- sshStream = null;
52506
- parser;
52507
- onEvent;
52508
- onTerminalOutput;
52509
- onTerminalHistory;
52510
- onSnapshot;
52511
- onError;
52512
- onClose;
52513
- activePaneId = null;
52514
- activeWindowId = null;
52813
+ sessionName = "tmex";
52515
52814
  connected = false;
52516
52815
  manualDisconnect = false;
52517
- ready = false;
52518
- readyFailed = false;
52519
- readyPromise;
52520
- resolveReady = null;
52521
- rejectReady = null;
52522
- startupNonControlOutput = [];
52523
- lastExitReason = null;
52524
- pendingCommandKinds = [];
52525
- commandKindsByNo = new Map;
52526
- pendingCapturePaneRequests = [];
52527
- pendingCapturePaneModeRequests = [];
52816
+ closeNotified = false;
52817
+ cleanupPromise = null;
52818
+ activeWindowId = null;
52819
+ activePaneId = null;
52820
+ pendingPaneTitles = new Map;
52528
52821
  snapshotSession = null;
52529
52822
  snapshotWindows = new Map;
52530
- pendingPaneTitles = new Map;
52531
- snapshotPanesReady = false;
52532
- historyCaptureStates = new Map;
52533
- resizeSnapshotTimer = null;
52534
- bellControlEventSeen = false;
52823
+ currentPipePaneId = null;
52824
+ pipeReadAbort = null;
52825
+ pipeTransition = Promise.resolve();
52826
+ hookReadAbort = null;
52827
+ hookBuffer = "";
52535
52828
  bellDedup = new Map;
52536
- constructor(options) {
52829
+ fsPaths = createRuntimeFsPaths({
52830
+ deviceId: "pending",
52831
+ sessionName: "pending",
52832
+ gatewayPid: process.pid
52833
+ });
52834
+ sshClient = null;
52835
+ commandStream = null;
52836
+ commandStdoutBuffer = "";
52837
+ pendingCommand = null;
52838
+ tmuxBin = "tmux";
52839
+ remoteHomeDir = ".";
52840
+ commandQueue = Promise.resolve();
52841
+ constructor(options, inputDeps = {}) {
52537
52842
  this.deviceId = options.deviceId;
52538
- this.onEvent = options.onEvent;
52539
- this.onTerminalOutput = options.onTerminalOutput;
52540
- this.onTerminalHistory = options.onTerminalHistory;
52541
- this.onSnapshot = options.onSnapshot;
52542
- this.onError = options.onError;
52543
- this.onClose = options.onClose;
52544
- this.readyPromise = new Promise((resolve, reject) => {
52545
- this.resolveReady = resolve;
52546
- this.rejectReady = reject;
52547
- });
52548
- this.parser = new TmuxControlParser({
52549
- onEvent: (event) => this.handleTmuxEvent(event),
52550
- onTerminalOutput: (paneId, data) => this.emitTerminalOutput(paneId, data),
52551
- onPaneTitle: (paneId, title) => this.handlePaneTitleUpdate(paneId, title),
52552
- onOutputBlockBegin: (meta) => this.handleOutputBlockBegin(meta),
52553
- onOutputBlock: (block) => this.handleOutputBlock(block),
52554
- onNonControlOutput: (line) => this.handleNonControlOutput(line),
52555
- onReady: () => this.markReady(),
52556
- onExit: (reason) => {
52557
- this.lastExitReason = reason;
52558
- if (!this.ready) {
52559
- this.failReady(new Error(reason ? `tmux exited: ${reason}` : "tmux exited"));
52560
- }
52561
- }
52562
- });
52563
- }
52564
- markReady() {
52565
- if (this.ready || this.readyFailed)
52566
- return;
52567
- this.ready = true;
52568
- this.resolveReady?.();
52569
- this.resolveReady = null;
52570
- this.rejectReady = null;
52571
- this.startupNonControlOutput = [];
52572
- }
52573
- failReady(error) {
52574
- if (this.ready || this.readyFailed)
52575
- return;
52576
- this.readyFailed = true;
52577
- const detail = this.startupNonControlOutput.filter(Boolean).join(`
52578
- `);
52579
- const nextError = detail ? new Error(`${error.message}
52580
- ${detail}`) : error;
52581
- this.rejectReady?.(nextError);
52582
- this.resolveReady = null;
52583
- this.rejectReady = null;
52584
- }
52585
- waitForReady(timeoutMs = 5000) {
52586
- if (this.ready) {
52587
- return Promise.resolve();
52588
- }
52589
- return new Promise((resolve, reject) => {
52590
- const timer = setTimeout(() => {
52591
- const detail = this.startupNonControlOutput.filter(Boolean).join(`
52592
- `);
52593
- reject(new Error(detail ? `tmux control mode not ready: ${detail}` : "tmux control mode not ready"));
52594
- }, timeoutMs);
52595
- this.readyPromise.then(() => {
52596
- clearTimeout(timer);
52597
- resolve();
52598
- }).catch((err) => {
52599
- clearTimeout(timer);
52600
- reject(err);
52601
- });
52602
- });
52843
+ this.callbacks = options;
52844
+ this.deps = {
52845
+ getDevice: inputDeps.getDevice ?? ((deviceId) => getDeviceById(deviceId)),
52846
+ decrypt: inputDeps.decrypt ?? decryptWithContext,
52847
+ createClient: inputDeps.createClient ?? (() => new import_ssh2.Client)
52848
+ };
52603
52849
  }
52604
52850
  async connect() {
52605
52851
  this.manualDisconnect = false;
52606
- this.device = getDeviceById(this.deviceId);
52852
+ this.closeNotified = false;
52853
+ this.device = this.deps.getDevice(this.deviceId);
52607
52854
  if (!this.device) {
52608
52855
  throw new Error(`Device not found: ${this.deviceId}`);
52609
52856
  }
52610
- if (this.device.type === "local") {
52611
- await this.connectLocal();
52612
- } else {
52613
- await this.connectSSH();
52857
+ if (this.device.type !== "ssh") {
52858
+ throw new Error(`SshExternalTmuxConnection only supports ssh device: ${this.deviceId}`);
52614
52859
  }
52615
- }
52616
- async connectLocal() {
52617
- const sessionName = this.device?.session ?? "tmex";
52618
- const env = buildLocalTmuxEnv(getLocalShellPath());
52619
- const startDirectory = homedir();
52620
- this.subprocess = Bun.spawn(buildLocalTmuxCommand(sessionName, startDirectory), {
52621
- env,
52622
- terminal: {
52623
- name: "xterm-256color",
52624
- cols: 80,
52625
- rows: 30,
52626
- data: (_term, data) => {
52627
- this.parser.processData(data);
52628
- },
52629
- exit: () => {
52630
- if (!this.ready) {
52631
- this.failReady(new Error("tmux terminal closed before ready"));
52632
- }
52633
- if (this.lastExitReason) {
52634
- this.onError(new Error(`tmux exited: ${this.lastExitReason}`));
52635
- }
52636
- this.cleanup();
52637
- if (!this.manualDisconnect) {
52638
- this.onClose();
52639
- }
52640
- }
52641
- }
52642
- });
52643
- this.terminal = this.subprocess.terminal ?? null;
52860
+ this.sessionName = this.device.session?.trim() || "tmex";
52861
+ this.fsPaths = createRuntimeFsPaths({
52862
+ deviceId: this.deviceId,
52863
+ sessionName: this.sessionName,
52864
+ gatewayPid: process.pid
52865
+ });
52866
+ await this.connectSshClient();
52867
+ await this.openCommandChannel();
52868
+ await this.ensureRemoteRuntimeDirs();
52869
+ await this.ensureSession();
52870
+ await this.startHooks();
52644
52871
  this.connected = true;
52645
52872
  updateDeviceRuntimeStatus(this.deviceId, {
52646
52873
  lastSeenAt: new Date().toISOString(),
52647
52874
  tmuxAvailable: true,
52648
52875
  lastError: null
52649
52876
  });
52650
- try {
52651
- await this.waitForReady();
52652
- this.configureWindowSizePolicy();
52653
- } catch (err) {
52654
- this.cleanup();
52655
- throw err;
52877
+ await this.requestSnapshotInternal();
52878
+ }
52879
+ disconnect() {
52880
+ if (this.manualDisconnect) {
52881
+ return;
52656
52882
  }
52883
+ this.manualDisconnect = true;
52884
+ this.shutdownInternal(false);
52885
+ }
52886
+ requestSnapshot() {
52887
+ this.requestSnapshotInternal();
52657
52888
  }
52658
- async connectSSH() {
52659
- if (!this.device)
52889
+ sendInput(paneId, data) {
52890
+ if (!this.connected) {
52660
52891
  return;
52661
- const conn = new import_ssh2.Client;
52662
- this.sshClient = conn;
52663
- const host = this.device.host;
52664
- const port = this.device.port ?? 22;
52665
- const username = this.device.username;
52666
- const sessionName = this.device.session ?? "tmex";
52667
- const resolvedUsername = resolveSshUsername(username, this.device.authMode);
52668
- const logStage = (stage, extra = {}) => {
52669
- console.log("[ssh]", {
52670
- stage,
52671
- deviceId: this.deviceId,
52672
- host,
52673
- port,
52674
- username: resolvedUsername,
52675
- authMode: this.device?.authMode,
52676
- ...extra
52892
+ }
52893
+ for (const chunk2 of encodeInputToHexChunks(data)) {
52894
+ this.runTmux(["send-keys", "-H", "-t", paneId, ...chunk2]).catch((error) => {
52895
+ this.callbacks.onError(error);
52677
52896
  });
52678
- };
52679
- logStage("connect_start", {
52680
- hasHost: Boolean(host),
52681
- hasSshConfigRef: Boolean(this.device.sshConfigRef),
52682
- sessionName
52897
+ }
52898
+ }
52899
+ resizePane(paneId, cols, rows) {
52900
+ if (!this.connected) {
52901
+ return;
52902
+ }
52903
+ this.resizePaneInternal(paneId, cols, rows).catch((error) => {
52904
+ this.callbacks.onError(error);
52905
+ });
52906
+ }
52907
+ selectPane(windowId, paneId) {
52908
+ if (!this.connected) {
52909
+ return;
52910
+ }
52911
+ this.selectPaneInternal(windowId, paneId, null).catch((error) => {
52912
+ this.callbacks.onError(error);
52683
52913
  });
52914
+ }
52915
+ selectPaneWithSize(windowId, paneId, cols, rows) {
52916
+ if (!this.connected) {
52917
+ return;
52918
+ }
52919
+ this.selectPaneInternal(windowId, paneId, { cols, rows }).catch((error) => {
52920
+ this.callbacks.onError(error);
52921
+ });
52922
+ }
52923
+ selectWindow(windowId) {
52924
+ if (!this.connected) {
52925
+ return;
52926
+ }
52927
+ this.runAndRefresh(["select-window", "-t", windowId]).catch((error) => {
52928
+ this.callbacks.onError(error);
52929
+ });
52930
+ }
52931
+ createWindow(name) {
52932
+ if (!this.connected) {
52933
+ return;
52934
+ }
52935
+ const argv = name ? ["new-window", "-n", name] : ["new-window"];
52936
+ this.runAndRefresh(argv).catch((error) => {
52937
+ this.callbacks.onError(error);
52938
+ });
52939
+ }
52940
+ closeWindow(windowId) {
52941
+ if (!this.connected) {
52942
+ return;
52943
+ }
52944
+ this.closeWindowInternal(windowId).catch((error) => {
52945
+ this.callbacks.onError(error);
52946
+ });
52947
+ }
52948
+ closePane(paneId) {
52949
+ if (!this.connected) {
52950
+ return;
52951
+ }
52952
+ this.runAndRefresh(["kill-pane", "-t", paneId], true).catch((error) => {
52953
+ this.callbacks.onError(error);
52954
+ });
52955
+ }
52956
+ renameWindow(windowId, name) {
52957
+ if (!this.connected) {
52958
+ return;
52959
+ }
52960
+ this.runAndRefresh(["rename-window", "-t", windowId, name]).catch((error) => {
52961
+ this.callbacks.onError(error);
52962
+ });
52963
+ }
52964
+ async connectSshClient() {
52965
+ if (!this.device) {
52966
+ throw new Error("SSH device not loaded");
52967
+ }
52968
+ const host = this.device.host;
52969
+ const port = this.device.port ?? 22;
52970
+ const username = resolveSshUsername(this.device.username, this.device.authMode);
52684
52971
  if (this.device.authMode === "configRef" || !host && this.device.sshConfigRef) {
52685
52972
  throw new Error("ssh_config_ref_not_supported: \u5F53\u524D\u7248\u672C\u6682\u4E0D\u652F\u6301 SSH Config \u5F15\u7528\uFF0C\u8BF7\u6539\u4E3A\u586B\u5199 host + username\uFF0C\u5E76\u9009\u62E9 Agent/\u79C1\u94A5/\u5BC6\u7801\u8BA4\u8BC1");
52686
52973
  }
@@ -52690,14 +52977,14 @@ ${detail}`) : error;
52690
52977
  const authConfig = {
52691
52978
  host,
52692
52979
  port,
52693
- username: resolvedUsername
52980
+ username
52694
52981
  };
52695
52982
  switch (this.device.authMode) {
52696
52983
  case "password": {
52697
52984
  if (!this.device.passwordEnc) {
52698
52985
  throw new Error("auth_password_missing: \u5BC6\u7801\u8BA4\u8BC1\u672A\u63D0\u4F9B\u5BC6\u7801");
52699
52986
  }
52700
- authConfig.password = await decryptWithContext(this.device.passwordEnc, {
52987
+ authConfig.password = await this.deps.decrypt(this.device.passwordEnc, {
52701
52988
  scope: "device",
52702
52989
  entityId: this.device.id,
52703
52990
  field: "password_enc"
@@ -52708,13 +52995,13 @@ ${detail}`) : error;
52708
52995
  if (!this.device.privateKeyEnc) {
52709
52996
  throw new Error("auth_key_missing: \u79C1\u94A5\u8BA4\u8BC1\u672A\u63D0\u4F9B\u79C1\u94A5");
52710
52997
  }
52711
- authConfig.privateKey = await decryptWithContext(this.device.privateKeyEnc, {
52998
+ authConfig.privateKey = await this.deps.decrypt(this.device.privateKeyEnc, {
52712
52999
  scope: "device",
52713
53000
  entityId: this.device.id,
52714
53001
  field: "private_key_enc"
52715
53002
  });
52716
53003
  if (this.device.privateKeyPassphraseEnc) {
52717
- authConfig.passphrase = await decryptWithContext(this.device.privateKeyPassphraseEnc, {
53004
+ authConfig.passphrase = await this.deps.decrypt(this.device.privateKeyPassphraseEnc, {
52718
53005
  scope: "device",
52719
53006
  entityId: this.device.id,
52720
53007
  field: "private_key_passphrase_enc"
@@ -52726,22 +53013,19 @@ ${detail}`) : error;
52726
53013
  authConfig.agent = resolveSshAgentSocket("agent");
52727
53014
  break;
52728
53015
  }
52729
- case "configRef": {
52730
- break;
52731
- }
52732
53016
  case "auto": {
52733
53017
  const agentSocket = resolveSshAgentSocket("auto");
52734
53018
  if (agentSocket) {
52735
53019
  authConfig.agent = agentSocket;
52736
53020
  }
52737
53021
  if (this.device.privateKeyEnc) {
52738
- authConfig.privateKey = await decryptWithContext(this.device.privateKeyEnc, {
53022
+ authConfig.privateKey = await this.deps.decrypt(this.device.privateKeyEnc, {
52739
53023
  scope: "device",
52740
53024
  entityId: this.device.id,
52741
53025
  field: "private_key_enc"
52742
53026
  });
52743
53027
  } else if (this.device.passwordEnc) {
52744
- authConfig.password = await decryptWithContext(this.device.passwordEnc, {
53028
+ authConfig.password = await this.deps.decrypt(this.device.passwordEnc, {
52745
53029
  scope: "device",
52746
53030
  entityId: this.device.id,
52747
53031
  field: "password_enc"
@@ -52749,17 +53033,15 @@ ${detail}`) : error;
52749
53033
  }
52750
53034
  break;
52751
53035
  }
53036
+ case "configRef":
53037
+ break;
52752
53038
  }
52753
53039
  if (this.device.authMode === "auto" && !authConfig.agent && !authConfig.privateKey && !authConfig.password) {
52754
53040
  throw new Error("auth_auto_missing: auto \u6A21\u5F0F\u4E0B\u672A\u627E\u5230\u53EF\u7528\u8BA4\u8BC1\u65B9\u5F0F\uFF08SSH_AUTH_SOCK / \u79C1\u94A5 / \u5BC6\u7801\uFF09");
52755
53041
  }
52756
- logStage("auth_config_resolved", {
52757
- hasAgent: Boolean(authConfig.agent),
52758
- hasPrivateKey: Boolean(authConfig.privateKey),
52759
- hasPassphrase: Boolean(authConfig.passphrase),
52760
- hasPassword: Boolean(authConfig.password)
52761
- });
52762
- return new Promise((resolve, reject) => {
53042
+ const client = this.deps.createClient();
53043
+ this.sshClient = client;
53044
+ await new Promise((resolve, reject) => {
52763
53045
  let settled = false;
52764
53046
  const resolveOnce = () => {
52765
53047
  if (settled) {
@@ -52773,429 +53055,289 @@ ${detail}`) : error;
52773
53055
  return;
52774
53056
  }
52775
53057
  settled = true;
52776
- reject(error instanceof Error ? error : new Error(String(error)));
53058
+ reject(error);
52777
53059
  };
52778
- let stderrTail = "";
52779
- conn.on("ready", () => {
52780
- logStage("ssh_ready");
52781
- const tmuxCommand = `tmux -CC new-session -A -s ${sessionName}`;
52782
- logStage("tmux_exec_start", { command: tmuxCommand });
52783
- conn.exec(tmuxCommand, {
52784
- pty: {
52785
- term: "xterm-256color",
52786
- cols: 80,
52787
- rows: 30
52788
- }
52789
- }, (err, stream) => {
52790
- if (err) {
52791
- logStage("tmux_exec_failed", { error: err.message });
52792
- rejectOnce(new Error(`tmux_exec_failed: \u542F\u52A8\u8FDC\u7AEF tmux \u5931\u8D25\uFF1A${err.message}`));
52793
- return;
52794
- }
52795
- this.sshStream = stream;
52796
- stream.on("close", () => {
52797
- logStage("ssh_stream_closed");
52798
- if (!this.ready) {
52799
- this.failReady(new Error("SSH stream closed before tmux became ready"));
52800
- }
52801
- if (this.lastExitReason) {
52802
- this.onError(new Error(`tmux exited: ${this.lastExitReason}`));
52803
- }
52804
- this.cleanup();
52805
- if (!this.manualDisconnect) {
52806
- this.onClose();
52807
- }
52808
- });
52809
- stream.on("data", (data) => {
52810
- this.parser.processData(data);
52811
- });
52812
- stream.stderr.on("data", (data) => {
52813
- const chunk2 = data.toString();
52814
- stderrTail = `${stderrTail}${chunk2}`.slice(-2000);
52815
- console.error("[ssh] stderr:", chunk2);
52816
- });
52817
- this.connected = true;
52818
- updateDeviceRuntimeStatus(this.deviceId, {
52819
- lastSeenAt: new Date().toISOString(),
52820
- tmuxAvailable: true,
52821
- lastError: null
52822
- });
52823
- this.waitForReady().then(() => {
52824
- this.configureWindowSizePolicy();
52825
- logStage("tmux_ready");
52826
- resolveOnce();
52827
- }).catch((err2) => {
52828
- const stderrText = stderrTail.trim();
52829
- const nextError = stderrText.length > 0 ? new Error(`${err2 instanceof Error ? err2.message : String(err2)}
52830
- ssh stderr: ${stderrText}`) : err2;
52831
- logStage("tmux_ready_failed", {
52832
- error: nextError instanceof Error ? nextError.message : String(nextError)
52833
- });
52834
- this.cleanup();
52835
- rejectOnce(nextError);
52836
- });
52837
- });
53060
+ client.on("ready", () => {
53061
+ resolveOnce();
52838
53062
  });
52839
- conn.on("error", (err) => {
52840
- logStage("connect_error", { error: err.message });
53063
+ client.on("error", (error) => {
52841
53064
  updateDeviceRuntimeStatus(this.deviceId, {
52842
53065
  lastSeenAt: new Date().toISOString(),
52843
53066
  tmuxAvailable: false,
52844
- lastError: err.message
53067
+ lastError: error.message
52845
53068
  });
52846
- rejectOnce(err);
53069
+ if (!settled) {
53070
+ rejectOnce(error);
53071
+ return;
53072
+ }
53073
+ if (!this.manualDisconnect) {
53074
+ this.callbacks.onError(error);
53075
+ this.shutdownInternal(true);
53076
+ }
52847
53077
  });
52848
- conn.on("close", () => {
52849
- logStage("connection_closed", { manualDisconnect: this.manualDisconnect });
52850
- if (!this.ready) {
52851
- this.failReady(new Error("SSH connection closed before tmux became ready"));
53078
+ client.on("close", () => {
53079
+ if (!settled) {
53080
+ rejectOnce(new Error("SSH connection closed before ready"));
53081
+ return;
52852
53082
  }
52853
- this.cleanup();
52854
53083
  if (!this.manualDisconnect) {
52855
- this.onClose();
53084
+ this.shutdownInternal(true);
52856
53085
  }
52857
53086
  });
52858
- logStage("connect_attempt");
52859
- conn.connect(authConfig);
53087
+ client.connect(authConfig);
52860
53088
  });
52861
53089
  }
52862
- shouldPassBellDedup(key) {
52863
- const now = Date.now();
52864
- const previous = this.bellDedup.get(key) ?? 0;
52865
- if (now - previous < BELL_DEDUP_WINDOW_MS) {
52866
- return false;
52867
- }
52868
- this.bellDedup.set(key, now);
52869
- return true;
52870
- }
52871
- handleTmuxEvent(event) {
52872
- if (event.type === "pane-active") {
52873
- const data = event.data ?? {};
52874
- const windowId = typeof data.windowId === "string" && data.windowId ? data.windowId : null;
52875
- const paneId = typeof data.paneId === "string" && data.paneId ? data.paneId : null;
52876
- if (windowId)
52877
- this.activeWindowId = windowId;
52878
- if (paneId)
52879
- this.activePaneId = paneId;
52880
- }
52881
- if (event.type === "bell") {
52882
- this.bellControlEventSeen = true;
52883
- const data = event.data ?? {};
52884
- const resolvedPaneId = (typeof data.paneId === "string" && data.paneId ? data.paneId : null) ?? this.activePaneId ?? undefined;
52885
- const windowId = typeof data.windowId === "string" && data.windowId ? data.windowId : undefined;
52886
- const key = resolvedPaneId ?? windowId ?? "-";
52887
- if (!this.shouldPassBellDedup(key)) {
52888
- return;
53090
+ async openCommandChannel() {
53091
+ const sshClient = this.requireSshClient();
53092
+ const stream = await new Promise((resolve, reject) => {
53093
+ sshClient.exec("/bin/sh -s", { pty: false }, (error, channel) => {
53094
+ if (error) {
53095
+ reject(error);
53096
+ return;
53097
+ }
53098
+ resolve(channel);
53099
+ });
53100
+ });
53101
+ this.commandStdoutBuffer = "";
53102
+ this.pendingCommand = null;
53103
+ this.commandStream = stream;
53104
+ stream.on("data", (data) => {
53105
+ this.commandStdoutBuffer += data.toString();
53106
+ this.flushCommandBuffer();
53107
+ });
53108
+ stream.stderr.on("data", (data) => {
53109
+ if (this.pendingCommand) {
53110
+ this.pendingCommand.stderr += data.toString();
52889
53111
  }
52890
- if (resolvedPaneId && !data.paneId) {
52891
- this.onEvent({
52892
- ...event,
52893
- data: {
52894
- ...data,
52895
- paneId: resolvedPaneId
52896
- }
52897
- });
52898
- return;
53112
+ });
53113
+ stream.on("close", () => {
53114
+ this.rejectPendingCommand(new Error("SSH command channel closed"));
53115
+ this.commandStream = null;
53116
+ if (!this.manualDisconnect) {
53117
+ this.shutdownInternal(true);
52899
53118
  }
52900
- }
52901
- this.onEvent(event);
53119
+ });
53120
+ const bootstrap = await this.runShell(buildSshBootstrapScript());
53121
+ const parsed = parseSshBootstrapOutput(bootstrap.stdout);
53122
+ if (!parsed.ok) {
53123
+ updateDeviceRuntimeStatus(this.deviceId, {
53124
+ lastSeenAt: new Date().toISOString(),
53125
+ tmuxAvailable: false,
53126
+ lastError: parsed.reason
53127
+ });
53128
+ throw new Error(`remote tmux unavailable: ${parsed.reason}`);
53129
+ }
53130
+ this.tmuxBin = parsed.tmuxBin;
53131
+ this.remoteHomeDir = parsed.homeDir;
53132
+ }
53133
+ async ensureRemoteRuntimeDirs() {
53134
+ await this.runShell([
53135
+ `mkdir -p ${quoteShellArg(this.fsPaths.rootDir)}`,
53136
+ `mkdir -p ${quoteShellArg(this.fsPaths.panesDir)}`,
53137
+ `mkdir -p ${quoteShellArg(this.fsPaths.hooksDir)}`,
53138
+ `chmod 700 ${quoteShellArg(this.fsPaths.rootDir)}`,
53139
+ `chmod 700 ${quoteShellArg(this.fsPaths.panesDir)}`,
53140
+ `chmod 700 ${quoteShellArg(this.fsPaths.hooksDir)}`
53141
+ ].join(`
53142
+ `));
53143
+ }
53144
+ async ensureSession() {
53145
+ const exists3 = await this.runTmuxAllowFailure(["has-session", "-t", this.sessionName]);
53146
+ if (exists3.exitCode === 0) {
53147
+ return;
53148
+ }
53149
+ await this.runTmux(["new-session", "-d", "-c", this.remoteHomeDir, "-s", this.sessionName]);
53150
+ }
53151
+ async startHooks() {
53152
+ await this.ensureRemoteRuntimeDirs();
53153
+ const fifoPath = this.fsPaths.hookFifoPath;
53154
+ await this.runShell(`rm -f ${quoteShellArg(fifoPath)} && mkfifo ${quoteShellArg(fifoPath)} && chmod 600 ${quoteShellArg(fifoPath)}`);
53155
+ const stopReader = await this.openReaderChannel(`exec tail -n +1 -f ${quoteShellArg(fifoPath)}`, {
53156
+ onData: (data) => {
53157
+ this.handleHookChunk(data.toString());
53158
+ },
53159
+ onClose: () => {
53160
+ if (!this.manualDisconnect) {
53161
+ this.callbacks.onError(new Error("SSH hook reader closed unexpectedly"));
53162
+ }
53163
+ }
53164
+ });
53165
+ this.hookReadAbort = () => {
53166
+ stopReader();
53167
+ this.runShellAllowFailure(`rm -f ${quoteShellArg(fifoPath)}`);
53168
+ };
53169
+ await this.installHook("alert-bell", ["bell", "#{window_id}", "#{pane_id}"]);
53170
+ await this.installHook("pane-exited", ["pane-exited", "#{window_id}", "#{pane_id}"]);
53171
+ await this.installHook("pane-died", ["pane-died", "#{window_id}", "#{pane_id}"]);
53172
+ }
53173
+ async stopHooks() {
53174
+ await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "alert-bell"]);
53175
+ await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "pane-exited"]);
53176
+ await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "pane-died"]);
53177
+ this.hookReadAbort?.();
53178
+ this.hookReadAbort = null;
53179
+ this.hookBuffer = "";
53180
+ }
53181
+ async installHook(hookName, fields) {
53182
+ const fifoPath = this.fsPaths.hookFifoPath;
53183
+ const innerScript = `printf '%s\\t%s\\t%s\\n' ${fields.map((field) => quoteShellArg(field)).join(" ")} >> ${quoteShellArg(fifoPath)}`;
53184
+ await this.runTmux([
53185
+ "set-hook",
53186
+ "-t",
53187
+ this.sessionName,
53188
+ hookName,
53189
+ `run-shell -b ${quoteShellArg(innerScript)}`
53190
+ ]);
52902
53191
  }
52903
- emitBellEventIfNeeded(paneId, data) {
52904
- if (this.bellControlEventSeen) {
52905
- return;
52906
- }
52907
- for (const byte of data) {
52908
- if (byte !== 7) {
52909
- continue;
53192
+ handleHookChunk(text2) {
53193
+ this.hookBuffer += text2;
53194
+ while (true) {
53195
+ const newlineIndex = this.hookBuffer.indexOf(`
53196
+ `);
53197
+ if (newlineIndex < 0) {
53198
+ return;
52910
53199
  }
52911
- if (!this.shouldPassBellDedup(paneId)) {
52912
- break;
53200
+ const line = this.hookBuffer.slice(0, newlineIndex).trim();
53201
+ this.hookBuffer = this.hookBuffer.slice(newlineIndex + 1);
53202
+ if (!line) {
53203
+ continue;
52913
53204
  }
52914
- this.onEvent({
52915
- type: "bell",
52916
- data: {
52917
- paneId
52918
- }
52919
- });
52920
- break;
52921
- }
52922
- }
52923
- handlePaneTitleUpdate(paneId, title) {
52924
- const nextTitle = title.trim();
52925
- if (!nextTitle) {
52926
- return;
52927
- }
52928
- let found = false;
52929
- let updated = false;
52930
- for (const window2 of this.snapshotWindows.values()) {
52931
- const pane = window2.panes.find((item) => item.id === paneId);
52932
- if (!pane) {
53205
+ const [type, windowId, paneId] = line.split("\t");
53206
+ if (type === "bell") {
53207
+ const key = paneId || windowId || "-";
53208
+ const previous = this.bellDedup.get(key) ?? 0;
53209
+ const now = Date.now();
53210
+ if (now - previous >= BELL_DEDUP_WINDOW_MS2) {
53211
+ this.bellDedup.set(key, now);
53212
+ this.callbacks.onEvent({
53213
+ type: "bell",
53214
+ data: {
53215
+ windowId: windowId || undefined,
53216
+ paneId: paneId || this.activePaneId || undefined
53217
+ }
53218
+ });
53219
+ }
52933
53220
  continue;
52934
53221
  }
52935
- found = true;
52936
- if (pane.title !== nextTitle) {
52937
- pane.title = nextTitle;
52938
- updated = true;
53222
+ if (type === "pane-exited" || type === "pane-died") {
53223
+ this.requestSnapshot();
52939
53224
  }
52940
- this.pendingPaneTitles.delete(paneId);
52941
- break;
52942
- }
52943
- if (found && !updated) {
52944
- return;
52945
- }
52946
- if (!updated) {
52947
- this.pendingPaneTitles.set(paneId, nextTitle);
52948
- return;
52949
53225
  }
52950
- this.emitSnapshotIfReady();
52951
53226
  }
52952
- configureWindowSizePolicy() {
52953
- if (!this.connected)
52954
- return;
52955
- this.sendCommand(`set-option -g -w window-size latest
52956
- `);
52957
- this.sendCommand(`set-option -g -w aggressive-resize off
52958
- `);
53227
+ async runAndRefresh(argv, allowTargetMissing = false) {
53228
+ await this.runTmux(argv, allowTargetMissing);
53229
+ await this.requestSnapshotInternal();
52959
53230
  }
52960
- scheduleSnapshotAfterResize(delayMs = 120) {
52961
- if (this.resizeSnapshotTimer) {
52962
- clearTimeout(this.resizeSnapshotTimer);
53231
+ async closeWindowInternal(windowId) {
53232
+ const count2 = Number.parseInt((await this.runTmux(["display-message", "-p", "-t", this.sessionName, "#{session_windows}"])).stdout.trim() || "0", 10);
53233
+ if (count2 <= 1) {
53234
+ await this.runTmux(["new-window", "-d", "-t", this.sessionName]);
52963
53235
  }
52964
- this.resizeSnapshotTimer = setTimeout(() => {
52965
- this.resizeSnapshotTimer = null;
52966
- this.requestSnapshot();
52967
- }, delayMs);
53236
+ await this.runAndRefresh(["kill-window", "-t", windowId], true);
52968
53237
  }
52969
- sendInput(paneId, data) {
52970
- if (!this.connected)
52971
- return;
52972
- this.sendUtf8Bytes(paneId, new TextEncoder().encode(data));
52973
- }
52974
- sendUtf8Bytes(paneId, data) {
52975
- if (data.length === 0) {
53238
+ async resizePaneInternal(paneId, cols, rows) {
53239
+ const safeCols = Math.max(2, Math.floor(cols));
53240
+ const safeRows = Math.max(2, Math.floor(rows));
53241
+ const windowId = this.findPaneWindowId(paneId) ?? (await this.runTmux(["display-message", "-p", "-t", paneId, "#{window_id}"], true)).stdout.trim();
53242
+ if (!windowId) {
52976
53243
  return;
52977
53244
  }
52978
- const chunkSize = 256;
52979
- for (let offset = 0;offset < data.length; offset += chunkSize) {
52980
- const chunk2 = data.slice(offset, offset + chunkSize);
52981
- const hex = [];
52982
- for (const byte of chunk2) {
52983
- hex.push(byte.toString(16).padStart(2, "0"));
52984
- }
52985
- this.sendCommand(`send-keys -H -t ${paneId} ${hex.join(" ")}
52986
- `);
52987
- }
53245
+ await this.runTmux(["resize-window", "-t", windowId, "-x", String(safeCols), "-y", String(safeRows)], true);
53246
+ await this.requestSnapshotInternal();
52988
53247
  }
52989
- sendKey(paneId, key) {
52990
- if (!this.connected)
52991
- return;
52992
- const cmd = `send-keys -t ${paneId} ${key}
52993
- `;
52994
- this.sendCommand(cmd);
52995
- }
52996
- selectWindow(windowId) {
52997
- if (!this.connected) {
52998
- console.log("[tmux] cannot select window: not connected");
52999
- return;
53000
- }
53248
+ async selectPaneInternal(windowId, paneId, size) {
53001
53249
  this.activeWindowId = windowId;
53002
- this.sendCommand(`select-window -t ${windowId}
53003
- `);
53004
- this.requestSnapshot();
53005
- }
53006
- selectPane(windowId, paneId) {
53007
- if (!this.connected) {
53008
- console.log("[tmux] cannot select pane: not connected");
53009
- return;
53010
- }
53011
- console.log("[tmux] selecting pane", paneId, "in window", windowId);
53012
53250
  this.activePaneId = paneId;
53013
- this.activeWindowId = windowId;
53014
- this.sendCommand(`select-window -t ${windowId}
53015
- `);
53016
- this.sendCommand(`select-pane -t ${paneId}
53017
- `);
53018
- this.capturePaneHistory(paneId);
53019
- }
53020
- capturePaneHistory(paneId) {
53021
- if (!this.connected) {
53022
- console.log("[tmux] cannot capture history: not connected");
53023
- return;
53024
- }
53025
- console.log("[tmux] capturing history for pane", paneId);
53026
- const existing = this.historyCaptureStates.get(paneId);
53027
- if (existing?.timeout) {
53028
- clearTimeout(existing.timeout);
53029
- }
53030
- const timeout = setTimeout(() => {
53031
- this.emitCapturedHistory(paneId);
53032
- }, 220);
53033
- this.historyCaptureStates.set(paneId, {
53034
- normal: null,
53035
- alternate: null,
53036
- preferAlternate: null,
53037
- timeout
53038
- });
53039
- this.pendingCapturePaneModeRequests.push(paneId);
53040
- this.sendCommand(`display-message -p -t ${paneId} "#{alternate_on}"
53041
- `, "capture-pane-mode");
53042
- this.pendingCapturePaneRequests.push({ paneId, mode: "normal" });
53043
- this.sendCommand(`capture-pane -t ${paneId} -S -1000 -e -p
53044
- `, "capture-pane");
53045
- this.pendingCapturePaneRequests.push({ paneId, mode: "alternate" });
53046
- this.sendCommand(`capture-pane -t ${paneId} -a -S -1000 -e -p -q
53047
- `, "capture-pane");
53048
- }
53049
- resizePane(_paneId, cols, rows) {
53050
- if (!this.connected)
53051
- return;
53052
- const safeCols = Math.max(2, Math.floor(cols));
53053
- const safeRows = Math.max(2, Math.floor(rows));
53054
- this.sendCommand(`refresh-client -C ${safeCols}x${safeRows}
53055
- `);
53056
- const windowId = this.activeWindowId;
53057
- if (windowId) {
53058
- this.sendCommand(`resize-window -x ${safeCols} -y ${safeRows} -t ${windowId}
53059
- `);
53060
- this.sendCommand(`set-window-option -t ${windowId} window-size latest
53061
- `);
53251
+ await this.runTmux(["select-window", "-t", windowId], true);
53252
+ await this.runTmux(["select-pane", "-t", paneId], true);
53253
+ await this.startPipeForPane(paneId);
53254
+ if (size) {
53255
+ await this.resizePaneInternal(paneId, size.cols, size.rows);
53062
53256
  }
53063
- this.scheduleSnapshotAfterResize();
53257
+ this.callbacks.onEvent({
53258
+ type: "pane-active",
53259
+ data: { windowId, paneId }
53260
+ });
53261
+ await this.capturePaneHistory(paneId);
53262
+ await this.requestSnapshotInternal();
53064
53263
  }
53065
- createWindow(name) {
53066
- if (!this.connected)
53067
- return;
53068
- if (name) {
53069
- this.sendCommand(`new-window -n "${name}"
53070
- `);
53071
- } else {
53072
- this.sendCommand(`new-window
53073
- `);
53264
+ async capturePaneHistory(paneId) {
53265
+ const mode = (await this.runTmux(["display-message", "-p", "-t", paneId, "#{alternate_on}"], true)).stdout.trim();
53266
+ const normal = (await this.runTmux(["capture-pane", "-t", paneId, "-S", DEFAULT_HISTORY_LINES, "-e", "-p"], true, 30000)).stdout;
53267
+ const alternate = (await this.runTmux(["capture-pane", "-t", paneId, "-a", "-S", DEFAULT_HISTORY_LINES, "-e", "-p", "-q"], true, 30000)).stdout;
53268
+ const history = normal || alternate;
53269
+ if (history) {
53270
+ this.callbacks.onTerminalHistory(paneId, history);
53074
53271
  }
53075
53272
  }
53076
- closeWindow(windowId) {
53077
- if (!this.connected)
53078
- return;
53079
- this.sendCommand(`if-shell -F '#{==:#{session_windows},1}' 'new-window -d' ''
53080
- `);
53081
- this.sendCommand(`kill-window -t ${windowId}
53082
- `);
53083
- }
53084
- closePane(paneId) {
53085
- if (!this.connected)
53086
- return;
53087
- this.sendCommand(`kill-pane -t ${paneId}
53088
- `);
53089
- }
53090
- renameWindow(windowId, name) {
53091
- if (!this.connected)
53092
- return;
53093
- this.sendCommand(`rename-window -t ${windowId} "${name}"
53094
- `);
53095
- }
53096
- requestSnapshot() {
53097
- if (!this.connected)
53098
- return;
53099
- this.sendCommand(`display-message -p "#{session_id} #{session_name}"
53100
- `, "snapshot-session");
53101
- this.sendCommand(`list-windows -F "#{window_id} #{window_index} #{window_name} #{window_active}"
53102
- `, "snapshot-windows");
53103
- this.sendCommand(`list-panes -F "#{pane_id} #{window_id} #{pane_index} #{pane_title} #{pane_active} #{pane_width} #{pane_height}"
53104
- `, "snapshot-panes");
53105
- }
53106
- handleOutputBlockBegin(meta) {
53107
- const kind = this.pendingCommandKinds.shift() ?? "noop";
53108
- this.commandKindsByNo.set(meta.commandNo, kind);
53109
- }
53110
- handleOutputBlock(block) {
53111
- this.markReady();
53112
- const kind = this.commandKindsByNo.get(block.commandNo);
53113
- this.commandKindsByNo.delete(block.commandNo);
53114
- console.log("[tmux] handleOutputBlock commandNo:", block.commandNo, "kind:", kind, "lines:", block.lines.length);
53115
- const resolvedKind = kind ?? "noop";
53116
- if (block.isError) {
53117
- const message = block.lines.join(`
53118
- `).trim();
53119
- this.handleCaptureErrorFallback(resolvedKind);
53120
- if (message) {
53121
- if (this.isRecoverableTargetMissingError(message)) {
53122
- this.recoverFromTargetMissingError(message);
53123
- return;
53124
- }
53125
- this.onError(new Error(message));
53126
- }
53273
+ async requestSnapshotInternal() {
53274
+ if (!this.connected) {
53127
53275
  return;
53128
53276
  }
53129
- switch (resolvedKind) {
53130
- case "noop":
53131
- break;
53132
- case "snapshot-session":
53133
- this.parseSnapshotSession(block.lines);
53134
- break;
53135
- case "snapshot-windows":
53136
- this.parseSnapshotWindows(block.lines);
53137
- break;
53138
- case "snapshot-panes":
53139
- this.parseSnapshotPanes(block.lines);
53140
- break;
53141
- case "capture-pane":
53142
- this.handleCapturePaneOutput(block.lines);
53143
- break;
53144
- case "capture-pane-mode":
53145
- this.handleCapturePaneModeOutput(block.lines);
53146
- break;
53147
- }
53148
- this.emitSnapshotIfReady();
53149
- }
53150
- handleCaptureErrorFallback(kind) {
53151
- if (kind === "capture-pane") {
53152
- this.handleCapturePaneOutput([]);
53277
+ const [sessionRes, windowsRes, panesRes] = await Promise.all([
53278
+ this.runTmuxAllowFailure([
53279
+ "display-message",
53280
+ "-p",
53281
+ "-t",
53282
+ this.sessionName,
53283
+ "#{session_id}\t#{session_name}"
53284
+ ]),
53285
+ this.runTmuxAllowFailure([
53286
+ "list-windows",
53287
+ "-t",
53288
+ this.sessionName,
53289
+ "-F",
53290
+ "#{window_id}\t#{window_index}\t#{window_name}\t#{window_active}"
53291
+ ]),
53292
+ this.runTmuxAllowFailure([
53293
+ "list-panes",
53294
+ "-t",
53295
+ this.sessionName,
53296
+ "-F",
53297
+ "#{pane_id}\t#{window_id}\t#{pane_index}\t#{pane_title}\t#{pane_active}\t#{pane_width}\t#{pane_height}"
53298
+ ])
53299
+ ]);
53300
+ if (sessionRes.exitCode !== 0 || windowsRes.exitCode !== 0 || panesRes.exitCode !== 0) {
53301
+ this.callbacks.onSnapshot({ deviceId: this.deviceId, session: null });
53153
53302
  return;
53154
53303
  }
53155
- if (kind === "capture-pane-mode") {
53156
- this.handleCapturePaneModeOutput([]);
53157
- }
53158
- }
53159
- isRecoverableTargetMissingError(message) {
53160
- const normalized = message.toLowerCase();
53161
- return normalized.includes("can't find window") || normalized.includes("can't find pane") || normalized.includes("no such window") || normalized.includes("no such pane");
53162
- }
53163
- recoverFromTargetMissingError(message) {
53164
- const normalized = message.toLowerCase();
53165
- if (normalized.includes("window")) {
53166
- this.activeWindowId = null;
53167
- }
53168
- if (normalized.includes("pane")) {
53169
- this.activePaneId = null;
53170
- }
53171
- this.requestSnapshot();
53304
+ this.parseSnapshotSession(sessionRes.stdout.split(/\r?\n/));
53305
+ this.parseSnapshotWindows(windowsRes.stdout.split(/\r?\n/));
53306
+ this.parseSnapshotPanes(panesRes.stdout.split(/\r?\n/));
53307
+ this.emitSnapshot();
53172
53308
  }
53173
53309
  parseSnapshotSession(lines) {
53310
+ this.snapshotSession = null;
53174
53311
  for (const line of lines) {
53175
- if (!line.trim())
53312
+ if (!line.trim()) {
53176
53313
  continue;
53314
+ }
53177
53315
  const [id, name] = line.split("\t");
53178
- if (!id)
53179
- continue;
53180
- this.snapshotSession = { id, name: name ?? "" };
53316
+ if (id) {
53317
+ this.snapshotSession = { id, name: name ?? "" };
53318
+ }
53181
53319
  return;
53182
53320
  }
53183
53321
  }
53184
53322
  parseSnapshotWindows(lines) {
53185
53323
  this.snapshotWindows.clear();
53186
- this.snapshotPanesReady = false;
53187
53324
  for (const line of lines) {
53188
- if (!line.trim())
53325
+ if (!line.trim()) {
53189
53326
  continue;
53327
+ }
53190
53328
  const [id, indexRaw, name, activeRaw] = line.split("\t");
53191
- if (!id)
53329
+ if (!id) {
53192
53330
  continue;
53331
+ }
53193
53332
  const index = Number.parseInt(indexRaw ?? "", 10);
53194
53333
  const active = activeRaw === "1";
53334
+ if (active) {
53335
+ this.activeWindowId = id;
53336
+ }
53195
53337
  this.snapshotWindows.set(id, {
53196
53338
  id,
53197
- name: name ?? "",
53198
53339
  index: Number.isNaN(index) ? 0 : index,
53340
+ name: name ?? "",
53199
53341
  active,
53200
53342
  panes: []
53201
53343
  });
@@ -53206,195 +53348,493 @@ ssh stderr: ${stderrText}`) : err2;
53206
53348
  window2.panes = [];
53207
53349
  }
53208
53350
  for (const line of lines) {
53209
- if (!line.trim())
53351
+ if (!line.trim()) {
53210
53352
  continue;
53353
+ }
53211
53354
  const [paneId, windowId, indexRaw, titleRaw, activeRaw, widthRaw, heightRaw] = line.split("\t");
53212
- if (!paneId || !windowId)
53355
+ if (!paneId || !windowId) {
53213
53356
  continue;
53357
+ }
53214
53358
  const index = Number.parseInt(indexRaw ?? "", 10);
53215
53359
  const width = Number.parseInt(widthRaw ?? "", 10);
53216
53360
  const height = Number.parseInt(heightRaw ?? "", 10);
53217
- const title = titleRaw?.trim() ? titleRaw : undefined;
53218
53361
  const pane = {
53219
53362
  id: paneId,
53220
53363
  windowId,
53221
53364
  index: Number.isNaN(index) ? 0 : index,
53222
- title: this.pendingPaneTitles.get(paneId) ?? title,
53365
+ title: this.pendingPaneTitles.get(paneId) ?? (titleRaw?.trim() ? titleRaw : undefined),
53223
53366
  active: activeRaw === "1",
53224
53367
  width: Number.isNaN(width) ? 0 : width,
53225
53368
  height: Number.isNaN(height) ? 0 : height
53226
53369
  };
53227
- const win = this.snapshotWindows.get(windowId);
53228
- if (!win)
53370
+ if (pane.active) {
53371
+ this.activePaneId = paneId;
53372
+ this.activeWindowId = windowId;
53373
+ }
53374
+ const window2 = this.snapshotWindows.get(windowId);
53375
+ if (!window2) {
53229
53376
  continue;
53230
- win.panes.push(pane);
53377
+ }
53378
+ window2.panes.push(pane);
53231
53379
  this.pendingPaneTitles.delete(paneId);
53232
53380
  }
53233
- for (const win of this.snapshotWindows.values()) {
53234
- win.panes.sort((a, b3) => a.index - b3.index);
53381
+ for (const window2 of this.snapshotWindows.values()) {
53382
+ window2.panes.sort((left, right) => left.index - right.index);
53235
53383
  }
53236
- this.snapshotPanesReady = true;
53237
53384
  }
53238
- emitSnapshotIfReady() {
53239
- if (!this.snapshotSession)
53240
- return;
53241
- if (this.snapshotWindows.size === 0)
53242
- return;
53243
- if (!this.snapshotPanesReady)
53244
- return;
53245
- const windows = Array.from(this.snapshotWindows.values()).sort((a, b3) => a.index - b3.index);
53246
- const session = {
53385
+ emitSnapshot() {
53386
+ const session = this.snapshotSession ? {
53247
53387
  id: this.snapshotSession.id,
53248
53388
  name: this.snapshotSession.name,
53249
- windows
53250
- };
53251
- this.onSnapshot({
53389
+ windows: Array.from(this.snapshotWindows.values()).sort((left, right) => left.index - right.index)
53390
+ } : null;
53391
+ this.callbacks.onSnapshot({
53252
53392
  deviceId: this.deviceId,
53253
53393
  session
53254
53394
  });
53255
53395
  }
53256
- sendCommand(cmd, kind = "noop") {
53257
- this.pendingCommandKinds.push(kind);
53258
- if (this.terminal) {
53259
- this.terminal.write(cmd);
53260
- return;
53261
- }
53262
- if (this.sshStream) {
53263
- this.sshStream.write(cmd);
53396
+ findPaneWindowId(paneId) {
53397
+ for (const window2 of this.snapshotWindows.values()) {
53398
+ if (window2.panes.some((pane) => pane.id === paneId)) {
53399
+ return window2.id;
53400
+ }
53264
53401
  }
53402
+ return null;
53403
+ }
53404
+ async startPipeForPane(paneId) {
53405
+ await this.queuePipeTransition(async () => {
53406
+ if (this.currentPipePaneId === paneId) {
53407
+ return;
53408
+ }
53409
+ await this.stopPipeNow();
53410
+ const fifoPath = this.fsPaths.paneFifoPath(paneId);
53411
+ await this.ensureRemoteRuntimeDirs();
53412
+ await this.runShell(`rm -f ${quoteShellArg(fifoPath)} && mkfifo ${quoteShellArg(fifoPath)} && chmod 600 ${quoteShellArg(fifoPath)}`);
53413
+ const parser = createPaneTitleParser({
53414
+ onTitle: (title) => {
53415
+ this.pendingPaneTitles.set(paneId, title);
53416
+ this.requestSnapshot();
53417
+ }
53418
+ });
53419
+ const stopReader = await this.openReaderChannel(`exec cat ${quoteShellArg(fifoPath)}`, {
53420
+ onData: (raw) => {
53421
+ const output = parser.push(raw);
53422
+ if (Array.from(raw).includes(7)) {
53423
+ this.callbacks.onEvent({ type: "bell", data: { paneId } });
53424
+ }
53425
+ if (output.length > 0) {
53426
+ this.callbacks.onTerminalOutput(paneId, output);
53427
+ }
53428
+ },
53429
+ onClose: () => {
53430
+ if (!this.manualDisconnect && this.currentPipePaneId === paneId) {
53431
+ this.callbacks.onError(new Error(`SSH pane reader closed unexpectedly: ${paneId}`));
53432
+ }
53433
+ }
53434
+ });
53435
+ this.pipeReadAbort = () => {
53436
+ stopReader();
53437
+ this.runShellAllowFailure(`rm -f ${quoteShellArg(fifoPath)}`);
53438
+ };
53439
+ await this.runTmux(["pipe-pane", "-O", "-t", paneId, `cat >${fifoPath}`]);
53440
+ this.currentPipePaneId = paneId;
53441
+ });
53265
53442
  }
53266
- emitTerminalOutput(paneId, data) {
53267
- this.emitBellEventIfNeeded(paneId, data);
53268
- this.onTerminalOutput(paneId, data);
53443
+ async stopPipe() {
53444
+ await this.queuePipeTransition(() => this.stopPipeNow());
53269
53445
  }
53270
- handleNonControlOutput(line) {
53271
- if (this.ready) {
53272
- return;
53446
+ async stopPipeNow() {
53447
+ const paneId = this.currentPipePaneId;
53448
+ this.currentPipePaneId = null;
53449
+ if (paneId) {
53450
+ await this.runTmuxAllowFailure(["pipe-pane", "-t", paneId]);
53273
53451
  }
53274
- if (this.startupNonControlOutput.length >= 20) {
53452
+ this.pipeReadAbort?.();
53453
+ this.pipeReadAbort = null;
53454
+ }
53455
+ queuePipeTransition(task) {
53456
+ const next = this.pipeTransition.catch(() => {
53275
53457
  return;
53458
+ }).then(task);
53459
+ this.pipeTransition = next;
53460
+ return next;
53461
+ }
53462
+ async runTmux(argv, allowTargetMissing = false, timeoutMs = 1e4) {
53463
+ const result = await this.runTmuxAllowFailure(argv, timeoutMs);
53464
+ if (result.exitCode === 0) {
53465
+ return result;
53466
+ }
53467
+ const message = (result.stderr.trim() || result.stdout.trim() || `tmux command failed: ${argv.join(" ")}`).trim();
53468
+ if (allowTargetMissing && this.isRecoverableTargetMissingError(message)) {
53469
+ this.recoverFromTargetMissingError(message);
53470
+ return result;
53276
53471
  }
53277
- this.startupNonControlOutput.push(line);
53472
+ updateDeviceRuntimeStatus(this.deviceId, {
53473
+ lastSeenAt: new Date().toISOString(),
53474
+ tmuxAvailable: false,
53475
+ lastError: message
53476
+ });
53477
+ throw new Error(message);
53278
53478
  }
53279
- disconnect() {
53280
- this.manualDisconnect = true;
53281
- this.cleanup();
53479
+ async runTmuxAllowFailure(argv, timeoutMs = 1e4) {
53480
+ return this.runShell(`${quoteShellArg(this.tmuxBin)} ${joinShellArgs(argv)}`, timeoutMs);
53282
53481
  }
53283
- wasManuallyDisconnected() {
53284
- return this.manualDisconnect;
53482
+ async runShell(command, timeoutMs = 1e4) {
53483
+ return this.enqueueShellCommand(command, timeoutMs);
53285
53484
  }
53286
- handleCapturePaneOutput(lines) {
53287
- console.log("[tmux] capture-pane output:", lines.length, "lines");
53288
- const request = this.pendingCapturePaneRequests.shift() ?? null;
53289
- if (!request) {
53290
- console.log("[tmux] no pending pane id for capture-pane output");
53291
- return;
53485
+ async runShellAllowFailure(command, timeoutMs = 1e4) {
53486
+ try {
53487
+ return await this.enqueueShellCommand(command, timeoutMs);
53488
+ } catch (error) {
53489
+ return {
53490
+ exitCode: 1,
53491
+ stdout: "",
53492
+ stderr: error instanceof Error ? error.message : String(error)
53493
+ };
53292
53494
  }
53293
- const data = lines.join(`
53294
- `);
53295
- const state = this.historyCaptureStates.get(request.paneId);
53296
- if (!state) {
53495
+ }
53496
+ enqueueShellCommand(command, timeoutMs) {
53497
+ const next = this.commandQueue.catch(() => {
53297
53498
  return;
53499
+ }).then(() => this.executeShellCommand(command, timeoutMs));
53500
+ this.commandQueue = next.then(() => {
53501
+ return;
53502
+ });
53503
+ return next;
53504
+ }
53505
+ executeShellCommand(command, timeoutMs) {
53506
+ const stream = this.commandStream;
53507
+ if (!stream) {
53508
+ return Promise.reject(new Error("SSH command channel not ready"));
53298
53509
  }
53299
- if (request.mode === "normal") {
53300
- state.normal = data;
53301
- } else {
53302
- state.alternate = data;
53303
- }
53304
- if (state.normal !== null && state.alternate !== null) {
53305
- this.emitCapturedHistory(request.paneId);
53510
+ const commandId = crypto.randomUUID();
53511
+ const wrappedCommand = `{ ${command}; } 2>&1
53512
+ printf '\\036TMEX_END %s %d\\036\\n' ${quoteShellArg(commandId)} $?
53513
+ `;
53514
+ return new Promise((resolve, reject) => {
53515
+ const timer = setTimeout(() => {
53516
+ if (!this.pendingCommand || this.pendingCommand.id !== commandId) {
53517
+ return;
53518
+ }
53519
+ this.pendingCommand = null;
53520
+ reject(new Error(`remote command timed out: ${command}`));
53521
+ }, timeoutMs);
53522
+ this.pendingCommand = {
53523
+ id: commandId,
53524
+ stderr: "",
53525
+ resolve,
53526
+ reject,
53527
+ timer
53528
+ };
53529
+ stream.write(wrappedCommand);
53530
+ });
53531
+ }
53532
+ flushCommandBuffer() {
53533
+ while (true) {
53534
+ const sentinelIndex = this.commandStdoutBuffer.indexOf(COMMAND_SENTINEL);
53535
+ if (sentinelIndex < 0) {
53536
+ return;
53537
+ }
53538
+ const sentinelEnd = this.commandStdoutBuffer.indexOf("\x1E", sentinelIndex + COMMAND_SENTINEL.length);
53539
+ if (sentinelEnd < 0) {
53540
+ return;
53541
+ }
53542
+ const payload = this.commandStdoutBuffer.slice(sentinelIndex + COMMAND_SENTINEL.length, sentinelEnd).trim();
53543
+ const [commandId = "", exitCodeRaw = "1"] = payload.split(/\s+/);
53544
+ const stdout = this.commandStdoutBuffer.slice(0, sentinelIndex);
53545
+ this.commandStdoutBuffer = this.commandStdoutBuffer.slice(sentinelEnd + 1).replace(/^\r?\n/, "");
53546
+ const pending = this.pendingCommand;
53547
+ if (!pending || pending.id !== commandId) {
53548
+ continue;
53549
+ }
53550
+ this.pendingCommand = null;
53551
+ clearTimeout(pending.timer);
53552
+ pending.resolve({
53553
+ exitCode: Number.parseInt(exitCodeRaw, 10) || 0,
53554
+ stdout,
53555
+ stderr: pending.stderr
53556
+ });
53306
53557
  }
53307
53558
  }
53308
- handleCapturePaneModeOutput(lines) {
53309
- const paneId = this.pendingCapturePaneModeRequests.shift() ?? null;
53310
- if (!paneId) {
53559
+ rejectPendingCommand(error) {
53560
+ const pending = this.pendingCommand;
53561
+ if (!pending) {
53311
53562
  return;
53312
53563
  }
53313
- const state = this.historyCaptureStates.get(paneId);
53314
- if (!state) {
53564
+ this.pendingCommand = null;
53565
+ clearTimeout(pending.timer);
53566
+ pending.reject(error);
53567
+ }
53568
+ async openReaderChannel(command, options) {
53569
+ const sshClient = this.requireSshClient();
53570
+ const stream = await new Promise((resolve, reject) => {
53571
+ sshClient.exec("/bin/sh -s", { pty: false }, (error, channel) => {
53572
+ if (error) {
53573
+ reject(error);
53574
+ return;
53575
+ }
53576
+ resolve(channel);
53577
+ });
53578
+ });
53579
+ stream.on("data", (data) => {
53580
+ options.onData(data);
53581
+ });
53582
+ stream.stderr.on("data", (data) => {
53583
+ if (!this.manualDisconnect) {
53584
+ this.callbacks.onError(new Error(data.toString().trim() || "SSH reader stderr output"));
53585
+ }
53586
+ });
53587
+ stream.on("close", () => {
53588
+ options.onClose?.();
53589
+ });
53590
+ stream.write(`${command}
53591
+ `);
53592
+ return () => {
53593
+ stream.end();
53594
+ stream.close();
53595
+ stream.destroy();
53596
+ };
53597
+ }
53598
+ isRecoverableTargetMissingError(message) {
53599
+ const normalized = message.toLowerCase();
53600
+ return normalized.includes("can't find window") || normalized.includes("can't find pane") || normalized.includes("no such window") || normalized.includes("no such pane");
53601
+ }
53602
+ recoverFromTargetMissingError(message) {
53603
+ const normalized = message.toLowerCase();
53604
+ if (normalized.includes("window")) {
53605
+ this.activeWindowId = null;
53606
+ }
53607
+ if (normalized.includes("pane")) {
53608
+ this.activePaneId = null;
53609
+ }
53610
+ this.requestSnapshot();
53611
+ }
53612
+ async shutdownInternal(notifyClose) {
53613
+ if (this.cleanupPromise) {
53614
+ await this.cleanupPromise;
53615
+ if (notifyClose && !this.closeNotified && !this.manualDisconnect) {
53616
+ this.closeNotified = true;
53617
+ this.callbacks.onClose();
53618
+ }
53315
53619
  return;
53316
53620
  }
53317
- const firstLine = lines.find((line) => line.trim().length > 0)?.trim() ?? "";
53318
- if (firstLine === "1") {
53319
- state.preferAlternate = true;
53320
- } else if (firstLine === "0") {
53321
- state.preferAlternate = false;
53621
+ this.connected = false;
53622
+ this.cleanupPromise = (async () => {
53623
+ await this.stopPipe().catch(() => {
53624
+ return;
53625
+ });
53626
+ await this.stopHooks().catch(() => {
53627
+ return;
53628
+ });
53629
+ await this.runShellAllowFailure(`rm -rf ${quoteShellArg(this.fsPaths.rootDir)}`).catch(() => {
53630
+ return;
53631
+ });
53632
+ this.rejectPendingCommand(new Error("SSH command channel closed"));
53633
+ this.commandStream?.end();
53634
+ this.commandStream?.close();
53635
+ this.commandStream?.destroy();
53636
+ this.commandStream = null;
53637
+ this.sshClient?.end();
53638
+ this.sshClient = null;
53639
+ })();
53640
+ await this.cleanupPromise;
53641
+ this.cleanupPromise = null;
53642
+ if (notifyClose && !this.closeNotified && !this.manualDisconnect) {
53643
+ this.closeNotified = true;
53644
+ this.callbacks.onClose();
53322
53645
  }
53323
- if (state.normal !== null && state.alternate !== null) {
53324
- this.emitCapturedHistory(paneId);
53646
+ }
53647
+ requireSshClient() {
53648
+ if (!this.sshClient) {
53649
+ throw new Error("SSH client not connected");
53325
53650
  }
53651
+ return this.sshClient;
53326
53652
  }
53327
- emitCapturedHistory(paneId) {
53328
- const state = this.historyCaptureStates.get(paneId);
53329
- if (!state)
53330
- return;
53331
- if (state.timeout) {
53332
- clearTimeout(state.timeout);
53333
- state.timeout = null;
53653
+ }
53654
+
53655
+ // ../../apps/gateway/src/tmux-client/device-session-runtime.ts
53656
+ function createDefaultConnection(options) {
53657
+ const device = getDeviceById(options.deviceId);
53658
+ if (device?.type === "local") {
53659
+ return new LocalExternalTmuxConnection(options);
53660
+ }
53661
+ return new SshExternalTmuxConnection(options);
53662
+ }
53663
+
53664
+ class DeviceSessionRuntime {
53665
+ deviceId;
53666
+ connection;
53667
+ listeners = new Set;
53668
+ connectPromise = null;
53669
+ terminated = false;
53670
+ closeEmitted = false;
53671
+ manualDisconnect = false;
53672
+ constructor(options) {
53673
+ this.deviceId = options.deviceId;
53674
+ const createConnection = options.createConnection ?? createDefaultConnection;
53675
+ this.connection = createConnection({
53676
+ deviceId: this.deviceId,
53677
+ onEvent: (event) => {
53678
+ this.broadcast((listener) => listener.onEvent?.(event));
53679
+ },
53680
+ onTerminalOutput: (paneId, data) => {
53681
+ this.broadcast((listener) => listener.onTerminalOutput?.(paneId, data));
53682
+ },
53683
+ onTerminalHistory: (paneId, data) => {
53684
+ this.broadcast((listener) => listener.onTerminalHistory?.(paneId, data));
53685
+ },
53686
+ onSnapshot: (payload) => {
53687
+ this.broadcast((listener) => listener.onSnapshot?.(payload));
53688
+ },
53689
+ onError: (error) => {
53690
+ this.broadcast((listener) => listener.onError?.(error));
53691
+ },
53692
+ onClose: () => {
53693
+ if (this.manualDisconnect || this.closeEmitted) {
53694
+ return;
53695
+ }
53696
+ this.closeEmitted = true;
53697
+ this.terminated = true;
53698
+ this.broadcast((listener) => listener.onClose?.());
53699
+ }
53700
+ });
53701
+ }
53702
+ subscribe(listener) {
53703
+ this.listeners.add(listener);
53704
+ return () => {
53705
+ this.listeners.delete(listener);
53706
+ };
53707
+ }
53708
+ async connect() {
53709
+ if (this.terminated && !this.connectPromise) {
53710
+ return Promise.reject(new Error(`Device session runtime already terminated: ${this.deviceId}`));
53334
53711
  }
53335
- const normal = state.normal ?? "";
53336
- const alternate = state.alternate ?? "";
53337
- let selected = normal;
53338
- if (state.preferAlternate === true) {
53339
- selected = alternate || normal;
53340
- } else if (state.preferAlternate === false) {
53341
- selected = normal || alternate;
53342
- } else if (alternate.length > normal.length) {
53343
- selected = alternate;
53712
+ if (this.connectPromise) {
53713
+ return this.connectPromise;
53344
53714
  }
53345
- if (selected) {
53346
- console.log("[tmux] sending history for pane", paneId, "data length:", selected.length);
53347
- this.onTerminalHistory(paneId, selected);
53715
+ this.connectPromise = this.connection.connect().catch((error) => {
53716
+ this.terminated = true;
53717
+ throw error;
53718
+ });
53719
+ return this.connectPromise;
53720
+ }
53721
+ disconnect() {
53722
+ if (this.terminated) {
53723
+ return;
53348
53724
  }
53349
- this.historyCaptureStates.delete(paneId);
53725
+ this.terminated = true;
53726
+ this.manualDisconnect = true;
53727
+ this.connection.disconnect();
53350
53728
  }
53351
- cleanup() {
53352
- this.connected = false;
53353
- this.parser.flush();
53354
- this.pendingCommandKinds = [];
53355
- this.commandKindsByNo.clear();
53356
- this.pendingCapturePaneRequests = [];
53357
- this.pendingCapturePaneModeRequests = [];
53358
- for (const state of this.historyCaptureStates.values()) {
53359
- if (state.timeout) {
53360
- clearTimeout(state.timeout);
53729
+ async shutdown() {
53730
+ this.disconnect();
53731
+ }
53732
+ requestSnapshot() {
53733
+ this.connection.requestSnapshot();
53734
+ }
53735
+ sendInput(paneId, data) {
53736
+ this.connection.sendInput(paneId, data);
53737
+ }
53738
+ resizePane(paneId, cols, rows) {
53739
+ this.connection.resizePane(paneId, cols, rows);
53740
+ }
53741
+ selectPane(windowId, paneId) {
53742
+ this.connection.selectPane(windowId, paneId);
53743
+ }
53744
+ selectPaneWithSize(windowId, paneId, cols, rows) {
53745
+ this.connection.selectPaneWithSize(windowId, paneId, cols, rows);
53746
+ }
53747
+ selectWindow(windowId) {
53748
+ this.connection.selectWindow(windowId);
53749
+ }
53750
+ createWindow(name) {
53751
+ this.connection.createWindow(name);
53752
+ }
53753
+ closeWindow(windowId) {
53754
+ this.connection.closeWindow(windowId);
53755
+ }
53756
+ closePane(paneId) {
53757
+ this.connection.closePane(paneId);
53758
+ }
53759
+ renameWindow(windowId, name) {
53760
+ this.connection.renameWindow(windowId, name);
53761
+ }
53762
+ broadcast(action) {
53763
+ for (const listener of this.listeners) {
53764
+ try {
53765
+ action(listener);
53766
+ } catch (error) {
53767
+ console.error("[tmux-client] listener callback failed:", error);
53361
53768
  }
53362
53769
  }
53363
- this.historyCaptureStates.clear();
53364
- this.snapshotSession = null;
53365
- this.snapshotWindows.clear();
53366
- this.pendingPaneTitles.clear();
53367
- this.snapshotPanesReady = false;
53368
- this.lastExitReason = null;
53369
- this.activePaneId = null;
53370
- this.activeWindowId = null;
53371
- this.bellControlEventSeen = false;
53372
- this.bellDedup.clear();
53373
- if (this.resizeSnapshotTimer) {
53374
- clearTimeout(this.resizeSnapshotTimer);
53375
- this.resizeSnapshotTimer = null;
53376
- }
53377
- if (this.terminal) {
53378
- this.terminal.close();
53379
- this.terminal = null;
53380
- }
53381
- this.subprocess = null;
53382
- if (this.sshStream) {
53383
- this.sshStream.close();
53384
- this.sshStream = null;
53385
- }
53386
- if (this.sshClient) {
53387
- this.sshClient.end();
53388
- this.sshClient = null;
53770
+ }
53771
+ }
53772
+ function createDeviceSessionRuntime(options) {
53773
+ return new DeviceSessionRuntime(options);
53774
+ }
53775
+
53776
+ // ../../apps/gateway/src/tmux-client/runtime-registry.ts
53777
+ class TmuxRuntimeRegistry {
53778
+ options;
53779
+ entries = new Map;
53780
+ constructor(options) {
53781
+ this.options = options;
53782
+ }
53783
+ acquire(deviceId) {
53784
+ const existing = this.entries.get(deviceId);
53785
+ if (existing) {
53786
+ existing.refCount += 1;
53787
+ return existing.promise;
53389
53788
  }
53390
- updateDeviceRuntimeStatus(this.deviceId, {
53391
- lastSeenAt: new Date().toISOString()
53789
+ const entry = {
53790
+ refCount: 1,
53791
+ runtime: null,
53792
+ promise: this.options.createRuntime(deviceId).then((runtime) => {
53793
+ entry.runtime = runtime;
53794
+ return runtime;
53795
+ })
53796
+ };
53797
+ entry.promise = entry.promise.catch((error) => {
53798
+ if (this.entries.get(deviceId) === entry) {
53799
+ this.entries.delete(deviceId);
53800
+ }
53801
+ throw error;
53392
53802
  });
53803
+ this.entries.set(deviceId, entry);
53804
+ return entry.promise;
53393
53805
  }
53394
- isConnected() {
53395
- return this.connected;
53806
+ async release(deviceId) {
53807
+ const entry = this.entries.get(deviceId);
53808
+ if (!entry) {
53809
+ return;
53810
+ }
53811
+ entry.refCount -= 1;
53812
+ if (entry.refCount > 0) {
53813
+ return;
53814
+ }
53815
+ this.entries.delete(deviceId);
53816
+ const runtime = await entry.promise;
53817
+ await runtime.shutdown();
53818
+ }
53819
+ async shutdownAll() {
53820
+ const entries = Array.from(this.entries.values());
53821
+ this.entries.clear();
53822
+ await Promise.all(entries.map(async (entry) => {
53823
+ const runtime = entry.runtime ?? await entry.promise;
53824
+ await runtime.shutdown();
53825
+ }));
53396
53826
  }
53397
53827
  }
53828
+ function createTmuxRuntimeRegistry(options) {
53829
+ return new TmuxRuntimeRegistry(options);
53830
+ }
53831
+
53832
+ // ../../apps/gateway/src/tmux-client/registry.ts
53833
+ var tmuxRuntimeRegistry = createTmuxRuntimeRegistry({
53834
+ async createRuntime(deviceId) {
53835
+ return createDeviceSessionRuntime({ deviceId });
53836
+ }
53837
+ });
53398
53838
 
53399
53839
  // ../../apps/gateway/src/tmux/bell-context.ts
53400
53840
  function pickPaneById(windows, paneId) {
@@ -53451,7 +53891,10 @@ var defaultDeps = {
53451
53891
  listDevices: () => getAllDevices(),
53452
53892
  getDevice: (deviceId) => getDeviceById(deviceId),
53453
53893
  getSettings: () => getSiteSettings(),
53454
- createConnection: (options) => new TmuxConnection(options),
53894
+ acquireRuntime: async (deviceId) => tmuxRuntimeRegistry.acquire(deviceId),
53895
+ releaseRuntime: async (deviceId, _runtime) => {
53896
+ await tmuxRuntimeRegistry.release(deviceId);
53897
+ },
53455
53898
  async notifyBell(context) {
53456
53899
  const { device, settings, bell } = context;
53457
53900
  await eventNotifier.notify("terminal_bell", {
@@ -53503,7 +53946,7 @@ class PushSupervisor {
53503
53946
  this.running = false;
53504
53947
  for (const [deviceId, entry] of this.entries) {
53505
53948
  this.clearReconnectTimer(entry);
53506
- entry.connection?.disconnect();
53949
+ this.teardownEntry(entry);
53507
53950
  this.entries.delete(deviceId);
53508
53951
  }
53509
53952
  }
@@ -53519,7 +53962,8 @@ class PushSupervisor {
53519
53962
  generation: 1,
53520
53963
  reconnectAttempts: 0,
53521
53964
  reconnectTimer: null,
53522
- connection: null,
53965
+ runtime: null,
53966
+ detachRuntime: null,
53523
53967
  lastSnapshot: null
53524
53968
  };
53525
53969
  this.entries.set(deviceId, entry);
@@ -53546,8 +53990,13 @@ class PushSupervisor {
53546
53990
  }
53547
53991
  teardownEntry(entry) {
53548
53992
  this.clearReconnectTimer(entry);
53549
- entry.connection?.disconnect();
53550
- entry.connection = null;
53993
+ const runtime = entry.runtime;
53994
+ entry.detachRuntime?.();
53995
+ entry.detachRuntime = null;
53996
+ entry.runtime = null;
53997
+ if (runtime) {
53998
+ this.deps.releaseRuntime(entry.deviceId, runtime);
53999
+ }
53551
54000
  }
53552
54001
  clearReconnectTimer(entry) {
53553
54002
  if (!entry.reconnectTimer) {
@@ -53570,42 +54019,46 @@ class PushSupervisor {
53570
54019
  return;
53571
54020
  }
53572
54021
  const generation = entry.generation;
53573
- let connection;
53574
- connection = this.deps.createConnection({
53575
- deviceId: entry.deviceId,
54022
+ const runtime = await this.deps.acquireRuntime(entry.deviceId);
54023
+ const detachRuntime = runtime.subscribe({
53576
54024
  onEvent: (event) => {
53577
- this.handleTmuxEvent(entry.deviceId, generation, connection, event);
54025
+ this.handleTmuxEvent(entry.deviceId, generation, runtime, event);
53578
54026
  },
53579
- onTerminalOutput: () => {},
53580
- onTerminalHistory: () => {},
53581
54027
  onSnapshot: (payload) => {
53582
- this.handleSnapshot(entry.deviceId, generation, connection, payload);
54028
+ this.handleSnapshot(entry.deviceId, generation, runtime, payload);
53583
54029
  },
53584
- onError: (err) => {
53585
- console.error(`[push] tmux error on device ${entry.deviceId}:`, err);
54030
+ onError: (error) => {
54031
+ console.error(`[push] tmux error on device ${entry.deviceId}:`, error);
53586
54032
  },
53587
54033
  onClose: () => {
53588
- this.handleClose(entry.deviceId, generation, connection);
54034
+ this.handleClose(entry.deviceId, generation, runtime);
53589
54035
  }
53590
54036
  });
53591
- entry.connection = connection;
54037
+ entry.runtime = runtime;
54038
+ entry.detachRuntime = detachRuntime;
53592
54039
  try {
53593
- await connection.connect();
54040
+ await runtime.connect();
53594
54041
  const latest = this.entries.get(entry.deviceId);
53595
54042
  if (!this.running || latest !== entry || entry.generation !== generation) {
53596
- connection.disconnect();
54043
+ detachRuntime();
54044
+ entry.detachRuntime = null;
54045
+ entry.runtime = null;
54046
+ await this.deps.releaseRuntime(entry.deviceId, runtime);
53597
54047
  return;
53598
54048
  }
53599
54049
  entry.reconnectAttempts = 0;
53600
54050
  entry.lastSnapshot = null;
53601
- connection.requestSnapshot();
54051
+ runtime.requestSnapshot();
53602
54052
  } catch (err) {
53603
54053
  const latest = this.entries.get(entry.deviceId);
53604
54054
  if (!this.running || latest !== entry || entry.generation !== generation) {
53605
54055
  return;
53606
54056
  }
53607
54057
  console.error(`[push] failed connecting device ${entry.deviceId}:`, err);
53608
- entry.connection = null;
54058
+ detachRuntime();
54059
+ entry.detachRuntime = null;
54060
+ entry.runtime = null;
54061
+ await this.deps.releaseRuntime(entry.deviceId, runtime);
53609
54062
  this.scheduleReconnect(entry);
53610
54063
  }
53611
54064
  }
@@ -53631,31 +54084,35 @@ class PushSupervisor {
53631
54084
  entry.reconnectAttempts += 1;
53632
54085
  }
53633
54086
  this.clearReconnectTimer(entry);
53634
- entry.connection = null;
54087
+ entry.runtime = null;
54088
+ entry.detachRuntime = null;
53635
54089
  entry.generation += 1;
53636
54090
  entry.reconnectTimer = setTimeout(() => {
53637
54091
  entry.reconnectTimer = null;
53638
54092
  this.connectEntry(entry);
53639
54093
  }, delayMs);
53640
54094
  }
53641
- async handleClose(deviceId, generation, connection) {
54095
+ async handleClose(deviceId, generation, runtime) {
53642
54096
  const entry = this.entries.get(deviceId);
53643
- if (!entry || entry.generation !== generation || entry.connection !== connection) {
54097
+ if (!entry || entry.generation !== generation || entry.runtime !== runtime) {
53644
54098
  return;
53645
54099
  }
53646
- entry.connection = null;
54100
+ entry.detachRuntime?.();
54101
+ entry.detachRuntime = null;
54102
+ entry.runtime = null;
54103
+ await this.deps.releaseRuntime(deviceId, runtime);
53647
54104
  this.scheduleReconnect(entry);
53648
54105
  }
53649
- handleSnapshot(deviceId, generation, connection, payload) {
54106
+ handleSnapshot(deviceId, generation, runtime, payload) {
53650
54107
  const entry = this.entries.get(deviceId);
53651
- if (!entry || entry.generation !== generation || entry.connection !== connection) {
54108
+ if (!entry || entry.generation !== generation || entry.runtime !== runtime) {
53652
54109
  return;
53653
54110
  }
53654
54111
  entry.lastSnapshot = payload;
53655
54112
  }
53656
- async handleTmuxEvent(deviceId, generation, connection, event) {
54113
+ async handleTmuxEvent(deviceId, generation, runtime, event) {
53657
54114
  const entry = this.entries.get(deviceId);
53658
- if (!entry || entry.generation !== generation || entry.connection !== connection) {
54115
+ if (!entry || entry.generation !== generation || entry.runtime !== runtime) {
53659
54116
  return;
53660
54117
  }
53661
54118
  if (event.type !== "bell") {
@@ -54131,7 +54588,7 @@ function json(data, status = 200, headers = {}) {
54131
54588
  import { existsSync as existsSync2 } from "fs";
54132
54589
  import { resolve } from "path";
54133
54590
 
54134
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/migrator.js
54591
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/migrator.js
54135
54592
  import crypto2 from "crypto";
54136
54593
  import fs3 from "fs";
54137
54594
  function readMigrationFiles(config2) {
@@ -54163,7 +54620,7 @@ function readMigrationFiles(config2) {
54163
54620
  return migrationQueries;
54164
54621
  }
54165
54622
 
54166
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/bun-sqlite/migrator.js
54623
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/bun-sqlite/migrator.js
54167
54624
  function migrate(db2, config2) {
54168
54625
  const migrations = readMigrationFiles(config2);
54169
54626
  db2.dialect.migrate(migrations, db2.session, config2);
@@ -54185,82 +54642,6 @@ function runMigrations() {
54185
54642
  }
54186
54643
  if (false) {}
54187
54644
 
54188
- // ../../apps/gateway/src/ws/error-classify.ts
54189
- function classifySshError(error) {
54190
- const msg = error.message.toLowerCase();
54191
- if (msg.includes("ssh_config_ref_not_supported")) {
54192
- return {
54193
- type: "ssh_config_ref_not_supported",
54194
- messageKey: "sshError.configRefNotSupported"
54195
- };
54196
- }
54197
- if (msg.includes("ssh_auth_sock") || msg.includes("auth_sock")) {
54198
- return {
54199
- type: "agent_unavailable",
54200
- messageKey: "sshError.agentUnavailable"
54201
- };
54202
- }
54203
- if (msg.includes("agent") && (msg.includes("no identities") || msg.includes("failure"))) {
54204
- return {
54205
- type: "agent_no_identity",
54206
- messageKey: "sshError.agentNoIdentities"
54207
- };
54208
- }
54209
- if (msg.includes("permission denied")) {
54210
- return {
54211
- type: "auth_failed",
54212
- messageKey: "sshError.authFailed"
54213
- };
54214
- }
54215
- if (msg.includes("all configured authentication methods failed")) {
54216
- return {
54217
- type: "auth_failed",
54218
- messageKey: "sshError.authFailedGeneric"
54219
- };
54220
- }
54221
- if (msg.includes("enetunreach") || msg.includes("ehostunreach")) {
54222
- return {
54223
- type: "network_unreachable",
54224
- messageKey: "sshError.networkUnreachable"
54225
- };
54226
- }
54227
- if (msg.includes("connect refused") || msg.includes("connection refused") || msg.includes("econnrefused")) {
54228
- return {
54229
- type: "connection_refused",
54230
- messageKey: "sshError.connectionRefused"
54231
- };
54232
- }
54233
- if (msg.includes("timeout") || msg.includes("etimedout")) {
54234
- return {
54235
- type: "timeout",
54236
- messageKey: "sshError.connectionTimeout"
54237
- };
54238
- }
54239
- if (msg.includes("host not found") || msg.includes("getaddrinfo") || msg.includes("enotfound")) {
54240
- return {
54241
- type: "host_not_found",
54242
- messageKey: "sshError.hostNotFound"
54243
- };
54244
- }
54245
- if (msg.includes("handshake failed") || msg.includes("unable to verify")) {
54246
- return {
54247
- type: "handshake_failed",
54248
- messageKey: "sshError.handshakeFailed"
54249
- };
54250
- }
54251
- if (msg.includes("tmux: command not found") || msg.includes("tmux control mode not ready") || msg.includes("tmux exited") || msg.includes("tmux_exec_failed")) {
54252
- return {
54253
- type: "tmux_unavailable",
54254
- messageKey: "sshError.tmuxUnavailable"
54255
- };
54256
- }
54257
- return {
54258
- type: "unknown",
54259
- messageKey: "sshError.unknown",
54260
- messageParams: { message: error.message }
54261
- };
54262
- }
54263
-
54264
54645
  // ../../apps/gateway/src/ws/borsh/codec-borsh.ts
54265
54646
  function createBorshClientState() {
54266
54647
  return {
@@ -54638,6 +55019,10 @@ class SwitchBarrier {
54638
55019
  const pending = this.getPending(ws, deviceId);
54639
55020
  if (!pending)
54640
55021
  return;
55022
+ const selectState = sessionStateStore.getOrCreateSelectTransaction(ws, deviceId)?.state;
55023
+ if (selectState !== "SELECTING") {
55024
+ return;
55025
+ }
54641
55026
  const { context } = pending;
54642
55027
  const borshState = ws.data?.borshState;
54643
55028
  if (!borshState)
@@ -54645,7 +55030,9 @@ class SwitchBarrier {
54645
55030
  const ackTimer = pending.timers.shift();
54646
55031
  if (ackTimer)
54647
55032
  clearTimeout(ackTimer);
54648
- sessionStateStore.transitionSelectState(ws, deviceId, "ACKED");
55033
+ if (!sessionStateStore.transitionSelectState(ws, deviceId, "ACKED")) {
55034
+ return;
55035
+ }
54649
55036
  const seq = borshState.seqGen();
54650
55037
  const ackData = encodeSwitchAck({
54651
55038
  deviceId,
@@ -54670,6 +55057,10 @@ class SwitchBarrier {
54670
55057
  const pending = this.getPending(ws, deviceId);
54671
55058
  if (!pending)
54672
55059
  return;
55060
+ const selectState = sessionStateStore.getOrCreateSelectTransaction(ws, deviceId)?.state;
55061
+ if (selectState !== "ACKED") {
55062
+ return;
55063
+ }
54673
55064
  const { context } = pending;
54674
55065
  if (context.paneId !== paneId) {
54675
55066
  return;
@@ -54680,7 +55071,9 @@ class SwitchBarrier {
54680
55071
  const historyTimer = pending.timers.shift();
54681
55072
  if (historyTimer)
54682
55073
  clearTimeout(historyTimer);
54683
- sessionStateStore.transitionSelectState(ws, deviceId, "HISTORY_APPLIED");
55074
+ if (!sessionStateStore.transitionSelectState(ws, deviceId, "HISTORY_APPLIED")) {
55075
+ return;
55076
+ }
54684
55077
  const historyMessages = encodeTermHistory({
54685
55078
  deviceId,
54686
55079
  paneId: context.paneId,
@@ -54699,6 +55092,10 @@ class SwitchBarrier {
54699
55092
  const pending = this.getPending(ws, deviceId);
54700
55093
  if (!pending)
54701
55094
  return;
55095
+ const selectState = sessionStateStore.getOrCreateSelectTransaction(ws, deviceId)?.state;
55096
+ if (selectState !== "ACKED" && selectState !== "HISTORY_APPLIED") {
55097
+ return;
55098
+ }
54702
55099
  const { context } = pending;
54703
55100
  if (expectedToken && !this.tokensEqual(context.selectToken, expectedToken)) {
54704
55101
  return;
@@ -54710,7 +55107,9 @@ class SwitchBarrier {
54710
55107
  clearTimeout(timer);
54711
55108
  }
54712
55109
  pending.timers = [];
54713
- sessionStateStore.transitionSelectState(ws, deviceId, "LIVE");
55110
+ if (!sessionStateStore.transitionSelectState(ws, deviceId, "LIVE")) {
55111
+ return;
55112
+ }
54714
55113
  const bufferedOutput = sessionStateStore.stopOutputBuffering(ws, deviceId);
54715
55114
  const seq = borshState.seqGen();
54716
55115
  const liveResumeData = encodeLiveResume({
@@ -54782,7 +55181,9 @@ class SwitchBarrier {
54782
55181
  this.cleanupTransaction(ws, deviceId);
54783
55182
  }
54784
55183
  completeTransaction(ws, deviceId) {
54785
- sessionStateStore.transitionSelectState(ws, deviceId, "STABLE");
55184
+ if (!sessionStateStore.transitionSelectState(ws, deviceId, "STABLE")) {
55185
+ return;
55186
+ }
54786
55187
  this.cleanupTransaction(ws, deviceId);
54787
55188
  }
54788
55189
  cleanupTransaction(ws, deviceId) {
@@ -54799,10 +55200,100 @@ class SwitchBarrier {
54799
55200
  }
54800
55201
  var switchBarrier = new SwitchBarrier;
54801
55202
 
55203
+ // ../../apps/gateway/src/ws/error-classify.ts
55204
+ function classifySshError(error) {
55205
+ const msg = error.message.toLowerCase();
55206
+ if (msg.includes("ssh_config_ref_not_supported")) {
55207
+ return {
55208
+ type: "ssh_config_ref_not_supported",
55209
+ messageKey: "sshError.configRefNotSupported"
55210
+ };
55211
+ }
55212
+ if (msg.includes("ssh_auth_sock") || msg.includes("auth_sock")) {
55213
+ return {
55214
+ type: "agent_unavailable",
55215
+ messageKey: "sshError.agentUnavailable"
55216
+ };
55217
+ }
55218
+ if (msg.includes("agent") && (msg.includes("no identities") || msg.includes("failure"))) {
55219
+ return {
55220
+ type: "agent_no_identity",
55221
+ messageKey: "sshError.agentNoIdentities"
55222
+ };
55223
+ }
55224
+ if (msg.includes("permission denied")) {
55225
+ return {
55226
+ type: "auth_failed",
55227
+ messageKey: "sshError.authFailed"
55228
+ };
55229
+ }
55230
+ if (msg.includes("all configured authentication methods failed")) {
55231
+ return {
55232
+ type: "auth_failed",
55233
+ messageKey: "sshError.authFailedGeneric"
55234
+ };
55235
+ }
55236
+ if (msg.includes("enetunreach") || msg.includes("ehostunreach")) {
55237
+ return {
55238
+ type: "network_unreachable",
55239
+ messageKey: "sshError.networkUnreachable"
55240
+ };
55241
+ }
55242
+ if (msg.includes("connect refused") || msg.includes("connection refused") || msg.includes("econnrefused")) {
55243
+ return {
55244
+ type: "connection_refused",
55245
+ messageKey: "sshError.connectionRefused"
55246
+ };
55247
+ }
55248
+ if (msg.includes("timeout") || msg.includes("etimedout")) {
55249
+ return {
55250
+ type: "timeout",
55251
+ messageKey: "sshError.connectionTimeout"
55252
+ };
55253
+ }
55254
+ if (msg.includes("host not found") || msg.includes("getaddrinfo") || msg.includes("enotfound")) {
55255
+ return {
55256
+ type: "host_not_found",
55257
+ messageKey: "sshError.hostNotFound"
55258
+ };
55259
+ }
55260
+ if (msg.includes("handshake failed") || msg.includes("unable to verify")) {
55261
+ return {
55262
+ type: "handshake_failed",
55263
+ messageKey: "sshError.handshakeFailed"
55264
+ };
55265
+ }
55266
+ if (msg.includes("remote tmux unavailable") || msg.includes("tmux_not_found") || msg.includes("tmux: command not found") || msg.includes("tmux control mode not ready") || msg.includes("tmux exited") || msg.includes("tmux_exec_failed")) {
55267
+ return {
55268
+ type: "tmux_unavailable",
55269
+ messageKey: "sshError.tmuxUnavailable"
55270
+ };
55271
+ }
55272
+ return {
55273
+ type: "unknown",
55274
+ messageKey: "sshError.unknown",
55275
+ messageParams: { message: error.message }
55276
+ };
55277
+ }
55278
+
54802
55279
  // ../../apps/gateway/src/ws/index.ts
55280
+ var defaultDeps2 = {
55281
+ acquireRuntime: async (deviceId) => tmuxRuntimeRegistry.acquire(deviceId),
55282
+ releaseRuntime: async (deviceId) => {
55283
+ await tmuxRuntimeRegistry.release(deviceId);
55284
+ }
55285
+ };
55286
+
54803
55287
  class WebSocketServer {
54804
55288
  connections = new Map;
54805
55289
  pendingConnectionEntries = new Map;
55290
+ deps;
55291
+ constructor(options = {}) {
55292
+ this.deps = {
55293
+ ...defaultDeps2,
55294
+ ...options.deps ?? {}
55295
+ };
55296
+ }
54806
55297
  clearSnapshotTimer(entry) {
54807
55298
  if (!entry.snapshotTimer)
54808
55299
  return;
@@ -54821,6 +55312,37 @@ class WebSocketServer {
54821
55312
  clearTimeout(entry.reconnectTimer);
54822
55313
  entry.reconnectTimer = null;
54823
55314
  }
55315
+ releaseConnectionEntry(deviceId, entry) {
55316
+ this.clearSnapshotTimer(entry);
55317
+ this.clearSnapshotPollTimer(entry);
55318
+ this.clearReconnectTimer(entry);
55319
+ entry.detachRuntime?.();
55320
+ entry.detachRuntime = null;
55321
+ this.deps.releaseRuntime(deviceId, entry.runtime);
55322
+ }
55323
+ attachRuntime(deviceId, runtime) {
55324
+ const listener = {
55325
+ onEvent: (event) => {
55326
+ this.broadcastTmuxEvent(deviceId, event);
55327
+ },
55328
+ onTerminalOutput: (paneId, data) => {
55329
+ this.broadcastTerminalOutput(deviceId, paneId, data);
55330
+ },
55331
+ onTerminalHistory: (paneId, data) => {
55332
+ this.broadcastTerminalHistory(deviceId, paneId, data);
55333
+ },
55334
+ onSnapshot: (payload) => {
55335
+ this.broadcastStateSnapshot(deviceId, payload);
55336
+ },
55337
+ onError: (error) => {
55338
+ this.broadcastError(deviceId, error);
55339
+ },
55340
+ onClose: () => {
55341
+ this.handleConnectionClose(deviceId);
55342
+ }
55343
+ };
55344
+ return runtime.subscribe(listener);
55345
+ }
54824
55346
  refreshSnapshotPolling(deviceId) {
54825
55347
  const entry = this.connections.get(deviceId);
54826
55348
  if (!entry)
@@ -54838,7 +55360,7 @@ class WebSocketServer {
54838
55360
  return;
54839
55361
  }
54840
55362
  try {
54841
- entry.connection.requestSnapshot();
55363
+ entry.runtime.requestSnapshot();
54842
55364
  } catch (err) {
54843
55365
  console.error("[ws] polling snapshot failed:", err);
54844
55366
  }
@@ -54856,7 +55378,7 @@ class WebSocketServer {
54856
55378
  }
54857
55379
  entry.snapshotTimer = null;
54858
55380
  try {
54859
- entry.connection.requestSnapshot();
55381
+ entry.runtime.requestSnapshot();
54860
55382
  } catch (err) {
54861
55383
  console.error("[ws] failed to request snapshot:", err);
54862
55384
  }
@@ -54925,10 +55447,7 @@ class WebSocketServer {
54925
55447
  delete ws.data.borshState.selectedPanes[deviceId];
54926
55448
  if (entry.clients.size === 0) {
54927
55449
  console.log(`[ws] no more clients for device ${deviceId}, disconnecting`);
54928
- this.clearSnapshotTimer(entry);
54929
- this.clearSnapshotPollTimer(entry);
54930
- this.clearReconnectTimer(entry);
54931
- entry.connection.disconnect();
55450
+ this.releaseConnectionEntry(deviceId, entry);
54932
55451
  toDelete.push(deviceId);
54933
55452
  } else {
54934
55453
  this.refreshSnapshotPolling(deviceId);
@@ -54940,10 +55459,7 @@ class WebSocketServer {
54940
55459
  }
54941
55460
  closeAll() {
54942
55461
  for (const [deviceId, entry] of this.connections) {
54943
- this.clearSnapshotTimer(entry);
54944
- this.clearSnapshotPollTimer(entry);
54945
- this.clearReconnectTimer(entry);
54946
- entry.connection.disconnect();
55462
+ this.releaseConnectionEntry(deviceId, entry);
54947
55463
  this.connections.delete(deviceId);
54948
55464
  }
54949
55465
  this.pendingConnectionEntries.clear();
@@ -55106,8 +55622,7 @@ class WebSocketServer {
55106
55622
  if (pending) {
55107
55623
  return pending;
55108
55624
  }
55109
- let creationPromise;
55110
- creationPromise = this.createDeviceConnectionEntry(deviceId, ws).then((createdEntry) => {
55625
+ const creationPromise = this.createDeviceConnectionEntry(deviceId, ws).then((createdEntry) => {
55111
55626
  if (createdEntry) {
55112
55627
  this.connections.set(deviceId, createdEntry);
55113
55628
  }
@@ -55126,13 +55641,15 @@ class WebSocketServer {
55126
55641
  return;
55127
55642
  entry.clients.add(ws);
55128
55643
  ws.data.borshState.selectedPanes[deviceId] ??= null;
55129
- const connectedPayload = exports_ws_borsh.encodePayload(exports_ws_borsh.schema.DeviceConnectedSchema, { deviceId });
55644
+ const connectedPayload = exports_ws_borsh.encodePayload(exports_ws_borsh.schema.DeviceConnectedSchema, {
55645
+ deviceId
55646
+ });
55130
55647
  this.sendEnvelope(ws, exports_ws_borsh.KIND_DEVICE_CONNECTED, connectedPayload);
55131
55648
  if (entry.lastSnapshot) {
55132
55649
  const snapshotBytes = exports_ws_borsh.encodeStateSnapshot(entry.lastSnapshot);
55133
55650
  this.sendChunked(ws, exports_ws_borsh.KIND_STATE_SNAPSHOT, snapshotBytes);
55134
55651
  } else {
55135
- entry.connection.requestSnapshot();
55652
+ entry.runtime.requestSnapshot();
55136
55653
  }
55137
55654
  }
55138
55655
  handleDeviceDisconnect(ws, deviceId) {
@@ -55141,15 +55658,14 @@ class WebSocketServer {
55141
55658
  entry.clients.delete(ws);
55142
55659
  this.refreshSnapshotPolling(deviceId);
55143
55660
  if (entry.clients.size === 0) {
55144
- this.clearSnapshotTimer(entry);
55145
- this.clearSnapshotPollTimer(entry);
55146
- this.clearReconnectTimer(entry);
55147
- entry.connection.disconnect();
55661
+ this.releaseConnectionEntry(deviceId, entry);
55148
55662
  this.connections.delete(deviceId);
55149
55663
  }
55150
55664
  }
55151
55665
  delete ws.data.borshState.selectedPanes[deviceId];
55152
- const disconnectedPayload = exports_ws_borsh.encodePayload(exports_ws_borsh.schema.DeviceDisconnectedSchema, { deviceId });
55666
+ const disconnectedPayload = exports_ws_borsh.encodePayload(exports_ws_borsh.schema.DeviceDisconnectedSchema, {
55667
+ deviceId
55668
+ });
55153
55669
  this.sendEnvelope(ws, exports_ws_borsh.KIND_DEVICE_DISCONNECTED, disconnectedPayload);
55154
55670
  }
55155
55671
  handleTmuxSelect(ws, data) {
@@ -55179,30 +55695,31 @@ class WebSocketServer {
55179
55695
  return;
55180
55696
  }
55181
55697
  switchBarrier.sendSwitchAck(ws, deviceId);
55182
- entry.connection.selectPane(windowId, paneId);
55183
55698
  const cols = data.cols ?? null;
55184
55699
  const rows = data.rows ?? null;
55185
55700
  if (cols !== null && rows !== null) {
55186
- entry.connection.resizePane(paneId, cols, rows);
55701
+ entry.runtime.selectPaneWithSize(windowId, paneId, cols, rows);
55702
+ } else {
55703
+ entry.runtime.selectPane(windowId, paneId);
55187
55704
  }
55188
55705
  }
55189
55706
  handleTmuxSelectWindow(deviceId, windowId) {
55190
55707
  const entry = this.connections.get(deviceId);
55191
55708
  if (!entry)
55192
55709
  return;
55193
- entry.connection.selectWindow(windowId);
55710
+ entry.runtime.selectWindow(windowId);
55194
55711
  }
55195
55712
  handleTermInput(deviceId, paneId, data) {
55196
55713
  const entry = this.connections.get(deviceId);
55197
55714
  if (!entry)
55198
55715
  return;
55199
- entry.connection.sendInput(paneId, data);
55716
+ entry.runtime.sendInput(paneId, data);
55200
55717
  }
55201
55718
  handleTermResize(deviceId, paneId, cols, rows) {
55202
55719
  const entry = this.connections.get(deviceId);
55203
55720
  if (!entry)
55204
55721
  return;
55205
- entry.connection.resizePane(paneId, cols, rows);
55722
+ entry.runtime.resizePane(paneId, cols, rows);
55206
55723
  }
55207
55724
  handleTermPaste(deviceId, paneId, data) {
55208
55725
  const entry = this.connections.get(deviceId);
@@ -55211,51 +55728,43 @@ class WebSocketServer {
55211
55728
  const chunkSize = 1024;
55212
55729
  for (let i = 0;i < data.length; i += chunkSize) {
55213
55730
  const chunk2 = data.slice(i, i + chunkSize);
55214
- entry.connection.sendInput(paneId, chunk2);
55731
+ entry.runtime.sendInput(paneId, chunk2);
55215
55732
  }
55216
55733
  }
55217
55734
  handleCreateWindow(deviceId, name) {
55218
55735
  const entry = this.connections.get(deviceId);
55219
55736
  if (!entry)
55220
55737
  return;
55221
- entry.connection.createWindow(name);
55738
+ entry.runtime.createWindow(name);
55222
55739
  }
55223
55740
  handleCloseWindow(deviceId, windowId) {
55224
55741
  const entry = this.connections.get(deviceId);
55225
55742
  if (!entry)
55226
55743
  return;
55227
- entry.connection.closeWindow(windowId);
55744
+ entry.runtime.closeWindow(windowId);
55228
55745
  }
55229
55746
  handleClosePane(deviceId, paneId) {
55230
55747
  const entry = this.connections.get(deviceId);
55231
55748
  if (!entry)
55232
55749
  return;
55233
- entry.connection.closePane(paneId);
55750
+ entry.runtime.closePane(paneId);
55234
55751
  }
55235
55752
  handleRenameWindow(deviceId, windowId, name) {
55236
55753
  const entry = this.connections.get(deviceId);
55237
55754
  if (!entry)
55238
55755
  return;
55239
- entry.connection.renameWindow(windowId, name);
55756
+ entry.runtime.renameWindow(windowId, name);
55240
55757
  }
55241
55758
  async createDeviceConnectionEntry(deviceId, ws) {
55242
- const connection = new TmuxConnection({
55243
- deviceId,
55244
- onEvent: (event) => {
55245
- this.broadcastTmuxEvent(deviceId, event);
55246
- },
55247
- onTerminalOutput: (paneId, data) => this.broadcastTerminalOutput(deviceId, paneId, data),
55248
- onTerminalHistory: (paneId, data) => this.broadcastTerminalHistory(deviceId, paneId, data),
55249
- onSnapshot: (payload) => this.broadcastStateSnapshot(deviceId, payload),
55250
- onError: (err) => this.broadcastError(deviceId, err),
55251
- onClose: () => {
55252
- this.handleConnectionClose(deviceId);
55253
- }
55254
- });
55759
+ let runtime = null;
55760
+ let detachRuntime = null;
55255
55761
  try {
55256
- await connection.connect();
55762
+ runtime = await this.deps.acquireRuntime(deviceId);
55763
+ detachRuntime = this.attachRuntime(deviceId, runtime);
55764
+ await runtime.connect();
55257
55765
  return {
55258
- connection,
55766
+ runtime,
55767
+ detachRuntime,
55259
55768
  clients: new Set,
55260
55769
  lastSnapshot: null,
55261
55770
  snapshotTimer: null,
@@ -55264,6 +55773,10 @@ class WebSocketServer {
55264
55773
  reconnectTimer: null
55265
55774
  };
55266
55775
  } catch (err) {
55776
+ detachRuntime?.();
55777
+ if (runtime) {
55778
+ this.deps.releaseRuntime(deviceId, runtime);
55779
+ }
55267
55780
  const errorInfo = classifySshError(err instanceof Error ? err : new Error(String(err)));
55268
55781
  ws.send(exports_ws_borsh.encodeEnvelope(exports_ws_borsh.KIND_DEVICE_EVENT, exports_ws_borsh.encodeDeviceEventPayload({
55269
55782
  deviceId,
@@ -55391,6 +55904,10 @@ class WebSocketServer {
55391
55904
  }
55392
55905
  this.clearSnapshotTimer(entry);
55393
55906
  this.clearSnapshotPollTimer(entry);
55907
+ entry.detachRuntime?.();
55908
+ entry.detachRuntime = null;
55909
+ const closedRuntime = entry.runtime;
55910
+ this.deps.releaseRuntime(deviceId, closedRuntime);
55394
55911
  const { sshReconnectMaxRetries, sshReconnectDelaySeconds } = getSiteSettings();
55395
55912
  if (entry.clients.size > 0 && entry.reconnectAttempts < sshReconnectMaxRetries) {
55396
55913
  entry.reconnectAttempts += 1;
@@ -55437,7 +55954,7 @@ class WebSocketServer {
55437
55954
  message: t2("sshError.reconnected")
55438
55955
  };
55439
55956
  this.broadcastDeviceEvent(retryConnection, reconnected);
55440
- retryConnection.connection.requestSnapshot();
55957
+ retryConnection.runtime.requestSnapshot();
55441
55958
  }, delay);
55442
55959
  return;
55443
55960
  }
@@ -55510,6 +56027,7 @@ async function createGatewayRuntime(options = {}) {
55510
56027
  async stop() {
55511
56028
  wsServer.closeAll();
55512
56029
  await pushSupervisor.stopAll();
56030
+ await tmuxRuntimeRegistry.shutdownAll();
55513
56031
  await telegramService.stopAll();
55514
56032
  }
55515
56033
  };
@@ -55776,7 +56294,7 @@ async function serveFrontend(req, staticRoot) {
55776
56294
  if (!requestedPath) {
55777
56295
  return new Response(t3("runtime.forbidden"), { status: 403 });
55778
56296
  }
55779
- const indexPath = join3(staticRoot, "index.html");
56297
+ const indexPath = join4(staticRoot, "index.html");
55780
56298
  const targetPath = existsSync3(requestedPath) ? requestedPath : indexPath;
55781
56299
  if (!existsSync3(targetPath)) {
55782
56300
  return new Response(t3("runtime.frontendMissing"), { status: 500 });