tmex-cli 0.4.2 → 0.4.3

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 (31) hide show
  1. package/dist/runtime/server.js +396 -88
  2. package/package.json +1 -1
  3. package/resources/fe-dist/assets/DevicePage-B9rZioAr.js +26 -0
  4. package/resources/fe-dist/assets/{DevicePage-CKaPUo7L.js.map → DevicePage-B9rZioAr.js.map} +1 -1
  5. package/resources/fe-dist/assets/DevicesPage-Bco831ry.js +17 -0
  6. package/resources/fe-dist/assets/DevicesPage-Bco831ry.js.map +1 -0
  7. package/resources/fe-dist/assets/SettingsPage-BRs8Unfx.js +17 -0
  8. package/resources/fe-dist/assets/{SettingsPage-BfkOW0fc.js.map → SettingsPage-BRs8Unfx.js.map} +1 -1
  9. package/resources/fe-dist/assets/index-40zyi_9K.css +1 -0
  10. package/resources/fe-dist/assets/index-BhBqXsZI.js +215 -0
  11. package/resources/fe-dist/assets/index-BhBqXsZI.js.map +1 -0
  12. package/resources/fe-dist/assets/select-D70hG6p7.js +17 -0
  13. package/resources/fe-dist/assets/{select-CNlE6MiW.js.map → select-D70hG6p7.js.map} +1 -1
  14. package/resources/fe-dist/assets/switch-DsyIGzyC.js +12 -0
  15. package/resources/fe-dist/assets/{switch-CxkzOIL6.js.map → switch-DsyIGzyC.js.map} +1 -1
  16. package/resources/fe-dist/assets/useValueChanged-CBb-JR7o.js +7 -0
  17. package/resources/fe-dist/assets/{useValueChanged-CO2U5MoL.js.map → useValueChanged-CBb-JR7o.js.map} +1 -1
  18. package/resources/fe-dist/index.html +2 -2
  19. package/resources/gateway-drizzle/0003_glamorous_lizard.sql +1 -0
  20. package/resources/gateway-drizzle/meta/0003_snapshot.json +542 -0
  21. package/resources/gateway-drizzle/meta/_journal.json +7 -0
  22. package/resources/fe-dist/assets/DevicePage-CKaPUo7L.js +0 -26
  23. package/resources/fe-dist/assets/DevicesPage-FqU-Dxhu.js +0 -17
  24. package/resources/fe-dist/assets/DevicesPage-FqU-Dxhu.js.map +0 -1
  25. package/resources/fe-dist/assets/SettingsPage-BfkOW0fc.js +0 -17
  26. package/resources/fe-dist/assets/index-EgHfq93I.js +0 -449
  27. package/resources/fe-dist/assets/index-EgHfq93I.js.map +0 -1
  28. package/resources/fe-dist/assets/index-Ytlj3p_q.css +0 -1
  29. package/resources/fe-dist/assets/select-CNlE6MiW.js +0 -17
  30. package/resources/fe-dist/assets/switch-CxkzOIL6.js +0 -12
  31. package/resources/fe-dist/assets/useValueChanged-CO2U5MoL.js +0 -7
