tmex-cli 0.2.6 → 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 +1813 -1298
  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
@@ -52021,669 +52021,953 @@ function buildLocalTmuxEnv(resolvedPath, baseEnv = process.env) {
52021
52021
  return nextEnv;
52022
52022
  }
52023
52023
 
52024
- // ../../apps/gateway/src/tmux/ssh-auth.ts
52025
- function normalizeEnvValue(value) {
52026
- const trimmed = value?.trim();
52027
- return trimmed ? trimmed : undefined;
52024
+ // ../../apps/gateway/src/tmux-client/command-builder.ts
52025
+ function quoteShellArg(value) {
52026
+ return `'${value.replaceAll("'", "'\\''")}'`;
52028
52027
  }
52029
- function resolveSshUsername(configuredUsername, authMode, env = process.env) {
52030
- const explicitUsername = normalizeEnvValue(configuredUsername);
52031
- if (explicitUsername) {
52032
- return explicitUsername;
52033
- }
52034
- if (authMode === "agent" || authMode === "auto") {
52035
- const currentUser = normalizeEnvValue(env.USER) ?? normalizeEnvValue(env.LOGNAME);
52036
- if (currentUser) {
52037
- return currentUser;
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");
52053
+ return {
52054
+ rootDir: runtimeRootDir,
52055
+ panesDir,
52056
+ hooksDir,
52057
+ hookFifoPath: join3(hooksDir, "events.fifo"),
52058
+ paneFifoPath(paneId) {
52059
+ return join3(panesDir, `${safeSessionName}-${toSafePathSegment(paneId)}.fifo`);
52038
52060
  }
52061
+ };
52062
+ }
52063
+
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")));
52039
52073
  }
52040
- return "root";
52074
+ return chunks;
52041
52075
  }
52042
- function resolveSshAgentSocket(authMode, env = process.env) {
52043
- if (authMode !== "agent" && authMode !== "auto") {
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);
52139
+ }
52140
+ };
52141
+ }
52142
+ function emitTitle(onTitle, titleBytes) {
52143
+ const title = decoder.decode(new Uint8Array(titleBytes)).trim();
52144
+ if (!title) {
52044
52145
  return;
52045
52146
  }
52046
- const socket = normalizeEnvValue(env.SSH_AUTH_SOCK);
52047
- if (socket) {
52048
- return socket;
52049
- }
52050
- if (authMode === "agent") {
52051
- throw new Error("SSH_AUTH_SOCK \u672A\u8BBE\u7F6E\uFF0C\u65E0\u6CD5\u4F7F\u7528 SSH Agent \u8BA4\u8BC1");
52052
- }
52053
- return;
52147
+ onTitle(title);
52148
+ }
52149
+ function encoderFromString(value) {
52150
+ return new TextEncoder().encode(value);
52054
52151
  }
52055
52152
 
52056
- // ../../apps/gateway/src/tmux/parser.ts
52057
- function isSameBytes(left, right) {
52058
- 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") {
52059
52158
  return false;
52060
52159
  }
