tmex-cli 0.2.6 → 0.3.1

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 +1828 -1305
  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-BTbDSWYN.js +26 -0
  6. package/resources/fe-dist/assets/DevicePage-BTbDSWYN.js.map +1 -0
  7. package/resources/fe-dist/assets/DevicesPage-C76Xejy5.js +17 -0
  8. package/resources/fe-dist/assets/{DevicesPage-BrvmKNIQ.js.map → DevicesPage-C76Xejy5.js.map} +1 -1
  9. package/resources/fe-dist/assets/SettingsPage-DQ9W4fOo.js +17 -0
  10. package/resources/fe-dist/assets/{SettingsPage-XFRzVSzI.js.map → SettingsPage-DQ9W4fOo.js.map} +1 -1
  11. package/resources/fe-dist/assets/ghostty-vt-BKQMf-5X.wasm +0 -0
  12. package/resources/fe-dist/assets/index-Bmahx5fj.js +448 -0
  13. package/resources/fe-dist/assets/index-Bmahx5fj.js.map +1 -0
  14. package/resources/fe-dist/assets/index-CyKyNcdz.css +1 -0
  15. package/resources/fe-dist/assets/select-Wn7lKWHQ.js +17 -0
  16. package/resources/fe-dist/assets/{select-CRYPgkuV.js.map → select-Wn7lKWHQ.js.map} +1 -1
  17. package/resources/fe-dist/assets/switch-JVIhfemP.js +12 -0
  18. package/resources/fe-dist/assets/{switch-D216fzW5.js.map → switch-JVIhfemP.js.map} +1 -1
  19. package/resources/fe-dist/assets/useValueChanged-DU---PIl.js +7 -0
  20. package/resources/fe-dist/assets/{useValueChanged-CJav2OS1.js.map → useValueChanged-DU---PIl.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",
@@ -21687,6 +21687,7 @@ var TermHistorySchema = import_zorsh.b.struct({
21687
21687
  paneId: import_zorsh.b.string(),
21688
21688
  selectToken: import_zorsh.b.bytes(16),
21689
21689
  encoding: import_zorsh.b.u8(),
21690
+ alternateScreen: import_zorsh.b.bool(),
21690
21691
  data: import_zorsh.b.bytes()
21691
21692
  });
21692
21693
  var SwitchAckSchema = import_zorsh.b.struct({
@@ -22367,7 +22368,7 @@ async function decryptWithContext(ciphertext, context) {
22367
22368
  }
22368
22369
  }
22369
22370
 
22370
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/entity.js
22371
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/entity.js
22371
22372
  var entityKind = Symbol.for("drizzle:entityKind");
22372
22373
  var hasOwnEntityKind = Symbol.for("drizzle:hasOwnEntityKind");
22373
22374
  function is(value, type) {
@@ -22392,7 +22393,7 @@ function is(value, type) {
22392
22393
  return false;
22393
22394
  }
22394
22395
 
22395
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/column.js
22396
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/column.js
22396
22397
  class Column {
22397
22398
  constructor(table, config2) {
22398
22399
  this.table = table;
@@ -22442,7 +22443,7 @@ class Column {
22442
22443
  }
22443
22444
  }
22444
22445
 
22445
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/column-builder.js
22446
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/column-builder.js
22446
22447
  class ColumnBuilder {
22447
22448
  static [entityKind] = "ColumnBuilder";
22448
22449
  config;
@@ -22498,20 +22499,20 @@ class ColumnBuilder {
22498
22499
  }
22499
22500
  }
22500
22501
 
22501
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/table.utils.js
22502
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/table.utils.js
22502
22503
  var TableName = Symbol.for("drizzle:Name");
22503
22504
 
22504
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/tracing-utils.js
22505
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/tracing-utils.js
22505
22506
  function iife(fn, ...args) {
22506
22507
  return fn(...args);
22507
22508
  }
22508
22509
 
22509
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/pg-core/unique-constraint.js
22510
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/pg-core/unique-constraint.js
22510
22511
  function uniqueKeyName(table, columns) {
22511
22512
  return `${table[TableName]}_${columns.join("_")}_unique`;
22512
22513
  }
22513
22514
 
22514
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/pg-core/columns/common.js
22515
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/pg-core/columns/common.js
22515
22516
  class PgColumn extends Column {
22516
22517
  constructor(table, config2) {
22517
22518
  if (!config2.uniqueName) {
@@ -22560,7 +22561,7 @@ class ExtraConfigColumn extends PgColumn {
22560
22561
  }
22561
22562
  }
22562
22563
 
22563
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/pg-core/columns/enum.js
22564
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/pg-core/columns/enum.js
22564
22565
  class PgEnumObjectColumn extends PgColumn {
22565
22566
  static [entityKind] = "PgEnumObjectColumn";
22566
22567
  enum;
@@ -22590,7 +22591,7 @@ class PgEnumColumn extends PgColumn {
22590
22591
  }
22591
22592
  }
22592
22593
 
22593
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/subquery.js
22594
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/subquery.js
22594
22595
  class Subquery {
22595
22596
  static [entityKind] = "Subquery";
22596
22597
  constructor(sql, fields, alias, isWith = false, usedTables = []) {
@@ -22609,10 +22610,10 @@ class WithSubquery extends Subquery {
22609
22610
  static [entityKind] = "WithSubquery";
22610
22611
  }
22611
22612
 
22612
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/version.js
22613
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/version.js
22613
22614
  var version = "0.45.1";
22614
22615
 
22615
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/tracing.js
22616
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/tracing.js
22616
22617
  var otel;
22617
22618
  var rawTracer;
22618
22619
  var tracer = {
@@ -22639,10 +22640,10 @@ var tracer = {
22639
22640
  }
22640
22641
  };
22641
22642
 
22642
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/view-common.js
22643
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/view-common.js
22643
22644
  var ViewBaseConfig = Symbol.for("drizzle:ViewBaseConfig");
22644
22645
 
22645
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/table.js
22646
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/table.js
22646
22647
  var Schema = Symbol.for("drizzle:Schema");
22647
22648
  var Columns = Symbol.for("drizzle:Columns");
22648
22649
  var ExtraConfigColumns = Symbol.for("drizzle:ExtraConfigColumns");
@@ -22686,7 +22687,7 @@ function getTableUniqueName(table) {
22686
22687
  return `${table[Schema] ?? "public"}.${table[TableName]}`;
22687
22688
  }
22688
22689
 
22689
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sql/sql.js
22690
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sql/sql.js
22690
22691
  function isSQLWrapper(value) {
22691
22692
  return value !== null && value !== undefined && typeof value.getSQL === "function";
22692
22693
  }
@@ -23066,7 +23067,7 @@ Subquery.prototype.getSQL = function() {
23066
23067
  return new SQL([this]);
23067
23068
  };
23068
23069
 
23069
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/alias.js
23070
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/alias.js
23070
23071
  class ColumnAliasProxyHandler {
23071
23072
  constructor(table) {
23072
23073
  this.table = table;
@@ -23145,7 +23146,7 @@ function mapColumnsInSQLToAlias(query, alias) {
23145
23146
  }));
23146
23147
  }
23147
23148
 
23148
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/errors.js
23149
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/errors.js
23149
23150
  class DrizzleError extends Error {
23150
23151
  static [entityKind] = "DrizzleError";
23151
23152
  constructor({ message, cause }) {
@@ -23175,7 +23176,7 @@ class TransactionRollbackError extends DrizzleError {
23175
23176
  }
23176
23177
  }
23177
23178
 
23178
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/logger.js
23179
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/logger.js
23179
23180
  class ConsoleLogWriter {
23180
23181
  static [entityKind] = "ConsoleLogWriter";
23181
23182
  write(message) {
@@ -23207,7 +23208,7 @@ class NoopLogger {
23207
23208
  logQuery() {}
23208
23209
  }
23209
23210
 
23210
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/query-promise.js
23211
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/query-promise.js
23211
23212
  class QueryPromise {
23212
23213
  static [entityKind] = "QueryPromise";
23213
23214
  [Symbol.toStringTag] = "QueryPromise";
@@ -23228,7 +23229,7 @@ class QueryPromise {
23228
23229
  }
23229
23230
  }
23230
23231
 
23231
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/utils.js
23232
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/utils.js
23232
23233
  function mapResultRow(columns, row, joinsNotNullableMap) {
23233
23234
  const nullifyMap = {};
23234
23235
  const result = columns.reduce((result2, { path, field }, columnIndex) => {
@@ -23382,7 +23383,7 @@ function isConfig(data) {
23382
23383
  }
23383
23384
  var textDecoder = typeof TextDecoder === "undefined" ? null : new TextDecoder;
23384
23385
 
23385
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/pg-core/table.js
23386
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/pg-core/table.js
23386
23387
  var InlineForeignKeys = Symbol.for("drizzle:PgInlineForeignKeys");
23387
23388
  var EnableRLS = Symbol.for("drizzle:EnableRLS");
23388
23389
 
@@ -23398,7 +23399,7 @@ class PgTable extends Table {
23398
23399
  [Table.Symbol.ExtraConfigColumns] = {};
23399
23400
  }
23400
23401
 
23401
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/pg-core/primary-keys.js
23402
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/pg-core/primary-keys.js
23402
23403
  class PrimaryKeyBuilder {
23403
23404
  static [entityKind] = "PgPrimaryKeyBuilder";
23404
23405
  columns;
@@ -23426,7 +23427,7 @@ class PrimaryKey {
23426
23427
  }
23427
23428
  }
23428
23429
 
23429
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sql/expressions/conditions.js
23430
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sql/expressions/conditions.js
23430
23431
  function bindIfParam(value, column) {
23431
23432
  if (isDriverValueEncoder(column) && !isSQLWrapper(value) && !is(value, Param) && !is(value, Placeholder) && !is(value, Column) && !is(value, Table) && !is(value, View)) {
23432
23433
  return new Param(value, column);
@@ -23531,7 +23532,7 @@ function notIlike(column, value) {
23531
23532
  return sql`${column} not ilike ${value}`;
23532
23533
  }
23533
23534
 
23534
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sql/expressions/select.js
23535
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sql/expressions/select.js
23535
23536
  function asc(column) {
23536
23537
  return sql`${column} asc`;
23537
23538
  }
@@ -23539,7 +23540,7 @@ function desc(column) {
23539
23540
  return sql`${column} desc`;
23540
23541
  }
23541
23542
 
23542
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/relations.js
23543
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/relations.js
23543
23544
  class Relation {
23544
23545
  constructor(sourceTable, referencedTable, relationName) {
23545
23546
  this.sourceTable = sourceTable;
@@ -23759,7 +23760,7 @@ function mapRelationalRow(tablesConfig, tableConfig, row, buildQueryResultSelect
23759
23760
  return result;
23760
23761
  }
23761
23762
 
23762
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sql/functions/aggregate.js
23763
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sql/functions/aggregate.js
23763
23764
  function count(expression) {
23764
23765
  return sql`count(${expression || sql.raw("*")})`.mapWith(Number);
23765
23766
  }
@@ -26183,10 +26184,10 @@ var t2 = instance.t.bind(instance);
26183
26184
  // ../../apps/gateway/src/db/client.ts
26184
26185
  import { Database as Database2 } from "bun:sqlite";
26185
26186
 
26186
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/bun-sqlite/driver.js
26187
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/bun-sqlite/driver.js
26187
26188
  import { Database } from "bun:sqlite";
26188
26189
 
26189
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/selection-proxy.js
26190
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/selection-proxy.js
26190
26191
  class SelectionProxyHandler {
26191
26192
  static [entityKind] = "SelectionProxyHandler";
26192
26193
  config;
@@ -26238,7 +26239,7 @@ class SelectionProxyHandler {
26238
26239
  }
26239
26240
  }
26240
26241
 
26241
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/foreign-keys.js
26242
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/foreign-keys.js
26242
26243
  class ForeignKeyBuilder {
26243
26244
  static [entityKind] = "SQLiteForeignKeyBuilder";
26244
26245
  reference;
@@ -26292,7 +26293,7 @@ class ForeignKey {
26292
26293
  }
26293
26294
  }
26294
26295
 
26295
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/unique-constraint.js
26296
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/unique-constraint.js
26296
26297
  function uniqueKeyName2(table, columns) {
26297
26298
  return `${table[TableName]}_${columns.join("_")}_unique`;
26298
26299
  }
@@ -26337,7 +26338,7 @@ class UniqueConstraint {
26337
26338
  }
26338
26339
  }
26339
26340
 
26340
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/columns/common.js
26341
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/columns/common.js
26341
26342
  class SQLiteColumnBuilder extends ColumnBuilder {
26342
26343
  static [entityKind] = "SQLiteColumnBuilder";
26343
26344
  foreignKeyConfigs = [];
@@ -26388,7 +26389,7 @@ class SQLiteColumn extends Column {
26388
26389
  static [entityKind] = "SQLiteColumn";
26389
26390
  }
26390
26391
 
26391
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/columns/blob.js
26392
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/columns/blob.js
26392
26393
  class SQLiteBigIntBuilder extends SQLiteColumnBuilder {
26393
26394
  static [entityKind] = "SQLiteBigIntBuilder";
26394
26395
  constructor(name) {
@@ -26476,7 +26477,7 @@ function blob(a, b3) {
26476
26477
  return new SQLiteBlobBufferBuilder(name);
26477
26478
  }
26478
26479
 
26479
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/columns/custom.js
26480
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/columns/custom.js
26480
26481
  class SQLiteCustomColumnBuilder extends SQLiteColumnBuilder {
26481
26482
  static [entityKind] = "SQLiteCustomColumnBuilder";
26482
26483
  constructor(name, fieldConfig, customTypeParams) {
@@ -26517,7 +26518,7 @@ function customType(customTypeParams) {
26517
26518
  };
26518
26519
  }
26519
26520
 
26520
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/columns/integer.js
26521
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/columns/integer.js
26521
26522
  class SQLiteBaseIntegerBuilder extends SQLiteColumnBuilder {
26522
26523
  static [entityKind] = "SQLiteBaseIntegerBuilder";
26523
26524
  constructor(name, dataType, columnType) {
@@ -26619,7 +26620,7 @@ function integer(a, b3) {
26619
26620
  return new SQLiteIntegerBuilder(name);
26620
26621
  }
26621
26622
 
26622
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/columns/numeric.js
26623
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/columns/numeric.js
26623
26624
  class SQLiteNumericBuilder extends SQLiteColumnBuilder {
26624
26625
  static [entityKind] = "SQLiteNumericBuilder";
26625
26626
  constructor(name) {
@@ -26689,7 +26690,7 @@ function numeric(a, b3) {
26689
26690
  return mode === "number" ? new SQLiteNumericNumberBuilder(name) : mode === "bigint" ? new SQLiteNumericBigIntBuilder(name) : new SQLiteNumericBuilder(name);
26690
26691
  }
26691
26692
 
26692
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/columns/real.js
26693
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/columns/real.js
26693
26694
  class SQLiteRealBuilder extends SQLiteColumnBuilder {
26694
26695
  static [entityKind] = "SQLiteRealBuilder";
26695
26696
  constructor(name) {
@@ -26710,7 +26711,7 @@ function real(name) {
26710
26711
  return new SQLiteRealBuilder(name ?? "");
26711
26712
  }
26712
26713
 
26713
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/columns/text.js
26714
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/columns/text.js
26714
26715
  class SQLiteTextBuilder extends SQLiteColumnBuilder {
26715
26716
  static [entityKind] = "SQLiteTextBuilder";
26716
26717
  constructor(name, config2) {
@@ -26765,7 +26766,7 @@ function text(a, b3 = {}) {
26765
26766
  return new SQLiteTextBuilder(name, config2);
26766
26767
  }
26767
26768
 
26768
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/columns/all.js
26769
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/columns/all.js
26769
26770
  function getSQLiteColumnBuilders() {
26770
26771
  return {
26771
26772
  blob,
@@ -26777,7 +26778,7 @@ function getSQLiteColumnBuilders() {
26777
26778
  };
26778
26779
  }
26779
26780
 
26780
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/table.js
26781
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/table.js
26781
26782
  var InlineForeignKeys2 = Symbol.for("drizzle:SQLiteInlineForeignKeys");
26782
26783
 
26783
26784
  class SQLiteTable extends Table {
@@ -26811,7 +26812,7 @@ var sqliteTable = (name, columns, extraConfig) => {
26811
26812
  return sqliteTableBase(name, columns, extraConfig);
26812
26813
  };
26813
26814
 
26814
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/checks.js
26815
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/checks.js
26815
26816
  class CheckBuilder {
26816
26817
  constructor(name, value) {
26817
26818
  this.name = name;
@@ -26838,7 +26839,7 @@ function check(name, value) {
26838
26839
  return new CheckBuilder(name, value);
26839
26840
  }
26840
26841
 
26841
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/utils.js
26842
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/utils.js
26842
26843
  function extractUsedTable(table) {
26843
26844
  if (is(table, SQLiteTable)) {
26844
26845
  return [`${table[Table.Symbol.BaseName]}`];
@@ -26852,7 +26853,7 @@ function extractUsedTable(table) {
26852
26853
  return [];
26853
26854
  }
26854
26855
 
26855
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/query-builders/delete.js
26856
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/query-builders/delete.js
26856
26857
  class SQLiteDeleteBase extends QueryPromise {
26857
26858
  constructor(table, session, dialect, withList) {
26858
26859
  super();
@@ -26922,7 +26923,7 @@ class SQLiteDeleteBase extends QueryPromise {
26922
26923
  }
26923
26924
  }
26924
26925
 
26925
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/casing.js
26926
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/casing.js
26926
26927
  function toSnakeCase(input) {
26927
26928
  const words = input.replace(/['\u2019]/g, "").match(/[\da-z]+|[A-Z]+(?![a-z])|[A-Z][\da-z]+/g) ?? [];
26928
26929
  return words.map((word) => word.toLowerCase()).join("_");
@@ -26975,12 +26976,12 @@ class CasingCache {
26975
26976
  }
26976
26977
  }
26977
26978
 
26978
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/view-base.js
26979
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/view-base.js
26979
26980
  class SQLiteViewBase extends View {
26980
26981
  static [entityKind] = "SQLiteViewBase";
26981
26982
  }
26982
26983
 
26983
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/dialect.js
26984
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/dialect.js
26984
26985
  class SQLiteDialect {
26985
26986
  static [entityKind] = "SQLiteDialect";
26986
26987
  casing;
@@ -27530,7 +27531,7 @@ class SQLiteSyncDialect extends SQLiteDialect {
27530
27531
  }
27531
27532
  }
27532
27533
 
27533
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/query-builders/query-builder.js
27534
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/query-builders/query-builder.js
27534
27535
  class TypedQueryBuilder {
27535
27536
  static [entityKind] = "TypedQueryBuilder";
27536
27537
  getSelectedFields() {
@@ -27538,7 +27539,7 @@ class TypedQueryBuilder {
27538
27539
  }
27539
27540
  }
27540
27541
 
27541
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/query-builders/select.js
27542
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/query-builders/select.js
27542
27543
  class SQLiteSelectBuilder {
27543
27544
  static [entityKind] = "SQLiteSelectBuilder";
27544
27545
  fields;
@@ -27836,7 +27837,7 @@ var unionAll = createSetOperator("union", true);
27836
27837
  var intersect = createSetOperator("intersect", false);
27837
27838
  var except = createSetOperator("except", false);
27838
27839
 
27839
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/query-builders/query-builder.js
27840
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/query-builders/query-builder.js
27840
27841
  class QueryBuilder {
27841
27842
  static [entityKind] = "SQLiteQueryBuilder";
27842
27843
  dialect;
@@ -27895,7 +27896,7 @@ class QueryBuilder {
27895
27896
  }
27896
27897
  }
27897
27898
 
27898
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/query-builders/insert.js
27899
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/query-builders/insert.js
27899
27900
  class SQLiteInsertBuilder {
27900
27901
  constructor(table, session, dialect, withList) {
27901
27902
  this.table = table;
@@ -28004,7 +28005,7 @@ class SQLiteInsertBase extends QueryPromise {
28004
28005
  }
28005
28006
  }
28006
28007
 
28007
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/query-builders/update.js
28008
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/query-builders/update.js
28008
28009
  class SQLiteUpdateBuilder {
28009
28010
  constructor(table, session, dialect, withList) {
28010
28011
  this.table = table;
@@ -28108,7 +28109,7 @@ class SQLiteUpdateBase extends QueryPromise {
28108
28109
  }
28109
28110
  }
28110
28111
 
28111
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/query-builders/count.js
28112
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/query-builders/count.js
28112
28113
  class SQLiteCountBuilder extends SQL {
28113
28114
  constructor(params) {
28114
28115
  super(SQLiteCountBuilder.buildEmbeddedCount(params.source, params.filters).queryChunks);
@@ -28143,7 +28144,7 @@ class SQLiteCountBuilder extends SQL {
28143
28144
  }
28144
28145
  }
28145
28146
 
28146
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/query-builders/query.js
28147
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/query-builders/query.js
28147
28148
  class RelationalQueryBuilder {
28148
28149
  constructor(mode, fullSchema, schema, tableNamesMap, table, tableConfig, dialect, session) {
28149
28150
  this.mode = mode;
@@ -28237,7 +28238,7 @@ class SQLiteSyncRelationalQuery extends SQLiteRelationalQuery {
28237
28238
  }
28238
28239
  }
28239
28240
 
28240
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/query-builders/raw.js
28241
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/query-builders/raw.js
28241
28242
  class SQLiteRaw extends QueryPromise {
28242
28243
  constructor(execute, getSQL, action, dialect, mapBatchResult) {
28243
28244
  super();
@@ -28263,7 +28264,7 @@ class SQLiteRaw extends QueryPromise {
28263
28264
  }
28264
28265
  }
28265
28266
 
28266
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/db.js
28267
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/db.js
28267
28268
  class BaseSQLiteDatabase {
28268
28269
  constructor(resultKind, dialect, session, schema) {
28269
28270
  this.resultKind = resultKind;
@@ -28386,7 +28387,7 @@ class BaseSQLiteDatabase {
28386
28387
  }
28387
28388
  }
28388
28389
 
28389
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/cache/core/cache.js
28390
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/cache/core/cache.js
28390
28391
  class Cache {
28391
28392
  static [entityKind] = "Cache";
28392
28393
  }
@@ -28412,7 +28413,7 @@ async function hashQuery(sql2, params) {
28412
28413
  return hashHex;
28413
28414
  }
28414
28415
 
28415
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/sqlite-core/session.js
28416
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/sqlite-core/session.js
28416
28417
  class ExecuteResultSync extends QueryPromise {
28417
28418
  constructor(resultCb) {
28418
28419
  super();
@@ -28585,7 +28586,7 @@ class SQLiteTransaction extends BaseSQLiteDatabase {
28585
28586
  }
28586
28587
  }
28587
28588
 
28588
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/bun-sqlite/session.js
28589
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/bun-sqlite/session.js
28589
28590
  class SQLiteBunSession extends SQLiteSession {
28590
28591
  constructor(client, dialect, schema, options = {}) {
28591
28592
  super(dialect);
@@ -28684,7 +28685,7 @@ class PreparedQuery extends SQLitePreparedQuery {
28684
28685
  }
28685
28686
  }
28686
28687
 
28687
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/bun-sqlite/driver.js
28688
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/bun-sqlite/driver.js
28688
28689
  class BunSQLiteDatabase extends BaseSQLiteDatabase {
28689
28690
  static [entityKind] = "BunSQLiteDatabase";
28690
28691
  }
@@ -51863,8 +51864,8 @@ class EventNotifier {
51863
51864
  }
51864
51865
  var eventNotifier = new EventNotifier;
51865
51866
 
51866
- // ../../apps/gateway/src/tmux/connection.ts
51867
- var import_ssh2 = __toESM(require_lib3(), 1);
51867
+ // ../../apps/gateway/src/tmux-client/local-external-connection.ts
51868
+ import { mkdirSync, rmSync } from "fs";
51868
51869
  import { homedir } from "os";
51869
51870
 
51870
51871
  // ../../apps/gateway/src/tmux/local-shell-path.ts
@@ -52021,669 +52022,958 @@ function buildLocalTmuxEnv(resolvedPath, baseEnv = process.env) {
52021
52022
  return nextEnv;
52022
52023
  }
52023
52024
 
52024
- // ../../apps/gateway/src/tmux/ssh-auth.ts
52025
- function normalizeEnvValue(value) {
52026
- const trimmed = value?.trim();
52027
- return trimmed ? trimmed : undefined;
52025
+ // ../../apps/gateway/src/tmux-client/command-builder.ts
52026
+ function quoteShellArg(value) {
52027
+ return `'${value.replaceAll("'", "'\\''")}'`;
52028
52028
  }
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;
52029
+ function joinShellArgs(argv) {
52030
+ return argv.map((arg) => quoteShellArg(arg)).join(" ");
52031
+ }
52032
+
52033
+ // ../../apps/gateway/src/tmux-client/fs-paths.ts
52034
+ import { randomUUID as randomUUID2 } from "crypto";
52035
+ import { join as join3 } from "path";
52036
+ var DEFAULT_ROOT_DIR = "/tmp/tmex";
52037
+ var DEFAULT_GATEWAY_RUNTIME_ID = randomUUID2();
52038
+ function toSafePathSegment(value) {
52039
+ return Array.from(value).map((char) => {
52040
+ if (/^[A-Za-z0-9._-]$/.test(char)) {
52041
+ return char;
52042
+ }
52043
+ return `_${char.codePointAt(0)?.toString(16) ?? "00"}_`;
52044
+ }).join("");
52045
+ }
52046
+ function createRuntimeFsPaths(options) {
52047
+ const baseRootDir = options.rootDir ?? DEFAULT_ROOT_DIR;
52048
+ const safeSessionName = toSafePathSegment(options.sessionName?.trim() || "tmex");
52049
+ const safeGatewayRuntimeId = toSafePathSegment(options.gatewayRuntimeId?.trim() || DEFAULT_GATEWAY_RUNTIME_ID);
52050
+ const runtimeDirName = `${toSafePathSegment(options.deviceId)}-${safeGatewayRuntimeId}-${options.gatewayPid}`;
52051
+ const runtimeRootDir = join3(baseRootDir, runtimeDirName);
52052
+ const panesDir = join3(runtimeRootDir, "panes");
52053
+ const hooksDir = join3(runtimeRootDir, "hooks");
52054
+ return {
52055
+ rootDir: runtimeRootDir,
52056
+ panesDir,
52057
+ hooksDir,
52058
+ hookFifoPath: join3(hooksDir, "events.fifo"),
52059
+ paneFifoPath(paneId) {
52060
+ return join3(panesDir, `${safeSessionName}-${toSafePathSegment(paneId)}.fifo`);
52038
52061
  }
52062
+ };
52063
+ }
52064
+
52065
+ // ../../apps/gateway/src/tmux-client/input-encoder.ts
52066
+ var encoder = new TextEncoder;
52067
+ var SEND_KEYS_HEX_CHUNK_BYTES = 256;
52068
+ function encodeInputToHexChunks(input, chunkBytes = SEND_KEYS_HEX_CHUNK_BYTES) {
52069
+ const bytes = encoder.encode(input);
52070
+ const chunks = [];
52071
+ for (let offset = 0;offset < bytes.length; offset += chunkBytes) {
52072
+ const chunk2 = bytes.slice(offset, offset + chunkBytes);
52073
+ chunks.push(Array.from(chunk2, (byte) => byte.toString(16).padStart(2, "0")));
52039
52074
  }
52040
- return "root";
52075
+ return chunks;
52041
52076
  }
52042
- function resolveSshAgentSocket(authMode, env = process.env) {
52043
- if (authMode !== "agent" && authMode !== "auto") {
52077
+
52078
+ // ../../apps/gateway/src/tmux-client/pane-title-parser.ts
52079
+ var decoder = new TextDecoder;
52080
+ function createPaneTitleParser(options) {
52081
+ let phase = "normal";
52082
+ let oscKind = "";
52083
+ let titleBytes = [];
52084
+ return {
52085
+ push(data) {
52086
+ const output = [];
52087
+ for (const byte of data) {
52088
+ if (phase === "normal") {
52089
+ if (byte === 27) {
52090
+ phase = "esc";
52091
+ } else {
52092
+ output.push(byte);
52093
+ }
52094
+ continue;
52095
+ }
52096
+ if (phase === "esc") {
52097
+ if (byte === 93) {
52098
+ phase = "osc";
52099
+ oscKind = "";
52100
+ titleBytes = [];
52101
+ continue;
52102
+ }
52103
+ output.push(27, byte);
52104
+ phase = "normal";
52105
+ continue;
52106
+ }
52107
+ if (phase === "osc") {
52108
+ if (byte === 59) {
52109
+ phase = oscKind === "0" || oscKind === "2" ? "osc-data" : "normal";
52110
+ if (phase === "normal") {
52111
+ output.push(27, 93, ...encoderFromString(oscKind), 59);
52112
+ }
52113
+ continue;
52114
+ }
52115
+ oscKind += String.fromCharCode(byte);
52116
+ continue;
52117
+ }
52118
+ if (phase === "osc-data") {
52119
+ if (byte === 7) {
52120
+ emitTitle(options.onTitle, titleBytes);
52121
+ phase = "normal";
52122
+ continue;
52123
+ }
52124
+ if (byte === 27) {
52125
+ phase = "osc-st";
52126
+ continue;
52127
+ }
52128
+ titleBytes.push(byte);
52129
+ continue;
52130
+ }
52131
+ if (byte === 92) {
52132
+ emitTitle(options.onTitle, titleBytes);
52133
+ phase = "normal";
52134
+ continue;
52135
+ }
52136
+ titleBytes.push(27, byte);
52137
+ phase = "osc-data";
52138
+ }
52139
+ return new Uint8Array(output);
52140
+ }
52141
+ };
52142
+ }
52143
+ function emitTitle(onTitle, titleBytes) {
52144
+ const title = decoder.decode(new Uint8Array(titleBytes)).trim();
52145
+ if (!title) {
52044
52146
  return;
52045
52147
  }
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;
52148
+ onTitle(title);
52149
+ }
52150
+ function encoderFromString(value) {
52151
+ return new TextEncoder().encode(value);
52054
52152
  }
52055
52153
 
52056
- // ../../apps/gateway/src/tmux/parser.ts
52057
- function isSameBytes(left, right) {
52058
- if (left.length !== right.length) {
52154
+ // ../../apps/gateway/src/tmux-client/local-external-connection.ts
52155
+ function hasRenderableTerminalContent(value) {
52156
+ return value.trim().length > 0;
52157
+ }
52158
+ var BELL_DEDUP_WINDOW_MS = 200;
52159
+ function shouldIgnoreReaderAbortError(error) {
52160
+ if (!error || typeof error !== "object") {
52059
52161
  return false;
52060
52162
  }
52061
- for (let i = 0;i < left.length; i++) {
52062
- if (left[i] !== right[i]) {
52063
- return false;
52163
+ const maybeError = error;
52164
+ return maybeError.name === "AbortError" && maybeError.code === "ERR_STREAM_RELEASE_LOCK" && typeof maybeError.message === "string" && maybeError.message.includes("releaseLock");
52165
+ }
52166
+ function defaultRun(argv) {
52167
+ return new Promise((resolve, reject) => {
52168
+ const subprocess = Bun.spawn(argv, {
52169
+ env: buildLocalTmuxEnv(getLocalShellPath()),
52170
+ stdout: "pipe",
52171
+ stderr: "pipe"
52172
+ });
52173
+ Promise.all([
52174
+ new Response(subprocess.stdout).text(),
52175
+ new Response(subprocess.stderr).text(),
52176
+ subprocess.exited
52177
+ ]).then(([stdout, stderr, exitCode]) => {
52178
+ resolve({ stdout, stderr, exitCode });
52179
+ }).catch(reject);
52180
+ });
52181
+ }
52182
+
52183
+ class LocalExternalTmuxConnection {
52184
+ deviceId;
52185
+ deps;
52186
+ callbacks;
52187
+ device = null;
52188
+ sessionName = "tmex";
52189
+ connected = false;
52190
+ manualDisconnect = false;
52191
+ activeWindowId = null;
52192
+ activePaneId = null;
52193
+ pendingPaneTitles = new Map;
52194
+ snapshotSession = null;
52195
+ snapshotWindows = new Map;
52196
+ currentPipePaneId = null;
52197
+ pipeReadAbort = null;
52198
+ pipeTransition = Promise.resolve();
52199
+ inputTransition = Promise.resolve();
52200
+ hookReadAbort = null;
52201
+ hookBuffer = "";
52202
+ bellDedup = new Map;
52203
+ fsPaths = createRuntimeFsPaths({
52204
+ deviceId: "pending",
52205
+ sessionName: "pending",
52206
+ gatewayPid: process.pid
52207
+ });
52208
+ constructor(options, inputDeps = {}) {
52209
+ this.deviceId = options.deviceId;
52210
+ this.callbacks = options;
52211
+ this.deps = {
52212
+ enableHooks: inputDeps.enableHooks ?? true,
52213
+ getDevice: inputDeps.getDevice ?? ((deviceId) => getDeviceById(deviceId)),
52214
+ run: inputDeps.run ?? defaultRun
52215
+ };
52216
+ }
52217
+ async connect() {
52218
+ this.manualDisconnect = false;
52219
+ this.device = this.deps.getDevice(this.deviceId);
52220
+ if (!this.device) {
52221
+ throw new Error(`Device not found: ${this.deviceId}`);
52222
+ }
52223
+ if (this.device.type !== "local") {
52224
+ throw new Error(`LocalExternalTmuxConnection only supports local device: ${this.deviceId}`);
52225
+ }
52226
+ this.sessionName = this.device.session?.trim() || "tmex";
52227
+ this.fsPaths = createRuntimeFsPaths({
52228
+ deviceId: this.deviceId,
52229
+ sessionName: this.sessionName,
52230
+ gatewayPid: process.pid
52231
+ });
52232
+ this.ensureRuntimeDirs();
52233
+ await this.ensureSession();
52234
+ if (this.deps.enableHooks) {
52235
+ await this.startHooks();
52064
52236
  }
52237
+ this.connected = true;
52238
+ updateDeviceRuntimeStatus(this.deviceId, {
52239
+ lastSeenAt: new Date().toISOString(),
52240
+ tmuxAvailable: true,
52241
+ lastError: null
52242
+ });
52243
+ await this.requestSnapshotInternal();
52065
52244
  }
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;
52245
+ disconnect() {
52246
+ if (!this.connected && this.manualDisconnect) {
52247
+ return;
52075
52248
  }
52076
- const octal = value.slice(i + 1, i + 4);
52077
- if (!/^[0-7]{3}$/.test(octal)) {
52078
- continue;
52249
+ this.manualDisconnect = true;
52250
+ this.connected = false;
52251
+ this.stopPipe();
52252
+ if (this.deps.enableHooks) {
52253
+ this.stopHooks();
52079
52254
  }
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);
52255
+ rmSync(this.fsPaths.rootDir, { recursive: true, force: true });
52256
+ }
52257
+ requestSnapshot() {
52258
+ this.requestSnapshotInternal();
52259
+ }
52260
+ sendInput(paneId, data) {
52261
+ if (!this.connected) {
52262
+ return;
52263
+ }
52264
+ const task = async () => {
52265
+ for (const chunk2 of encodeInputToHexChunks(data)) {
52266
+ await this.runTmux(["send-keys", "-H", "-t", paneId, ...chunk2]);
52148
52267
  }
52268
+ };
52269
+ const next = this.inputTransition.catch(() => {
52270
+ return;
52271
+ }).then(task);
52272
+ this.inputTransition = next;
52273
+ next.catch((error) => {
52274
+ this.callbacks.onError(error);
52275
+ });
52276
+ }
52277
+ resizePane(paneId, cols, rows) {
52278
+ if (!this.connected) {
52279
+ return;
52149
52280
  }
52281
+ this.resizePaneInternal(paneId, cols, rows).catch((error) => {
52282
+ this.callbacks.onError(error);
52283
+ });
52150
52284
  }
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);
52285
+ selectPane(windowId, paneId) {
52286
+ if (!this.connected) {
52159
52287
  return;
52160
52288
  }
52161
- if (!cleanLine.trim())
52289
+ this.selectPaneInternal(windowId, paneId, null).catch((error) => {
52290
+ this.callbacks.onError(error);
52291
+ });
52292
+ }
52293
+ selectPaneWithSize(windowId, paneId, cols, rows) {
52294
+ if (!this.connected) {
52295
+ return;
52296
+ }
52297
+ this.selectPaneInternal(windowId, paneId, { cols, rows }).catch((error) => {
52298
+ this.callbacks.onError(error);
52299
+ });
52300
+ }
52301
+ selectWindow(windowId) {
52302
+ if (!this.connected) {
52162
52303
  return;
52163
- if (cleanLine.startsWith("%begin")) {
52164
- this.startOutputBlock(cleanLine);
52304
+ }
52305
+ this.runAndRefresh(["select-window", "-t", windowId]).catch((error) => {
52306
+ this.callbacks.onError(error);
52307
+ });
52308
+ }
52309
+ createWindow(name) {
52310
+ if (!this.connected) {
52165
52311
  return;
52166
52312
  }
52167
- if (cleanLine.startsWith("%")) {
52168
- this.parseControlLine(cleanLine);
52169
- this.notifyReady();
52313
+ const argv = name ? ["new-window", "-n", name] : ["new-window"];
52314
+ this.runAndRefresh(argv).catch((error) => {
52315
+ this.callbacks.onError(error);
52316
+ });
52317
+ }
52318
+ closeWindow(windowId) {
52319
+ if (!this.connected) {
52170
52320
  return;
52171
52321
  }
52172
- this.onNonControlOutput?.(cleanLine);
52173
- console.log("[tmux] non-control output:", cleanLine);
52322
+ this.closeWindowInternal(windowId).catch((error) => {
52323
+ this.callbacks.onError(error);
52324
+ });
52174
52325
  }
52175
- notifyReady() {
52176
- if (this.readyNotified)
52326
+ closePane(paneId) {
52327
+ if (!this.connected) {
52177
52328
  return;
52178
- this.readyNotified = true;
52179
- this.onReady?.();
52329
+ }
52330
+ this.runAndRefresh(["kill-pane", "-t", paneId], true).catch((error) => {
52331
+ this.callbacks.onError(error);
52332
+ });
52180
52333
  }
52181
- startOutputBlock(line) {
52182
- const meta = this.parseOutputBlockMeta(line);
52183
- if (!meta) {
52334
+ renameWindow(windowId, name) {
52335
+ if (!this.connected) {
52184
52336
  return;
52185
52337
  }
52186
- this.inOutputBlock = true;
52187
- this.outputBlockMeta = meta;
52188
- this.outputBlockLines = [];
52189
- this.onOutputBlockBegin?.(meta);
52338
+ this.runAndRefresh(["rename-window", "-t", windowId, name]).catch((error) => {
52339
+ this.callbacks.onError(error);
52340
+ });
52190
52341
  }
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
- });
52342
+ async ensureSession() {
52343
+ const exists3 = await this.runTmuxAllowFailure(["has-session", "-t", this.sessionName]);
52344
+ if (exists3.exitCode === 0) {
52345
+ return;
52204
52346
  }
52205
- this.outputBlockLines = [];
52206
- this.notifyReady();
52347
+ await this.runTmux(["new-session", "-d", "-c", homedir(), "-s", this.sessionName]);
52207
52348
  }
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 };
52349
+ ensureRuntimeDirs() {
52350
+ mkdirSync(this.fsPaths.rootDir, { recursive: true, mode: 448 });
52351
+ mkdirSync(this.fsPaths.panesDir, { recursive: true, mode: 448 });
52352
+ mkdirSync(this.fsPaths.hooksDir, { recursive: true, mode: 448 });
52222
52353
  }
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;
52354
+ async startHooks() {
52355
+ this.ensureRuntimeDirs();
52356
+ const fifoPath = this.fsPaths.hookFifoPath;
52357
+ rmSync(fifoPath, { force: true });
52358
+ await this.runShell(`mkfifo ${quoteShellArg(fifoPath)}`);
52359
+ const readerProcess = Bun.spawn(["/bin/sh", "-lc", `tail -n +1 -f ${quoteShellArg(fifoPath)}`], {
52360
+ stdout: "pipe",
52361
+ stderr: "pipe"
52362
+ });
52363
+ const reader = readerProcess.stdout.getReader();
52364
+ (async () => {
52365
+ try {
52366
+ while (true) {
52367
+ const chunk2 = await reader.read();
52368
+ if (chunk2.done) {
52369
+ break;
52370
+ }
52371
+ this.handleHookChunk(new TextDecoder().decode(chunk2.value));
52372
+ }
52373
+ } catch (error) {
52374
+ if (!this.manualDisconnect && !shouldIgnoreReaderAbortError(error)) {
52375
+ this.callbacks.onError(error instanceof Error ? error : new Error(String(error)));
52376
+ }
52230
52377
  }
52231
- previousWasCR = byte === 13;
52232
- }
52233
- this.lastOutputEndedWithCR = previousWasCR;
52234
- if (extraCRCount === 0) {
52235
- return data;
52236
- }
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;
52378
+ })();
52379
+ this.hookReadAbort = () => {
52380
+ reader.releaseLock();
52381
+ readerProcess.kill();
52382
+ rmSync(fifoPath, { force: true });
52383
+ };
52384
+ await this.installHook("alert-bell", ["bell", "#{window_id}", "#{pane_id}"]);
52385
+ await this.installHook("pane-exited", ["pane-exited", "#{window_id}", "#{pane_id}"]);
52386
+ await this.installHook("pane-died", ["pane-died", "#{window_id}", "#{pane_id}"]);
52387
+ }
52388
+ async stopHooks() {
52389
+ await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "alert-bell"]);
52390
+ await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "pane-exited"]);
52391
+ await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "pane-died"]);
52392
+ this.hookReadAbort?.();
52393
+ this.hookReadAbort = null;
52394
+ this.hookBuffer = "";
52395
+ }
52396
+ async installHook(hookName, fields) {
52397
+ const fifoPath = this.fsPaths.hookFifoPath;
52398
+ const innerScript = `printf '%s\\t%s\\t%s\\n' ${fields.map((field) => quoteShellArg(field)).join(" ")} >> ${quoteShellArg(fifoPath)}`;
52399
+ await this.runTmux([
52400
+ "set-hook",
52401
+ "-t",
52402
+ this.sessionName,
52403
+ hookName,
52404
+ `run-shell -b ${quoteShellArg(innerScript)}`
52405
+ ]);
52406
+ }
52407
+ handleHookChunk(text2) {
52408
+ this.hookBuffer += text2;
52409
+ while (true) {
52410
+ const newlineIndex = this.hookBuffer.indexOf(`
52411
+ `);
52412
+ if (newlineIndex < 0) {
52413
+ return;
52414
+ }
52415
+ const line = this.hookBuffer.slice(0, newlineIndex).trim();
52416
+ this.hookBuffer = this.hookBuffer.slice(newlineIndex + 1);
52417
+ if (!line) {
52418
+ continue;
52419
+ }
52420
+ const [type, windowId, paneId] = line.split("\t");
52421
+ if (type === "bell") {
52422
+ const key = paneId || windowId || "-";
52423
+ const previous = this.bellDedup.get(key) ?? 0;
52424
+ const now = Date.now();
52425
+ if (now - previous >= BELL_DEDUP_WINDOW_MS) {
52426
+ this.bellDedup.set(key, now);
52427
+ this.callbacks.onEvent({
52428
+ type: "bell",
52429
+ data: {
52430
+ windowId: windowId || undefined,
52431
+ paneId: paneId || this.activePaneId || undefined
52432
+ }
52433
+ });
52434
+ }
52435
+ continue;
52436
+ }
52437
+ if (type === "pane-exited" || type === "pane-died") {
52438
+ this.requestSnapshot();
52244
52439
  }
52245
- normalized[writeIndex] = byte;
52246
- writeIndex += 1;
52247
- previousWasCR = byte === 13;
52248
52440
  }
52249
- return normalized;
52250
52441
  }
52251
- getTitleParseState(paneId) {
52252
- const existing = this.outputTitleStates.get(paneId);
52253
- if (existing) {
52254
- return existing;
52442
+ async runAndRefresh(argv, allowTargetMissing = false) {
52443
+ await this.runTmux(argv, allowTargetMissing);
52444
+ await this.requestSnapshotInternal();
52445
+ }
52446
+ async closeWindowInternal(windowId) {
52447
+ const count2 = Number.parseInt((await this.runTmux(["display-message", "-p", "-t", this.sessionName, "#{session_windows}"])).stdout.trim() || "0", 10);
52448
+ if (count2 <= 1) {
52449
+ await this.runTmux(["new-window", "-d", "-t", this.sessionName]);
52255
52450
  }
52256
- const created = {
52257
- phase: "normal",
52258
- titleBytes: []
52259
- };
52260
- this.outputTitleStates.set(paneId, created);
52261
- return created;
52451
+ await this.runAndRefresh(["kill-window", "-t", windowId], true);
52262
52452
  }
52263
- emitPaneTitleIfNeeded(paneId, titleBytes) {
52264
- if (titleBytes.length === 0) {
52453
+ async resizePaneInternal(paneId, cols, rows) {
52454
+ const safeCols = Math.max(2, Math.floor(cols));
52455
+ const safeRows = Math.max(2, Math.floor(rows));
52456
+ const windowId = this.findPaneWindowId(paneId) ?? (await this.runTmux(["display-message", "-p", "-t", paneId, "#{window_id}"], true)).stdout.trim();
52457
+ if (!windowId) {
52265
52458
  return;
52266
52459
  }
52267
- const title = new TextDecoder().decode(new Uint8Array(titleBytes)).trim();
52268
- if (!title) {
52460
+ await this.runTmux(["resize-window", "-t", windowId, "-x", String(safeCols), "-y", String(safeRows)], true);
52461
+ await this.requestSnapshotInternal();
52462
+ }
52463
+ async selectPaneInternal(windowId, paneId, size) {
52464
+ this.activeWindowId = windowId;
52465
+ this.activePaneId = paneId;
52466
+ await this.runTmux(["select-window", "-t", windowId], true);
52467
+ await this.runTmux(["select-pane", "-t", paneId], true);
52468
+ await this.startPipeForPane(paneId);
52469
+ if (size) {
52470
+ await this.resizePaneInternal(paneId, size.cols, size.rows);
52471
+ }
52472
+ this.callbacks.onEvent({
52473
+ type: "pane-active",
52474
+ data: { windowId, paneId }
52475
+ });
52476
+ await this.capturePaneHistory(paneId);
52477
+ await this.requestSnapshotInternal();
52478
+ }
52479
+ async capturePaneHistory(paneId) {
52480
+ const mode = (await this.runTmux(["display-message", "-p", "-t", paneId, "#{alternate_on}"], true)).stdout.trim();
52481
+ const alternateScreen = mode === "1";
52482
+ const normal = (await this.runTmux(["capture-pane", "-t", paneId, "-S", "-", "-E", "-", "-e", "-p"], true)).stdout;
52483
+ const alternate = (await this.runTmux(["capture-pane", "-t", paneId, "-a", "-S", "-", "-E", "-", "-e", "-p", "-q"], true)).stdout;
52484
+ const history = alternateScreen ? hasRenderableTerminalContent(normal) ? normal : alternate : normal || alternate;
52485
+ if (history) {
52486
+ this.callbacks.onTerminalHistory(paneId, history, alternateScreen);
52487
+ }
52488
+ }
52489
+ async requestSnapshotInternal() {
52490
+ if (!this.connected) {
52269
52491
  return;
52270
52492
  }
52271
- this.onPaneTitle?.(paneId, title);
52272
- }
52273
- stripScreenTitleSequence(paneId, data) {
52274
- if (data.length === 0) {
52275
- return data;
52493
+ const [sessionRes, windowsRes, panesRes] = await Promise.all([
52494
+ this.runTmuxAllowFailure([
52495
+ "display-message",
52496
+ "-p",
52497
+ "-t",
52498
+ this.sessionName,
52499
+ "#{session_id}\t#{session_name}"
52500
+ ]),
52501
+ this.runTmuxAllowFailure([
52502
+ "list-windows",
52503
+ "-t",
52504
+ this.sessionName,
52505
+ "-F",
52506
+ "#{window_id}\t#{window_index}\t#{window_name}\t#{window_active}"
52507
+ ]),
52508
+ this.runTmuxAllowFailure([
52509
+ "list-panes",
52510
+ "-t",
52511
+ this.sessionName,
52512
+ "-F",
52513
+ "#{pane_id}\t#{window_id}\t#{pane_index}\t#{pane_title}\t#{pane_active}\t#{pane_width}\t#{pane_height}"
52514
+ ])
52515
+ ]);
52516
+ if (sessionRes.exitCode !== 0 || windowsRes.exitCode !== 0 || panesRes.exitCode !== 0) {
52517
+ this.callbacks.onSnapshot({ deviceId: this.deviceId, session: null });
52518
+ return;
52276
52519
  }
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
- }
52520
+ this.parseSnapshotSession(sessionRes.stdout.split(/\r?\n/));
52521
+ this.parseSnapshotWindows(windowsRes.stdout.split(/\r?\n/));
52522
+ this.parseSnapshotPanes(panesRes.stdout.split(/\r?\n/));
52523
+ this.emitSnapshot();
52524
+ }
52525
+ parseSnapshotSession(lines) {
52526
+ this.snapshotSession = null;
52527
+ for (const line of lines) {
52528
+ if (!line.trim()) {
52288
52529
  continue;
52289
52530
  }
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
- }
52531
+ const [id, name] = line.split("\t");
52532
+ if (id) {
52533
+ this.snapshotSession = { id, name: name ?? "" };
52534
+ }
52535
+ return;
52536
+ }
52537
+ }
52538
+ parseSnapshotWindows(lines) {
52539
+ this.snapshotWindows.clear();
52540
+ for (const line of lines) {
52541
+ if (!line.trim()) {
52303
52542
  continue;
52304
52543
  }
52305
- if (phase === "title") {
52306
- if (byte === 27) {
52307
- phase = "titleEsc";
52308
- } else {
52309
- titleBytes.push(byte);
52310
- }
52544
+ const [id, indexRaw, name, activeRaw] = line.split("\t");
52545
+ if (!id) {
52311
52546
  continue;
52312
52547
  }
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";
52548
+ const index = Number.parseInt(indexRaw ?? "", 10);
52549
+ const active = activeRaw === "1";
52550
+ if (active) {
52551
+ this.activeWindowId = id;
52322
52552
  }
52553
+ this.snapshotWindows.set(id, {
52554
+ id,
52555
+ index: Number.isNaN(index) ? 0 : index,
52556
+ name: name ?? "",
52557
+ active,
52558
+ panes: []
52559
+ });
52323
52560
  }
52324
- parseState.phase = phase;
52325
- return new Uint8Array(output);
52326
52561
  }
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);
52562
+ parseSnapshotPanes(lines) {
52563
+ for (const window2 of this.snapshotWindows.values()) {
52564
+ window2.panes = [];
52332
52565
  }
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;
52566
+ for (const line of lines) {
52567
+ if (!line.trim()) {
52568
+ continue;
52360
52569
  }
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;
52570
+ const [paneId, windowId, indexRaw, titleRaw, activeRaw, widthRaw, heightRaw] = line.split("\t");
52571
+ if (!paneId || !windowId) {
52572
+ continue;
52368
52573
  }
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;
52574
+ const index = Number.parseInt(indexRaw ?? "", 10);
52575
+ const width = Number.parseInt(widthRaw ?? "", 10);
52576
+ const height = Number.parseInt(heightRaw ?? "", 10);
52577
+ const pane = {
52578
+ id: paneId,
52579
+ windowId,
52580
+ index: Number.isNaN(index) ? 0 : index,
52581
+ title: this.pendingPaneTitles.get(paneId) ?? (titleRaw?.trim() ? titleRaw : undefined),
52582
+ active: activeRaw === "1",
52583
+ width: Number.isNaN(width) ? 0 : width,
52584
+ height: Number.isNaN(height) ? 0 : height
52585
+ };
52586
+ if (pane.active) {
52587
+ this.activePaneId = paneId;
52588
+ this.activeWindowId = windowId;
52382
52589
  }
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;
52590
+ const window2 = this.snapshotWindows.get(windowId);
52591
+ if (!window2) {
52592
+ continue;
52392
52593
  }
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;
52594
+ window2.panes.push(pane);
52595
+ this.pendingPaneTitles.delete(paneId);
52596
+ }
52597
+ for (const window2 of this.snapshotWindows.values()) {
52598
+ window2.panes.sort((left, right) => left.index - right.index);
52599
+ }
52600
+ }
52601
+ emitSnapshot() {
52602
+ const session = this.snapshotSession ? {
52603
+ id: this.snapshotSession.id,
52604
+ name: this.snapshotSession.name,
52605
+ windows: Array.from(this.snapshotWindows.values()).sort((left, right) => left.index - right.index)
52606
+ } : null;
52607
+ this.callbacks.onSnapshot({
52608
+ deviceId: this.deviceId,
52609
+ session
52610
+ });
52611
+ }
52612
+ findPaneWindowId(paneId) {
52613
+ for (const window2 of this.snapshotWindows.values()) {
52614
+ if (window2.panes.some((pane) => pane.id === paneId)) {
52615
+ return window2.id;
52402
52616
  }
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;
52617
+ }
52618
+ return null;
52619
+ }
52620
+ async startPipeForPane(paneId) {
52621
+ await this.queuePipeTransition(async () => {
52622
+ if (this.currentPipePaneId === paneId) {
52623
+ return;
52410
52624
  }
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);
52625
+ await this.stopPipeNow();
52626
+ const fifoPath = this.fsPaths.paneFifoPath(paneId);
52627
+ this.ensureRuntimeDirs();
52628
+ rmSync(fifoPath, { force: true });
52629
+ await this.runShell(`mkfifo ${quoteShellArg(fifoPath)}`);
52630
+ const parser = createPaneTitleParser({
52631
+ onTitle: (title) => {
52632
+ this.pendingPaneTitles.set(paneId, title);
52633
+ this.requestSnapshot();
52420
52634
  }
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 });
52635
+ });
52636
+ const readerProcess = Bun.spawn(["/bin/sh", "-lc", `cat ${quoteShellArg(fifoPath)}`], {
52637
+ stdout: "pipe",
52638
+ stderr: "pipe"
52639
+ });
52640
+ const reader = readerProcess.stdout.getReader();
52641
+ (async () => {
52642
+ try {
52643
+ while (true) {
52644
+ const chunk2 = await reader.read();
52645
+ if (chunk2.done) {
52646
+ break;
52647
+ }
52648
+ const raw = chunk2.value;
52649
+ const output = parser.push(raw);
52650
+ if (Array.from(raw).includes(7)) {
52651
+ this.callbacks.onEvent({ type: "bell", data: { paneId } });
52652
+ }
52653
+ if (output.length > 0) {
52654
+ this.callbacks.onTerminalOutput(paneId, output);
52655
+ }
52656
+ }
52657
+ } catch (error) {
52658
+ if (!this.manualDisconnect && !shouldIgnoreReaderAbortError(error)) {
52659
+ this.callbacks.onError(error instanceof Error ? error : new Error(String(error)));
52660
+ }
52452
52661
  }
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);
52662
+ })();
52663
+ this.pipeReadAbort = () => {
52664
+ reader.releaseLock();
52665
+ readerProcess.kill();
52666
+ rmSync(fifoPath, { force: true });
52667
+ };
52668
+ await this.runTmux(["pipe-pane", "-O", "-t", paneId, `cat >${fifoPath}`]);
52669
+ this.currentPipePaneId = paneId;
52670
+ });
52671
+ }
52672
+ async stopPipe() {
52673
+ await this.queuePipeTransition(() => this.stopPipeNow());
52674
+ }
52675
+ async stopPipeNow() {
52676
+ const paneId = this.currentPipePaneId;
52677
+ this.currentPipePaneId = null;
52678
+ if (paneId) {
52679
+ await this.runTmuxAllowFailure(["pipe-pane", "-t", paneId]);
52464
52680
  }
52681
+ this.pipeReadAbort?.();
52682
+ this.pipeReadAbort = null;
52465
52683
  }
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
- }
52684
+ queuePipeTransition(task) {
52685
+ const next = this.pipeTransition.catch(() => {
52686
+ return;
52687
+ }).then(task);
52688
+ this.pipeTransition = next;
52689
+ return next;
52690
+ }
52691
+ async runShell(command) {
52692
+ const result = await this.deps.run(["/bin/sh", "-lc", command]);
52693
+ if (result.exitCode !== 0) {
52694
+ throw new Error(result.stderr.trim() || `shell command failed: ${command}`);
52482
52695
  }
52483
- if (current) {
52484
- result.push(current);
52696
+ }
52697
+ async runTmux(argv, allowTargetMissing = false) {
52698
+ const result = await this.runTmuxAllowFailure(argv);
52699
+ if (result.exitCode === 0) {
52700
+ return result;
52485
52701
  }
52486
- return result;
52702
+ const message = (result.stderr.trim() || result.stdout.trim() || `tmux command failed: ${argv.join(" ")}`).trim();
52703
+ if (allowTargetMissing && this.isRecoverableTargetMissingError(message)) {
52704
+ this.recoverFromTargetMissingError(message);
52705
+ return result;
52706
+ }
52707
+ updateDeviceRuntimeStatus(this.deviceId, {
52708
+ lastSeenAt: new Date().toISOString(),
52709
+ tmuxAvailable: false,
52710
+ lastError: message
52711
+ });
52712
+ throw new Error(message);
52713
+ }
52714
+ async runTmuxAllowFailure(argv) {
52715
+ return this.deps.run(["tmux", ...argv]);
52716
+ }
52717
+ isRecoverableTargetMissingError(message) {
52718
+ const normalized = message.toLowerCase();
52719
+ return normalized.includes("can't find window") || normalized.includes("can't find pane") || normalized.includes("no such window") || normalized.includes("no such pane");
52720
+ }
52721
+ recoverFromTargetMissingError(message) {
52722
+ const normalized = message.toLowerCase();
52723
+ if (normalized.includes("window")) {
52724
+ this.activeWindowId = null;
52725
+ }
52726
+ if (normalized.includes("pane")) {
52727
+ this.activePaneId = null;
52728
+ }
52729
+ this.requestSnapshot();
52730
+ }
52731
+ }
52732
+
52733
+ // ../../apps/gateway/src/tmux-client/ssh-external-connection.ts
52734
+ var import_ssh2 = __toESM(require_lib3(), 1);
52735
+
52736
+ // ../../apps/gateway/src/tmux/ssh-auth.ts
52737
+ function normalizeEnvValue(value) {
52738
+ const trimmed = value?.trim();
52739
+ return trimmed ? trimmed : undefined;
52740
+ }
52741
+ function resolveSshUsername(configuredUsername, authMode, env = process.env) {
52742
+ const explicitUsername = normalizeEnvValue(configuredUsername);
52743
+ if (explicitUsername) {
52744
+ return explicitUsername;
52745
+ }
52746
+ if (authMode === "agent" || authMode === "auto") {
52747
+ const currentUser = normalizeEnvValue(env.USER) ?? normalizeEnvValue(env.LOGNAME);
52748
+ if (currentUser) {
52749
+ return currentUser;
52750
+ }
52751
+ }
52752
+ return "root";
52753
+ }
52754
+ function resolveSshAgentSocket(authMode, env = process.env) {
52755
+ if (authMode !== "agent" && authMode !== "auto") {
52756
+ return;
52757
+ }
52758
+ const socket = normalizeEnvValue(env.SSH_AUTH_SOCK);
52759
+ if (socket) {
52760
+ return socket;
52761
+ }
52762
+ if (authMode === "agent") {
52763
+ throw new Error("SSH_AUTH_SOCK \u672A\u8BBE\u7F6E\uFF0C\u65E0\u6CD5\u4F7F\u7528 SSH Agent \u8BA4\u8BC1");
52487
52764
  }
52488
- flush() {
52489
- this.buffer = "";
52490
- this.lastOutputEndedWithCR = false;
52491
- this.lastOutputFrame = null;
52492
- this.outputTitleStates.clear();
52765
+ return;
52766
+ }
52767
+
52768
+ // ../../apps/gateway/src/tmux-client/ssh-bootstrap.ts
52769
+ function buildSshBootstrapScript() {
52770
+ return [
52771
+ ". /etc/profile 2>/dev/null || true",
52772
+ '[ -f "$HOME/.profile" ] && . "$HOME/.profile" 2>/dev/null || true',
52773
+ '[ -f "$HOME/.bash_profile" ] && . "$HOME/.bash_profile" 2>/dev/null || true',
52774
+ 'TMUX_BIN="$(command -v tmux 2>/dev/null || true)"',
52775
+ 'if [ -z "$TMUX_BIN" ]; then',
52776
+ " for p in /usr/local/bin/tmux /opt/homebrew/bin/tmux /usr/bin/tmux /bin/tmux; do",
52777
+ ' [ -x "$p" ] && TMUX_BIN="$p" && break',
52778
+ " done",
52779
+ "fi",
52780
+ 'HOME_DIR="${HOME:-$(pwd)}"',
52781
+ 'if [ -z "$TMUX_BIN" ]; then',
52782
+ " printf 'TMEX_BOOT_FAIL\\ttmux_not_found\\n'",
52783
+ "else",
52784
+ ` printf 'TMEX_BOOT_OK\\t%s\\t%s\\t%s\\n' "$TMUX_BIN" "$("$TMUX_BIN" -V 2>/dev/null)" "$HOME_DIR"`,
52785
+ "fi"
52786
+ ].join(`
52787
+ `);
52788
+ }
52789
+ function parseSshBootstrapOutput(output) {
52790
+ const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
52791
+ for (const line of lines) {
52792
+ if (line.startsWith("TMEX_BOOT_OK\t")) {
52793
+ const [, tmuxBin = "", tmuxVersion = "", homeDir = ""] = line.split("\t");
52794
+ if (!tmuxBin || !homeDir) {
52795
+ return { ok: false, reason: "invalid_bootstrap_payload" };
52796
+ }
52797
+ return { ok: true, tmuxBin, tmuxVersion, homeDir };
52798
+ }
52799
+ if (line.startsWith("TMEX_BOOT_FAIL\t")) {
52800
+ const [, reason = "tmux_bootstrap_failed"] = line.split("\t");
52801
+ return { ok: false, reason };
52802
+ }
52493
52803
  }
52804
+ return { ok: false, reason: "missing_bootstrap_marker" };
52494
52805
  }
52495
52806
 
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];
52807
+ // ../../apps/gateway/src/tmux-client/ssh-external-connection.ts
52808
+ function hasRenderableTerminalContent2(value) {
52809
+ return value.trim().length > 0;
52500
52810
  }
52811
+ var BELL_DEDUP_WINDOW_MS2 = 200;
52812
+ var COMMAND_SENTINEL = "\x1ETMEX_END ";
52501
52813
 
52502
- class TmuxConnection {
52814
+ class SshExternalTmuxConnection {
52503
52815
  deviceId;
52816
+ callbacks;
52817
+ deps;
52504
52818
  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;
52819
+ sessionName = "tmex";
52518
52820
  connected = false;
52519
52821
  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 = [];
52822
+ closeNotified = false;
52823
+ cleanupPromise = null;
52824
+ activeWindowId = null;
52825
+ activePaneId = null;
52826
+ pendingPaneTitles = new Map;
52531
52827
  snapshotSession = null;
52532
52828
  snapshotWindows = new Map;
52533
- pendingPaneTitles = new Map;
52534
- snapshotPanesReady = false;
52535
- historyCaptureStates = new Map;
52536
- resizeSnapshotTimer = null;
52537
- bellControlEventSeen = false;
52829
+ currentPipePaneId = null;
52830
+ pipeReadAbort = null;
52831
+ pipeTransition = Promise.resolve();
52832
+ hookReadAbort = null;
52833
+ hookBuffer = "";
52538
52834
  bellDedup = new Map;
52539
- constructor(options) {
52835
+ fsPaths = createRuntimeFsPaths({
52836
+ deviceId: "pending",
52837
+ sessionName: "pending",
52838
+ gatewayPid: process.pid
52839
+ });
52840
+ sshClient = null;
52841
+ commandStream = null;
52842
+ commandStdoutBuffer = "";
52843
+ pendingCommand = null;
52844
+ tmuxBin = "tmux";
52845
+ remoteHomeDir = ".";
52846
+ commandQueue = Promise.resolve();
52847
+ constructor(options, inputDeps = {}) {
52540
52848
  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
- });
52849
+ this.callbacks = options;
52850
+ this.deps = {
52851
+ getDevice: inputDeps.getDevice ?? ((deviceId) => getDeviceById(deviceId)),
52852
+ decrypt: inputDeps.decrypt ?? decryptWithContext,
52853
+ createClient: inputDeps.createClient ?? (() => new import_ssh2.Client)
52854
+ };
52606
52855
  }
52607
52856
  async connect() {
52608
52857
  this.manualDisconnect = false;
52609
- this.device = getDeviceById(this.deviceId);
52858
+ this.closeNotified = false;
52859
+ this.device = this.deps.getDevice(this.deviceId);
52610
52860
  if (!this.device) {
52611
52861
  throw new Error(`Device not found: ${this.deviceId}`);
52612
52862
  }
52613
- if (this.device.type === "local") {
52614
- await this.connectLocal();
52615
- } else {
52616
- await this.connectSSH();
52863
+ if (this.device.type !== "ssh") {
52864
+ throw new Error(`SshExternalTmuxConnection only supports ssh device: ${this.deviceId}`);
52617
52865
  }
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;
52866
+ this.sessionName = this.device.session?.trim() || "tmex";
52867
+ this.fsPaths = createRuntimeFsPaths({
52868
+ deviceId: this.deviceId,
52869
+ sessionName: this.sessionName,
52870
+ gatewayPid: process.pid
52871
+ });
52872
+ await this.connectSshClient();
52873
+ await this.openCommandChannel();
52874
+ await this.ensureRemoteRuntimeDirs();
52875
+ await this.ensureSession();
52876
+ await this.startHooks();
52647
52877
  this.connected = true;
52648
52878
  updateDeviceRuntimeStatus(this.deviceId, {
52649
52879
  lastSeenAt: new Date().toISOString(),
52650
52880
  tmuxAvailable: true,
52651
52881
  lastError: null
52652
52882
  });
52653
- try {
52654
- await this.waitForReady();
52655
- this.configureWindowSizePolicy();
52656
- } catch (err) {
52657
- this.cleanup();
52658
- throw err;
52883
+ await this.requestSnapshotInternal();
52884
+ }
52885
+ disconnect() {
52886
+ if (this.manualDisconnect) {
52887
+ return;
52659
52888
  }
52889
+ this.manualDisconnect = true;
52890
+ this.shutdownInternal(false);
52891
+ }
52892
+ requestSnapshot() {
52893
+ this.requestSnapshotInternal();
52660
52894
  }
52661
- async connectSSH() {
52662
- if (!this.device)
52895
+ sendInput(paneId, data) {
52896
+ if (!this.connected) {
52663
52897
  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
52898
+ }
52899
+ for (const chunk2 of encodeInputToHexChunks(data)) {
52900
+ this.runTmux(["send-keys", "-H", "-t", paneId, ...chunk2]).catch((error) => {
52901
+ this.callbacks.onError(error);
52680
52902
  });
52681
- };
52682
- logStage("connect_start", {
52683
- hasHost: Boolean(host),
52684
- hasSshConfigRef: Boolean(this.device.sshConfigRef),
52685
- sessionName
52903
+ }
52904
+ }
52905
+ resizePane(paneId, cols, rows) {
52906
+ if (!this.connected) {
52907
+ return;
52908
+ }
52909
+ this.resizePaneInternal(paneId, cols, rows).catch((error) => {
52910
+ this.callbacks.onError(error);
52911
+ });
52912
+ }
52913
+ selectPane(windowId, paneId) {
52914
+ if (!this.connected) {
52915
+ return;
52916
+ }
52917
+ this.selectPaneInternal(windowId, paneId, null).catch((error) => {
52918
+ this.callbacks.onError(error);
52919
+ });
52920
+ }
52921
+ selectPaneWithSize(windowId, paneId, cols, rows) {
52922
+ if (!this.connected) {
52923
+ return;
52924
+ }
52925
+ this.selectPaneInternal(windowId, paneId, { cols, rows }).catch((error) => {
52926
+ this.callbacks.onError(error);
52927
+ });
52928
+ }
52929
+ selectWindow(windowId) {
52930
+ if (!this.connected) {
52931
+ return;
52932
+ }
52933
+ this.runAndRefresh(["select-window", "-t", windowId]).catch((error) => {
52934
+ this.callbacks.onError(error);
52935
+ });
52936
+ }
52937
+ createWindow(name) {
52938
+ if (!this.connected) {
52939
+ return;
52940
+ }
52941
+ const argv = name ? ["new-window", "-n", name] : ["new-window"];
52942
+ this.runAndRefresh(argv).catch((error) => {
52943
+ this.callbacks.onError(error);
52944
+ });
52945
+ }
52946
+ closeWindow(windowId) {
52947
+ if (!this.connected) {
52948
+ return;
52949
+ }
52950
+ this.closeWindowInternal(windowId).catch((error) => {
52951
+ this.callbacks.onError(error);
52952
+ });
52953
+ }
52954
+ closePane(paneId) {
52955
+ if (!this.connected) {
52956
+ return;
52957
+ }
52958
+ this.runAndRefresh(["kill-pane", "-t", paneId], true).catch((error) => {
52959
+ this.callbacks.onError(error);
52686
52960
  });
52961
+ }
52962
+ renameWindow(windowId, name) {
52963
+ if (!this.connected) {
52964
+ return;
52965
+ }
52966
+ this.runAndRefresh(["rename-window", "-t", windowId, name]).catch((error) => {
52967
+ this.callbacks.onError(error);
52968
+ });
52969
+ }
52970
+ async connectSshClient() {
52971
+ if (!this.device) {
52972
+ throw new Error("SSH device not loaded");
52973
+ }
52974
+ const host = this.device.host;
52975
+ const port = this.device.port ?? 22;
52976
+ const username = resolveSshUsername(this.device.username, this.device.authMode);
52687
52977
  if (this.device.authMode === "configRef" || !host && this.device.sshConfigRef) {
52688
52978
  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
52979
  }
@@ -52693,14 +52983,14 @@ ${detail}`) : error;
52693
52983
  const authConfig = {
52694
52984
  host,
52695
52985
  port,
52696
- username: resolvedUsername
52986
+ username
52697
52987
  };
52698
52988
  switch (this.device.authMode) {
52699
52989
  case "password": {
52700
52990
  if (!this.device.passwordEnc) {
52701
52991
  throw new Error("auth_password_missing: \u5BC6\u7801\u8BA4\u8BC1\u672A\u63D0\u4F9B\u5BC6\u7801");
52702
52992
  }
52703
- authConfig.password = await decryptWithContext(this.device.passwordEnc, {
52993
+ authConfig.password = await this.deps.decrypt(this.device.passwordEnc, {
52704
52994
  scope: "device",
52705
52995
  entityId: this.device.id,
52706
52996
  field: "password_enc"
@@ -52711,13 +53001,13 @@ ${detail}`) : error;
52711
53001
  if (!this.device.privateKeyEnc) {
52712
53002
  throw new Error("auth_key_missing: \u79C1\u94A5\u8BA4\u8BC1\u672A\u63D0\u4F9B\u79C1\u94A5");
52713
53003
  }
52714
- authConfig.privateKey = await decryptWithContext(this.device.privateKeyEnc, {
53004
+ authConfig.privateKey = await this.deps.decrypt(this.device.privateKeyEnc, {
52715
53005
  scope: "device",
52716
53006
  entityId: this.device.id,
52717
53007
  field: "private_key_enc"
52718
53008
  });
52719
53009
  if (this.device.privateKeyPassphraseEnc) {
52720
- authConfig.passphrase = await decryptWithContext(this.device.privateKeyPassphraseEnc, {
53010
+ authConfig.passphrase = await this.deps.decrypt(this.device.privateKeyPassphraseEnc, {
52721
53011
  scope: "device",
52722
53012
  entityId: this.device.id,
52723
53013
  field: "private_key_passphrase_enc"
@@ -52729,22 +53019,19 @@ ${detail}`) : error;
52729
53019
  authConfig.agent = resolveSshAgentSocket("agent");
52730
53020
  break;
52731
53021
  }
52732
- case "configRef": {
52733
- break;
52734
- }
52735
53022
  case "auto": {
52736
53023
  const agentSocket = resolveSshAgentSocket("auto");
52737
53024
  if (agentSocket) {
52738
53025
  authConfig.agent = agentSocket;
52739
53026
  }
52740
53027
  if (this.device.privateKeyEnc) {
52741
- authConfig.privateKey = await decryptWithContext(this.device.privateKeyEnc, {
53028
+ authConfig.privateKey = await this.deps.decrypt(this.device.privateKeyEnc, {
52742
53029
  scope: "device",
52743
53030
  entityId: this.device.id,
52744
53031
  field: "private_key_enc"
52745
53032
  });
52746
53033
  } else if (this.device.passwordEnc) {
52747
- authConfig.password = await decryptWithContext(this.device.passwordEnc, {
53034
+ authConfig.password = await this.deps.decrypt(this.device.passwordEnc, {
52748
53035
  scope: "device",
52749
53036
  entityId: this.device.id,
52750
53037
  field: "password_enc"
@@ -52752,17 +53039,15 @@ ${detail}`) : error;
52752
53039
  }
52753
53040
  break;
52754
53041
  }
53042
+ case "configRef":
53043
+ break;
52755
53044
  }
52756
53045
  if (this.device.authMode === "auto" && !authConfig.agent && !authConfig.privateKey && !authConfig.password) {
52757
53046
  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
53047
  }
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) => {
53048
+ const client = this.deps.createClient();
53049
+ this.sshClient = client;
53050
+ await new Promise((resolve, reject) => {
52766
53051
  let settled = false;
52767
53052
  const resolveOnce = () => {
52768
53053
  if (settled) {
@@ -52776,429 +53061,290 @@ ${detail}`) : error;
52776
53061
  return;
52777
53062
  }
52778
53063
  settled = true;
52779
- reject(error instanceof Error ? error : new Error(String(error)));
53064
+ reject(error);
52780
53065
  };
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
- });
53066
+ client.on("ready", () => {
53067
+ resolveOnce();
52841
53068
  });
52842
- conn.on("error", (err) => {
52843
- logStage("connect_error", { error: err.message });
53069
+ client.on("error", (error) => {
52844
53070
  updateDeviceRuntimeStatus(this.deviceId, {
52845
53071
  lastSeenAt: new Date().toISOString(),
52846
53072
  tmuxAvailable: false,
52847
- lastError: err.message
53073
+ lastError: error.message
52848
53074
  });
52849
- rejectOnce(err);
53075
+ if (!settled) {
53076
+ rejectOnce(error);
53077
+ return;
53078
+ }
53079
+ if (!this.manualDisconnect) {
53080
+ this.callbacks.onError(error);
53081
+ this.shutdownInternal(true);
53082
+ }
52850
53083
  });
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"));
53084
+ client.on("close", () => {
53085
+ if (!settled) {
53086
+ rejectOnce(new Error("SSH connection closed before ready"));
53087
+ return;
52855
53088
  }
52856
- this.cleanup();
52857
53089
  if (!this.manualDisconnect) {
52858
- this.onClose();
53090
+ this.shutdownInternal(true);
52859
53091
  }
52860
53092
  });
52861
- logStage("connect_attempt");
52862
- conn.connect(authConfig);
53093
+ client.connect(authConfig);
52863
53094
  });
52864
53095
  }
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;
53096
+ async openCommandChannel() {
53097
+ const sshClient = this.requireSshClient();
53098
+ const stream = await new Promise((resolve, reject) => {
53099
+ sshClient.exec("/bin/sh -s", { pty: false }, (error, channel) => {
53100
+ if (error) {
53101
+ reject(error);
53102
+ return;
53103
+ }
53104
+ resolve(channel);
53105
+ });
53106
+ });
53107
+ this.commandStdoutBuffer = "";
53108
+ this.pendingCommand = null;
53109
+ this.commandStream = stream;
53110
+ stream.on("data", (data) => {
53111
+ this.commandStdoutBuffer += data.toString();
53112
+ this.flushCommandBuffer();
53113
+ });
53114
+ stream.stderr.on("data", (data) => {
53115
+ if (this.pendingCommand) {
53116
+ this.pendingCommand.stderr += data.toString();
52892
53117
  }
52893
- if (resolvedPaneId && !data.paneId) {
52894
- this.onEvent({
52895
- ...event,
52896
- data: {
52897
- ...data,
52898
- paneId: resolvedPaneId
52899
- }
52900
- });
52901
- return;
53118
+ });
53119
+ stream.on("close", () => {
53120
+ this.rejectPendingCommand(new Error("SSH command channel closed"));
53121
+ this.commandStream = null;
53122
+ if (!this.manualDisconnect) {
53123
+ this.shutdownInternal(true);
52902
53124
  }
52903
- }
52904
- this.onEvent(event);
53125
+ });
53126
+ const bootstrap = await this.runShell(buildSshBootstrapScript());
53127
+ const parsed = parseSshBootstrapOutput(bootstrap.stdout);
53128
+ if (!parsed.ok) {
53129
+ updateDeviceRuntimeStatus(this.deviceId, {
53130
+ lastSeenAt: new Date().toISOString(),
53131
+ tmuxAvailable: false,
53132
+ lastError: parsed.reason
53133
+ });
53134
+ throw new Error(`remote tmux unavailable: ${parsed.reason}`);
53135
+ }
53136
+ this.tmuxBin = parsed.tmuxBin;
53137
+ this.remoteHomeDir = parsed.homeDir;
53138
+ }
53139
+ async ensureRemoteRuntimeDirs() {
53140
+ await this.runShell([
53141
+ `mkdir -p ${quoteShellArg(this.fsPaths.rootDir)}`,
53142
+ `mkdir -p ${quoteShellArg(this.fsPaths.panesDir)}`,
53143
+ `mkdir -p ${quoteShellArg(this.fsPaths.hooksDir)}`,
53144
+ `chmod 700 ${quoteShellArg(this.fsPaths.rootDir)}`,
53145
+ `chmod 700 ${quoteShellArg(this.fsPaths.panesDir)}`,
53146
+ `chmod 700 ${quoteShellArg(this.fsPaths.hooksDir)}`
53147
+ ].join(`
53148
+ `));
53149
+ }
53150
+ async ensureSession() {
53151
+ const exists3 = await this.runTmuxAllowFailure(["has-session", "-t", this.sessionName]);
53152
+ if (exists3.exitCode === 0) {
53153
+ return;
53154
+ }
53155
+ await this.runTmux(["new-session", "-d", "-c", this.remoteHomeDir, "-s", this.sessionName]);
53156
+ }
53157
+ async startHooks() {
53158
+ await this.ensureRemoteRuntimeDirs();
53159
+ const fifoPath = this.fsPaths.hookFifoPath;
53160
+ await this.runShell(`rm -f ${quoteShellArg(fifoPath)} && mkfifo ${quoteShellArg(fifoPath)} && chmod 600 ${quoteShellArg(fifoPath)}`);
53161
+ const stopReader = await this.openReaderChannel(`exec tail -n +1 -f ${quoteShellArg(fifoPath)}`, {
53162
+ onData: (data) => {
53163
+ this.handleHookChunk(data.toString());
53164
+ },
53165
+ onClose: () => {
53166
+ if (!this.manualDisconnect) {
53167
+ this.callbacks.onError(new Error("SSH hook reader closed unexpectedly"));
53168
+ }
53169
+ }
53170
+ });
53171
+ this.hookReadAbort = () => {
53172
+ stopReader();
53173
+ this.runShellAllowFailure(`rm -f ${quoteShellArg(fifoPath)}`);
53174
+ };
53175
+ await this.installHook("alert-bell", ["bell", "#{window_id}", "#{pane_id}"]);
53176
+ await this.installHook("pane-exited", ["pane-exited", "#{window_id}", "#{pane_id}"]);
53177
+ await this.installHook("pane-died", ["pane-died", "#{window_id}", "#{pane_id}"]);
53178
+ }
53179
+ async stopHooks() {
53180
+ await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "alert-bell"]);
53181
+ await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "pane-exited"]);
53182
+ await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "pane-died"]);
53183
+ this.hookReadAbort?.();
53184
+ this.hookReadAbort = null;
53185
+ this.hookBuffer = "";
53186
+ }
53187
+ async installHook(hookName, fields) {
53188
+ const fifoPath = this.fsPaths.hookFifoPath;
53189
+ const innerScript = `printf '%s\\t%s\\t%s\\n' ${fields.map((field) => quoteShellArg(field)).join(" ")} >> ${quoteShellArg(fifoPath)}`;
53190
+ await this.runTmux([
53191
+ "set-hook",
53192
+ "-t",
53193
+ this.sessionName,
53194
+ hookName,
53195
+ `run-shell -b ${quoteShellArg(innerScript)}`
53196
+ ]);
52905
53197
  }
52906
- emitBellEventIfNeeded(paneId, data) {
52907
- if (this.bellControlEventSeen) {
52908
- return;
52909
- }
52910
- for (const byte of data) {
52911
- if (byte !== 7) {
52912
- continue;
53198
+ handleHookChunk(text2) {
53199
+ this.hookBuffer += text2;
53200
+ while (true) {
53201
+ const newlineIndex = this.hookBuffer.indexOf(`
53202
+ `);
53203
+ if (newlineIndex < 0) {
53204
+ return;
52913
53205
  }
52914
- if (!this.shouldPassBellDedup(paneId)) {
52915
- break;
53206
+ const line = this.hookBuffer.slice(0, newlineIndex).trim();
53207
+ this.hookBuffer = this.hookBuffer.slice(newlineIndex + 1);
53208
+ if (!line) {
53209
+ continue;
52916
53210
  }
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) {
53211
+ const [type, windowId, paneId] = line.split("\t");
53212
+ if (type === "bell") {
53213
+ const key = paneId || windowId || "-";
53214
+ const previous = this.bellDedup.get(key) ?? 0;
53215
+ const now = Date.now();
53216
+ if (now - previous >= BELL_DEDUP_WINDOW_MS2) {
53217
+ this.bellDedup.set(key, now);
53218
+ this.callbacks.onEvent({
53219
+ type: "bell",
53220
+ data: {
53221
+ windowId: windowId || undefined,
53222
+ paneId: paneId || this.activePaneId || undefined
53223
+ }
53224
+ });
53225
+ }
52936
53226
  continue;
52937
53227
  }
52938
- found = true;
52939
- if (pane.title !== nextTitle) {
52940
- pane.title = nextTitle;
52941
- updated = true;
53228
+ if (type === "pane-exited" || type === "pane-died") {
53229
+ this.requestSnapshot();
52942
53230
  }
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
53231
  }
52953
- this.emitSnapshotIfReady();
52954
53232
  }
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
- `);
53233
+ async runAndRefresh(argv, allowTargetMissing = false) {
53234
+ await this.runTmux(argv, allowTargetMissing);
53235
+ await this.requestSnapshotInternal();
52962
53236
  }
52963
- scheduleSnapshotAfterResize(delayMs = 120) {
52964
- if (this.resizeSnapshotTimer) {
52965
- clearTimeout(this.resizeSnapshotTimer);
53237
+ async closeWindowInternal(windowId) {
53238
+ const count2 = Number.parseInt((await this.runTmux(["display-message", "-p", "-t", this.sessionName, "#{session_windows}"])).stdout.trim() || "0", 10);
53239
+ if (count2 <= 1) {
53240
+ await this.runTmux(["new-window", "-d", "-t", this.sessionName]);
52966
53241
  }
52967
- this.resizeSnapshotTimer = setTimeout(() => {
52968
- this.resizeSnapshotTimer = null;
52969
- this.requestSnapshot();
52970
- }, delayMs);
52971
- }
52972
- sendInput(paneId, data) {
52973
- if (!this.connected)
52974
- return;
52975
- this.sendUtf8Bytes(paneId, new TextEncoder().encode(data));
53242
+ await this.runAndRefresh(["kill-window", "-t", windowId], true);
52976
53243
  }
52977
- sendUtf8Bytes(paneId, data) {
52978
- if (data.length === 0) {
53244
+ async resizePaneInternal(paneId, cols, rows) {
53245
+ const safeCols = Math.max(2, Math.floor(cols));
53246
+ const safeRows = Math.max(2, Math.floor(rows));
53247
+ const windowId = this.findPaneWindowId(paneId) ?? (await this.runTmux(["display-message", "-p", "-t", paneId, "#{window_id}"], true)).stdout.trim();
53248
+ if (!windowId) {
52979
53249
  return;
52980
53250
  }
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
- }
52991
- }
52992
- sendKey(paneId, key) {
52993
- if (!this.connected)
52994
- return;
52995
- const cmd = `send-keys -t ${paneId} ${key}
52996
- `;
52997
- this.sendCommand(cmd);
53251
+ await this.runTmux(["resize-window", "-t", windowId, "-x", String(safeCols), "-y", String(safeRows)], true);
53252
+ await this.requestSnapshotInternal();
52998
53253
  }
52999
- selectWindow(windowId) {
53000
- if (!this.connected) {
53001
- console.log("[tmux] cannot select window: not connected");
53002
- return;
53003
- }
53254
+ async selectPaneInternal(windowId, paneId, size) {
53004
53255
  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
53256
  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) {
53257
+ await this.runTmux(["select-window", "-t", windowId], true);
53258
+ await this.runTmux(["select-pane", "-t", paneId], true);
53259
+ await this.startPipeForPane(paneId);
53260
+ if (size) {
53261
+ await this.resizePaneInternal(paneId, size.cols, size.rows);
53262
+ }
53263
+ this.callbacks.onEvent({
53264
+ type: "pane-active",
53265
+ data: { windowId, paneId }
53266
+ });
53267
+ await this.capturePaneHistory(paneId);
53268
+ await this.requestSnapshotInternal();
53269
+ }
53270
+ async capturePaneHistory(paneId) {
53271
+ const mode = (await this.runTmux(["display-message", "-p", "-t", paneId, "#{alternate_on}"], true)).stdout.trim();
53272
+ const alternateScreen = mode === "1";
53273
+ const normal = (await this.runTmux(["capture-pane", "-t", paneId, "-S", "-", "-E", "-", "-e", "-p"], true, 30000)).stdout;
53274
+ const alternate = (await this.runTmux(["capture-pane", "-t", paneId, "-a", "-S", "-", "-E", "-", "-e", "-p", "-q"], true, 30000)).stdout;
53275
+ const history = alternateScreen ? hasRenderableTerminalContent2(normal) ? normal : alternate : normal || alternate;
53276
+ if (history) {
53277
+ this.callbacks.onTerminalHistory(paneId, history, alternateScreen);
53278
+ }
53279
+ }
53280
+ async requestSnapshotInternal() {
53024
53281
  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
- `);
53065
- }
53066
- this.scheduleSnapshotAfterResize();
53067
- }
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
- `);
53077
- }
53078
- }
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
- }
53130
53282
  return;
53131
53283
  }
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([]);
53284
+ const [sessionRes, windowsRes, panesRes] = await Promise.all([
53285
+ this.runTmuxAllowFailure([
53286
+ "display-message",
53287
+ "-p",
53288
+ "-t",
53289
+ this.sessionName,
53290
+ "#{session_id}\t#{session_name}"
53291
+ ]),
53292
+ this.runTmuxAllowFailure([
53293
+ "list-windows",
53294
+ "-t",
53295
+ this.sessionName,
53296
+ "-F",
53297
+ "#{window_id}\t#{window_index}\t#{window_name}\t#{window_active}"
53298
+ ]),
53299
+ this.runTmuxAllowFailure([
53300
+ "list-panes",
53301
+ "-t",
53302
+ this.sessionName,
53303
+ "-F",
53304
+ "#{pane_id}\t#{window_id}\t#{pane_index}\t#{pane_title}\t#{pane_active}\t#{pane_width}\t#{pane_height}"
53305
+ ])
53306
+ ]);
53307
+ if (sessionRes.exitCode !== 0 || windowsRes.exitCode !== 0 || panesRes.exitCode !== 0) {
53308
+ this.callbacks.onSnapshot({ deviceId: this.deviceId, session: null });
53156
53309
  return;
53157
53310
  }
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();
53311
+ this.parseSnapshotSession(sessionRes.stdout.split(/\r?\n/));
53312
+ this.parseSnapshotWindows(windowsRes.stdout.split(/\r?\n/));
53313
+ this.parseSnapshotPanes(panesRes.stdout.split(/\r?\n/));
53314
+ this.emitSnapshot();
53175
53315
  }
53176
53316
  parseSnapshotSession(lines) {
53317
+ this.snapshotSession = null;
53177
53318
  for (const line of lines) {
53178
- if (!line.trim())
53319
+ if (!line.trim()) {
53179
53320
  continue;
53321
+ }
53180
53322
  const [id, name] = line.split("\t");
53181
- if (!id)
53182
- continue;
53183
- this.snapshotSession = { id, name: name ?? "" };
53323
+ if (id) {
53324
+ this.snapshotSession = { id, name: name ?? "" };
53325
+ }
53184
53326
  return;
53185
53327
  }
53186
53328
  }
53187
53329
  parseSnapshotWindows(lines) {
53188
53330
  this.snapshotWindows.clear();
53189
- this.snapshotPanesReady = false;
53190
53331
  for (const line of lines) {
53191
- if (!line.trim())
53332
+ if (!line.trim()) {
53192
53333
  continue;
53334
+ }
53193
53335
  const [id, indexRaw, name, activeRaw] = line.split("\t");
53194
- if (!id)
53336
+ if (!id) {
53195
53337
  continue;
53338
+ }
53196
53339
  const index = Number.parseInt(indexRaw ?? "", 10);
53197
53340
  const active = activeRaw === "1";
53341
+ if (active) {
53342
+ this.activeWindowId = id;
53343
+ }
53198
53344
  this.snapshotWindows.set(id, {
53199
53345
  id,
53200
- name: name ?? "",
53201
53346
  index: Number.isNaN(index) ? 0 : index,
53347
+ name: name ?? "",
53202
53348
  active,
53203
53349
  panes: []
53204
53350
  });
@@ -53209,195 +53355,493 @@ ssh stderr: ${stderrText}`) : err2;
53209
53355
  window2.panes = [];
53210
53356
  }
53211
53357
  for (const line of lines) {
53212
- if (!line.trim())
53358
+ if (!line.trim()) {
53213
53359
  continue;
53360
+ }
53214
53361
  const [paneId, windowId, indexRaw, titleRaw, activeRaw, widthRaw, heightRaw] = line.split("\t");
53215
- if (!paneId || !windowId)
53362
+ if (!paneId || !windowId) {
53216
53363
  continue;
53364
+ }
53217
53365
  const index = Number.parseInt(indexRaw ?? "", 10);
53218
53366
  const width = Number.parseInt(widthRaw ?? "", 10);
53219
53367
  const height = Number.parseInt(heightRaw ?? "", 10);
53220
- const title = titleRaw?.trim() ? titleRaw : undefined;
53221
53368
  const pane = {
53222
53369
  id: paneId,
53223
53370
  windowId,
53224
53371
  index: Number.isNaN(index) ? 0 : index,
53225
- title: this.pendingPaneTitles.get(paneId) ?? title,
53372
+ title: this.pendingPaneTitles.get(paneId) ?? (titleRaw?.trim() ? titleRaw : undefined),
53226
53373
  active: activeRaw === "1",
53227
53374
  width: Number.isNaN(width) ? 0 : width,
53228
53375
  height: Number.isNaN(height) ? 0 : height
53229
53376
  };
53230
- const win = this.snapshotWindows.get(windowId);
53231
- if (!win)
53377
+ if (pane.active) {
53378
+ this.activePaneId = paneId;
53379
+ this.activeWindowId = windowId;
53380
+ }
53381
+ const window2 = this.snapshotWindows.get(windowId);
53382
+ if (!window2) {
53232
53383
  continue;
53233
- win.panes.push(pane);
53384
+ }
53385
+ window2.panes.push(pane);
53234
53386
  this.pendingPaneTitles.delete(paneId);
53235
53387
  }
53236
- for (const win of this.snapshotWindows.values()) {
53237
- win.panes.sort((a, b3) => a.index - b3.index);
53388
+ for (const window2 of this.snapshotWindows.values()) {
53389
+ window2.panes.sort((left, right) => left.index - right.index);
53238
53390
  }
53239
- this.snapshotPanesReady = true;
53240
53391
  }
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 = {
53392
+ emitSnapshot() {
53393
+ const session = this.snapshotSession ? {
53250
53394
  id: this.snapshotSession.id,
53251
53395
  name: this.snapshotSession.name,
53252
- windows
53253
- };
53254
- this.onSnapshot({
53396
+ windows: Array.from(this.snapshotWindows.values()).sort((left, right) => left.index - right.index)
53397
+ } : null;
53398
+ this.callbacks.onSnapshot({
53255
53399
  deviceId: this.deviceId,
53256
53400
  session
53257
53401
  });
53258
53402
  }
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);
53403
+ findPaneWindowId(paneId) {
53404
+ for (const window2 of this.snapshotWindows.values()) {
53405
+ if (window2.panes.some((pane) => pane.id === paneId)) {
53406
+ return window2.id;
53407
+ }
53267
53408
  }
53409
+ return null;
53410
+ }
53411
+ async startPipeForPane(paneId) {
53412
+ await this.queuePipeTransition(async () => {
53413
+ if (this.currentPipePaneId === paneId) {
53414
+ return;
53415
+ }
53416
+ await this.stopPipeNow();
53417
+ const fifoPath = this.fsPaths.paneFifoPath(paneId);
53418
+ await this.ensureRemoteRuntimeDirs();
53419
+ await this.runShell(`rm -f ${quoteShellArg(fifoPath)} && mkfifo ${quoteShellArg(fifoPath)} && chmod 600 ${quoteShellArg(fifoPath)}`);
53420
+ const parser = createPaneTitleParser({
53421
+ onTitle: (title) => {
53422
+ this.pendingPaneTitles.set(paneId, title);
53423
+ this.requestSnapshot();
53424
+ }
53425
+ });
53426
+ const stopReader = await this.openReaderChannel(`exec cat ${quoteShellArg(fifoPath)}`, {
53427
+ onData: (raw) => {
53428
+ const output = parser.push(raw);
53429
+ if (Array.from(raw).includes(7)) {
53430
+ this.callbacks.onEvent({ type: "bell", data: { paneId } });
53431
+ }
53432
+ if (output.length > 0) {
53433
+ this.callbacks.onTerminalOutput(paneId, output);
53434
+ }
53435
+ },
53436
+ onClose: () => {
53437
+ if (!this.manualDisconnect && this.currentPipePaneId === paneId) {
53438
+ this.callbacks.onError(new Error(`SSH pane reader closed unexpectedly: ${paneId}`));
53439
+ }
53440
+ }
53441
+ });
53442
+ this.pipeReadAbort = () => {
53443
+ stopReader();
53444
+ this.runShellAllowFailure(`rm -f ${quoteShellArg(fifoPath)}`);
53445
+ };
53446
+ await this.runTmux(["pipe-pane", "-O", "-t", paneId, `cat >${fifoPath}`]);
53447
+ this.currentPipePaneId = paneId;
53448
+ });
53268
53449
  }
53269
- emitTerminalOutput(paneId, data) {
53270
- this.emitBellEventIfNeeded(paneId, data);
53271
- this.onTerminalOutput(paneId, data);
53450
+ async stopPipe() {
53451
+ await this.queuePipeTransition(() => this.stopPipeNow());
53272
53452
  }
53273
- handleNonControlOutput(line) {
53274
- if (this.ready) {
53275
- return;
53453
+ async stopPipeNow() {
53454
+ const paneId = this.currentPipePaneId;
53455
+ this.currentPipePaneId = null;
53456
+ if (paneId) {
53457
+ await this.runTmuxAllowFailure(["pipe-pane", "-t", paneId]);
53276
53458
  }
53277
- if (this.startupNonControlOutput.length >= 20) {
53459
+ this.pipeReadAbort?.();
53460
+ this.pipeReadAbort = null;
53461
+ }
53462
+ queuePipeTransition(task) {
53463
+ const next = this.pipeTransition.catch(() => {
53278
53464
  return;
53465
+ }).then(task);
53466
+ this.pipeTransition = next;
53467
+ return next;
53468
+ }
53469
+ async runTmux(argv, allowTargetMissing = false, timeoutMs = 1e4) {
53470
+ const result = await this.runTmuxAllowFailure(argv, timeoutMs);
53471
+ if (result.exitCode === 0) {
53472
+ return result;
53473
+ }
53474
+ const message = (result.stderr.trim() || result.stdout.trim() || `tmux command failed: ${argv.join(" ")}`).trim();
53475
+ if (allowTargetMissing && this.isRecoverableTargetMissingError(message)) {
53476
+ this.recoverFromTargetMissingError(message);
53477
+ return result;
53279
53478
  }
53280
- this.startupNonControlOutput.push(line);
53479
+ updateDeviceRuntimeStatus(this.deviceId, {
53480
+ lastSeenAt: new Date().toISOString(),
53481
+ tmuxAvailable: false,
53482
+ lastError: message
53483
+ });
53484
+ throw new Error(message);
53281
53485
  }
53282
- disconnect() {
53283
- this.manualDisconnect = true;
53284
- this.cleanup();
53486
+ async runTmuxAllowFailure(argv, timeoutMs = 1e4) {
53487
+ return this.runShell(`${quoteShellArg(this.tmuxBin)} ${joinShellArgs(argv)}`, timeoutMs);
53285
53488
  }
53286
- wasManuallyDisconnected() {
53287
- return this.manualDisconnect;
53489
+ async runShell(command, timeoutMs = 1e4) {
53490
+ return this.enqueueShellCommand(command, timeoutMs);
53288
53491
  }
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;
53492
+ async runShellAllowFailure(command, timeoutMs = 1e4) {
53493
+ try {
53494
+ return await this.enqueueShellCommand(command, timeoutMs);
53495
+ } catch (error) {
53496
+ return {
53497
+ exitCode: 1,
53498
+ stdout: "",
53499
+ stderr: error instanceof Error ? error.message : String(error)
53500
+ };
53295
53501
  }
53296
- const data = lines.join(`
53297
- `);
53298
- const state = this.historyCaptureStates.get(request.paneId);
53299
- if (!state) {
53502
+ }
53503
+ enqueueShellCommand(command, timeoutMs) {
53504
+ const next = this.commandQueue.catch(() => {
53300
53505
  return;
53506
+ }).then(() => this.executeShellCommand(command, timeoutMs));
53507
+ this.commandQueue = next.then(() => {
53508
+ return;
53509
+ });
53510
+ return next;
53511
+ }
53512
+ executeShellCommand(command, timeoutMs) {
53513
+ const stream = this.commandStream;
53514
+ if (!stream) {
53515
+ return Promise.reject(new Error("SSH command channel not ready"));
53301
53516
  }
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);
53517
+ const commandId = crypto.randomUUID();
53518
+ const wrappedCommand = `{ ${command}; } 2>&1
53519
+ printf '\\036TMEX_END %s %d\\036\\n' ${quoteShellArg(commandId)} $?
53520
+ `;
53521
+ return new Promise((resolve, reject) => {
53522
+ const timer = setTimeout(() => {
53523
+ if (!this.pendingCommand || this.pendingCommand.id !== commandId) {
53524
+ return;
53525
+ }
53526
+ this.pendingCommand = null;
53527
+ reject(new Error(`remote command timed out: ${command}`));
53528
+ }, timeoutMs);
53529
+ this.pendingCommand = {
53530
+ id: commandId,
53531
+ stderr: "",
53532
+ resolve,
53533
+ reject,
53534
+ timer
53535
+ };
53536
+ stream.write(wrappedCommand);
53537
+ });
53538
+ }
53539
+ flushCommandBuffer() {
53540
+ while (true) {
53541
+ const sentinelIndex = this.commandStdoutBuffer.indexOf(COMMAND_SENTINEL);
53542
+ if (sentinelIndex < 0) {
53543
+ return;
53544
+ }
53545
+ const sentinelEnd = this.commandStdoutBuffer.indexOf("\x1E", sentinelIndex + COMMAND_SENTINEL.length);
53546
+ if (sentinelEnd < 0) {
53547
+ return;
53548
+ }
53549
+ const payload = this.commandStdoutBuffer.slice(sentinelIndex + COMMAND_SENTINEL.length, sentinelEnd).trim();
53550
+ const [commandId = "", exitCodeRaw = "1"] = payload.split(/\s+/);
53551
+ const stdout = this.commandStdoutBuffer.slice(0, sentinelIndex);
53552
+ this.commandStdoutBuffer = this.commandStdoutBuffer.slice(sentinelEnd + 1).replace(/^\r?\n/, "");
53553
+ const pending = this.pendingCommand;
53554
+ if (!pending || pending.id !== commandId) {
53555
+ continue;
53556
+ }
53557
+ this.pendingCommand = null;
53558
+ clearTimeout(pending.timer);
53559
+ pending.resolve({
53560
+ exitCode: Number.parseInt(exitCodeRaw, 10) || 0,
53561
+ stdout,
53562
+ stderr: pending.stderr
53563
+ });
53309
53564
  }
53310
53565
  }
53311
- handleCapturePaneModeOutput(lines) {
53312
- const paneId = this.pendingCapturePaneModeRequests.shift() ?? null;
53313
- if (!paneId) {
53566
+ rejectPendingCommand(error) {
53567
+ const pending = this.pendingCommand;
53568
+ if (!pending) {
53314
53569
  return;
53315
53570
  }
53316
- const state = this.historyCaptureStates.get(paneId);
53317
- if (!state) {
53571
+ this.pendingCommand = null;
53572
+ clearTimeout(pending.timer);
53573
+ pending.reject(error);
53574
+ }
53575
+ async openReaderChannel(command, options) {
53576
+ const sshClient = this.requireSshClient();
53577
+ const stream = await new Promise((resolve, reject) => {
53578
+ sshClient.exec("/bin/sh -s", { pty: false }, (error, channel) => {
53579
+ if (error) {
53580
+ reject(error);
53581
+ return;
53582
+ }
53583
+ resolve(channel);
53584
+ });
53585
+ });
53586
+ stream.on("data", (data) => {
53587
+ options.onData(data);
53588
+ });
53589
+ stream.stderr.on("data", (data) => {
53590
+ if (!this.manualDisconnect) {
53591
+ this.callbacks.onError(new Error(data.toString().trim() || "SSH reader stderr output"));
53592
+ }
53593
+ });
53594
+ stream.on("close", () => {
53595
+ options.onClose?.();
53596
+ });
53597
+ stream.write(`${command}
53598
+ `);
53599
+ return () => {
53600
+ stream.end();
53601
+ stream.close();
53602
+ stream.destroy();
53603
+ };
53604
+ }
53605
+ isRecoverableTargetMissingError(message) {
53606
+ const normalized = message.toLowerCase();
53607
+ return normalized.includes("can't find window") || normalized.includes("can't find pane") || normalized.includes("no such window") || normalized.includes("no such pane");
53608
+ }
53609
+ recoverFromTargetMissingError(message) {
53610
+ const normalized = message.toLowerCase();
53611
+ if (normalized.includes("window")) {
53612
+ this.activeWindowId = null;
53613
+ }
53614
+ if (normalized.includes("pane")) {
53615
+ this.activePaneId = null;
53616
+ }
53617
+ this.requestSnapshot();
53618
+ }
53619
+ async shutdownInternal(notifyClose) {
53620
+ if (this.cleanupPromise) {
53621
+ await this.cleanupPromise;
53622
+ if (notifyClose && !this.closeNotified && !this.manualDisconnect) {
53623
+ this.closeNotified = true;
53624
+ this.callbacks.onClose();
53625
+ }
53318
53626
  return;
53319
53627
  }
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;
53628
+ this.connected = false;
53629
+ this.cleanupPromise = (async () => {
53630
+ await this.stopPipe().catch(() => {
53631
+ return;
53632
+ });
53633
+ await this.stopHooks().catch(() => {
53634
+ return;
53635
+ });
53636
+ await this.runShellAllowFailure(`rm -rf ${quoteShellArg(this.fsPaths.rootDir)}`).catch(() => {
53637
+ return;
53638
+ });
53639
+ this.rejectPendingCommand(new Error("SSH command channel closed"));
53640
+ this.commandStream?.end();
53641
+ this.commandStream?.close();
53642
+ this.commandStream?.destroy();
53643
+ this.commandStream = null;
53644
+ this.sshClient?.end();
53645
+ this.sshClient = null;
53646
+ })();
53647
+ await this.cleanupPromise;
53648
+ this.cleanupPromise = null;
53649
+ if (notifyClose && !this.closeNotified && !this.manualDisconnect) {
53650
+ this.closeNotified = true;
53651
+ this.callbacks.onClose();
53325
53652
  }
53326
- if (state.normal !== null && state.alternate !== null) {
53327
- this.emitCapturedHistory(paneId);
53653
+ }
53654
+ requireSshClient() {
53655
+ if (!this.sshClient) {
53656
+ throw new Error("SSH client not connected");
53328
53657
  }
53658
+ return this.sshClient;
53329
53659
  }
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;
53660
+ }
53661
+
53662
+ // ../../apps/gateway/src/tmux-client/device-session-runtime.ts
53663
+ function createDefaultConnection(options) {
53664
+ const device = getDeviceById(options.deviceId);
53665
+ if (device?.type === "local") {
53666
+ return new LocalExternalTmuxConnection(options);
53667
+ }
53668
+ return new SshExternalTmuxConnection(options);
53669
+ }
53670
+
53671
+ class DeviceSessionRuntime {
53672
+ deviceId;
53673
+ connection;
53674
+ listeners = new Set;
53675
+ connectPromise = null;
53676
+ terminated = false;
53677
+ closeEmitted = false;
53678
+ manualDisconnect = false;
53679
+ constructor(options) {
53680
+ this.deviceId = options.deviceId;
53681
+ const createConnection = options.createConnection ?? createDefaultConnection;
53682
+ this.connection = createConnection({
53683
+ deviceId: this.deviceId,
53684
+ onEvent: (event) => {
53685
+ this.broadcast((listener) => listener.onEvent?.(event));
53686
+ },
53687
+ onTerminalOutput: (paneId, data) => {
53688
+ this.broadcast((listener) => listener.onTerminalOutput?.(paneId, data));
53689
+ },
53690
+ onTerminalHistory: (paneId, data, alternateScreen) => {
53691
+ this.broadcast((listener) => listener.onTerminalHistory?.(paneId, data, alternateScreen));
53692
+ },
53693
+ onSnapshot: (payload) => {
53694
+ this.broadcast((listener) => listener.onSnapshot?.(payload));
53695
+ },
53696
+ onError: (error) => {
53697
+ this.broadcast((listener) => listener.onError?.(error));
53698
+ },
53699
+ onClose: () => {
53700
+ if (this.manualDisconnect || this.closeEmitted) {
53701
+ return;
53702
+ }
53703
+ this.closeEmitted = true;
53704
+ this.terminated = true;
53705
+ this.broadcast((listener) => listener.onClose?.());
53706
+ }
53707
+ });
53708
+ }
53709
+ subscribe(listener) {
53710
+ this.listeners.add(listener);
53711
+ return () => {
53712
+ this.listeners.delete(listener);
53713
+ };
53714
+ }
53715
+ async connect() {
53716
+ if (this.terminated && !this.connectPromise) {
53717
+ return Promise.reject(new Error(`Device session runtime already terminated: ${this.deviceId}`));
53337
53718
  }
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;
53719
+ if (this.connectPromise) {
53720
+ return this.connectPromise;
53347
53721
  }
53348
- if (selected) {
53349
- console.log("[tmux] sending history for pane", paneId, "data length:", selected.length);
53350
- this.onTerminalHistory(paneId, selected);
53722
+ this.connectPromise = this.connection.connect().catch((error) => {
53723
+ this.terminated = true;
53724
+ throw error;
53725
+ });
53726
+ return this.connectPromise;
53727
+ }
53728
+ disconnect() {
53729
+ if (this.terminated) {
53730
+ return;
53351
53731
  }
53352
- this.historyCaptureStates.delete(paneId);
53732
+ this.terminated = true;
53733
+ this.manualDisconnect = true;
53734
+ this.connection.disconnect();
53353
53735
  }
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);
53736
+ async shutdown() {
53737
+ this.disconnect();
53738
+ }
53739
+ requestSnapshot() {
53740
+ this.connection.requestSnapshot();
53741
+ }
53742
+ sendInput(paneId, data) {
53743
+ this.connection.sendInput(paneId, data);
53744
+ }
53745
+ resizePane(paneId, cols, rows) {
53746
+ this.connection.resizePane(paneId, cols, rows);
53747
+ }
53748
+ selectPane(windowId, paneId) {
53749
+ this.connection.selectPane(windowId, paneId);
53750
+ }
53751
+ selectPaneWithSize(windowId, paneId, cols, rows) {
53752
+ this.connection.selectPaneWithSize(windowId, paneId, cols, rows);
53753
+ }
53754
+ selectWindow(windowId) {
53755
+ this.connection.selectWindow(windowId);
53756
+ }
53757
+ createWindow(name) {
53758
+ this.connection.createWindow(name);
53759
+ }
53760
+ closeWindow(windowId) {
53761
+ this.connection.closeWindow(windowId);
53762
+ }
53763
+ closePane(paneId) {
53764
+ this.connection.closePane(paneId);
53765
+ }
53766
+ renameWindow(windowId, name) {
53767
+ this.connection.renameWindow(windowId, name);
53768
+ }
53769
+ broadcast(action) {
53770
+ for (const listener of this.listeners) {
53771
+ try {
53772
+ action(listener);
53773
+ } catch (error) {
53774
+ console.error("[tmux-client] listener callback failed:", error);
53364
53775
  }
53365
53776
  }
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;
53777
+ }
53778
+ }
53779
+ function createDeviceSessionRuntime(options) {
53780
+ return new DeviceSessionRuntime(options);
53781
+ }
53782
+
53783
+ // ../../apps/gateway/src/tmux-client/runtime-registry.ts
53784
+ class TmuxRuntimeRegistry {
53785
+ options;
53786
+ entries = new Map;
53787
+ constructor(options) {
53788
+ this.options = options;
53789
+ }
53790
+ acquire(deviceId) {
53791
+ const existing = this.entries.get(deviceId);
53792
+ if (existing) {
53793
+ existing.refCount += 1;
53794
+ return existing.promise;
53392
53795
  }
53393
- updateDeviceRuntimeStatus(this.deviceId, {
53394
- lastSeenAt: new Date().toISOString()
53796
+ const entry = {
53797
+ refCount: 1,
53798
+ runtime: null,
53799
+ promise: this.options.createRuntime(deviceId).then((runtime) => {
53800
+ entry.runtime = runtime;
53801
+ return runtime;
53802
+ })
53803
+ };
53804
+ entry.promise = entry.promise.catch((error) => {
53805
+ if (this.entries.get(deviceId) === entry) {
53806
+ this.entries.delete(deviceId);
53807
+ }
53808
+ throw error;
53395
53809
  });
53810
+ this.entries.set(deviceId, entry);
53811
+ return entry.promise;
53396
53812
  }
53397
- isConnected() {
53398
- return this.connected;
53813
+ async release(deviceId) {
53814
+ const entry = this.entries.get(deviceId);
53815
+ if (!entry) {
53816
+ return;
53817
+ }
53818
+ entry.refCount -= 1;
53819
+ if (entry.refCount > 0) {
53820
+ return;
53821
+ }
53822
+ this.entries.delete(deviceId);
53823
+ const runtime = await entry.promise;
53824
+ await runtime.shutdown();
53825
+ }
53826
+ async shutdownAll() {
53827
+ const entries = Array.from(this.entries.values());
53828
+ this.entries.clear();
53829
+ await Promise.all(entries.map(async (entry) => {
53830
+ const runtime = entry.runtime ?? await entry.promise;
53831
+ await runtime.shutdown();
53832
+ }));
53399
53833
  }
53400
53834
  }
53835
+ function createTmuxRuntimeRegistry(options) {
53836
+ return new TmuxRuntimeRegistry(options);
53837
+ }
53838
+
53839
+ // ../../apps/gateway/src/tmux-client/registry.ts
53840
+ var tmuxRuntimeRegistry = createTmuxRuntimeRegistry({
53841
+ async createRuntime(deviceId) {
53842
+ return createDeviceSessionRuntime({ deviceId });
53843
+ }
53844
+ });
53401
53845
 
53402
53846
  // ../../apps/gateway/src/tmux/bell-context.ts
53403
53847
  function pickPaneById(windows, paneId) {
@@ -53454,7 +53898,10 @@ var defaultDeps = {
53454
53898
  listDevices: () => getAllDevices(),
53455
53899
  getDevice: (deviceId) => getDeviceById(deviceId),
53456
53900
  getSettings: () => getSiteSettings(),
53457
- createConnection: (options) => new TmuxConnection(options),
53901
+ acquireRuntime: async (deviceId) => tmuxRuntimeRegistry.acquire(deviceId),
53902
+ releaseRuntime: async (deviceId, _runtime) => {
53903
+ await tmuxRuntimeRegistry.release(deviceId);
53904
+ },
53458
53905
  async notifyBell(context) {
53459
53906
  const { device, settings, bell } = context;
53460
53907
  await eventNotifier.notify("terminal_bell", {
@@ -53506,7 +53953,7 @@ class PushSupervisor {
53506
53953
  this.running = false;
53507
53954
  for (const [deviceId, entry] of this.entries) {
53508
53955
  this.clearReconnectTimer(entry);
53509
- entry.connection?.disconnect();
53956
+ this.teardownEntry(entry);
53510
53957
  this.entries.delete(deviceId);
53511
53958
  }
53512
53959
  }
@@ -53522,7 +53969,8 @@ class PushSupervisor {
53522
53969
  generation: 1,
53523
53970
  reconnectAttempts: 0,
53524
53971
  reconnectTimer: null,
53525
- connection: null,
53972
+ runtime: null,
53973
+ detachRuntime: null,
53526
53974
  lastSnapshot: null
53527
53975
  };
53528
53976
  this.entries.set(deviceId, entry);
@@ -53549,8 +53997,13 @@ class PushSupervisor {
53549
53997
  }
53550
53998
  teardownEntry(entry) {
53551
53999
  this.clearReconnectTimer(entry);
53552
- entry.connection?.disconnect();
53553
- entry.connection = null;
54000
+ const runtime = entry.runtime;
54001
+ entry.detachRuntime?.();
54002
+ entry.detachRuntime = null;
54003
+ entry.runtime = null;
54004
+ if (runtime) {
54005
+ this.deps.releaseRuntime(entry.deviceId, runtime);
54006
+ }
53554
54007
  }
53555
54008
  clearReconnectTimer(entry) {
53556
54009
  if (!entry.reconnectTimer) {
@@ -53573,42 +54026,46 @@ class PushSupervisor {
53573
54026
  return;
53574
54027
  }
53575
54028
  const generation = entry.generation;
53576
- let connection;
53577
- connection = this.deps.createConnection({
53578
- deviceId: entry.deviceId,
54029
+ const runtime = await this.deps.acquireRuntime(entry.deviceId);
54030
+ const detachRuntime = runtime.subscribe({
53579
54031
  onEvent: (event) => {
53580
- this.handleTmuxEvent(entry.deviceId, generation, connection, event);
54032
+ this.handleTmuxEvent(entry.deviceId, generation, runtime, event);
53581
54033
  },
53582
- onTerminalOutput: () => {},
53583
- onTerminalHistory: () => {},
53584
54034
  onSnapshot: (payload) => {
53585
- this.handleSnapshot(entry.deviceId, generation, connection, payload);
54035
+ this.handleSnapshot(entry.deviceId, generation, runtime, payload);
53586
54036
  },
53587
- onError: (err) => {
53588
- console.error(`[push] tmux error on device ${entry.deviceId}:`, err);
54037
+ onError: (error) => {
54038
+ console.error(`[push] tmux error on device ${entry.deviceId}:`, error);
53589
54039
  },
53590
54040
  onClose: () => {
53591
- this.handleClose(entry.deviceId, generation, connection);
54041
+ this.handleClose(entry.deviceId, generation, runtime);
53592
54042
  }
53593
54043
  });
53594
- entry.connection = connection;
54044
+ entry.runtime = runtime;
54045
+ entry.detachRuntime = detachRuntime;
53595
54046
  try {
53596
- await connection.connect();
54047
+ await runtime.connect();
53597
54048
  const latest = this.entries.get(entry.deviceId);
53598
54049
  if (!this.running || latest !== entry || entry.generation !== generation) {
53599
- connection.disconnect();
54050
+ detachRuntime();
54051
+ entry.detachRuntime = null;
54052
+ entry.runtime = null;
54053
+ await this.deps.releaseRuntime(entry.deviceId, runtime);
53600
54054
  return;
53601
54055
  }
53602
54056
  entry.reconnectAttempts = 0;
53603
54057
  entry.lastSnapshot = null;
53604
- connection.requestSnapshot();
54058
+ runtime.requestSnapshot();
53605
54059
  } catch (err) {
53606
54060
  const latest = this.entries.get(entry.deviceId);
53607
54061
  if (!this.running || latest !== entry || entry.generation !== generation) {
53608
54062
  return;
53609
54063
  }
53610
54064
  console.error(`[push] failed connecting device ${entry.deviceId}:`, err);
53611
- entry.connection = null;
54065
+ detachRuntime();
54066
+ entry.detachRuntime = null;
54067
+ entry.runtime = null;
54068
+ await this.deps.releaseRuntime(entry.deviceId, runtime);
53612
54069
  this.scheduleReconnect(entry);
53613
54070
  }
53614
54071
  }
@@ -53634,31 +54091,35 @@ class PushSupervisor {
53634
54091
  entry.reconnectAttempts += 1;
53635
54092
  }
53636
54093
  this.clearReconnectTimer(entry);
53637
- entry.connection = null;
54094
+ entry.runtime = null;
54095
+ entry.detachRuntime = null;
53638
54096
  entry.generation += 1;
53639
54097
  entry.reconnectTimer = setTimeout(() => {
53640
54098
  entry.reconnectTimer = null;
53641
54099
  this.connectEntry(entry);
53642
54100
  }, delayMs);
53643
54101
  }
53644
- async handleClose(deviceId, generation, connection) {
54102
+ async handleClose(deviceId, generation, runtime) {
53645
54103
  const entry = this.entries.get(deviceId);
53646
- if (!entry || entry.generation !== generation || entry.connection !== connection) {
54104
+ if (!entry || entry.generation !== generation || entry.runtime !== runtime) {
53647
54105
  return;
53648
54106
  }
53649
- entry.connection = null;
54107
+ entry.detachRuntime?.();
54108
+ entry.detachRuntime = null;
54109
+ entry.runtime = null;
54110
+ await this.deps.releaseRuntime(deviceId, runtime);
53650
54111
  this.scheduleReconnect(entry);
53651
54112
  }
53652
- handleSnapshot(deviceId, generation, connection, payload) {
54113
+ handleSnapshot(deviceId, generation, runtime, payload) {
53653
54114
  const entry = this.entries.get(deviceId);
53654
- if (!entry || entry.generation !== generation || entry.connection !== connection) {
54115
+ if (!entry || entry.generation !== generation || entry.runtime !== runtime) {
53655
54116
  return;
53656
54117
  }
53657
54118
  entry.lastSnapshot = payload;
53658
54119
  }
53659
- async handleTmuxEvent(deviceId, generation, connection, event) {
54120
+ async handleTmuxEvent(deviceId, generation, runtime, event) {
53660
54121
  const entry = this.entries.get(deviceId);
53661
- if (!entry || entry.generation !== generation || entry.connection !== connection) {
54122
+ if (!entry || entry.generation !== generation || entry.runtime !== runtime) {
53662
54123
  return;
53663
54124
  }
53664
54125
  if (event.type !== "bell") {
@@ -54134,7 +54595,7 @@ function json(data, status = 200, headers = {}) {
54134
54595
  import { existsSync as existsSync2 } from "fs";
54135
54596
  import { resolve } from "path";
54136
54597
 
54137
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/migrator.js
54598
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/migrator.js
54138
54599
  import crypto2 from "crypto";
54139
54600
  import fs3 from "fs";
54140
54601
  function readMigrationFiles(config2) {
@@ -54166,7 +54627,7 @@ function readMigrationFiles(config2) {
54166
54627
  return migrationQueries;
54167
54628
  }
54168
54629
 
54169
- // ../../node_modules/.bun/drizzle-orm@0.45.1+3f568c6259dbead9/node_modules/drizzle-orm/bun-sqlite/migrator.js
54630
+ // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/bun-sqlite/migrator.js
54170
54631
  function migrate(db2, config2) {
54171
54632
  const migrations = readMigrationFiles(config2);
54172
54633
  db2.dialect.migrate(migrations, db2.session, config2);
@@ -54188,82 +54649,6 @@ function runMigrations() {
54188
54649
  }
54189
54650
  if (false) {}
54190
54651
 
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
54652
  // ../../apps/gateway/src/ws/borsh/codec-borsh.ts
54268
54653
  function createBorshClientState() {
54269
54654
  return {
@@ -54641,6 +55026,10 @@ class SwitchBarrier {
54641
55026
  const pending = this.getPending(ws, deviceId);
54642
55027
  if (!pending)
54643
55028
  return;
55029
+ const selectState = sessionStateStore.getOrCreateSelectTransaction(ws, deviceId)?.state;
55030
+ if (selectState !== "SELECTING") {
55031
+ return;
55032
+ }
54644
55033
  const { context } = pending;
54645
55034
  const borshState = ws.data?.borshState;
54646
55035
  if (!borshState)
@@ -54648,7 +55037,9 @@ class SwitchBarrier {
54648
55037
  const ackTimer = pending.timers.shift();
54649
55038
  if (ackTimer)
54650
55039
  clearTimeout(ackTimer);
54651
- sessionStateStore.transitionSelectState(ws, deviceId, "ACKED");
55040
+ if (!sessionStateStore.transitionSelectState(ws, deviceId, "ACKED")) {
55041
+ return;
55042
+ }
54652
55043
  const seq = borshState.seqGen();
54653
55044
  const ackData = encodeSwitchAck({
54654
55045
  deviceId,
@@ -54669,10 +55060,14 @@ class SwitchBarrier {
54669
55060
  }
54670
55061
  pending.callbacks.onAckSent?.();
54671
55062
  }
54672
- sendTermHistory(ws, deviceId, paneId, historyData) {
55063
+ sendTermHistory(ws, deviceId, paneId, historyData, alternateScreen) {
54673
55064
  const pending = this.getPending(ws, deviceId);
54674
55065
  if (!pending)
54675
55066
  return;
55067
+ const selectState = sessionStateStore.getOrCreateSelectTransaction(ws, deviceId)?.state;
55068
+ if (selectState !== "ACKED") {
55069
+ return;
55070
+ }
54676
55071
  const { context } = pending;
54677
55072
  if (context.paneId !== paneId) {
54678
55073
  return;
@@ -54683,12 +55078,15 @@ class SwitchBarrier {
54683
55078
  const historyTimer = pending.timers.shift();
54684
55079
  if (historyTimer)
54685
55080
  clearTimeout(historyTimer);
54686
- sessionStateStore.transitionSelectState(ws, deviceId, "HISTORY_APPLIED");
55081
+ if (!sessionStateStore.transitionSelectState(ws, deviceId, "HISTORY_APPLIED")) {
55082
+ return;
55083
+ }
54687
55084
  const historyMessages = encodeTermHistory({
54688
55085
  deviceId,
54689
55086
  paneId: context.paneId,
54690
55087
  selectToken: context.selectToken,
54691
55088
  encoding: 2,
55089
+ alternateScreen,
54692
55090
  data: historyData
54693
55091
  }, borshState.seqGen, borshState.maxFrameBytes);
54694
55092
  sendToClient(ws, historyMessages);
@@ -54702,6 +55100,10 @@ class SwitchBarrier {
54702
55100
  const pending = this.getPending(ws, deviceId);
54703
55101
  if (!pending)
54704
55102
  return;
55103
+ const selectState = sessionStateStore.getOrCreateSelectTransaction(ws, deviceId)?.state;
55104
+ if (selectState !== "ACKED" && selectState !== "HISTORY_APPLIED") {
55105
+ return;
55106
+ }
54705
55107
  const { context } = pending;
54706
55108
  if (expectedToken && !this.tokensEqual(context.selectToken, expectedToken)) {
54707
55109
  return;
@@ -54713,7 +55115,9 @@ class SwitchBarrier {
54713
55115
  clearTimeout(timer);
54714
55116
  }
54715
55117
  pending.timers = [];
54716
- sessionStateStore.transitionSelectState(ws, deviceId, "LIVE");
55118
+ if (!sessionStateStore.transitionSelectState(ws, deviceId, "LIVE")) {
55119
+ return;
55120
+ }
54717
55121
  const bufferedOutput = sessionStateStore.stopOutputBuffering(ws, deviceId);
54718
55122
  const seq = borshState.seqGen();
54719
55123
  const liveResumeData = encodeLiveResume({
@@ -54785,7 +55189,9 @@ class SwitchBarrier {
54785
55189
  this.cleanupTransaction(ws, deviceId);
54786
55190
  }
54787
55191
  completeTransaction(ws, deviceId) {
54788
- sessionStateStore.transitionSelectState(ws, deviceId, "STABLE");
55192
+ if (!sessionStateStore.transitionSelectState(ws, deviceId, "STABLE")) {
55193
+ return;
55194
+ }
54789
55195
  this.cleanupTransaction(ws, deviceId);
54790
55196
  }
54791
55197
  cleanupTransaction(ws, deviceId) {
@@ -54802,10 +55208,100 @@ class SwitchBarrier {
54802
55208
  }
54803
55209
  var switchBarrier = new SwitchBarrier;
54804
55210
 
55211
+ // ../../apps/gateway/src/ws/error-classify.ts
55212
+ function classifySshError(error) {
55213
+ const msg = error.message.toLowerCase();
55214
+ if (msg.includes("ssh_config_ref_not_supported")) {
55215
+ return {
55216
+ type: "ssh_config_ref_not_supported",
55217
+ messageKey: "sshError.configRefNotSupported"
55218
+ };
55219
+ }
55220
+ if (msg.includes("ssh_auth_sock") || msg.includes("auth_sock")) {
55221
+ return {
55222
+ type: "agent_unavailable",
55223
+ messageKey: "sshError.agentUnavailable"
55224
+ };
55225
+ }
55226
+ if (msg.includes("agent") && (msg.includes("no identities") || msg.includes("failure"))) {
55227
+ return {
55228
+ type: "agent_no_identity",
55229
+ messageKey: "sshError.agentNoIdentities"
55230
+ };
55231
+ }
55232
+ if (msg.includes("permission denied")) {
55233
+ return {
55234
+ type: "auth_failed",
55235
+ messageKey: "sshError.authFailed"
55236
+ };
55237
+ }
55238
+ if (msg.includes("all configured authentication methods failed")) {
55239
+ return {
55240
+ type: "auth_failed",
55241
+ messageKey: "sshError.authFailedGeneric"
55242
+ };
55243
+ }
55244
+ if (msg.includes("enetunreach") || msg.includes("ehostunreach")) {
55245
+ return {
55246
+ type: "network_unreachable",
55247
+ messageKey: "sshError.networkUnreachable"
55248
+ };
55249
+ }
55250
+ if (msg.includes("connect refused") || msg.includes("connection refused") || msg.includes("econnrefused")) {
55251
+ return {
55252
+ type: "connection_refused",
55253
+ messageKey: "sshError.connectionRefused"
55254
+ };
55255
+ }
55256
+ if (msg.includes("timeout") || msg.includes("etimedout")) {
55257
+ return {
55258
+ type: "timeout",
55259
+ messageKey: "sshError.connectionTimeout"
55260
+ };
55261
+ }
55262
+ if (msg.includes("host not found") || msg.includes("getaddrinfo") || msg.includes("enotfound")) {
55263
+ return {
55264
+ type: "host_not_found",
55265
+ messageKey: "sshError.hostNotFound"
55266
+ };
55267
+ }
55268
+ if (msg.includes("handshake failed") || msg.includes("unable to verify")) {
55269
+ return {
55270
+ type: "handshake_failed",
55271
+ messageKey: "sshError.handshakeFailed"
55272
+ };
55273
+ }
55274
+ 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")) {
55275
+ return {
55276
+ type: "tmux_unavailable",
55277
+ messageKey: "sshError.tmuxUnavailable"
55278
+ };
55279
+ }
55280
+ return {
55281
+ type: "unknown",
55282
+ messageKey: "sshError.unknown",
55283
+ messageParams: { message: error.message }
55284
+ };
55285
+ }
55286
+
54805
55287
  // ../../apps/gateway/src/ws/index.ts
55288
+ var defaultDeps2 = {
55289
+ acquireRuntime: async (deviceId) => tmuxRuntimeRegistry.acquire(deviceId),
55290
+ releaseRuntime: async (deviceId) => {
55291
+ await tmuxRuntimeRegistry.release(deviceId);
55292
+ }
55293
+ };
55294
+
54806
55295
  class WebSocketServer {
54807
55296
  connections = new Map;
54808
55297
  pendingConnectionEntries = new Map;
55298
+ deps;
55299
+ constructor(options = {}) {
55300
+ this.deps = {
55301
+ ...defaultDeps2,
55302
+ ...options.deps ?? {}
55303
+ };
55304
+ }
54809
55305
  clearSnapshotTimer(entry) {
54810
55306
  if (!entry.snapshotTimer)
54811
55307
  return;
@@ -54824,6 +55320,37 @@ class WebSocketServer {
54824
55320
  clearTimeout(entry.reconnectTimer);
54825
55321
  entry.reconnectTimer = null;
54826
55322
  }
55323
+ releaseConnectionEntry(deviceId, entry) {
55324
+ this.clearSnapshotTimer(entry);
55325
+ this.clearSnapshotPollTimer(entry);
55326
+ this.clearReconnectTimer(entry);
55327
+ entry.detachRuntime?.();
55328
+ entry.detachRuntime = null;
55329
+ this.deps.releaseRuntime(deviceId, entry.runtime);
55330
+ }
55331
+ attachRuntime(deviceId, runtime) {
55332
+ const listener = {
55333
+ onEvent: (event) => {
55334
+ this.broadcastTmuxEvent(deviceId, event);
55335
+ },
55336
+ onTerminalOutput: (paneId, data) => {
55337
+ this.broadcastTerminalOutput(deviceId, paneId, data);
55338
+ },
55339
+ onTerminalHistory: (paneId, data, alternateScreen) => {
55340
+ this.broadcastTerminalHistory(deviceId, paneId, data, alternateScreen);
55341
+ },
55342
+ onSnapshot: (payload) => {
55343
+ this.broadcastStateSnapshot(deviceId, payload);
55344
+ },
55345
+ onError: (error) => {
55346
+ this.broadcastError(deviceId, error);
55347
+ },
55348
+ onClose: () => {
55349
+ this.handleConnectionClose(deviceId);
55350
+ }
55351
+ };
55352
+ return runtime.subscribe(listener);
55353
+ }
54827
55354
  refreshSnapshotPolling(deviceId) {
54828
55355
  const entry = this.connections.get(deviceId);
54829
55356
  if (!entry)
@@ -54841,7 +55368,7 @@ class WebSocketServer {
54841
55368
  return;
54842
55369
  }
54843
55370
  try {
54844
- entry.connection.requestSnapshot();
55371
+ entry.runtime.requestSnapshot();
54845
55372
  } catch (err) {
54846
55373
  console.error("[ws] polling snapshot failed:", err);
54847
55374
  }
@@ -54859,7 +55386,7 @@ class WebSocketServer {
54859
55386
  }
54860
55387
  entry.snapshotTimer = null;
54861
55388
  try {
54862
- entry.connection.requestSnapshot();
55389
+ entry.runtime.requestSnapshot();
54863
55390
  } catch (err) {
54864
55391
  console.error("[ws] failed to request snapshot:", err);
54865
55392
  }
@@ -54928,10 +55455,7 @@ class WebSocketServer {
54928
55455
  delete ws.data.borshState.selectedPanes[deviceId];
54929
55456
  if (entry.clients.size === 0) {
54930
55457
  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();
55458
+ this.releaseConnectionEntry(deviceId, entry);
54935
55459
  toDelete.push(deviceId);
54936
55460
  } else {
54937
55461
  this.refreshSnapshotPolling(deviceId);
@@ -54943,10 +55467,7 @@ class WebSocketServer {
54943
55467
  }
54944
55468
  closeAll() {
54945
55469
  for (const [deviceId, entry] of this.connections) {
54946
- this.clearSnapshotTimer(entry);
54947
- this.clearSnapshotPollTimer(entry);
54948
- this.clearReconnectTimer(entry);
54949
- entry.connection.disconnect();
55470
+ this.releaseConnectionEntry(deviceId, entry);
54950
55471
  this.connections.delete(deviceId);
54951
55472
  }
54952
55473
  this.pendingConnectionEntries.clear();
@@ -55109,8 +55630,7 @@ class WebSocketServer {
55109
55630
  if (pending) {
55110
55631
  return pending;
55111
55632
  }
55112
- let creationPromise;
55113
- creationPromise = this.createDeviceConnectionEntry(deviceId, ws).then((createdEntry) => {
55633
+ const creationPromise = this.createDeviceConnectionEntry(deviceId, ws).then((createdEntry) => {
55114
55634
  if (createdEntry) {
55115
55635
  this.connections.set(deviceId, createdEntry);
55116
55636
  }
@@ -55129,13 +55649,15 @@ class WebSocketServer {
55129
55649
  return;
55130
55650
  entry.clients.add(ws);
55131
55651
  ws.data.borshState.selectedPanes[deviceId] ??= null;
55132
- const connectedPayload = exports_ws_borsh.encodePayload(exports_ws_borsh.schema.DeviceConnectedSchema, { deviceId });
55652
+ const connectedPayload = exports_ws_borsh.encodePayload(exports_ws_borsh.schema.DeviceConnectedSchema, {
55653
+ deviceId
55654
+ });
55133
55655
  this.sendEnvelope(ws, exports_ws_borsh.KIND_DEVICE_CONNECTED, connectedPayload);
55134
55656
  if (entry.lastSnapshot) {
55135
55657
  const snapshotBytes = exports_ws_borsh.encodeStateSnapshot(entry.lastSnapshot);
55136
55658
  this.sendChunked(ws, exports_ws_borsh.KIND_STATE_SNAPSHOT, snapshotBytes);
55137
55659
  } else {
55138
- entry.connection.requestSnapshot();
55660
+ entry.runtime.requestSnapshot();
55139
55661
  }
55140
55662
  }
55141
55663
  handleDeviceDisconnect(ws, deviceId) {
@@ -55144,15 +55666,14 @@ class WebSocketServer {
55144
55666
  entry.clients.delete(ws);
55145
55667
  this.refreshSnapshotPolling(deviceId);
55146
55668
  if (entry.clients.size === 0) {
55147
- this.clearSnapshotTimer(entry);
55148
- this.clearSnapshotPollTimer(entry);
55149
- this.clearReconnectTimer(entry);
55150
- entry.connection.disconnect();
55669
+ this.releaseConnectionEntry(deviceId, entry);
55151
55670
  this.connections.delete(deviceId);
55152
55671
  }
55153
55672
  }
55154
55673
  delete ws.data.borshState.selectedPanes[deviceId];
55155
- const disconnectedPayload = exports_ws_borsh.encodePayload(exports_ws_borsh.schema.DeviceDisconnectedSchema, { deviceId });
55674
+ const disconnectedPayload = exports_ws_borsh.encodePayload(exports_ws_borsh.schema.DeviceDisconnectedSchema, {
55675
+ deviceId
55676
+ });
55156
55677
  this.sendEnvelope(ws, exports_ws_borsh.KIND_DEVICE_DISCONNECTED, disconnectedPayload);
55157
55678
  }
55158
55679
  handleTmuxSelect(ws, data) {
@@ -55182,30 +55703,31 @@ class WebSocketServer {
55182
55703
  return;
55183
55704
  }
55184
55705
  switchBarrier.sendSwitchAck(ws, deviceId);
55185
- entry.connection.selectPane(windowId, paneId);
55186
55706
  const cols = data.cols ?? null;
55187
55707
  const rows = data.rows ?? null;
55188
55708
  if (cols !== null && rows !== null) {
55189
- entry.connection.resizePane(paneId, cols, rows);
55709
+ entry.runtime.selectPaneWithSize(windowId, paneId, cols, rows);
55710
+ } else {
55711
+ entry.runtime.selectPane(windowId, paneId);
55190
55712
  }
55191
55713
  }
55192
55714
  handleTmuxSelectWindow(deviceId, windowId) {
55193
55715
  const entry = this.connections.get(deviceId);
55194
55716
  if (!entry)
55195
55717
  return;
55196
- entry.connection.selectWindow(windowId);
55718
+ entry.runtime.selectWindow(windowId);
55197
55719
  }
55198
55720
  handleTermInput(deviceId, paneId, data) {
55199
55721
  const entry = this.connections.get(deviceId);
55200
55722
  if (!entry)
55201
55723
  return;
55202
- entry.connection.sendInput(paneId, data);
55724
+ entry.runtime.sendInput(paneId, data);
55203
55725
  }
55204
55726
  handleTermResize(deviceId, paneId, cols, rows) {
55205
55727
  const entry = this.connections.get(deviceId);
55206
55728
  if (!entry)
55207
55729
  return;
55208
- entry.connection.resizePane(paneId, cols, rows);
55730
+ entry.runtime.resizePane(paneId, cols, rows);
55209
55731
  }
55210
55732
  handleTermPaste(deviceId, paneId, data) {
55211
55733
  const entry = this.connections.get(deviceId);
@@ -55214,51 +55736,43 @@ class WebSocketServer {
55214
55736
  const chunkSize = 1024;
55215
55737
  for (let i = 0;i < data.length; i += chunkSize) {
55216
55738
  const chunk2 = data.slice(i, i + chunkSize);
55217
- entry.connection.sendInput(paneId, chunk2);
55739
+ entry.runtime.sendInput(paneId, chunk2);
55218
55740
  }
55219
55741
  }
55220
55742
  handleCreateWindow(deviceId, name) {
55221
55743
  const entry = this.connections.get(deviceId);
55222
55744
  if (!entry)
55223
55745
  return;
55224
- entry.connection.createWindow(name);
55746
+ entry.runtime.createWindow(name);
55225
55747
  }
55226
55748
  handleCloseWindow(deviceId, windowId) {
55227
55749
  const entry = this.connections.get(deviceId);
55228
55750
  if (!entry)
55229
55751
  return;
55230
- entry.connection.closeWindow(windowId);
55752
+ entry.runtime.closeWindow(windowId);
55231
55753
  }
55232
55754
  handleClosePane(deviceId, paneId) {
55233
55755
  const entry = this.connections.get(deviceId);
55234
55756
  if (!entry)
55235
55757
  return;
55236
- entry.connection.closePane(paneId);
55758
+ entry.runtime.closePane(paneId);
55237
55759
  }
55238
55760
  handleRenameWindow(deviceId, windowId, name) {
55239
55761
  const entry = this.connections.get(deviceId);
55240
55762
  if (!entry)
55241
55763
  return;
55242
- entry.connection.renameWindow(windowId, name);
55764
+ entry.runtime.renameWindow(windowId, name);
55243
55765
  }
55244
55766
  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
- });
55767
+ let runtime = null;
55768
+ let detachRuntime = null;
55258
55769
  try {
55259
- await connection.connect();
55770
+ runtime = await this.deps.acquireRuntime(deviceId);
55771
+ detachRuntime = this.attachRuntime(deviceId, runtime);
55772
+ await runtime.connect();
55260
55773
  return {
55261
- connection,
55774
+ runtime,
55775
+ detachRuntime,
55262
55776
  clients: new Set,
55263
55777
  lastSnapshot: null,
55264
55778
  snapshotTimer: null,
@@ -55267,6 +55781,10 @@ class WebSocketServer {
55267
55781
  reconnectTimer: null
55268
55782
  };
55269
55783
  } catch (err) {
55784
+ detachRuntime?.();
55785
+ if (runtime) {
55786
+ this.deps.releaseRuntime(deviceId, runtime);
55787
+ }
55270
55788
  const errorInfo = classifySshError(err instanceof Error ? err : new Error(String(err)));
55271
55789
  ws.send(exports_ws_borsh.encodeEnvelope(exports_ws_borsh.KIND_DEVICE_EVENT, exports_ws_borsh.encodeDeviceEventPayload({
55272
55790
  deviceId,
@@ -55353,7 +55871,7 @@ class WebSocketServer {
55353
55871
  this.sendChunked(client, exports_ws_borsh.KIND_TERM_OUTPUT, payloadBytes);
55354
55872
  }
55355
55873
  }
55356
- broadcastTerminalHistory(deviceId, paneId, data) {
55874
+ broadcastTerminalHistory(deviceId, paneId, data, alternateScreen) {
55357
55875
  const entry = this.connections.get(deviceId);
55358
55876
  if (!entry)
55359
55877
  return;
@@ -55362,7 +55880,7 @@ class WebSocketServer {
55362
55880
  if (client.data.borshState.selectedPanes[deviceId] !== paneId) {
55363
55881
  continue;
55364
55882
  }
55365
- switchBarrier.sendTermHistory(client, deviceId, paneId, historyBytes);
55883
+ switchBarrier.sendTermHistory(client, deviceId, paneId, historyBytes, alternateScreen);
55366
55884
  }
55367
55885
  }
55368
55886
  broadcastError(deviceId, err) {
@@ -55394,6 +55912,10 @@ class WebSocketServer {
55394
55912
  }
55395
55913
  this.clearSnapshotTimer(entry);
55396
55914
  this.clearSnapshotPollTimer(entry);
55915
+ entry.detachRuntime?.();
55916
+ entry.detachRuntime = null;
55917
+ const closedRuntime = entry.runtime;
55918
+ this.deps.releaseRuntime(deviceId, closedRuntime);
55397
55919
  const { sshReconnectMaxRetries, sshReconnectDelaySeconds } = getSiteSettings();
55398
55920
  if (entry.clients.size > 0 && entry.reconnectAttempts < sshReconnectMaxRetries) {
55399
55921
  entry.reconnectAttempts += 1;
@@ -55440,7 +55962,7 @@ class WebSocketServer {
55440
55962
  message: t2("sshError.reconnected")
55441
55963
  };
55442
55964
  this.broadcastDeviceEvent(retryConnection, reconnected);
55443
- retryConnection.connection.requestSnapshot();
55965
+ retryConnection.runtime.requestSnapshot();
55444
55966
  }, delay);
55445
55967
  return;
55446
55968
  }
@@ -55513,6 +56035,7 @@ async function createGatewayRuntime(options = {}) {
55513
56035
  async stop() {
55514
56036
  wsServer.closeAll();
55515
56037
  await pushSupervisor.stopAll();
56038
+ await tmuxRuntimeRegistry.shutdownAll();
55516
56039
  await telegramService.stopAll();
55517
56040
  }
55518
56041
  };
@@ -55779,7 +56302,7 @@ async function serveFrontend(req, staticRoot) {
55779
56302
  if (!requestedPath) {
55780
56303
  return new Response(t3("runtime.forbidden"), { status: 403 });
55781
56304
  }
55782
- const indexPath = join3(staticRoot, "index.html");
56305
+ const indexPath = join4(staticRoot, "index.html");
55783
56306
  const targetPath = existsSync3(requestedPath) ? requestedPath : indexPath;
55784
56307
  if (!existsSync3(targetPath)) {
55785
56308
  return new Response(t3("runtime.frontendMissing"), { status: 500 });