@@ -20524,6 +20524,8 @@ var I18N_RESOURCES = {
20524
20524
  chatId: "Chat ID",
20525
20525
  applyTime: "Application Time",
20526
20526
  gatewayOnline: "\uD83D\uDFE2 Gateway online @ {{siteName}}",
20527
+ deviceConnectionError: `\uD83D\uDD34 {{siteName}}: Connection error on device "{{deviceName}}" ({{host}}) [{{category}}]
20528
+ {{error}}`,
20527
20529
  authSuccess: "\u2705 Authorized. You will now receive notifications.",
20528
20530
  authPending: "\u23F3 Authorization request received. Please approve in tmex settings.",
20529
20531
  authFailed: "\u274C Authorization request failed. Please contact administrator.",
@@ -20564,11 +20566,30 @@ Time: {{time}}`,
20564
20566
  hostNotFound: "Host not found: Unable to resolve hostname. Please check DNS or hostname configuration.",
20565
20567
  handshakeFailed: "Handshake failed: Unable to establish secure connection. Possibly incompatible key exchange algorithm.",
20566
20568
  tmuxUnavailable: "Remote tmux unavailable or failed to start. Please ensure tmux is installed and available in the remote shell PATH.",
20569
+ connectionClosed: "Connection closed, attempting to reconnect",
20567
20570
  unknown: "Connection failed: {{message}}",
20568
20571
  reconnecting: "Connection interrupted, reconnecting in {{delay}} seconds ({{attempt}}/{{maxRetries}})",
20569
20572
  reconnectFailed: "Auto-reconnect failed, please retry manually",
20570
20573
  reconnected: "Device reconnected automatically"
20571
20574
  },
20575
+ deviceStatus: {
20576
+ reconnecting: "Reconnecting {{delay}}s",
20577
+ offline: "Offline",
20578
+ errorBadge: {
20579
+ authFailed: "Auth failed",
20580
+ agentUnavailable: "Agent unavailable",
20581
+ agentNoIdentity: "Agent has no keys",
20582
+ configRefNotSupported: "SSH Config unsupported",
20583
+ networkUnreachable: "Network unreachable",
20584
+ connectionRefused: "Refused",
20585
+ timeout: "Timeout",
20586
+ hostNotFound: "Host not found",
20587
+ handshakeFailed: "Handshake failed",
20588
+ tmuxUnavailable: "Tmux unavailable",
20589
+ connectionClosed: "Disconnected",
20590
+ unknown: "Connection error"
20591
+ }
20592
+ },
20572
20593
  websocket: {
20573
20594
  error: "WebSocket connection error",
20574
20595
  checkGateway: "Please check Gateway status",
@@ -20862,6 +20883,8 @@ Time: {{time}}`,
20862
20883
  chatId: "chatId",
20863
20884
  applyTime: "\u7533\u8BF7\u65F6\u95F4",
20864
20885
  gatewayOnline: "\uD83D\uDFE2 Gateway online @ {{siteName}}",
20886
+ deviceConnectionError: `\uD83D\uDD34 {{siteName}}\uFF1A\u8BBE\u5907\u300C{{deviceName}}\u300D({{host}}) \u8FDE\u63A5\u5F02\u5E38 [{{category}}]
20887
+ {{error}}`,
20865
20888
  authSuccess: "\u2705 \u5DF2\u6388\u6743\uFF0C\u53EF\u63A5\u6536\u901A\u77E5\u3002",
20866
20889
  authPending: "\u23F3 \u5DF2\u6536\u5230\u6388\u6743\u7533\u8BF7\uFF0C\u8BF7\u5728 tmex \u8BBE\u7F6E\u9875\u5BA1\u6279\u3002",
20867
20890
  authFailed: "\u274C \u6388\u6743\u7533\u8BF7\u5931\u8D25\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u3002",
@@ -20902,11 +20925,30 @@ Bot\uFF1A{{botName}}
20902
20925
  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",
20903
20926
  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",
20904
20927
  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",
20928
+ connectionClosed: "\u8FDE\u63A5\u5DF2\u65AD\u5F00\uFF0C\u5C1D\u8BD5\u91CD\u8FDE\u4E2D",
20905
20929
  unknown: "\u8FDE\u63A5\u5931\u8D25\uFF1A{{message}}",
20906
20930
  reconnecting: "\u8FDE\u63A5\u4E2D\u65AD\uFF0C{{delay}} \u79D2\u540E\u81EA\u52A8\u91CD\u8FDE\uFF08{{attempt}}/{{maxRetries}}\uFF09",
20907
20931
  reconnectFailed: "\u81EA\u52A8\u91CD\u8FDE\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u91CD\u8BD5",
20908
20932
  reconnected: "\u8BBE\u5907\u5DF2\u81EA\u52A8\u91CD\u8FDE"
20909
20933
  },