52061
- for (let i = 0;i < left.length; i++) {
52062
- if (left[i] !== right[i]) {
52063
- 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();
52064
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();
52065
52241
  }
52066
- return true;
52067
- }
52068
- function decodeTmuxEscapedValue(value) {
52069
- const bytes = [];
52070
- const encoder = new TextEncoder;
52071
- let cursor = 0;
52072
- for (let i = 0;i < value.length; i++) {
52073
- if (value[i] !== "\\") {
52074
- continue;
52242
+ disconnect() {
52243
+ if (!this.connected && this.manualDisconnect) {
52244
+ return;
52075
52245
  }
52076
- const octal = value.slice(i + 1, i + 4);
52077
- if (!/^[0-7]{3}$/.test(octal)) {
52078
- continue;
52246
+ this.manualDisconnect = true;
52247
+ this.connected = false;
52248
+ this.stopPipe();
52249
+ if (this.deps.enableHooks) {
52250
+ this.stopHooks();
52079
52251
  }
52080
- if (cursor < i) {
52081
- bytes.push(...encoder.encode(value.slice(cursor, i)));
52082
- }
52083
- bytes.push(Number.parseInt(octal, 8));
52084
- i += 3;
52085
- cursor = i + 1;
52086
- }
52087
- if (cursor < value.length) {
52088
- bytes.push(...encoder.encode(value.slice(cursor)));
52089
- }
52090
- return new Uint8Array(bytes);
52091
- }
52092
- function stripTmuxDcsWrapper(line) {
52093
- let cleanLine = line;
52094
- cleanLine = cleanLine.replace(/^\u001bP\d+p/, "");
52095
- if (cleanLine.endsWith("\x1B\\")) {
52096
- cleanLine = cleanLine.slice(0, -2);
52097
- } else if (cleanLine.endsWith("\x9C")) {
52098
- cleanLine = cleanLine.slice(0, -1);
52099
- }
52100
- return cleanLine;
52101
- }
52102
-
52103
- class TmuxControlParser {
52104
- buffer = "";
52105
- onEvent;
52106
- onTerminalOutput;
52107
- onPaneTitle;
52108
- onOutputBlockBegin;
52109
- onOutputBlock;
52110
- onNonControlOutput;
52111
- onExit;
52112
- onReady;
52113
- inOutputBlock = false;
52114
- outputBlockMeta = null;
52115
- outputBlockLines = [];
52116
- readyNotified = false;
52117
- lastOutputEndedWithCR = false;
52118
- lastOutputFrame = null;
52119
- outputTitleStates = new Map;
52120
- constructor(options) {
52121
- this.onEvent = options.onEvent;
52122
- this.onTerminalOutput = options.onTerminalOutput;
52123
- this.onPaneTitle = options.onPaneTitle;
52124
- this.onOutputBlockBegin = options.onOutputBlockBegin;
52125
- this.onOutputBlock = options.onOutputBlock;
52126
- this.onNonControlOutput = options.onNonControlOutput;
52127
- this.onExit = options.onExit;
52128
- this.onReady = options.onReady;
52129
- }
52130
- processData(data) {
52131
- const text2 = typeof data === "string" ? data : new TextDecoder().decode(data);
52132
- this.buffer += text2;
52133
- this.parseBuffer();
52134
- }
52135
- parseBuffer() {
52136
- while (true) {
52137
- const nlIndex = this.buffer.indexOf(`
52138
- `);
52139
- if (nlIndex === -1)
52140
- break;
52141
- let line = this.buffer.slice(0, nlIndex);
52142
- this.buffer = this.buffer.slice(nlIndex + 1);
52143
- if (line.endsWith("\r")) {
52144
- line = line.slice(0, -1);
52145
- }
52146
- if (line) {
52147
- 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]);
52148
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;
52149
52277
  }
52278
+ this.resizePaneInternal(paneId, cols, rows).catch((error) => {
52279
+ this.callbacks.onError(error);
52280
+ });
52150
52281
  }
52151
- parseLine(line) {
52152
- const cleanLine = stripTmuxDcsWrapper(line);
52153
- if (this.inOutputBlock) {
52154
- if (cleanLine.startsWith("%end") || cleanLine.startsWith("%error")) {
52155
- this.finishOutputBlock(cleanLine);
52156
- return;
52157
- }
52158
- this.outputBlockLines.push(cleanLine);
52282
+ selectPane(windowId, paneId) {
52283
+ if (!this.connected) {
52159
52284
  return;
52160
52285
  }
52161
- 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) {
52292
+ return;
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
- if (cleanLine.startsWith("%begin")) {
52164
- this.startOutputBlock(cleanLine);
52301
+ }
52302
+ this.runAndRefresh(["select-window", "-t", windowId]).catch((error) => {
52303
+ this.callbacks.onError(error);
52304
+ });
52305
+ }
52306
+ createWindow(name) {
52307
+ if (!this.connected) {
52165
52308
  return;
52166
52309
  }
52167
- if (cleanLine.startsWith("%")) {
52168
- this.parseControlLine(cleanLine);
52169
- this.notifyReady();
52310
+ const argv = name ? ["new-window", "-n", name] : ["new-window"];
52311
+ this.runAndRefresh(argv).catch((error) => {
52312
+ this.callbacks.onError(error);
52313
+ });
52314
+ }
52315
+ closeWindow(windowId) {
52316
+ if (!this.connected) {
52170
52317
  return;
52171
52318
  }
52172
- this.onNonControlOutput?.(cleanLine);
52173
- console.log("[tmux] non-control output:", cleanLine);
52319
+ this.closeWindowInternal(windowId).catch((error) => {
52320
+ this.callbacks.onError(error);
52321
+ });
52174
52322
  }
52175
- notifyReady() {
52176
- if (this.readyNotified)
52323
+ closePane(paneId) {
52324
+ if (!this.connected) {
52177
52325
  return;
52178
- this.readyNotified = true;
52179
- this.onReady?.();
52326
+ }
52327
+ this.runAndRefresh(["kill-pane", "-t", paneId], true).catch((error) => {
52328
+ this.callbacks.onError(error);
52329
+ });
52180
52330
  }
52181
- startOutputBlock(line) {
52182
- const meta = this.parseOutputBlockMeta(line);
52183
- if (!meta) {
52331
+ renameWindow(windowId, name) {
52332
+ if (!this.connected) {
52184
52333
  return;
52185
52334
  }
52186
- this.inOutputBlock = true;
52187
- this.outputBlockMeta = meta;
52188
- this.outputBlockLines = [];
52189
- this.onOutputBlockBegin?.(meta);
52335
+ this.runAndRefresh(["rename-window", "-t", windowId, name]).catch((error) => {
52336
+ this.callbacks.onError(error);
52337
+ });
52190
52338
  }
52191
- finishOutputBlock(line) {
52192
- const meta = this.parseOutputBlockMeta(line);
52193
- const currentMeta = this.outputBlockMeta;
52194
- this.inOutputBlock = false;
52195
- this.outputBlockMeta = null;
52196
- if (currentMeta && meta) {
52197
- this.onOutputBlock?.({
52198
- time: currentMeta.time,
52199
- commandNo: currentMeta.commandNo,
52200
- flags: currentMeta.flags,
52201
- lines: this.outputBlockLines,
52202
- isError: line.startsWith("%error")
52203
- });
52339
+ async ensureSession() {
52340
+ const exists3 = await this.runTmuxAllowFailure(["has-session", "-t", this.sessionName]);
52341
+ if (exists3.exitCode === 0) {
52342
+ return;
52204
52343
  }
52205
- this.outputBlockLines = [];
52206
- this.notifyReady();
52344
+ await this.runTmux(["new-session", "-d", "-c", homedir(), "-s", this.sessionName]);
52207
52345
  }
52208
- parseOutputBlockMeta(line) {
52209
- const spaceIndex = line.indexOf(" ");
52210
- if (spaceIndex === -1)
52211
- return null;
52212
- const args = line.slice(spaceIndex + 1).trim();
52213
- const parts = args.split(/\s+/);
52214
- if (parts.length < 3)
52215
- return null;
52216
- const time = Number(parts[0]);
52217
- const commandNo = Number(parts[1]);
52218
- const flags = Number(parts[2]);
52219
- if (Number.isNaN(time) || Number.isNaN(commandNo) || Number.isNaN(flags))
52220
- return null;
52221
- return { time, commandNo, flags };
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
+ ]);
52222
52403
  }
52223
- normalizeTerminalOutputNewline(data) {
52224
- const startWithCR = this.lastOutputEndedWithCR;
52225
- let previousWasCR = startWithCR;
52226
- let extraCRCount = 0;
52227
- for (const byte of data) {
52228
- if (byte === 10 && !previousWasCR) {
52229
- 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();
52230
52436
  }
52231
- previousWasCR = byte === 13;
52232
52437
  }
52233
- this.lastOutputEndedWithCR = previousWasCR;
52234
- if (extraCRCount === 0) {
52235
- 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]);
52236
52447
  }
52237
- const normalized = new Uint8Array(data.length + extraCRCount);
52238
- let writeIndex = 0;
52239
- previousWasCR = startWithCR;
52240
- for (const byte of data) {
52241
- if (byte === 10 && !previousWasCR) {
52242
- normalized[writeIndex] = 13;
52243
- writeIndex += 1;
52244
- }
52245
- normalized[writeIndex] = byte;
52246
- writeIndex += 1;
52247
- 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;
52248
52456
  }
52249
- return normalized;
52457
+ await this.runTmux(["resize-window", "-t", windowId, "-x", String(safeCols), "-y", String(safeRows)], true);
52458
+ await this.requestSnapshotInternal();
52250
52459
  }
52251
- getTitleParseState(paneId) {
52252
- const existing = this.outputTitleStates.get(paneId);
52253
- if (existing) {
52254
- 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);
52468
+ }
52469
+ this.callbacks.onEvent({
52470
+ type: "pane-active",
52471
+ data: { windowId, paneId }
52472
+ });
52473
+ await this.capturePaneHistory(paneId);
52474
+ await this.requestSnapshotInternal();
52475
+ }
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);
52255
52483
  }
52256
- const created = {
52257
- phase: "normal",
52258
- titleBytes: []
52259
- };
52260
- this.outputTitleStates.set(paneId, created);
52261
- return created;
52262
52484
  }
52263
- emitPaneTitleIfNeeded(paneId, titleBytes) {
52264
- if (titleBytes.length === 0) {
52485
+ async requestSnapshotInternal() {
52486
+ if (!this.connected) {
52265
52487
  return;
52266
52488
  }
52267
- const title = new TextDecoder().decode(new Uint8Array(titleBytes)).trim();
52268
- 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 });
52269
52514
  return;
52270
52515
  }
52271
- 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();
52272
52520
  }
52273
- stripScreenTitleSequence(paneId, data) {
52274
- if (data.length === 0) {
52275
- return data;
52276
- }
52277
- const parseState = this.getTitleParseState(paneId);
52278
- const output = [];
52279
- let phase = parseState.phase;
52280
- const titleBytes = parseState.titleBytes;
52281
- for (const byte of data) {
52282
- if (phase === "normal") {
52283
- if (byte === 27) {
52284
- phase = "esc";
52285
- } else {
52286
- output.push(byte);
52287
- }
52521
+ parseSnapshotSession(lines) {
52522
+ this.snapshotSession = null;
52523
+ for (const line of lines) {
52524
+ if (!line.trim()) {
52288
52525
  continue;
52289
52526
  }
52290
- if (phase === "esc") {
52291
- if (byte === 107) {
52292
- phase = "title";
52293
- titleBytes.length = 0;
52294
- continue;
52295
- }
52296
- output.push(27);
52297
- if (byte === 27) {
52298
- phase = "esc";
52299
- } else {
52300
- output.push(byte);
52301
- phase = "normal";
52302
- }
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()) {
52303
52538
  continue;
52304
52539
  }
52305
- if (phase === "title") {
52306
- if (byte === 27) {
52307
- phase = "titleEsc";
52308
- } else {
52309
- titleBytes.push(byte);
52310
- }
52540
+ const [id, indexRaw, name, activeRaw] = line.split("\t");
52541
+ if (!id) {
52311
52542
  continue;
52312
52543
  }
52313
- if (byte === 92) {
52314
- this.emitPaneTitleIfNeeded(paneId, titleBytes);
52315
- titleBytes.length = 0;
52316
- phase = "normal";
52317
- } else if (byte === 27) {
52318
- phase = "titleEsc";
52319
- } else {
52320
- titleBytes.push(27, byte);
52321
- phase = "title";
52544
+ const index = Number.parseInt(indexRaw ?? "", 10);
52545
+ const active = activeRaw === "1";
52546
+ if (active) {
52547
+ this.activeWindowId = id;
52322
52548
  }
52549
+ this.snapshotWindows.set(id, {
52550
+ id,
52551
+ index: Number.isNaN(index) ? 0 : index,
52552
+ name: name ?? "",
52553
+ active,
52554
+ panes: []
52555
+ });
52323
52556
  }
52324
- parseState.phase = phase;
52325
- return new Uint8Array(output);
52326
52557
  }
52327
- emitTerminalOutput(mode, paneId, data) {
52328
- const last = this.lastOutputFrame;
52329
- const isCrossModeDuplicate = last !== null && last.mode !== mode && last.paneId === paneId && isSameBytes(last.data, data);
52330
- if (!isCrossModeDuplicate) {
52331
- this.onTerminalOutput(paneId, data);
52558
+ parseSnapshotPanes(lines) {
52559
+ for (const window2 of this.snapshotWindows.values()) {
52560
+ window2.panes = [];
52332
52561
  }
52333
- this.lastOutputFrame = {
52334
- mode,
52335
- paneId,
52336
- data: data.slice()
52337
- };
52338
- }
52339
- parseControlLine(line) {
52340
- const spaceIndex = line.indexOf(" ");
52341
- const command = spaceIndex === -1 ? line : line.slice(0, spaceIndex);
52342
- const args = spaceIndex === -1 ? "" : line.slice(spaceIndex + 1);
52343
- switch (command) {
52344
- case "%window-add":
52345
- case "%unlinked-window-add":
52346
- this.onEvent({ type: "window-add", data: { windowId: args } });
52347
- break;
52348
- case "%window-close":
52349
- case "%unlinked-window-close":
52350
- this.onEvent({ type: "window-close", data: { windowId: args } });
52351
- break;
52352
- case "%window-renamed":
52353
- case "%unlinked-window-renamed": {
52354
- const parts = this.parseArgs(args);
52355
- this.onEvent({
52356
- type: "window-renamed",
52357
- data: { windowId: parts[0], name: parts[1] }
52358
- });
52359
- break;
52562
+ for (const line of lines) {
52563
+ if (!line.trim()) {
52564
+ continue;
52360
52565
  }
52361
- case "%window-pane-changed": {
52362
- const parts = this.parseArgs(args);
52363
- this.onEvent({
52364
- type: "pane-active",
52365
- data: { windowId: parts[0], paneId: parts[1] }
52366
- });
52367
- break;
52566
+ const [paneId, windowId, indexRaw, titleRaw, activeRaw, widthRaw, heightRaw] = line.split("\t");
52567
+ if (!paneId || !windowId) {
52568
+ continue;
52368
52569
  }
52369
- case "%pane-close":
52370
- this.onEvent({ type: "pane-close", data: { paneId: args } });
52371
- break;
52372
- case "%pane-add": {
52373
- const parts = this.parseArgs(args);
52374
- this.onEvent({
52375
- type: "pane-add",
52376
- data: {
52377
- paneId: parts[0] ?? args,
52378
- windowId: parts[1]
52379
- }
52380
- });
52381
- 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;
52382
52585
  }
52383
- case "%pane-mode-changed":
52384
- break;
52385
- case "%session-changed": {
52386
- const parts = this.parseArgs(args);
52387
- this.onEvent({
52388
- type: "window-add",
52389
- data: { sessionId: parts[0], name: parts[1] }
52390
- });
52391
- break;
52586
+ const window2 = this.snapshotWindows.get(windowId);
52587
+ if (!window2) {
52588
+ continue;
52392
52589
  }
52393
- case "%sessions-changed":
52394
- break;
52395
- case "%session-window-changed": {
52396
- const parts = this.parseArgs(args);
52397
- this.onEvent({
52398
- type: "window-active",
52399
- data: { sessionId: parts[0], windowId: parts[1] }
52400
- });
52401
- 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;
52402
52612
  }
52403
- case "%layout-change": {
52404
- const parts = this.parseArgs(args);
52405
- this.onEvent({
52406
- type: "layout-change",
52407
- data: { windowId: parts[0], layout: parts[1] }
52408
- });
52409
- break;
52613
+ }
52614
+ return null;
52615
+ }
52616
+ async startPipeForPane(paneId) {
52617
+ await this.queuePipeTransition(async () => {
52618
+ if (this.currentPipePaneId === paneId) {
52619
+ return;
52410
52620
  }
52411
- case "%output": {
52412
- const firstSpace = args.indexOf(" ");
52413
- if (firstSpace !== -1) {
52414
- const paneId = args.slice(0, firstSpace);
52415
- const value = args.slice(firstSpace + 1);
52416
- const decoded = decodeTmuxEscapedValue(value);
52417
- const stripped = this.stripScreenTitleSequence(paneId, decoded);
52418
- const normalized = this.normalizeTerminalOutputNewline(stripped);
52419
- 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();
52420
52630
  }
52421
- break;
52422
- }
52423
- case "%extended-output": {
52424
- const firstSpace = args.indexOf(" ");
52425
- if (firstSpace === -1)
52426
- break;
52427
- const paneId = args.slice(0, firstSpace);
52428
- const colonIndex = args.indexOf(" : ");
52429
- if (colonIndex === -1)
52430
- break;
52431
- const value = args.slice(colonIndex + 3);
52432
- const decoded = decodeTmuxEscapedValue(value);
52433
- const stripped = this.stripScreenTitleSequence(paneId, decoded);
52434
- const normalized = this.normalizeTerminalOutputNewline(stripped);
52435
- this.emitTerminalOutput("extended", paneId, normalized);
52436
- break;
52437
- }
52438
- case "%exit":
52439
- this.onExit?.(args.trim() ? args : null);
52440
- break;
52441
- case "%bell":
52442
- {
52443
- const parts = this.parseArgs(args);
52444
- const windowId = parts[0] ?? args;
52445
- const paneId = parts[1];
52446
- const data = {};
52447
- if (windowId)
52448
- data.windowId = windowId;
52449
- if (paneId)
52450
- data.paneId = paneId;
52451
- 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
+ }
52452
52657
  }
52453
- break;
52454
- case "%pause":
52455
- case "%resume":
52456
- break;
52457
- case "%client-session-changed":
52458
- case "%client-detached":
52459
- case "lient-session-changed":
52460
- case "lient-detached":
52461
- break;
52462
- default:
52463
- 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]);
52464
52676
  }
52677
+ this.pipeReadAbort?.();
52678
+ this.pipeReadAbort = null;
52465
52679
  }
52466
- parseArgs(args) {
52467
- const result = [];
52468
- let current = "";
52469
- let inQuotes = false;
52470
- for (let i = 0;i < args.length; i++) {
52471
- const char = args[i];
52472
- if (char === '"' && args[i - 1] !== "\\") {
52473
- inQuotes = !inQuotes;
52474
- } else if (char === " " && !inQuotes) {
52475
- if (current) {
52476
- result.push(current);
52477
- current = "";
52478
- }
52479
- } else {
52480
- current += char;
52481
- }
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;
52482
52697
  }
52483
- if (current) {
52484
- 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;
52485
52702
  }
52486
- 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]);
52712
+ }
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();
52726
+ }
52727
+ }
52728
+
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;
52487
52753
  }
52488
- flush() {
52489
- this.buffer = "";
52490
- this.lastOutputEndedWithCR = false;
52491
- this.lastOutputFrame = null;
52492
- this.outputTitleStates.clear();
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");
52493
52760
  }
52761
+ return;
52494
52762
  }
52495
52763
 
52496
- // ../../apps/gateway/src/tmux/connection.ts
52497
- var BELL_DEDUP_WINDOW_MS = 200;
52498
- function buildLocalTmuxCommand(sessionName, startDirectory) {
52499
- return ["tmux", "-CC", "new-session", "-A", "-c", startDirectory, "-s", sessionName];
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" };
52500
52801
  }
52501
52802
 
52502
- 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 {
52503
52809
  deviceId;
52810
+ callbacks;
52811
+ deps;
52504
52812
  device = null;
52505
- subprocess = null;
52506
- terminal = null;
52507
- sshClient = null;
52508
- sshStream = null;
52509
- parser;
52510
- onEvent;
52511
- onTerminalOutput;
52512
- onTerminalHistory;
52513
- onSnapshot;
52514
- onError;
52515
- onClose;
52516
- activePaneId = null;
52517
- activeWindowId = null;
52813
+ sessionName = "tmex";
52518
52814
  connected = false;
52519
52815
  manualDisconnect = false;
52520
- ready = false;
52521
- readyFailed = false;
52522
- readyPromise;
52523
- resolveReady = null;
52524
- rejectReady = null;
52525
- startupNonControlOutput = [];
52526
- lastExitReason = null;
52527
- pendingCommandKinds = [];
52528
- commandKindsByNo = new Map;
52529
- pendingCapturePaneRequests = [];
52530
- pendingCapturePaneModeRequests = [];
52816
+ closeNotified = false;
52817
+ cleanupPromise = null;
52818
+ activeWindowId = null;
52819
+ activePaneId = null;
52820
+ pendingPaneTitles = new Map;
52531
52821
  snapshotSession = null;
52532
52822
  snapshotWindows = new Map;
52533
- pendingPaneTitles = new Map;
52534
- snapshotPanesReady = false;
52535
- historyCaptureStates = new Map;
52536
- resizeSnapshotTimer = null;
52537
- bellControlEventSeen = false;
52823
+ currentPipePaneId = null;
52824
+ pipeReadAbort = null;
52825
+ pipeTransition = Promise.resolve();
52826
+ hookReadAbort = null;
52827
+ hookBuffer = "";
52538
52828
  bellDedup = new Map;
52539
- 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 = {}) {
52540
52842
  this.deviceId = options.deviceId;
52541
- this.onEvent = options.onEvent;
52542
- this.onTerminalOutput = options.onTerminalOutput;
52543
- this.onTerminalHistory = options.onTerminalHistory;
52544
- this.onSnapshot = options.onSnapshot;
52545
- this.onError = options.onError;
52546
- this.onClose = options.onClose;
52547
- this.readyPromise = new Promise((resolve, reject) => {
52548
- this.resolveReady = resolve;
52549
- this.rejectReady = reject;
52550
- });
52551
- this.parser = new TmuxControlParser({
52552
- onEvent: (event) => this.handleTmuxEvent(event),
52553
- onTerminalOutput: (paneId, data) => this.emitTerminalOutput(paneId, data),
52554
- onPaneTitle: (paneId, title) => this.handlePaneTitleUpdate(paneId, title),
52555
- onOutputBlockBegin: (meta) => this.handleOutputBlockBegin(meta),
52556
- onOutputBlock: (block) => this.handleOutputBlock(block),
52557
- onNonControlOutput: (line) => this.handleNonControlOutput(line),
52558
- onReady: () => this.markReady(),
52559
- onExit: (reason) => {
52560
- this.lastExitReason = reason;
52561
- if (!this.ready) {
52562
- this.failReady(new Error(reason ? `tmux exited: ${reason}` : "tmux exited"));
52563
- }
52564
- }
52565
- });
52566
- }
52567
- markReady() {
52568
- if (this.ready || this.readyFailed)
52569
- return;
52570
- this.ready = true;
52571
- this.resolveReady?.();
52572
- this.resolveReady = null;
52573
- this.rejectReady = null;
52574
- this.startupNonControlOutput = [];
52575
- }
52576
- failReady(error) {
52577
- if (this.ready || this.readyFailed)
52578
- return;
52579
- this.readyFailed = true;
52580
- const detail = this.startupNonControlOutput.filter(Boolean).join(`
52581
- `);
52582
- const nextError = detail ? new Error(`${error.message}
52583
- ${detail}`) : error;
52584
- this.rejectReady?.(nextError);
52585
- this.resolveReady = null;
52586
- this.rejectReady = null;
52587
- }
52588
- waitForReady(timeoutMs = 5000) {
52589
- if (this.ready) {
52590
- return Promise.resolve();
52591
- }
52592
- return new Promise((resolve, reject) => {
52593
- const timer = setTimeout(() => {
52594
- const detail = this.startupNonControlOutput.filter(Boolean).join(`
52595
- `);
52596
- reject(new Error(detail ? `tmux control mode not ready: ${detail}` : "tmux control mode not ready"));
52597
- }, timeoutMs);
52598
- this.readyPromise.then(() => {
52599
- clearTimeout(timer);
52600
- resolve();
52601
- }).catch((err) => {
52602
- clearTimeout(timer);
52603
- reject(err);
52604
- });
52605
- });
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
+ };
52606
52849
  }
52607
52850
  async connect() {
52608
52851
  this.manualDisconnect = false;
52609
- this.device = getDeviceById(this.deviceId);
52852
+ this.closeNotified = false;
52853
+ this.device = this.deps.getDevice(this.deviceId);
52610
52854
  if (!this.device) {
52611
52855
  throw new Error(`Device not found: ${this.deviceId}`);
52612
52856
  }
52613
- if (this.device.type === "local") {
52614
- await this.connectLocal();
52615
- } else {
52616
- await this.connectSSH();
52857
+ if (this.device.type !== "ssh") {
52858
+ throw new Error(`SshExternalTmuxConnection only supports ssh device: ${this.deviceId}`);
52617
52859
  }
52618
- }
52619
- async connectLocal() {
52620
- const sessionName = this.device?.session ?? "tmex";
52621
- const env = buildLocalTmuxEnv(getLocalShellPath());
52622
- const startDirectory = homedir();
52623
- this.subprocess = Bun.spawn(buildLocalTmuxCommand(sessionName, startDirectory), {
52624
- env,
52625
- terminal: {
52626
- name: "xterm-256color",
52627
- cols: 80,
52628
- rows: 30,
52629
- data: (_term, data) => {
52630
- this.parser.processData(data);
52631
- },
52632
- exit: () => {
52633
- if (!this.ready) {
52634
- this.failReady(new Error("tmux terminal closed before ready"));
52635
- }
52636
- if (this.lastExitReason) {
52637
- this.onError(new Error(`tmux exited: ${this.lastExitReason}`));
52638
- }
52639
- this.cleanup();
52640
- if (!this.manualDisconnect) {
52641
- this.onClose();
52642
- }
52643
- }
52644
- }
52645
- });
52646
- 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();
52647
52871
  this.connected = true;
52648
52872
  updateDeviceRuntimeStatus(this.deviceId, {
52649
52873
  lastSeenAt: new Date().toISOString(),
52650
52874
  tmuxAvailable: true,
52651
52875
  lastError: null
52652
52876
  });
52653
- try {
52654
- await this.waitForReady();
52655
- this.configureWindowSizePolicy();
52656
- } catch (err) {
52657
- this.cleanup();
52658
- throw err;
52877
+ await this.requestSnapshotInternal();
52878
+ }
52879
+ disconnect() {
52880
+ if (this.manualDisconnect) {
52881
+ return;
52659
52882
  }
52883
+ this.manualDisconnect = true;
52884
+ this.shutdownInternal(false);
52885
+ }
52886
+ requestSnapshot() {
52887
+ this.requestSnapshotInternal();
52660
52888
  }
52661
- async connectSSH() {
52662
- if (!this.device)
52889
+ sendInput(paneId, data) {
52890
+ if (!this.connected) {
52663
52891
  return;
52664
- const conn = new import_ssh2.Client;
52665
- this.sshClient = conn;
52666
- const host = this.device.host;
52667
- const port = this.device.port ?? 22;
52668
- const username = this.device.username;
52669
- const sessionName = this.device.session ?? "tmex";
52670
- const resolvedUsername = resolveSshUsername(username, this.device.authMode);
52671
- const logStage = (stage, extra = {}) => {
52672
- console.log("[ssh]", {
52673
- stage,
52674
- deviceId: this.deviceId,
52675
- host,
52676
- port,
52677
- username: resolvedUsername,
52678
- authMode: this.device?.authMode,
52679
- ...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);
52680
52896
  });
52681
- };
52682
- logStage("connect_start", {
52683
- hasHost: Boolean(host),
52684
- hasSshConfigRef: Boolean(this.device.sshConfigRef),
52685
- 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);
52686
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);
52687
52971
  if (this.device.authMode === "configRef" || !host && this.device.sshConfigRef) {
52688
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");
52689
52973
  }
@@ -52693,14 +52977,14 @@ ${detail}`) : error;
52693
52977
  const authConfig = {
52694
52978
  host,
52695
52979
  port,
52696
- username: resolvedUsername
52980
+ username
52697
52981
  };
52698
52982
  switch (this.device.authMode) {
52699
52983
  case "password": {
52700
52984
  if (!this.device.passwordEnc) {
52701
52985
  throw new Error("auth_password_missing: \u5BC6\u7801\u8BA4\u8BC1\u672A\u63D0\u4F9B\u5BC6\u7801");
52702
52986
  }
52703
- authConfig.password = await decryptWithContext(this.device.passwordEnc, {
52987
+ authConfig.password = await this.deps.decrypt(this.device.passwordEnc, {
52704
52988
  scope: "device",
52705
52989
  entityId: this.device.id,
52706
52990
  field: "password_enc"
@@ -52711,13 +52995,13 @@ ${detail}`) : error;
52711
52995
  if (!this.device.privateKeyEnc) {
52712
52996
  throw new Error("auth_key_missing: \u79C1\u94A5\u8BA4\u8BC1\u672A\u63D0\u4F9B\u79C1\u94A5");
52713
52997
  }
52714
- authConfig.privateKey = await decryptWithContext(this.device.privateKeyEnc, {
52998
+ authConfig.privateKey = await this.deps.decrypt(this.device.privateKeyEnc, {
52715
52999
  scope: "device",
52716
53000
  entityId: this.device.id,
52717
53001
  field: "private_key_enc"
52718
53002
  });
52719
53003
  if (this.device.privateKeyPassphraseEnc) {
52720
- authConfig.passphrase = await decryptWithContext(this.device.privateKeyPassphraseEnc, {
53004
+ authConfig.passphrase = await this.deps.decrypt(this.device.privateKeyPassphraseEnc, {
52721
53005
  scope: "device",
52722
53006
  entityId: this.device.id,
52723
53007
  field: "private_key_passphrase_enc"
@@ -52729,22 +53013,19 @@ ${detail}`) : error;
52729
53013
  authConfig.agent = resolveSshAgentSocket("agent");
52730
53014
  break;
52731
53015
  }
52732
- case "configRef": {
52733
- break;
52734
- }
52735
53016
  case "auto": {
52736
53017
  const agentSocket = resolveSshAgentSocket("auto");
52737
53018
  if (agentSocket) {
52738
53019
  authConfig.agent = agentSocket;
52739
53020
  }
52740
53021
  if (this.device.privateKeyEnc) {
52741
- authConfig.privateKey = await decryptWithContext(this.device.privateKeyEnc, {
53022
+ authConfig.privateKey = await this.deps.decrypt(this.device.privateKeyEnc, {
52742
53023
  scope: "device",
52743
53024
  entityId: this.device.id,
52744
53025
  field: "private_key_enc"
52745
53026
  });
52746
53027
  } else if (this.device.passwordEnc) {
52747
- authConfig.password = await decryptWithContext(this.device.passwordEnc, {
53028
+ authConfig.password = await this.deps.decrypt(this.device.passwordEnc, {
52748
53029
  scope: "device",
52749
53030
  entityId: this.device.id,
52750
53031
  field: "password_enc"
@@ -52752,17 +53033,15 @@ ${detail}`) : error;
52752
53033
  }
52753
53034
  break;
52754
53035
  }
53036
+ case "configRef":
53037
+ break;
52755
53038
  }
52756
53039
  if (this.device.authMode === "auto" && !authConfig.agent && !authConfig.privateKey && !authConfig.password) {
52757
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");
52758
53041
  }
52759
- logStage("auth_config_resolved", {
52760
- hasAgent: Boolean(authConfig.agent),
52761
- hasPrivateKey: Boolean(authConfig.privateKey),
52762
- hasPassphrase: Boolean(authConfig.passphrase),
52763
- hasPassword: Boolean(authConfig.password)
52764
- });
52765
- return new Promise((resolve, reject) => {
53042
+ const client = this.deps.createClient();
53043
+ this.sshClient = client;
53044
+ await new Promise((resolve, reject) => {
52766
53045
  let settled = false;
52767
53046
  const resolveOnce = () => {
52768
53047
  if (settled) {
@@ -52776,429 +53055,289 @@ ${detail}`) : error;
52776
53055
  return;
52777
53056
  }
52778
53057
  settled = true;
52779
- reject(error instanceof Error ? error : new Error(String(error)));
53058
+ reject(error);
52780
53059
  };
52781
- let stderrTail = "";
52782
- conn.on("ready", () => {
52783
- logStage("ssh_ready");
52784
- const tmuxCommand = `tmux -CC new-session -A -s ${sessionName}`;
52785
- logStage("tmux_exec_start", { command: tmuxCommand });
52786
- conn.exec(tmuxCommand, {
52787
- pty: {
52788
- term: "xterm-256color",
52789
- cols: 80,
52790
- rows: 30
52791
- }
52792
- }, (err, stream) => {
52793
- if (err) {
52794
- logStage("tmux_exec_failed", { error: err.message });
52795
- rejectOnce(new Error(`tmux_exec_failed: \u542F\u52A8\u8FDC\u7AEF tmux \u5931\u8D25\uFF1A${err.message}`));
52796
- return;
52797
- }
52798
- this.sshStream = stream;
52799
- stream.on("close", () => {
52800
- logStage("ssh_stream_closed");
52801
- if (!this.ready) {
52802
- this.failReady(new Error("SSH stream closed before tmux became ready"));
52803
- }
52804
- if (this.lastExitReason) {
52805
- this.onError(new Error(`tmux exited: ${this.lastExitReason}`));
52806
- }
52807
- this.cleanup();
52808
- if (!this.manualDisconnect) {
52809
- this.onClose();
52810
- }
52811
- });
52812
- stream.on("data", (data) => {
52813
- this.parser.processData(data);
52814
- });
52815
- stream.stderr.on("data", (data) => {
52816
- const chunk2 = data.toString();
52817
- stderrTail = `${stderrTail}${chunk2}`.slice(-2000);
52818
- console.error("[ssh] stderr:", chunk2);
52819
- });
52820
- this.connected = true;
52821
- updateDeviceRuntimeStatus(this.deviceId, {
52822
- lastSeenAt: new Date().toISOString(),
52823
- tmuxAvailable: true,
52824
- lastError: null
52825
- });
52826
- this.waitForReady().then(() => {
52827
- this.configureWindowSizePolicy();
52828
- logStage("tmux_ready");
52829
- resolveOnce();
52830
- }).catch((err2) => {
52831
- const stderrText = stderrTail.trim();
52832
- const nextError = stderrText.length > 0 ? new Error(`${err2 instanceof Error ? err2.message : String(err2)}
52833
- ssh stderr: ${stderrText}`) : err2;
52834
- logStage("tmux_ready_failed", {
52835
- error: nextError instanceof Error ? nextError.message : String(nextError)
52836
- });
52837
- this.cleanup();
52838
- rejectOnce(nextError);
52839
- });
52840
- });
53060
+ client.on("ready", () => {
53061
+ resolveOnce();
52841
53062
  });
52842
- conn.on("error", (err) => {
52843
- logStage("connect_error", { error: err.message });
53063
+ client.on("error", (error) => {
52844
53064
  updateDeviceRuntimeStatus(this.deviceId, {
52845
53065
  lastSeenAt: new Date().toISOString(),
52846
53066
  tmuxAvailable: false,
52847
- lastError: err.message
53067
+ lastError: error.message
52848
53068
  });
52849
- 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
+ }
52850
53077
  });
52851
- conn.on("close", () => {
52852
- logStage("connection_closed", { manualDisconnect: this.manualDisconnect });
52853
- if (!this.ready) {
52854
- 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;
52855
53082
  }
52856
- this.cleanup();
52857
53083
  if (!this.manualDisconnect) {
52858
- this.onClose();
53084
+ this.shutdownInternal(true);
52859
53085
  }
52860
53086
  });
52861
- logStage("connect_attempt");
52862
- conn.connect(authConfig);
53087
+ client.connect(authConfig);
52863
53088
  });
52864
53089
  }
52865
- shouldPassBellDedup(key) {
52866
- const now = Date.now();
52867
- const previous = this.bellDedup.get(key) ?? 0;
52868
- if (now - previous < BELL_DEDUP_WINDOW_MS) {
52869
- return false;
52870
- }
52871
- this.bellDedup.set(key, now);
52872
- return true;
52873
- }
52874
- handleTmuxEvent(event) {
52875
- if (event.type === "pane-active") {
52876
- const data = event.data ?? {};
52877
- const windowId = typeof data.windowId === "string" && data.windowId ? data.windowId : null;
52878
- const paneId = typeof data.paneId === "string" && data.paneId ? data.paneId : null;
52879
- if (windowId)
52880
- this.activeWindowId = windowId;
52881
- if (paneId)
52882
- this.activePaneId = paneId;
52883
- }
52884
- if (event.type === "bell") {
52885
- this.bellControlEventSeen = true;
52886
- const data = event.data ?? {};
52887
- const resolvedPaneId = (typeof data.paneId === "string" && data.paneId ? data.paneId : null) ?? this.activePaneId ?? undefined;
52888
- const windowId = typeof data.windowId === "string" && data.windowId ? data.windowId : undefined;
52889
- const key = resolvedPaneId ?? windowId ?? "-";
52890
- if (!this.shouldPassBellDedup(key)) {
52891
- 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();
52892
53111
  }
52893
- if (resolvedPaneId && !data.paneId) {
52894
- this.onEvent({
52895
- ...event,
52896
- data: {
52897
- ...data,
52898
- paneId: resolvedPaneId
52899
- }
52900
- });
52901
- 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);
52902
53118
  }
52903
- }
52904
- 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
+ ]);
52905
53191
  }
52906
- emitBellEventIfNeeded(paneId, data) {
52907
- if (this.bellControlEventSeen) {
52908
- return;
52909
- }
52910
- for (const byte of data) {
52911
- if (byte !== 7) {
52912
- continue;
53192
+ handleHookChunk(text2) {
53193
+ this.hookBuffer += text2;
53194
+ while (true) {
53195
+ const newlineIndex = this.hookBuffer.indexOf(`
53196
+ `);
53197
+ if (newlineIndex < 0) {
53198
+ return;
52913
53199
  }
52914
- if (!this.shouldPassBellDedup(paneId)) {
52915
- break;
53200
+ const line = this.hookBuffer.slice(0, newlineIndex).trim();
53201
+ this.hookBuffer = this.hookBuffer.slice(newlineIndex + 1);
53202
+ if (!line) {
53203
+ continue;
52916
53204
  }
52917
- this.onEvent({
52918
- type: "bell",
52919
- data: {
52920
- paneId
52921
- }
52922
- });
52923
- break;
52924
- }
52925
- }
52926
- handlePaneTitleUpdate(paneId, title) {
52927
- const nextTitle = title.trim();
52928
- if (!nextTitle) {
52929
- return;
52930
- }
52931
- let found = false;
52932
- let updated = false;
52933
- for (const window2 of this.snapshotWindows.values()) {
52934
- const pane = window2.panes.find((item) => item.id === paneId);
52935
- 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
+ }
52936
53220
  continue;
52937
53221
  }
52938
- found = true;
52939
- if (pane.title !== nextTitle) {
52940
- pane.title = nextTitle;
52941
- updated = true;
53222
+ if (type === "pane-exited" || type === "pane-died") {
53223
+ this.requestSnapshot();
52942
53224
  }
52943
- this.pendingPaneTitles.delete(paneId);
52944
- break;
52945
- }
52946
- if (found && !updated) {
52947
- return;
52948
- }
52949
- if (!updated) {
52950
- this.pendingPaneTitles.set(paneId, nextTitle);
52951
- return;
52952
53225
  }
52953
- this.emitSnapshotIfReady();
52954
53226
  }
52955
- configureWindowSizePolicy() {
52956
- if (!this.connected)
52957
- return;
52958
- this.sendCommand(`set-option -g -w window-size latest
52959
- `);
52960
- this.sendCommand(`set-option -g -w aggressive-resize off
52961
- `);
53227
+ async runAndRefresh(argv, allowTargetMissing = false) {
53228
+ await this.runTmux(argv, allowTargetMissing);
53229
+ await this.requestSnapshotInternal();
52962
53230
  }
52963
- scheduleSnapshotAfterResize(delayMs = 120) {
52964
- if (this.resizeSnapshotTimer) {
52965
- 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]);
52966
53235
  }
52967
- this.resizeSnapshotTimer = setTimeout(() => {
52968
- this.resizeSnapshotTimer = null;
52969
- this.requestSnapshot();
52970
- }, delayMs);
53236
+ await this.runAndRefresh(["kill-window", "-t", windowId], true);
52971
53237
  }
52972
- sendInput(paneId, data) {
52973
- if (!this.connected)
52974
- return;
52975
- this.sendUtf8Bytes(paneId, new TextEncoder().encode(data));
52976
- }
52977
- sendUtf8Bytes(paneId, data) {
52978
- 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) {
52979
53243
  return;
52980
53244
  }
52981
- const chunkSize = 256;
52982
- for (let offset = 0;offset < data.length; offset += chunkSize) {
52983
- const chunk2 = data.slice(offset, offset + chunkSize);
52984
- const hex = [];
52985
- for (const byte of chunk2) {
52986
- hex.push(byte.toString(16).padStart(2, "0"));
52987
- }
52988
- this.sendCommand(`send-keys -H -t ${paneId} ${hex.join(" ")}
52989
- `);
52990
- }
53245
+ await this.runTmux(["resize-window", "-t", windowId, "-x", String(safeCols), "-y", String(safeRows)], true);
53246
+ await this.requestSnapshotInternal();
52991
53247
  }
52992
- sendKey(paneId, key) {
52993
- if (!this.connected)
52994
- return;
52995
- const cmd = `send-keys -t ${paneId} ${key}
52996
- `;
52997
- this.sendCommand(cmd);
52998
- }
52999
- selectWindow(windowId) {
53000
- if (!this.connected) {
53001
- console.log("[tmux] cannot select window: not connected");
53002
- return;
53003
- }
53248
+ async selectPaneInternal(windowId, paneId, size) {
53004
53249
  this.activeWindowId = windowId;
53005
- this.sendCommand(`select-window -t ${windowId}
53006
- `);
53007
- this.requestSnapshot();
53008
- }
53009
- selectPane(windowId, paneId) {
53010
- if (!this.connected) {
53011
- console.log("[tmux] cannot select pane: not connected");
53012
- return;
53013
- }
53014
- console.log("[tmux] selecting pane", paneId, "in window", windowId);
53015
53250
  this.activePaneId = paneId;
53016
- this.activeWindowId = windowId;
53017
- this.sendCommand(`select-window -t ${windowId}
53018
- `);
53019
- this.sendCommand(`select-pane -t ${paneId}
53020
- `);
53021
- this.capturePaneHistory(paneId);
53022
- }
53023
- capturePaneHistory(paneId) {
53024
- if (!this.connected) {
53025
- console.log("[tmux] cannot capture history: not connected");
53026
- return;
53027
- }
53028
- console.log("[tmux] capturing history for pane", paneId);
53029
- const existing = this.historyCaptureStates.get(paneId);
53030
- if (existing?.timeout) {
53031
- clearTimeout(existing.timeout);
53032
- }
53033
- const timeout = setTimeout(() => {
53034
- this.emitCapturedHistory(paneId);
53035
- }, 220);
53036
- this.historyCaptureStates.set(paneId, {
53037
- normal: null,
53038
- alternate: null,
53039
- preferAlternate: null,
53040
- timeout
53041
- });
53042
- this.pendingCapturePaneModeRequests.push(paneId);
53043
- this.sendCommand(`display-message -p -t ${paneId} "#{alternate_on}"
53044
- `, "capture-pane-mode");
53045
- this.pendingCapturePaneRequests.push({ paneId, mode: "normal" });
53046
- this.sendCommand(`capture-pane -t ${paneId} -S -1000 -e -p
53047
- `, "capture-pane");
53048
- this.pendingCapturePaneRequests.push({ paneId, mode: "alternate" });
53049
- this.sendCommand(`capture-pane -t ${paneId} -a -S -1000 -e -p -q
53050
- `, "capture-pane");
53051
- }
53052
- resizePane(_paneId, cols, rows) {
53053
- if (!this.connected)
53054
- return;
53055
- const safeCols = Math.max(2, Math.floor(cols));
53056
- const safeRows = Math.max(2, Math.floor(rows));
53057
- this.sendCommand(`refresh-client -C ${safeCols}x${safeRows}
53058
- `);
53059
- const windowId = this.activeWindowId;
53060
- if (windowId) {
53061
- this.sendCommand(`resize-window -x ${safeCols} -y ${safeRows} -t ${windowId}
53062
- `);
53063
- this.sendCommand(`set-window-option -t ${windowId} window-size latest
53064
- `);
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);
53065
53256
  }
53066
- this.scheduleSnapshotAfterResize();
53257
+ this.callbacks.onEvent({
53258
+ type: "pane-active",
53259
+ data: { windowId, paneId }
53260
+ });
53261
+ await this.capturePaneHistory(paneId);
53262
+ await this.requestSnapshotInternal();
53067
53263
  }
53068
- createWindow(name) {
53069
- if (!this.connected)
53070
- return;
53071
- if (name) {
53072
- this.sendCommand(`new-window -n "${name}"
53073
- `);
53074
- } else {
53075
- this.sendCommand(`new-window
53076
- `);
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);
53077
53271
  }
53078
53272
  }
53079
- closeWindow(windowId) {
53080
- if (!this.connected)
53081
- return;
53082
- this.sendCommand(`if-shell -F '#{==:#{session_windows},1}' 'new-window -d' ''
53083
- `);
53084
- this.sendCommand(`kill-window -t ${windowId}
53085
- `);
53086
- }
53087
- closePane(paneId) {
53088
- if (!this.connected)
53089
- return;
53090
- this.sendCommand(`kill-pane -t ${paneId}
53091
- `);
53092
- }
53093
- renameWindow(windowId, name) {
53094
- if (!this.connected)
53095
- return;
53096
- this.sendCommand(`rename-window -t ${windowId} "${name}"
53097
- `);
53098
- }
53099
- requestSnapshot() {
53100
- if (!this.connected)
53101
- return;
53102
- this.sendCommand(`display-message -p "#{session_id} #{session_name}"
53103
- `, "snapshot-session");
53104
- this.sendCommand(`list-windows -F "#{window_id} #{window_index} #{window_name} #{window_active}"
53105
- `, "snapshot-windows");
53106
- this.sendCommand(`list-panes -F "#{pane_id} #{window_id} #{pane_index} #{pane_title} #{pane_active} #{pane_width} #{pane_height}"
53107
- `, "snapshot-panes");
53108
- }
53109
- handleOutputBlockBegin(meta) {
53110
- const kind = this.pendingCommandKinds.shift() ?? "noop";
53111
- this.commandKindsByNo.set(meta.commandNo, kind);
53112
- }
53113
- handleOutputBlock(block) {
53114
- this.markReady();
53115
- const kind = this.commandKindsByNo.get(block.commandNo);
53116
- this.commandKindsByNo.delete(block.commandNo);
53117
- console.log("[tmux] handleOutputBlock commandNo:", block.commandNo, "kind:", kind, "lines:", block.lines.length);
53118
- const resolvedKind = kind ?? "noop";
53119
- if (block.isError) {
53120
- const message = block.lines.join(`
53121
- `).trim();
53122
- this.handleCaptureErrorFallback(resolvedKind);
53123
- if (message) {
53124
- if (this.isRecoverableTargetMissingError(message)) {
53125
- this.recoverFromTargetMissingError(message);
53126
- return;
53127
- }
53128
- this.onError(new Error(message));
53129
- }
53273
+ async requestSnapshotInternal() {
53274
+ if (!this.connected) {
53130
53275
  return;
53131
53276
  }
53132
- switch (resolvedKind) {
53133
- case "noop":
53134
- break;
53135
- case "snapshot-session":
53136
- this.parseSnapshotSession(block.lines);
53137
- break;
53138
- case "snapshot-windows":
53139
- this.parseSnapshotWindows(block.lines);
53140
- break;
53141
- case "snapshot-panes":
53142
- this.parseSnapshotPanes(block.lines);
53143
- break;
53144
- case "capture-pane":
53145
- this.handleCapturePaneOutput(block.lines);
53146
- break;
53147
- case "capture-pane-mode":
53148
- this.handleCapturePaneModeOutput(block.lines);
53149
- break;
53150
- }
53151
- this.emitSnapshotIfReady();
53152
- }
53153
- handleCaptureErrorFallback(kind) {
53154
- if (kind === "capture-pane") {
53155
- 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 });
53156
53302
  return;
53157
53303
  }
53158
- if (kind === "capture-pane-mode") {
53159
- this.handleCapturePaneModeOutput([]);
53160
- }
53161
- }
53162
- isRecoverableTargetMissingError(message) {
53163
- const normalized = message.toLowerCase();
53164
- return normalized.includes("can't find window") || normalized.includes("can't find pane") || normalized.includes("no such window") || normalized.includes("no such pane");
53165
- }
53166
- recoverFromTargetMissingError(message) {
53167
- const normalized = message.toLowerCase();
53168
- if (normalized.includes("window")) {
53169
- this.activeWindowId = null;
53170
- }
53171
- if (normalized.includes("pane")) {
53172
- this.activePaneId = null;
53173
- }
53174
- 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();
53175
53308
  }
53176
53309
  parseSnapshotSession(lines) {
53310
+ this.snapshotSession = null;
53177
53311
  for (const line of lines) {
53178
- if (!line.trim())
53312
+ if (!line.trim()) {
53179
53313
  continue;
53314
+ }
53180
53315
  const [id, name] = line.split("\t");
53181
- if (!id)
53182
- continue;
53183
- this.snapshotSession = { id, name: name ?? "" };
53316
+ if (id) {
53317
+ this.snapshotSession = { id, name: name ?? "" };
53318
+ }
53184
53319
  return;
53185
53320
  }
53186
53321
  }
53187
53322
  parseSnapshotWindows(lines) {
53188
53323
  this.snapshotWindows.clear();
53189
- this.snapshotPanesReady = false;
53190
53324
  for (const line of lines) {
53191
- if (!line.trim())
53325
+ if (!line.trim()) {
53192
53326
  continue;
53327
+ }
53193
53328
  const [id, indexRaw, name, activeRaw] = line.split("\t");
53194
- if (!id)
53329
+ if (!id) {
53195
53330
  continue;
53331
+ }
53196
53332
  const index = Number.parseInt(indexRaw ?? "", 10);
53197
53333
  const active = activeRaw === "1";
53334
+ if (active) {
53335
+ this.activeWindowId = id;
53336
+ }
53198
53337
  this.snapshotWindows.set(id, {
53199
53338
  id,
53200
- name: name ?? "",
53201
53339
  index: Number.isNaN(index) ? 0 : index,
53340
+ name: name ?? "",
53202
53341
  active,
53203
53342
  panes: []
53204
53343
  });
@@ -53209,195 +53348,493 @@ ssh stderr: ${stderrText}`) : err2;
53209
53348
  window2.panes = [];
53210
53349
  }
53211
53350
  for (const line of lines) {
53212
- if (!line.trim())
53351
+ if (!line.trim()) {
53213
53352
  continue;
53353
+ }
53214
53354
  const [paneId, windowId, indexRaw, titleRaw, activeRaw, widthRaw, heightRaw] = line.split("\t");
53215
- if (!paneId || !windowId)
53355
+ if (!paneId || !windowId) {
53216
53356
  continue;
53357
+ }
53217
53358
  const index = Number.parseInt(indexRaw ?? "", 10);
53218
53359
  const width = Number.parseInt(widthRaw ?? "", 10);
53219
53360
  const height = Number.parseInt(heightRaw ?? "", 10);
53220
- const title = titleRaw?.trim() ? titleRaw : undefined;
53221
53361
  const pane = {
53222
53362
  id: paneId,
53223
53363
  windowId,
53224
53364
  index: Number.isNaN(index) ? 0 : index,
53225
- title: this.pendingPaneTitles.get(paneId) ?? title,
53365
+ title: this.pendingPaneTitles.get(paneId) ?? (titleRaw?.trim() ? titleRaw : undefined),
53226
53366
  active: activeRaw === "1",
53227
53367
  width: Number.isNaN(width) ? 0 : width,
53228
53368
  height: Number.isNaN(height) ? 0 : height
53229
53369
  };
53230
- const win = this.snapshotWindows.get(windowId);
53231
- 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) {
53232
53376
  continue;
53233
- win.panes.push(pane);
53377
+ }
53378
+ window2.panes.push(pane);
53234
53379
  this.pendingPaneTitles.delete(paneId);
53235
53380
  }
53236
- for (const win of this.snapshotWindows.values()) {
53237
- 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);
53238
53383
  }
53239
- this.snapshotPanesReady = true;
53240
53384
  }
53241
- emitSnapshotIfReady() {
53242
- if (!this.snapshotSession)
53243
- return;
53244
- if (this.snapshotWindows.size === 0)
53245
- return;
53246
- if (!this.snapshotPanesReady)
53247
- return;
53248
- const windows = Array.from(this.snapshotWindows.values()).sort((a, b3) => a.index - b3.index);
53249
- const session = {
53385
+ emitSnapshot() {
53386
+ const session = this.snapshotSession ? {
53250
53387
  id: this.snapshotSession.id,
53251
53388
  name: this.snapshotSession.name,
53252
- windows
53253
- };
53254
- this.onSnapshot({
53389
+ windows: Array.from(this.snapshotWindows.values()).sort((left, right) => left.index - right.index)
53390
+ } : null;
53391
+ this.callbacks.onSnapshot({
53255
53392
  deviceId: this.deviceId,
53256
53393
  session
53257
53394
  });
53258
53395
  }
53259
- sendCommand(cmd, kind = "noop") {
53260
- this.pendingCommandKinds.push(kind);
53261
- if (this.terminal) {
53262
- this.terminal.write(cmd);
53263
- return;
53264
- }
53265
- if (this.sshStream) {
53266
- 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
+ }
53267
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
+ });
53268
53442
  }
53269
- emitTerminalOutput(paneId, data) {
53270
- this.emitBellEventIfNeeded(paneId, data);
53271
- this.onTerminalOutput(paneId, data);
53443
+ async stopPipe() {
53444
+ await this.queuePipeTransition(() => this.stopPipeNow());
53272
53445
  }
53273
- handleNonControlOutput(line) {
53274
- if (this.ready) {
53275
- return;
53446
+ async stopPipeNow() {
53447
+ const paneId = this.currentPipePaneId;
53448
+ this.currentPipePaneId = null;
53449
+ if (paneId) {
53450
+ await this.runTmuxAllowFailure(["pipe-pane", "-t", paneId]);
53276
53451
  }
53277
- if (this.startupNonControlOutput.length >= 20) {
53452
+ this.pipeReadAbort?.();
53453
+ this.pipeReadAbort = null;
53454
+ }
53455
+ queuePipeTransition(task) {
53456
+ const next = this.pipeTransition.catch(() => {
53278
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;
53279
53471
  }
53280
- 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);
53281
53478
  }
53282
- disconnect() {
53283
- this.manualDisconnect = true;
53284
- this.cleanup();
53479
+ async runTmuxAllowFailure(argv, timeoutMs = 1e4) {
53480
+ return this.runShell(`${quoteShellArg(this.tmuxBin)} ${joinShellArgs(argv)}`, timeoutMs);
53285
53481
  }
53286
- wasManuallyDisconnected() {
53287
- return this.manualDisconnect;
53482
+ async runShell(command, timeoutMs = 1e4) {
53483
+ return this.enqueueShellCommand(command, timeoutMs);
53288
53484
  }
53289
- handleCapturePaneOutput(lines) {
53290
- console.log("[tmux] capture-pane output:", lines.length, "lines");
53291
- const request = this.pendingCapturePaneRequests.shift() ?? null;
53292
- if (!request) {
53293
- console.log("[tmux] no pending pane id for capture-pane output");
53294
- 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
+ };
53295
53494
  }
53296
- const data = lines.join(`
53297
- `);
53298
- const state = this.historyCaptureStates.get(request.paneId);
53299
- if (!state) {
53495
+ }
53496
+ enqueueShellCommand(command, timeoutMs) {
53497
+ const next = this.commandQueue.catch(() => {
53300
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"));
53301
53509
  }
53302
- if (request.mode === "normal") {
53303
- state.normal = data;
53304
- } else {
53305
- state.alternate = data;
53306
- }
53307
- if (state.normal !== null && state.alternate !== null) {
53308
- 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
+ });
53309
53557
  }
53310
53558
  }
53311
- handleCapturePaneModeOutput(lines) {
53312
- const paneId = this.pendingCapturePaneModeRequests.shift() ?? null;
53313
- if (!paneId) {
53559
+ rejectPendingCommand(error) {
53560
+ const pending = this.pendingCommand;
53561
+ if (!pending) {
53314
53562
  return;
53315
53563
  }
53316
- const state = this.historyCaptureStates.get(paneId);
53317
- 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
+ }
53318
53619
  return;
53319
53620
  }
53320
- const firstLine = lines.find((line) => line.trim().length > 0)?.trim() ?? "";
53321
- if (firstLine === "1") {
53322
- state.preferAlternate = true;
53323
- } else if (firstLine === "0") {
53324
- 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();
53325
53645
  }
53326
- if (state.normal !== null && state.alternate !== null) {
53327
- this.emitCapturedHistory(paneId);
53646
+ }
53647
+ requireSshClient() {
53648
+ if (!this.sshClient) {
53649
+ throw new Error("SSH client not connected");
53328
53650
  }
53651
+ return this.sshClient;
53329
53652
  }
53330
- emitCapturedHistory(paneId) {
53331
- const state = this.historyCaptureStates.get(paneId);
53332
- if (!state)
53333
- return;
53334
- if (state.timeout) {
53335
- clearTimeout(state.timeout);
53336
- 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}`));
53337
53711
  }
53338
- const normal = state.normal ?? "";
53339
- const alternate = state.alternate ?? "";
53340
- let selected = normal;
53341
- if (state.preferAlternate === true) {
53342
- selected = alternate || normal;
53343
- } else if (state.preferAlternate === false) {
53344
- selected = normal || alternate;
53345
- } else if (alternate.length > normal.length) {
53346
- selected = alternate;
53712
+ if (this.connectPromise) {
53713
+ return this.connectPromise;
53347
53714
  }
53348
- if (selected) {
53349
- console.log("[tmux] sending history for pane", paneId, "data length:", selected.length);
53350
- 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;
53351
53724
  }
53352
- this.historyCaptureStates.delete(paneId);
53725
+ this.terminated = true;
53726
+ this.manualDisconnect = true;
53727
+ this.connection.disconnect();
53353
53728
  }
53354
- cleanup() {
53355
- this.connected = false;
53356
- this.parser.flush();
53357
- this.pendingCommandKinds = [];
53358
- this.commandKindsByNo.clear();
53359
- this.pendingCapturePaneRequests = [];
53360
- this.pendingCapturePaneModeRequests = [];
53361
- for (const state of this.historyCaptureStates.values()) {
53362
- if (state.timeout) {
53363
- 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);
53364
53768
  }
53365
53769
  }
53366
- this.historyCaptureStates.clear();
53367
- this.snapshotSession = null;
53368
- this.snapshotWindows.clear();
53369
- this.pendingPaneTitles.clear();
53370
- this.snapshotPanesReady = false;
53371
- this.lastExitReason = null;
53372
- this.activePaneId = null;
53373
- this.activeWindowId = null;
53374
- this.bellControlEventSeen = false;
53375
- this.bellDedup.clear();
53376
- if (this.resizeSnapshotTimer) {
53377
- clearTimeout(this.resizeSnapshotTimer);
53378
- this.resizeSnapshotTimer = null;
53379
- }
53380
- if (this.terminal) {
53381
- this.terminal.close();
53382
- this.terminal = null;
53383
- }
53384
- this.subprocess = null;
53385
- if (this.sshStream) {
53386
- this.sshStream.close();
53387
- this.sshStream = null;
53388
- }
53389
- if (this.sshClient) {
53390
- this.sshClient.end();
53391
- 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;
53392
53788
  }
53393
- updateDeviceRuntimeStatus(this.deviceId, {
53394
- 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;
53395
53802
  });
53803
+ this.entries.set(deviceId, entry);
53804
+ return entry.promise;
53396
53805
  }
53397
- isConnected() {
53398
- 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
+ }));
53399
53826
  }
53400
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
+ });
53401
53838
 
53402
53839
  // ../../apps/gateway/src/tmux/bell-context.ts
53403
53840
  function pickPaneById(windows, paneId) {
@@ -53454,7 +53891,10 @@ var defaultDeps = {
53454
53891
  listDevices: () => getAllDevices(),
53455
53892
  getDevice: (deviceId) => getDeviceById(deviceId),
53456
53893
  getSettings: () => getSiteSettings(),
53457
- createConnection: (options) => new TmuxConnection(options),
53894
+ acquireRuntime: async (deviceId) => tmuxRuntimeRegistry.acquire(deviceId),
53895
+ releaseRuntime: async (deviceId, _runtime) => {
53896
+ await tmuxRuntimeRegistry.release(deviceId);
53897
+ },
53458
53898
  async notifyBell(context) {
53459
53899
  const { device, settings, bell } = context;
53460
53900
  await eventNotifier.notify("terminal_bell", {
@@ -53506,7 +53946,7 @@ class PushSupervisor {
53506
53946
  this.running = false;
53507
53947
  for (const [deviceId, entry] of this.entries) {
53508
53948
  this.clearReconnectTimer(entry);
53509
- entry.connection?.disconnect();
53949
+ this.teardownEntry(entry);
53510
53950
  this.entries.delete(deviceId);
53511
53951
  }
53512
53952
  }
@@ -53522,7 +53962,8 @@ class PushSupervisor {
53522
53962
  generation: 1,
53523
53963
  reconnectAttempts: 0,
53524
53964
  reconnectTimer: null,
53525
- connection: null,
53965
+ runtime: null,
53966
+ detachRuntime: null,
53526
53967
  lastSnapshot: null
53527
53968
  };
53528
53969
  this.entries.set(deviceId, entry);
@@ -53549,8 +53990,13 @@ class PushSupervisor {
53549
53990
  }
53550
53991
  teardownEntry(entry) {
53551
53992
  this.clearReconnectTimer(entry);
53552
- entry.connection?.disconnect();
53553
- 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
+ }
53554
54000
  }
53555
54001
  clearReconnectTimer(entry) {
53556
54002
  if (!entry.reconnectTimer) {
@@ -53573,42 +54019,46 @@ class PushSupervisor {
53573
54019
  return;
53574
54020
  }
53575
54021
  const generation = entry.generation;
53576
- let connection;
53577
- connection = this.deps.createConnection({
53578
- deviceId: entry.deviceId,
54022
+ const runtime = await this.deps.acquireRuntime(entry.deviceId);
54023
+ const detachRuntime = runtime.subscribe({
53579
54024
  onEvent: (event) => {
53580
- this.handleTmuxEvent(entry.deviceId, generation, connection, event);
54025
+ this.handleTmuxEvent(entry.deviceId, generation, runtime, event);
53581
54026
  },
53582
- onTerminalOutput: () => {},
53583
- onTerminalHistory: () => {},
53584
54027
  onSnapshot: (payload) => {
53585
- this.handleSnapshot(entry.deviceId, generation, connection, payload);
54028
+ this.handleSnapshot(entry.deviceId, generation, runtime, payload);
53586
54029
  },
53587
- onError: (err) => {
53588
- console.error(`[push] tmux error on device ${entry.deviceId}:`, err);
54030
+ onError: (error) => {
54031
+ console.error(`[push] tmux error on device ${entry.deviceId}:`, error);
53589
54032
  },
53590
54033
  onClose: () => {
53591
- this.handleClose(entry.deviceId, generation, connection);
54034
+ this.handleClose(entry.deviceId, generation, runtime);
53592
54035
  }
53593
54036
  });
53594
- entry.connection = connection;
54037
+ entry.runtime = runtime;
54038
+ entry.detachRuntime = detachRuntime;
53595
54039
  try {
53596
- await connection.connect();
54040
+ await runtime.connect();
53597
54041
  const latest = this.entries.get(entry.deviceId);
53598
54042
  if (!this.running || latest !== entry || entry.generation !== generation) {
53599
- connection.disconnect();
54043
+ detachRuntime();
54044
+ entry.detachRuntime = null;
54045
+ entry.runtime = null;
54046
+ await this.deps.releaseRuntime(entry.deviceId, runtime);
53600
54047
  return;
53601
54048
  }
53602
54049
  entry.reconnectAttempts = 0;
53603
54050
  entry.lastSnapshot = null;
53604
- connection.requestSnapshot();
54051
+ runtime.requestSnapshot();
53605
54052
  } catch (err) {
53606
54053
  const latest = this.entries.get(entry.deviceId);
53607
54054
  if (!this.running || latest !== entry || entry.generation !== generation) {
53608
54055
  return;
53609
54056
  }
53610
54057
  console.error(`[push] failed connecting device ${entry.deviceId}:`, err);
53611
- entry.connection = null;
54058
+ detachRuntime();
54059
+ entry.detachRuntime = null;
54060
+ entry.runtime = null;
54061
+ await this.deps.releaseRuntime(entry.deviceId, runtime);
53612
54062
  this.scheduleReconnect(entry);
53613
54063
  }
53614
54064
  }
@@ -53634,31 +54084,35 @@ class PushSupervisor {
53634
54084
  entry.reconnectAttempts += 1;
53635
54085
  }
53636
54086
  this.clearReconnectTimer(entry);
53637
- entry.connection = null;
54087
+ entry.runtime = null;
54088
+ entry.detachRuntime = null;
53638
54089
  entry.generation += 1;
53639
54090
  entry.reconnectTimer = setTimeout(() => {
53640
54091
  entry.reconnectTimer = null;
53641
54092
  this.connectEntry(entry);
53642
54093
  }, delayMs);
53643
54094
  }
53644
- async handleClose(deviceId, generation, connection) {
54095
+ async handleClose(deviceId, generation, runtime) {
53645
54096
  const entry = this.entries.get(deviceId);
53646
- if (!entry || entry.generation !== generation || entry.connection !== connection) {
54097
+ if (!entry || entry.generation !== generation || entry.runtime !== runtime) {
53647
54098
  return;
53648
54099
  }
53649
- entry.connection = null;
54100
+ entry.detachRuntime?.();
54101
+ entry.detachRuntime = null;
54102
+ entry.runtime = null;
54103
+ await this.deps.releaseRuntime(deviceId, runtime);
53650
54104
  this.scheduleReconnect(entry);
53651
54105
  }
53652
- handleSnapshot(deviceId, generation, connection, payload) {
54106
+ handleSnapshot(deviceId, generation, runtime, payload) {
53653
54107
  const entry = this.entries.get(deviceId);
53654
- if (!entry || entry.generation !== generation || entry.connection !== connection) {
54108
+ if (!entry || entry.generation !== generation || entry.runtime !== runtime) {
53655
54109
  return;
53656
54110
  }
53657
54111
  entry.lastSnapshot = payload;
53658
54112
  }
53659
- async handleTmuxEvent(deviceId, generation, connection, event) {
54113
+ async handleTmuxEvent(deviceId, generation, runtime, event) {
53660
54114
  const entry = this.entries.get(deviceId);
53661
- if (!entry || entry.generation !== generation || entry.connection !== connection) {
54115
+ if (!entry || entry.generation !== generation || entry.runtime !== runtime) {
53662
54116
  return;
53663
54117
  }
53664
54118
  if (event.type !== "bell") {
@@ -54134,7 +54588,7 @@ function json(data, status = 200, headers = {}) {
54134
54588
  import { existsSync as existsSync2 } from "fs";
54135
54589
  import { resolve } from "path";
54136
54590
 
54137
- // ../../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
54138
54592
  import crypto2 from "crypto";
54139
54593
  import fs3 from "fs";
54140
54594
  function readMigrationFiles(config2) {
@@ -54166,7 +54620,7 @@ function readMigrationFiles(config2) {
54166
54620
  return migrationQueries;
54167
54621
  }
54168
54622
 
54169
- // ../../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
54170
54624
  function migrate(db2, config2) {
54171
54625
  const migrations = readMigrationFiles(config2);
54172
54626
  db2.dialect.migrate(migrations, db2.session, config2);
@@ -54188,82 +54642,6 @@ function runMigrations() {
54188
54642
  }
54189
54643
  if (false) {}
54190
54644
 
54191
- // ../../apps/gateway/src/ws/error-classify.ts
54192
- function classifySshError(error) {
54193
- const msg = error.message.toLowerCase();
54194
- if (msg.includes("ssh_config_ref_not_supported")) {
54195
- return {
54196
- type: "ssh_config_ref_not_supported",
54197
- messageKey: "sshError.configRefNotSupported"
54198
- };
54199
- }
54200
- if (msg.includes("ssh_auth_sock") || msg.includes("auth_sock")) {
54201
- return {
54202
- type: "agent_unavailable",
54203
- messageKey: "sshError.agentUnavailable"
54204
- };
54205
- }
54206
- if (msg.includes("agent") && (msg.includes("no identities") || msg.includes("failure"))) {
54207
- return {
54208
- type: "agent_no_identity",
54209
- messageKey: "sshError.agentNoIdentities"
54210
- };
54211
- }
54212
- if (msg.includes("permission denied")) {
54213
- return {
54214
- type: "auth_failed",
54215
- messageKey: "sshError.authFailed"
54216
- };
54217
- }
54218
- if (msg.includes("all configured authentication methods failed")) {
54219
- return {
54220
- type: "auth_failed",
54221
- messageKey: "sshError.authFailedGeneric"
54222
- };
54223
- }
54224
- if (msg.includes("enetunreach") || msg.includes("ehostunreach")) {
54225
- return {
54226
- type: "network_unreachable",
54227
- messageKey: "sshError.networkUnreachable"
54228
- };
54229
- }
54230
- if (msg.includes("connect refused") || msg.includes("connection refused") || msg.includes("econnrefused")) {
54231
- return {
54232
- type: "connection_refused",
54233
- messageKey: "sshError.connectionRefused"
54234
- };
54235
- }
54236
- if (msg.includes("timeout") || msg.includes("etimedout")) {
54237
- return {
54238
- type: "timeout",
54239
- messageKey: "sshError.connectionTimeout"
54240
- };
54241
- }
54242
- if (msg.includes("host not found") || msg.includes("getaddrinfo") || msg.includes("enotfound")) {
54243
- return {
54244
- type: "host_not_found",
54245
- messageKey: "sshError.hostNotFound"
54246
- };
54247
- }
54248
- if (msg.includes("handshake failed") || msg.includes("unable to verify")) {
54249
- return {
54250
- type: "handshake_failed",
54251
- messageKey: "sshError.handshakeFailed"
54252
- };
54253
- }
54254
- if (msg.includes("tmux: command not found") || msg.includes("tmux control mode not ready") || msg.includes("tmux exited") || msg.includes("tmux_exec_failed")) {
54255
- return {
54256
- type: "tmux_unavailable",
54257
- messageKey: "sshError.tmuxUnavailable"
54258
- };
54259
- }
54260
- return {
54261
- type: "unknown",
54262
- messageKey: "sshError.unknown",
54263
- messageParams: { message: error.message }
54264
- };
54265
- }
54266
-
54267
54645
  // ../../apps/gateway/src/ws/borsh/codec-borsh.ts
54268
54646
  function createBorshClientState() {
54269
54647
  return {
@@ -54641,6 +55019,10 @@ class SwitchBarrier {
54641
55019
  const pending = this.getPending(ws, deviceId);
54642
55020
  if (!pending)
54643
55021
  return;
55022
+ const selectState = sessionStateStore.getOrCreateSelectTransaction(ws, deviceId)?.state;
55023
+ if (selectState !== "SELECTING") {
55024
+ return;
55025
+ }
54644
55026
  const { context } = pending;
54645
55027
  const borshState = ws.data?.borshState;
54646
55028
  if (!borshState)
@@ -54648,7 +55030,9 @@ class SwitchBarrier {
54648
55030
  const ackTimer = pending.timers.shift();
54649
55031
  if (ackTimer)
54650
55032
  clearTimeout(ackTimer);
54651
- sessionStateStore.transitionSelectState(ws, deviceId, "ACKED");
55033
+ if (!sessionStateStore.transitionSelectState(ws, deviceId, "ACKED")) {
55034
+ return;
55035
+ }
54652
55036
  const seq = borshState.seqGen();
54653
55037
  const ackData = encodeSwitchAck({
54654
55038
  deviceId,
@@ -54673,6 +55057,10 @@ class SwitchBarrier {
54673
55057
  const pending = this.getPending(ws, deviceId);
54674
55058
  if (!pending)
54675
55059
  return;
55060
+ const selectState = sessionStateStore.getOrCreateSelectTransaction(ws, deviceId)?.state;
55061
+ if (selectState !== "ACKED") {
55062
+ return;
55063
+ }
54676
55064
  const { context } = pending;
54677
55065
  if (context.paneId !== paneId) {
54678
55066
  return;
@@ -54683,7 +55071,9 @@ class SwitchBarrier {
54683
55071
  const historyTimer = pending.timers.shift();
54684
55072
  if (historyTimer)
54685
55073
  clearTimeout(historyTimer);
54686
- sessionStateStore.transitionSelectState(ws, deviceId, "HISTORY_APPLIED");
55074
+ if (!sessionStateStore.transitionSelectState(ws, deviceId, "HISTORY_APPLIED")) {
55075
+ return;
55076
+ }
54687
55077
  const historyMessages = encodeTermHistory({
54688
55078
  deviceId,
54689
55079
  paneId: context.paneId,
@@ -54702,6 +55092,10 @@ class SwitchBarrier {
54702
55092
  const pending = this.getPending(ws, deviceId);
54703
55093
  if (!pending)
54704
55094
  return;
55095
+ const selectState = sessionStateStore.getOrCreateSelectTransaction(ws, deviceId)?.state;
55096
+ if (selectState !== "ACKED" && selectState !== "HISTORY_APPLIED") {
55097
+ return;
55098
+ }
54705
55099
  const { context } = pending;
54706
55100
  if (expectedToken && !this.tokensEqual(context.selectToken, expectedToken)) {
54707
55101
  return;
@@ -54713,7 +55107,9 @@ class SwitchBarrier {
54713
55107
  clearTimeout(timer);
54714
55108
  }
54715
55109
  pending.timers = [];
54716
- sessionStateStore.transitionSelectState(ws, deviceId, "LIVE");
55110
+ if (!sessionStateStore.transitionSelectState(ws, deviceId, "LIVE")) {
55111
+ return;
55112
+ }
54717
55113
  const bufferedOutput = sessionStateStore.stopOutputBuffering(ws, deviceId);
54718
55114
  const seq = borshState.seqGen();
54719
55115
  const liveResumeData = encodeLiveResume({
@@ -54785,7 +55181,9 @@ class SwitchBarrier {
54785
55181
  this.cleanupTransaction(ws, deviceId);
54786
55182
  }
54787
55183
  completeTransaction(ws, deviceId) {
54788
- sessionStateStore.transitionSelectState(ws, deviceId, "STABLE");
55184
+ if (!sessionStateStore.transitionSelectState(ws, deviceId, "STABLE")) {
55185
+ return;
55186
+ }
54789
55187
  this.cleanupTransaction(ws, deviceId);
54790
55188
  }
54791
55189
  cleanupTransaction(ws, deviceId) {
@@ -54802,10 +55200,100 @@ class SwitchBarrier {
54802
55200
  }
54803
55201
  var switchBarrier = new SwitchBarrier;
54804
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
+
54805
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
+
54806
55287
  class WebSocketServer {
54807
55288
  connections = new Map;
54808
55289
  pendingConnectionEntries = new Map;
55290
+ deps;
55291
+ constructor(options = {}) {
55292
+ this.deps = {
55293
+ ...defaultDeps2,
55294
+ ...options.deps ?? {}
55295
+ };
55296
+ }
54809
55297
  clearSnapshotTimer(entry) {
54810
55298
  if (!entry.snapshotTimer)
54811
55299
  return;
@@ -54824,6 +55312,37 @@ class WebSocketServer {
54824
55312
  clearTimeout(entry.reconnectTimer);
54825
55313
  entry.reconnectTimer = null;
54826
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
+ }
54827
55346
  refreshSnapshotPolling(deviceId) {
54828
55347
  const entry = this.connections.get(deviceId);
54829
55348
  if (!entry)
@@ -54841,7 +55360,7 @@ class WebSocketServer {
54841
55360
  return;
54842
55361
  }
54843
55362
  try {
54844
- entry.connection.requestSnapshot();
55363
+ entry.runtime.requestSnapshot();
54845
55364
  } catch (err) {
54846
55365
  console.error("[ws] polling snapshot failed:", err);
54847
55366
  }
@@ -54859,7 +55378,7 @@ class WebSocketServer {
54859
55378
  }
54860
55379
  entry.snapshotTimer = null;
54861
55380
  try {
54862
- entry.connection.requestSnapshot();
55381
+ entry.runtime.requestSnapshot();
54863
55382
  } catch (err) {
54864
55383
  console.error("[ws] failed to request snapshot:", err);
54865
55384
  }
@@ -54928,10 +55447,7 @@ class WebSocketServer {
54928
55447
  delete ws.data.borshState.selectedPanes[deviceId];
54929
55448
  if (entry.clients.size === 0) {
54930
55449
  console.log(`[ws] no more clients for device ${deviceId}, disconnecting`);
54931
- this.clearSnapshotTimer(entry);
54932
- this.clearSnapshotPollTimer(entry);
54933
- this.clearReconnectTimer(entry);
54934
- entry.connection.disconnect();
55450
+ this.releaseConnectionEntry(deviceId, entry);
54935
55451
  toDelete.push(deviceId);
54936
55452
  } else {
54937
55453
  this.refreshSnapshotPolling(deviceId);
@@ -54943,10 +55459,7 @@ class WebSocketServer {
54943
55459
  }
54944
55460
  closeAll() {
54945
55461
  for (const [deviceId, entry] of this.connections) {
54946
- this.clearSnapshotTimer(entry);
54947
- this.clearSnapshotPollTimer(entry);
54948
- this.clearReconnectTimer(entry);
54949
- entry.connection.disconnect();
55462
+ this.releaseConnectionEntry(deviceId, entry);
54950
55463
  this.connections.delete(deviceId);
54951
55464
  }
54952
55465
  this.pendingConnectionEntries.clear();
@@ -55109,8 +55622,7 @@ class WebSocketServer {
55109
55622
  if (pending) {
55110
55623
  return pending;
55111
55624
  }
55112
- let creationPromise;
55113
- creationPromise = this.createDeviceConnectionEntry(deviceId, ws).then((createdEntry) => {
55625
+ const creationPromise = this.createDeviceConnectionEntry(deviceId, ws).then((createdEntry) => {
55114
55626
  if (createdEntry) {
55115
55627
  this.connections.set(deviceId, createdEntry);
55116
55628
  }
@@ -55129,13 +55641,15 @@ class WebSocketServer {
55129
55641
  return;
55130
55642
  entry.clients.add(ws);
55131
55643
  ws.data.borshState.selectedPanes[deviceId] ??= null;
55132
- 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
+ });
55133
55647
  this.sendEnvelope(ws, exports_ws_borsh.KIND_DEVICE_CONNECTED, connectedPayload);
55134
55648
  if (entry.lastSnapshot) {
55135
55649
  const snapshotBytes = exports_ws_borsh.encodeStateSnapshot(entry.lastSnapshot);
55136
55650
  this.sendChunked(ws, exports_ws_borsh.KIND_STATE_SNAPSHOT, snapshotBytes);
55137
55651
  } else {
55138
- entry.connection.requestSnapshot();
55652
+ entry.runtime.requestSnapshot();
55139
55653
  }
55140
55654
  }
55141
55655
  handleDeviceDisconnect(ws, deviceId) {
@@ -55144,15 +55658,14 @@ class WebSocketServer {
55144
55658
  entry.clients.delete(ws);
55145
55659
  this.refreshSnapshotPolling(deviceId);
55146
55660
  if (entry.clients.size === 0) {
55147
- this.clearSnapshotTimer(entry);
55148
- this.clearSnapshotPollTimer(entry);
55149
- this.clearReconnectTimer(entry);
55150
- entry.connection.disconnect();
55661
+ this.releaseConnectionEntry(deviceId, entry);
55151
55662
  this.connections.delete(deviceId);
55152
55663
  }
55153
55664
  }
55154
55665
  delete ws.data.borshState.selectedPanes[deviceId];
55155
- 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
+ });
55156
55669
  this.sendEnvelope(ws, exports_ws_borsh.KIND_DEVICE_DISCONNECTED, disconnectedPayload);
55157
55670
  }
55158
55671
  handleTmuxSelect(ws, data) {
@@ -55182,30 +55695,31 @@ class WebSocketServer {
55182
55695
  return;
55183
55696
  }
55184
55697
  switchBarrier.sendSwitchAck(ws, deviceId);
55185
- entry.connection.selectPane(windowId, paneId);
55186
55698
  const cols = data.cols ?? null;
55187
55699
  const rows = data.rows ?? null;
55188
55700
  if (cols !== null && rows !== null) {
55189
- entry.connection.resizePane(paneId, cols, rows);
55701
+ entry.runtime.selectPaneWithSize(windowId, paneId, cols, rows);
55702
+ } else {
55703
+ entry.runtime.selectPane(windowId, paneId);
55190
55704
  }
55191
55705
  }
55192
55706
  handleTmuxSelectWindow(deviceId, windowId) {
55193
55707
  const entry = this.connections.get(deviceId);
55194
55708
  if (!entry)
55195
55709
  return;
55196
- entry.connection.selectWindow(windowId);
55710
+ entry.runtime.selectWindow(windowId);
55197
55711
  }
55198
55712
  handleTermInput(deviceId, paneId, data) {
55199
55713
  const entry = this.connections.get(deviceId);
55200
55714
  if (!entry)
55201
55715
  return;
55202
- entry.connection.sendInput(paneId, data);
55716
+ entry.runtime.sendInput(paneId, data);
55203
55717
  }
55204
55718
  handleTermResize(deviceId, paneId, cols, rows) {
55205
55719
  const entry = this.connections.get(deviceId);
55206
55720
  if (!entry)
55207
55721
  return;
55208
- entry.connection.resizePane(paneId, cols, rows);
55722
+ entry.runtime.resizePane(paneId, cols, rows);
55209
55723
  }
55210
55724
  handleTermPaste(deviceId, paneId, data) {
55211
55725
  const entry = this.connections.get(deviceId);
@@ -55214,51 +55728,43 @@ class WebSocketServer {
55214
55728
  const chunkSize = 1024;
55215
55729
  for (let i = 0;i < data.length; i += chunkSize) {
55216
55730
  const chunk2 = data.slice(i, i + chunkSize);
55217
- entry.connection.sendInput(paneId, chunk2);
55731
+ entry.runtime.sendInput(paneId, chunk2);
55218
55732
  }
55219
55733
  }
55220
55734
  handleCreateWindow(deviceId, name) {
55221
55735
  const entry = this.connections.get(deviceId);
55222
55736
  if (!entry)
55223
55737
  return;
55224
- entry.connection.createWindow(name);
55738
+ entry.runtime.createWindow(name);
55225
55739
  }
55226
55740
  handleCloseWindow(deviceId, windowId) {
55227
55741
  const entry = this.connections.get(deviceId);
55228
55742
  if (!entry)
55229
55743
  return;
55230
- entry.connection.closeWindow(windowId);
55744
+ entry.runtime.closeWindow(windowId);
55231
55745
  }
55232
55746
  handleClosePane(deviceId, paneId) {
55233
55747
  const entry = this.connections.get(deviceId);
55234
55748
  if (!entry)
55235
55749
  return;
55236
- entry.connection.closePane(paneId);
55750
+ entry.runtime.closePane(paneId);
55237
55751
  }
55238
55752
  handleRenameWindow(deviceId, windowId, name) {
55239
55753
  const entry = this.connections.get(deviceId);
55240
55754
  if (!entry)
55241
55755
  return;
55242
- entry.connection.renameWindow(windowId, name);
55756
+ entry.runtime.renameWindow(windowId, name);
55243
55757
  }
55244
55758
  async createDeviceConnectionEntry(deviceId, ws) {
55245
- const connection = new TmuxConnection({
55246
- deviceId,
55247
- onEvent: (event) => {
55248
- this.broadcastTmuxEvent(deviceId, event);
55249
- },
55250
- onTerminalOutput: (paneId, data) => this.broadcastTerminalOutput(deviceId, paneId, data),
55251
- onTerminalHistory: (paneId, data) => this.broadcastTerminalHistory(deviceId, paneId, data),
55252
- onSnapshot: (payload) => this.broadcastStateSnapshot(deviceId, payload),
55253
- onError: (err) => this.broadcastError(deviceId, err),
55254
- onClose: () => {
55255
- this.handleConnectionClose(deviceId);
55256
- }
55257
- });
55759
+ let runtime = null;
55760
+ let detachRuntime = null;
55258
55761
  try {
55259
- await connection.connect();
55762
+ runtime = await this.deps.acquireRuntime(deviceId);
55763
+ detachRuntime = this.attachRuntime(deviceId, runtime);
55764
+ await runtime.connect();
55260
55765
  return {
55261
- connection,
55766
+ runtime,
55767
+ detachRuntime,
55262
55768
  clients: new Set,
55263
55769
  lastSnapshot: null,
55264
55770
  snapshotTimer: null,
@@ -55267,6 +55773,10 @@ class WebSocketServer {
55267
55773
  reconnectTimer: null
55268
55774
  };
55269
55775
  } catch (err) {
55776
+ detachRuntime?.();
55777
+ if (runtime) {
55778
+ this.deps.releaseRuntime(deviceId, runtime);
55779
+ }
55270
55780
  const errorInfo = classifySshError(err instanceof Error ? err : new Error(String(err)));
55271
55781
  ws.send(exports_ws_borsh.encodeEnvelope(exports_ws_borsh.KIND_DEVICE_EVENT, exports_ws_borsh.encodeDeviceEventPayload({
55272
55782
  deviceId,
@@ -55394,6 +55904,10 @@ class WebSocketServer {
55394
55904
  }
55395
55905
  this.clearSnapshotTimer(entry);
55396
55906
  this.clearSnapshotPollTimer(entry);
55907
+ entry.detachRuntime?.();
55908
+ entry.detachRuntime = null;
55909
+ const closedRuntime = entry.runtime;
55910
+ this.deps.releaseRuntime(deviceId, closedRuntime);
55397
55911
  const { sshReconnectMaxRetries, sshReconnectDelaySeconds } = getSiteSettings();
55398
55912
  if (entry.clients.size > 0 && entry.reconnectAttempts < sshReconnectMaxRetries) {
55399
55913
  entry.reconnectAttempts += 1;
@@ -55440,7 +55954,7 @@ class WebSocketServer {
55440
55954
  message: t2("sshError.reconnected")
55441
55955
  };
55442
55956
  this.broadcastDeviceEvent(retryConnection, reconnected);
55443
- retryConnection.connection.requestSnapshot();
55957
+ retryConnection.runtime.requestSnapshot();
55444
55958
  }, delay);
55445
55959
  return;
55446
55960
  }
@@ -55513,6 +56027,7 @@ async function createGatewayRuntime(options = {}) {
55513
56027
  async stop() {
55514
56028
  wsServer.closeAll();
55515
56029
  await pushSupervisor.stopAll();
56030
+ await tmuxRuntimeRegistry.shutdownAll();
55516
56031
  await telegramService.stopAll();
55517
56032
  }
55518
56033
  };
@@ -55779,7 +56294,7 @@ async function serveFrontend(req, staticRoot) {
55779
56294
  if (!requestedPath) {
55780
56295
  return new Response(t3("runtime.forbidden"), { status: 403 });
55781
56296
  }
55782
- const indexPath = join3(staticRoot, "index.html");
56297
+ const indexPath = join4(staticRoot, "index.html");
55783
56298
  const targetPath = existsSync3(requestedPath) ? requestedPath : indexPath;
55784
56299
  if (!existsSync3(targetPath)) {
55785
56300
  return new Response(t3("runtime.frontendMissing"), { status: 500 });