20934
+ deviceStatus: {
20935
+ reconnecting: "\u91CD\u8FDE\u4E2D {{delay}}s",
20936
+ offline: "\u79BB\u7EBF",
20937
+ errorBadge: {
20938
+ authFailed: "\u8BA4\u8BC1\u5931\u8D25",
20939
+ agentUnavailable: "Agent \u4E0D\u53EF\u7528",
20940
+ agentNoIdentity: "Agent \u65E0\u5BC6\u94A5",
20941
+ configRefNotSupported: "\u4E0D\u652F\u6301 SSH Config",
20942
+ networkUnreachable: "\u7F51\u7EDC\u4E0D\u53EF\u8FBE",
20943
+ connectionRefused: "\u8FDE\u63A5\u88AB\u62D2",
20944
+ timeout: "\u8FDE\u63A5\u8D85\u65F6",
20945
+ hostNotFound: "\u4E3B\u673A\u672A\u627E\u5230",
20946
+ handshakeFailed: "\u63E1\u624B\u5931\u8D25",
20947
+ tmuxUnavailable: "tmux \u4E0D\u53EF\u7528",
20948
+ connectionClosed: "\u8FDE\u63A5\u5DF2\u65AD\u5F00",
20949
+ unknown: "\u8FDE\u63A5\u5F02\u5E38"
20950
+ }
20951
+ },
20910
20952
  websocket: {
20911
20953
  error: "WebSocket \u8FDE\u63A5\u9519\u8BEF",
20912
20954
  checkGateway: "\u8BF7\u68C0\u67E5 Gateway \u72B6\u6001",
@@ -21200,6 +21242,8 @@ Bot\uFF1A{{botName}}
21200
21242
  chatId: "Chat ID",
21201
21243
  applyTime: "\u7533\u8ACB\u6642\u9593",
21202
21244
  gatewayOnline: "\uD83D\uDFE2 Gateway online @ {{siteName}}",
21245
+ deviceConnectionError: `\uD83D\uDD34 {{siteName}}\uFF1A\u30C7\u30D0\u30A4\u30B9\u300C{{deviceName}}\u300D({{host}}) \u3067\u63A5\u7D9A\u30A8\u30E9\u30FC [{{category}}]
21246
+ {{error}}`,
21203
21247
  authSuccess: "\u2705 \u627F\u8A8D\u3055\u308C\u307E\u3057\u305F\u3002\u901A\u77E5\u3092\u53D7\u4FE1\u3067\u304D\u307E\u3059\u3002",
21204
21248
  authPending: "\u23F3 \u8A8D\u8A3C\u30EA\u30AF\u30A8\u30B9\u30C8\u3092\u53D7\u4FE1\u3057\u307E\u3057\u305F\u3002tmex \u8A2D\u5B9A\u30DA\u30FC\u30B8\u3067\u627F\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
21205
21249
  authFailed: "\u274C \u8A8D\u8A3C\u30EA\u30AF\u30A8\u30B9\u30C8\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002\u7BA1\u7406\u8005\u306B\u9023\u7D61\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
@@ -21240,11 +21284,30 @@ Bot\uFF1A{{botName}}
21240
21284
  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",
21241
21285
  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",
21242
21286
  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",
21287
+ connectionClosed: "\u63A5\u7D9A\u304C\u5207\u65AD\u3055\u308C\u307E\u3057\u305F\u3002\u518D\u63A5\u7D9A\u3092\u8A66\u307F\u3066\u3044\u307E\u3059",
21243
21288
  unknown: "\u63A5\u7D9A\u306B\u5931\u6557\u3057\u307E\u3057\u305F\uFF1A{{message}}",
21244
21289
  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",
21245
21290
  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",
21246
21291
  reconnected: "\u30C7\u30D0\u30A4\u30B9\u304C\u81EA\u52D5\u7684\u306B\u518D\u63A5\u7D9A\u3055\u308C\u307E\u3057\u305F"
21247
21292
  },
21293
+ deviceStatus: {
21294
+ reconnecting: "\u518D\u63A5\u7D9A\u4E2D {{delay}}s",
21295
+ offline: "\u30AA\u30D5\u30E9\u30A4\u30F3",
21296
+ errorBadge: {
21297
+ authFailed: "\u8A8D\u8A3C\u5931\u6557",
21298
+ agentUnavailable: "Agent \u5229\u7528\u4E0D\u53EF",
21299
+ agentNoIdentity: "Agent \u306B\u9375\u306A\u3057",
21300
+ configRefNotSupported: "SSH Config \u975E\u5BFE\u5FDC",
21301
+ networkUnreachable: "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u4E0D\u53EF\u9054",
21302
+ connectionRefused: "\u63A5\u7D9A\u62D2\u5426",
21303
+ timeout: "\u30BF\u30A4\u30E0\u30A2\u30A6\u30C8",
21304
+ hostNotFound: "\u30DB\u30B9\u30C8\u672A\u691C\u51FA",
21305
+ handshakeFailed: "\u30CF\u30F3\u30C9\u30B7\u30A7\u30A4\u30AF\u5931\u6557",
21306
+ tmuxUnavailable: "tmux \u5229\u7528\u4E0D\u53EF",
21307
+ connectionClosed: "\u5207\u65AD",
21308
+ unknown: "\u63A5\u7D9A\u30A8\u30E9\u30FC"
21309
+ }
21310
+ },
21248
21311
  websocket: {
21249
21312
  error: "WebSocket \u63A5\u7D9A\u30A8\u30E9\u30FC",
21250
21313
  checkGateway: "Gateway \u72B6\u614B\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044",
@@ -28884,7 +28947,8 @@ var deviceRuntimeStatus = sqliteTable("device_runtime_status", {
28884
28947
  deviceId: text("device_id").primaryKey().references(() => devices.id, { onDelete: "cascade" }),
28885
28948
  lastSeenAt: text("last_seen_at"),
28886
28949
  tmuxAvailable: integer("tmux_available", { mode: "boolean" }).notNull().default(false),
28887
- lastError: text("last_error")
28950
+ lastError: text("last_error"),
28951
+ lastErrorType: text("last_error_type")
28888
28952
  });
28889
28953
  var webhookEndpoints = sqliteTable("webhook_endpoints", {
28890
28954
  id: text("id").primaryKey(),
@@ -29057,7 +29121,8 @@ function createDevice(device) {
29057
29121
  deviceId: device.id,
29058
29122
  lastSeenAt: null,
29059
29123
  tmuxAvailable: false,
29060
- lastError: null
29124
+ lastError: null,
29125
+ lastErrorType: null
29061
29126
  }).onConflictDoNothing({ target: deviceRuntimeStatus.deviceId }).run();
29062
29127
  });
29063
29128
  }
@@ -29114,6 +29179,26 @@ function deleteDevice(id) {
29114
29179
  const orm = getDb();
29115
29180
  orm.delete(devices).where(eq(devices.id, id)).run();
29116
29181
  }
29182
+ function getDeviceRuntimeStatus(deviceId) {
29183
+ const orm = getDb();
29184
+ const row = orm.select().from(deviceRuntimeStatus).where(eq(deviceRuntimeStatus.deviceId, deviceId)).get();
29185
+ if (!row) {
29186
+ return {
29187
+ deviceId,
29188
+ lastSeenAt: null,
29189
+ tmuxAvailable: false,
29190
+ lastError: null,
29191
+ lastErrorType: null
29192
+ };
29193
+ }
29194
+ return {
29195
+ deviceId: row.deviceId,
29196
+ lastSeenAt: row.lastSeenAt,
29197
+ tmuxAvailable: row.tmuxAvailable,
29198
+ lastError: row.lastError,
29199
+ lastErrorType: row.lastErrorType
29200
+ };
29201
+ }
29117
29202
  function updateDeviceRuntimeStatus(deviceId, status) {
29118
29203
  const orm = getDb();
29119
29204
  const setValues = {};
@@ -29126,6 +29211,9 @@ function updateDeviceRuntimeStatus(deviceId, status) {
29126
29211
  if (status.lastError !== undefined) {
29127
29212
  setValues.lastError = status.lastError;
29128
29213
  }
29214
+ if (status.lastErrorType !== undefined) {
29215
+ setValues.lastErrorType = status.lastErrorType;
29216
+ }
29129
29217
  if (Object.keys(setValues).length === 0) {
29130
29218
  return;
29131
29219
  }
@@ -52033,6 +52121,241 @@ var eventNotifier = new EventNotifier;
52033
52121
  import { mkdirSync, rmSync } from "fs";
52034
52122
  import { homedir } from "os";
52035
52123
 
52124
+ // ../../apps/gateway/src/ws/error-classify.ts
52125
+ function classifySshError(error) {
52126
+ const msg = error.message.toLowerCase();
52127
+ if (msg.includes("ssh_config_ref_not_supported")) {
52128
+ return {
52129
+ type: "ssh_config_ref_not_supported",
52130
+ messageKey: "sshError.configRefNotSupported"
52131
+ };
52132
+ }
52133
+ if (msg.includes("ssh_auth_sock") || msg.includes("auth_sock")) {
52134
+ return {
52135
+ type: "agent_unavailable",
52136
+ messageKey: "sshError.agentUnavailable"
52137
+ };
52138
+ }
52139
+ if (msg.includes("agent") && (msg.includes("no identities") || msg.includes("failure"))) {
52140
+ return {
52141
+ type: "agent_no_identity",
52142
+ messageKey: "sshError.agentNoIdentities"
52143
+ };
52144
+ }
52145
+ if (msg.includes("permission denied")) {
52146
+ return {
52147
+ type: "auth_failed",
52148
+ messageKey: "sshError.authFailed"
52149
+ };
52150
+ }
52151
+ if (msg.includes("all configured authentication methods failed")) {
52152
+ return {
52153
+ type: "auth_failed",
52154
+ messageKey: "sshError.authFailedGeneric"
52155
+ };
52156
+ }
52157
+ if (msg.includes("enetunreach") || msg.includes("ehostunreach")) {
52158
+ return {
52159
+ type: "network_unreachable",
52160
+ messageKey: "sshError.networkUnreachable"
52161
+ };
52162
+ }
52163
+ if (msg.includes("connect refused") || msg.includes("connection refused") || msg.includes("econnrefused")) {
52164
+ return {
52165
+ type: "connection_refused",
52166
+ messageKey: "sshError.connectionRefused"
52167
+ };
52168
+ }
52169
+ if (msg.includes("timeout") || msg.includes("etimedout")) {
52170
+ return {
52171
+ type: "timeout",
52172
+ messageKey: "sshError.connectionTimeout"
52173
+ };
52174
+ }
52175
+ if (msg.includes("host not found") || msg.includes("getaddrinfo") || msg.includes("enotfound")) {
52176
+ return {
52177
+ type: "host_not_found",
52178
+ messageKey: "sshError.hostNotFound"
52179
+ };
52180
+ }
52181
+ if (msg.includes("handshake failed") || msg.includes("unable to verify")) {
52182
+ return {
52183
+ type: "handshake_failed",
52184
+ messageKey: "sshError.handshakeFailed"
52185
+ };
52186
+ }
52187
+ 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")) {
52188
+ return {
52189
+ type: "tmux_unavailable",
52190
+ messageKey: "sshError.tmuxUnavailable"
52191
+ };
52192
+ }
52193
+ if (msg.includes("ssh_connection_closed") || msg.includes("connection closed") || msg.includes("ssh command channel not ready") || msg.includes("ssh connection not ready") || msg.includes("channel closed")) {
52194
+ return {
52195
+ type: "connection_closed",
52196
+ messageKey: "sshError.connectionClosed"
52197
+ };
52198
+ }
52199
+ return {
52200
+ type: "unknown",
52201
+ messageKey: "sshError.unknown",
52202
+ messageParams: { message: error.message }
52203
+ };
52204
+ }
52205
+
52206
+ // ../../apps/gateway/src/push/connection-alerts.ts
52207
+ var NOTIFY_THROTTLE_MS = 5 * 60 * 1000;
52208
+ function toErrorObject(err) {
52209
+ if (err instanceof Error) {
52210
+ return err;
52211
+ }
52212
+ if (typeof err === "string") {
52213
+ return new Error(err);
52214
+ }
52215
+ try {
52216
+ return new Error(JSON.stringify(err));
52217
+ } catch {
52218
+ return new Error(String(err));
52219
+ }
52220
+ }
52221
+
52222
+ class ConnectionAlertNotifier {
52223
+ throttleMap = new Map;
52224
+ broadcaster = null;
52225
+ settingsProvider = () => getSiteSettings();
52226
+ persister = (deviceId, friendlyMessage, errorType) => {
52227
+ updateDeviceRuntimeStatus(deviceId, {
52228
+ lastSeenAt: new Date().toISOString(),
52229
+ lastError: friendlyMessage,
52230
+ lastErrorType: errorType
52231
+ });
52232
+ };
52233
+ telegramSender = (text2) => telegramService.sendToAuthorizedChats({ text: text2 });
52234
+ setBroadcaster(broadcaster) {
52235
+ this.broadcaster = broadcaster;
52236
+ }
52237
+ setSettingsProvider(provider) {
52238
+ this.settingsProvider = provider;
52239
+ }
52240
+ setPersister(persister) {
52241
+ this.persister = persister;
52242
+ }
52243
+ setTelegramSender(sender) {
52244
+ this.telegramSender = sender;
52245
+ }
52246
+ async notify(alert) {
52247
+ const { device, error, source, silentTelegram = false, persist = true } = alert;
52248
+ const errObj = toErrorObject(error);
52249
+ const classified = classifySshError(errObj);
52250
+ const friendlyMessage = t2(classified.messageKey, { ...classified.messageParams });
52251
+ const rawMessage = errObj.message;
52252
+ console.error(`[conn-alert] device ${device.id} (${device.name}) source=${source} type=${classified.type}: ${rawMessage}`);
52253
+ if (persist) {
52254
+ try {
52255
+ this.persister(device.id, friendlyMessage, classified.type);
52256
+ } catch (dbErr) {
52257
+ console.error("[conn-alert] failed to persist runtime status:", dbErr);
52258
+ }
52259
+ }
52260
+ if (this.broadcaster) {
52261
+ try {
52262
+ this.broadcaster(device.id, {
52263
+ deviceId: device.id,
52264
+ type: "error",
52265
+ errorType: classified.type,
52266
+ message: friendlyMessage,
52267
+ rawMessage
52268
+ });
52269
+ } catch (broadcastErr) {
52270
+ console.error("[conn-alert] failed to broadcast:", broadcastErr);
52271
+ }
52272
+ }
52273
+ if (!silentTelegram && this.shouldSendTelegram(device.id, classified.type)) {
52274
+ await this.sendTelegram(device, classified.type, friendlyMessage, rawMessage);
52275
+ }
52276
+ return {
52277
+ errorType: classified.type,
52278
+ messageKey: classified.messageKey,
52279
+ message: friendlyMessage,
52280
+ rawMessage
52281
+ };
52282
+ }
52283
+ clear(deviceId) {
52284
+ for (const key of this.throttleMap.keys()) {
52285
+ if (key.startsWith(`${deviceId}:`)) {
52286
+ this.throttleMap.delete(key);
52287
+ }
52288
+ }
52289
+ }
52290
+ shouldSendTelegram(deviceId, errorType) {
52291
+ const key = `${deviceId}:${errorType}`;
52292
+ const now = Date.now();
52293
+ const last = this.throttleMap.get(key) ?? 0;
52294
+ if (now - last < NOTIFY_THROTTLE_MS) {
52295
+ return false;
52296
+ }
52297
+ this.throttleMap.set(key, now);
52298
+ for (const [otherKey, ts] of this.throttleMap) {
52299
+ if (otherKey !== key && otherKey.startsWith(`${deviceId}:`) && now - ts >= NOTIFY_THROTTLE_MS) {
52300
+ this.throttleMap.delete(otherKey);
52301
+ }
52302
+ }
52303
+ return true;
52304
+ }
52305
+ async sendTelegram(device, errorType, friendlyMessage, rawMessage) {
52306
+ let settings;
52307
+ try {
52308
+ settings = this.settingsProvider();
52309
+ } catch (err) {
52310
+ console.error("[conn-alert] failed to read site settings:", err);
52311
+ return;
52312
+ }
52313
+ const categoryKey = `deviceStatus.errorBadge.${toBadgeKey(errorType)}`;
52314
+ const translatedCategory = t2(categoryKey, { defaultValue: errorType });
52315
+ const text2 = t2("telegram.deviceConnectionError", {
52316
+ siteName: settings.siteName,
52317
+ deviceName: device.name,
52318
+ host: device.host ?? "-",
52319
+ category: translatedCategory,
52320
+ error: friendlyMessage || rawMessage
52321
+ });
52322
+ try {
52323
+ await this.telegramSender(text2);
52324
+ } catch (notifyErr) {
52325
+ console.error("[conn-alert] telegram send failed:", notifyErr);
52326
+ }
52327
+ }
52328
+ }
52329
+ function toBadgeKey(errorType) {
52330
+ switch (errorType) {
52331
+ case "auth_failed":
52332
+ return "authFailed";
52333
+ case "agent_unavailable":
52334
+ return "agentUnavailable";
52335
+ case "agent_no_identity":
52336
+ return "agentNoIdentity";
52337
+ case "ssh_config_ref_not_supported":
52338
+ return "configRefNotSupported";
52339
+ case "network_unreachable":
52340
+ return "networkUnreachable";
52341
+ case "connection_refused":
52342
+ return "connectionRefused";
52343
+ case "timeout":
52344
+ return "timeout";
52345
+ case "host_not_found":
52346
+ return "hostNotFound";
52347
+ case "handshake_failed":
52348
+ return "handshakeFailed";
52349
+ case "tmux_unavailable":
52350
+ return "tmuxUnavailable";
52351
+ case "connection_closed":
52352
+ return "connectionClosed";
52353
+ default:
52354
+ return "unknown";
52355
+ }
52356
+ }
52357
+ var connectionAlertNotifier = new ConnectionAlertNotifier;
52358
+
52036
52359
  // ../../apps/gateway/src/tmux/local-shell-path.ts
52037
52360
  import { existsSync } from "fs";
52038
52361
  import { delimiter, join as join2 } from "path";
@@ -53058,13 +53381,26 @@ class LocalExternalTmuxConnection {
53058
53381
  this.recoverFromTargetMissingError(message);
53059
53382
  return result;
53060
53383
  }
53061
- updateDeviceRuntimeStatus(this.deviceId, {
53062
- lastSeenAt: new Date().toISOString(),
53063
- tmuxAvailable: false,
53064
- lastError: message
53065
- });
53384
+ this.notifyRuntimeError(message);
53066
53385
  throw new Error(message);
53067
53386
  }
53387
+ async notifyRuntimeError(message) {
53388
+ const device = getDeviceById(this.deviceId);
53389
+ if (!device) {
53390
+ updateDeviceRuntimeStatus(this.deviceId, {
53391
+ lastSeenAt: new Date().toISOString(),
53392
+ tmuxAvailable: false,
53393
+ lastError: message
53394
+ });
53395
+ return;
53396
+ }
53397
+ await connectionAlertNotifier.notify({
53398
+ device,
53399
+ error: new Error(message),
53400
+ source: "runtime",
53401
+ silentTelegram: true
53402
+ });
53403
+ }
53068
53404
  async runTmuxAllowFailure(argv) {
53069
53405
  return this.deps.run(["tmux", ...argv]);
53070
53406
  }
@@ -54053,7 +54389,9 @@ class SshExternalTmuxConnection {
54053
54389
  const next = this.pipeTransition.catch(() => {
54054
54390
  return;
54055
54391
  }).then(task);
54056
- this.pipeTransition = next;
54392
+ this.pipeTransition = next.catch(() => {
54393
+ return;
54394
+ });
54057
54395
  return next;
54058
54396
  }
54059
54397
  async runTmux(argv, allowTargetMissing = false, timeoutMs = 1e4) {
@@ -54096,6 +54434,8 @@ class SshExternalTmuxConnection {
54096
54434
  }).then(() => this.executeShellCommand(command, timeoutMs));
54097
54435
  this.commandQueue = next.then(() => {
54098
54436
  return;
54437
+ }, () => {
54438
+ return;
54099
54439
  });
54100
54440
  return next;
54101
54441
  }
@@ -54657,9 +54997,14 @@ class PushSupervisor {
54657
54997
  },
54658
54998
  onError: (error) => {
54659
54999
  console.error(`[push] tmux error on device ${entry.deviceId}:`, error);
55000
+ connectionAlertNotifier.notify({
55001
+ device,
55002
+ error,
55003
+ source: "runtime"
55004
+ });
54660
55005
  },
54661
55006
  onClose: () => {
54662
- this.handleClose(entry.deviceId, generation, runtime);
55007
+ this.handleClose(entry.deviceId, generation, runtime, device);
54663
55008
  }
54664
55009
  });
54665
55010
  entry.runtime = runtime;
@@ -54683,6 +55028,11 @@ class PushSupervisor {
54683
55028
  return;
54684
55029
  }
54685
55030
  console.error(`[push] failed connecting device ${entry.deviceId}:`, err);
55031
+ await connectionAlertNotifier.notify({
55032
+ device,
55033
+ error: err,
55034
+ source: "connect"
55035
+ });
54686
55036
  detachRuntime();
54687
55037
  entry.detachRuntime = null;
54688
55038
  entry.runtime = null;
@@ -54720,11 +55070,16 @@ class PushSupervisor {
54720
55070
  this.connectEntry(entry);
54721
55071
  }, delayMs);
54722
55072
  }
54723
- async handleClose(deviceId, generation, runtime) {
55073
+ async handleClose(deviceId, generation, runtime, device) {
54724
55074
  const entry = this.entries.get(deviceId);
54725
55075
  if (!entry || entry.generation !== generation || entry.runtime !== runtime) {
54726
55076
  return;
54727
55077
  }
55078
+ await connectionAlertNotifier.notify({
55079
+ device,
55080
+ error: new Error("ssh_connection_closed"),
55081
+ source: "close"
55082
+ });
54728
55083
  entry.detachRuntime?.();
54729
55084
  entry.detachRuntime = null;
54730
55085
  entry.runtime = null;
@@ -54785,82 +55140,6 @@ class PushSupervisor {
54785
55140
  }
54786
55141
  var pushSupervisor = new PushSupervisor;
54787
55142
 
54788
- // ../../apps/gateway/src/ws/error-classify.ts
54789
- function classifySshError(error) {
54790
- const msg = error.message.toLowerCase();
54791
- if (msg.includes("ssh_config_ref_not_supported")) {
54792
- return {
54793
- type: "ssh_config_ref_not_supported",
54794
- messageKey: "sshError.configRefNotSupported"
54795
- };
54796
- }
54797
- if (msg.includes("ssh_auth_sock") || msg.includes("auth_sock")) {
54798
- return {
54799
- type: "agent_unavailable",
54800
- messageKey: "sshError.agentUnavailable"
54801
- };
54802
- }
54803
- if (msg.includes("agent") && (msg.includes("no identities") || msg.includes("failure"))) {
54804
- return {
54805
- type: "agent_no_identity",
54806
- messageKey: "sshError.agentNoIdentities"
54807
- };
54808
- }
54809
- if (msg.includes("permission denied")) {
54810
- return {
54811
- type: "auth_failed",
54812
- messageKey: "sshError.authFailed"
54813
- };
54814
- }
54815
- if (msg.includes("all configured authentication methods failed")) {
54816
- return {
54817
- type: "auth_failed",
54818
- messageKey: "sshError.authFailedGeneric"
54819
- };
54820
- }
54821
- if (msg.includes("enetunreach") || msg.includes("ehostunreach")) {
54822
- return {
54823
- type: "network_unreachable",
54824
- messageKey: "sshError.networkUnreachable"
54825
- };
54826
- }
54827
- if (msg.includes("connect refused") || msg.includes("connection refused") || msg.includes("econnrefused")) {
54828
- return {
54829
- type: "connection_refused",
54830
- messageKey: "sshError.connectionRefused"
54831
- };
54832
- }
54833
- if (msg.includes("timeout") || msg.includes("etimedout")) {
54834
- return {
54835
- type: "timeout",
54836
- messageKey: "sshError.connectionTimeout"
54837
- };
54838
- }
54839
- if (msg.includes("host not found") || msg.includes("getaddrinfo") || msg.includes("enotfound")) {
54840
- return {
54841
- type: "host_not_found",
54842
- messageKey: "sshError.hostNotFound"
54843
- };
54844
- }
54845
- if (msg.includes("handshake failed") || msg.includes("unable to verify")) {
54846
- return {
54847
- type: "handshake_failed",
54848
- messageKey: "sshError.handshakeFailed"
54849
- };
54850
- }
54851
- 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")) {
54852
- return {
54853
- type: "tmux_unavailable",
54854
- messageKey: "sshError.tmuxUnavailable"
54855
- };
54856
- }
54857
- return {
54858
- type: "unknown",
54859
- messageKey: "sshError.unknown",
54860
- messageParams: { message: error.message }
54861
- };
54862
- }
54863
-
54864
55143
  // ../../apps/gateway/src/api/test-connection.ts
54865
55144
  function inferFailurePhase(errorType) {
54866
55145
  if (errorType === "tmux_unavailable") {
@@ -55094,8 +55373,18 @@ function handleApiRequest(req, _server) {
55094
55373
  }
55095
55374
  return json2({ error: t2("apiError.notFound") }, 404);
55096
55375
  }
55376
+ function enrichDeviceWithRuntime(device) {
55377
+ const status = getDeviceRuntimeStatus(device.id);
55378
+ return {
55379
+ ...device,
55380
+ lastSeenAt: status.lastSeenAt,
55381
+ lastError: status.lastError,
55382
+ lastErrorType: status.lastErrorType,
55383
+ tmuxAvailable: status.tmuxAvailable
55384
+ };
55385
+ }
55097
55386
  async function handleGetDevices() {
55098
- const devices2 = getAllDevices();
55387
+ const devices2 = getAllDevices().map(enrichDeviceWithRuntime);
55099
55388
  return json2({ devices: devices2 });
55100
55389
  }
55101
55390
  async function handleGetDevice(id) {
@@ -55103,7 +55392,7 @@ async function handleGetDevice(id) {
55103
55392
  if (!device) {
55104
55393
  return json2({ error: t2("apiError.deviceNotFound") }, 404);
55105
55394
  }
55106
- return json2({ device });
55395
+ return json2({ device: enrichDeviceWithRuntime(device) });
55107
55396
  }
55108
55397
  async function handleCreateDevice(req) {
55109
55398
  const body = await req.json();
@@ -56675,6 +56964,15 @@ class WebSocketServer {
56675
56964
  this.sendEnvelope(client, exports_ws_borsh.KIND_DEVICE_EVENT, payloadBytes);
56676
56965
  }
56677
56966
  }
56967
+ broadcastDeviceError(deviceId, payload) {
56968
+ const entry = this.connections.get(deviceId);
56969
+ if (!entry)
56970
+ return;
56971
+ const payloadBytes = exports_ws_borsh.encodeDeviceEventPayload(payload);
56972
+ for (const client of entry.clients) {
56973
+ this.sendEnvelope(client, exports_ws_borsh.KIND_DEVICE_EVENT, payloadBytes);
56974
+ }
56975
+ }
56678
56976
  broadcastDeviceEvent(entry, payload) {
56679
56977
  const payloadBytes = exports_ws_borsh.encodeDeviceEventPayload(payload);
56680
56978
  for (const client of entry.clients) {
@@ -56767,6 +57065,9 @@ async function createGatewayRuntime(options = {}) {
56767
57065
  runtimeController.reset();
56768
57066
  primeLocalShellPath();
56769
57067
  const wsServer = new WebSocketServer;
57068
+ connectionAlertNotifier.setBroadcaster((deviceId, payload) => {
57069
+ wsServer.broadcastDeviceError(deviceId, payload);
57070
+ });
56770
57071
  await telegramService.refresh();
56771
57072
  await pushSupervisor.start();
56772
57073
  try {
@@ -56809,6 +57110,7 @@ async function createGatewayRuntime(options = {}) {
56809
57110
  runtimeController.onRestart(listener);
56810
57111
  },
56811
57112
  async stop() {
57113
+ connectionAlertNotifier.setBroadcaster(null);
56812
57114
  wsServer.closeAll();
56813
57115
  await pushSupervisor.stopAll();
56814
57116
  await tmuxRuntimeRegistry.shutdownAll();
@@ -57115,6 +57417,12 @@ async function main() {
57115
57417
  });
57116
57418
  console.log(`[tmex] ${t3("runtime.started", { url: `http://${host}:${port}` })}`);
57117
57419
  }
57420
+ process.on("unhandledRejection", (reason) => {
57421
+ console.error("[tmex][unhandledRejection]", reason);
57422
+ });
57423
+ process.on("uncaughtException", (error) => {
57424
+ console.error("[tmex][uncaughtException]", error);
57425
+ });
57118
57426
  try {
57119
57427
  await main();
57120
57428
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmex-cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "tmex": "./bin/tmex.js",