tmex-cli 0.4.3 → 0.4.5

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.
@@ -52814,6 +52814,8 @@ class LocalExternalTmuxConnection {
52814
52814
  hookReadAbort = null;
52815
52815
  hookBuffer = "";
52816
52816
  bellDedup = new Map;
52817
+ closeNotified = false;
52818
+ cleanupPromise = null;
52817
52819
  fsPaths = createRuntimeFsPaths({
52818
52820
  deviceId: "pending",
52819
52821
  sessionName: "pending",
@@ -52830,6 +52832,7 @@ class LocalExternalTmuxConnection {
52830
52832
  }
52831
52833
  async connect() {
52832
52834
  this.manualDisconnect = false;
52835
+ this.closeNotified = false;
52833
52836
  this.device = this.deps.getDevice(this.deviceId);
52834
52837
  if (!this.device) {
52835
52838
  throw new Error(`Device not found: ${this.deviceId}`);
@@ -52853,7 +52856,8 @@ class LocalExternalTmuxConnection {
52853
52856
  updateDeviceRuntimeStatus(this.deviceId, {
52854
52857
  lastSeenAt: new Date().toISOString(),
52855
52858
  tmuxAvailable: true,
52856
- lastError: null
52859
+ lastError: null,
52860
+ lastErrorType: null
52857
52861
  });
52858
52862
  await this.requestSnapshotInternal();
52859
52863
  }
@@ -53137,6 +53141,20 @@ class LocalExternalTmuxConnection {
53137
53141
  ])
53138
53142
  ]);
53139
53143
  if (sessionRes.exitCode !== 0 || windowsRes.exitCode !== 0 || panesRes.exitCode !== 0) {
53144
+ const stderrBlob = `${sessionRes.stderr}
53145
+ ${windowsRes.stderr}
53146
+ ${panesRes.stderr}`;
53147
+ if (this.connected && !this.manualDisconnect && this.isTmuxServerGoneMessage(stderrBlob)) {
53148
+ const message = stderrBlob.trim().split(/\r?\n/).find((line) => line.trim())?.trim() ?? "tmux server gone";
53149
+ console.warn(`[local] tmux server gone during snapshot on ${this.deviceId}: ${message}`);
53150
+ updateDeviceRuntimeStatus(this.deviceId, {
53151
+ lastSeenAt: new Date().toISOString(),
53152
+ tmuxAvailable: false,
53153
+ lastError: message
53154
+ });
53155
+ this.shutdownInternal(true);
53156
+ return;
53157
+ }
53140
53158
  this.callbacks.onSnapshot({ deviceId: this.deviceId, session: null });
53141
53159
  return;
53142
53160
  }
@@ -53382,6 +53400,10 @@ class LocalExternalTmuxConnection {
53382
53400
  return result;
53383
53401
  }
53384
53402
  this.notifyRuntimeError(message);
53403
+ if (this.connected && !this.manualDisconnect && this.isTmuxServerGoneMessage(message)) {
53404
+ console.warn(`[local] tmux server gone on ${this.deviceId}: ${message}`);
53405
+ this.shutdownInternal(true);
53406
+ }
53385
53407
  throw new Error(message);
53386
53408
  }
53387
53409
  async notifyRuntimeError(message) {
@@ -53408,6 +53430,40 @@ class LocalExternalTmuxConnection {
53408
53430
  const normalized = message.toLowerCase();
53409
53431
  return normalized.includes("can't find window") || normalized.includes("can't find pane") || normalized.includes("no such window") || normalized.includes("no such pane");
53410
53432
  }
53433
+ isTmuxServerGoneMessage(message) {
53434
+ const normalized = message.toLowerCase();
53435
+ return normalized.includes("no server running on") || normalized.includes("no sessions") || normalized.includes("lost server") || normalized.includes("can't find session") || normalized.includes("session not found") || normalized.includes("no such session");
53436
+ }
53437
+ async shutdownInternal(notifyClose) {
53438
+ if (this.cleanupPromise) {
53439
+ await this.cleanupPromise;
53440
+ if (notifyClose && !this.closeNotified && !this.manualDisconnect) {
53441
+ this.closeNotified = true;
53442
+ this.callbacks.onClose();
53443
+ }
53444
+ return;
53445
+ }
53446
+ this.connected = false;
53447
+ this.cleanupPromise = (async () => {
53448
+ await this.stopAllPipeReaders().catch(() => {
53449
+ return;
53450
+ });
53451
+ if (this.deps.enableHooks) {
53452
+ await this.stopHooks().catch(() => {
53453
+ return;
53454
+ });
53455
+ }
53456
+ try {
53457
+ rmSync(this.fsPaths.rootDir, { recursive: true, force: true });
53458
+ } catch {}
53459
+ })();
53460
+ await this.cleanupPromise;
53461
+ this.cleanupPromise = null;
53462
+ if (notifyClose && !this.closeNotified && !this.manualDisconnect) {
53463
+ this.closeNotified = true;
53464
+ this.callbacks.onClose();
53465
+ }
53466
+ }
53411
53467
  recoverFromTargetMissingError(message) {
53412
53468
  const normalized = message.toLowerCase();
53413
53469
  if (normalized.includes("window")) {
@@ -53560,6 +53616,16 @@ function resolvePrivateKeyFromConfig(identityFiles, deps) {
53560
53616
  }
53561
53617
  return;
53562
53618
  }
53619
+ function resolvePrivateKeysFromConfig(identityFiles, deps) {
53620
+ const privateKeys = [];
53621
+ for (const identityFile of identityFiles) {
53622
+ if (!deps.fileExists(identityFile)) {
53623
+ continue;
53624
+ }
53625
+ privateKeys.push(deps.readTextFile(identityFile));
53626
+ }
53627
+ return privateKeys;
53628
+ }
53563
53629
  function resolveSshConfigRef(device, deps) {
53564
53630
  const ref = device.sshConfigRef?.trim();
53565
53631
  if (!ref) {
@@ -53572,6 +53638,21 @@ function resolveSshConfigRef(device, deps) {
53572
53638
  }
53573
53639
  return parseSshConfigOutput(result.stdout, deps.env);
53574
53640
  }
53641
+ function resolveImplicitIdentityFilesForAgentAuth(device, host, port, username, deps) {
53642
+ if (device.authMode !== "agent" || device.sshConfigRef?.trim()) {
53643
+ return [];
53644
+ }
53645
+ const target = username ? `${username}@${host}` : host;
53646
+ try {
53647
+ const result = deps.runSync(["ssh", "-G", "-p", String(port), target]);
53648
+ if (result.exitCode !== 0) {
53649
+ return [];
53650
+ }
53651
+ return parseSshConfigOutput(result.stdout, deps.env).identityFiles;
53652
+ } catch {
53653
+ return [];
53654
+ }
53655
+ }
53575
53656
  async function resolveSshConnectConfig(device, decrypt2, inputDeps = {}) {
53576
53657
  const deps = {
53577
53658
  env: inputDeps.env ?? process.env,
@@ -53595,6 +53676,7 @@ async function resolveSshConnectConfig(device, decrypt2, inputDeps = {}) {
53595
53676
  const configAgent = resolveAgentFromConfig(resolvedConfig?.identityAgent, deps);
53596
53677
  const envAgent = resolveSshAgentSocket("auto", sshEnv);
53597
53678
  const configPrivateKey = resolvePrivateKeyFromConfig(resolvedConfig?.identityFiles ?? [], deps);
53679
+ const implicitAgentFallbackPrivateKeys = resolvePrivateKeysFromConfig(resolveImplicitIdentityFilesForAgentAuth(device, host, port, username, deps), deps);
53598
53680
  switch (device.authMode) {
53599
53681
  case "password": {
53600
53682
  if (!device.passwordEnc) {
@@ -53626,7 +53708,24 @@ async function resolveSshConnectConfig(device, decrypt2, inputDeps = {}) {
53626
53708
  break;
53627
53709
  }
53628
53710
  case "agent": {
53629
- authConfig.agent = configAgent ?? resolveSshAgentSocket("agent", sshEnv);
53711
+ const agent = configAgent ?? resolveSshAgentSocket("agent", sshEnv);
53712
+ authConfig.agent = agent;
53713
+ if (implicitAgentFallbackPrivateKeys.length > 0) {
53714
+ const publicKeyFallbacks = implicitAgentFallbackPrivateKeys.map((key) => ({
53715
+ type: "publickey",
53716
+ username,
53717
+ key
53718
+ }));
53719
+ const authHandler = [
53720
+ {
53721
+ type: "agent",
53722
+ username,
53723
+ agent
53724
+ },
53725
+ ...publicKeyFallbacks
53726
+ ];
53727
+ authConfig.authHandler = authHandler;
53728
+ }
53630
53729
  break;
53631
53730
  }
53632
53731
  case "configRef": {
@@ -53810,7 +53909,8 @@ class SshExternalTmuxConnection {
53810
53909
  updateDeviceRuntimeStatus(this.deviceId, {
53811
53910
  lastSeenAt: new Date().toISOString(),
53812
53911
  tmuxAvailable: true,
53813
- lastError: null
53912
+ lastError: null,
53913
+ lastErrorType: null
53814
53914
  });
53815
53915
  await this.requestSnapshotInternal();
53816
53916
  }
@@ -54042,9 +54142,11 @@ class SshExternalTmuxConnection {
54042
54142
  this.handleHookChunk(data.toString());
54043
54143
  },
54044
54144
  onClose: () => {
54045
- if (!this.manualDisconnect) {
54046
- this.callbacks.onError(new Error("SSH hook reader closed unexpectedly"));
54145
+ if (this.manualDisconnect) {
54146
+ return;
54047
54147
  }
54148
+ console.error("[ssh] hook reader channel closed unexpectedly, tearing down");
54149
+ this.shutdownInternal(true);
54048
54150
  }
54049
54151
  });
54050
54152
  this.hookReadAbort = () => {
@@ -54172,6 +54274,20 @@ class SshExternalTmuxConnection {
54172
54274
  ])
54173
54275
  ]);
54174
54276
  if (sessionRes.exitCode !== 0 || windowsRes.exitCode !== 0 || panesRes.exitCode !== 0) {
54277
+ const stderrBlob = `${sessionRes.stderr}
54278
+ ${windowsRes.stderr}
54279
+ ${panesRes.stderr}`;
54280
+ if (this.connected && !this.manualDisconnect && this.isTmuxServerGoneMessage(stderrBlob)) {
54281
+ const message = stderrBlob.trim().split(/\r?\n/).find((line) => line.trim())?.trim() ?? "tmux server gone";
54282
+ console.warn(`[ssh] tmux server gone during snapshot on ${this.deviceId}: ${message}`);
54283
+ updateDeviceRuntimeStatus(this.deviceId, {
54284
+ lastSeenAt: new Date().toISOString(),
54285
+ tmuxAvailable: false,
54286
+ lastError: message
54287
+ });
54288
+ this.shutdownInternal(true);
54289
+ return;
54290
+ }
54175
54291
  this.callbacks.onSnapshot({ deviceId: this.deviceId, session: null });
54176
54292
  return;
54177
54293
  }
@@ -54331,9 +54447,17 @@ class SshExternalTmuxConnection {
54331
54447
  }
54332
54448
  },
54333
54449
  onClose: () => {
54334
- if (!this.manualDisconnect && this.paneReaders.has(paneId)) {
54335
- this.callbacks.onError(new Error(`SSH pane reader closed unexpectedly: ${paneId}`));
54450
+ if (this.manualDisconnect) {
54451
+ return;
54452
+ }
54453
+ const existing = this.paneReaders.get(paneId);
54454
+ if (!existing) {
54455
+ return;
54336
54456
  }
54457
+ console.warn(`[ssh] pane reader channel closed for ${paneId}, resync on next snapshot`);
54458
+ this.paneReaders.delete(paneId);
54459
+ this.runShellAllowFailure(`rm -f ${quoteShellArg(existing.fifoPath)}`);
54460
+ this.requestSnapshot();
54337
54461
  }
54338
54462
  });
54339
54463
  const handle = {
@@ -54409,6 +54533,10 @@ class SshExternalTmuxConnection {
54409
54533
  tmuxAvailable: false,
54410
54534
  lastError: message
54411
54535
  });
54536
+ if (this.connected && !this.manualDisconnect && this.isTmuxServerGoneMessage(message)) {
54537
+ console.warn(`[ssh] tmux server gone on ${this.deviceId}: ${message}`);
54538
+ this.shutdownInternal(true);
54539
+ }
54412
54540
  throw new Error(message);
54413
54541
  }
54414
54542
  async runTmuxAllowFailure(argv, timeoutMs = 1e4) {
@@ -54536,6 +54664,10 @@ printf '\\036TMEX_END %s %d\\036\\n' ${quoteShellArg(commandId)} $?
54536
54664
  const normalized = message.toLowerCase();
54537
54665
  return normalized.includes("can't find window") || normalized.includes("can't find pane") || normalized.includes("no such window") || normalized.includes("no such pane");
54538
54666
  }
54667
+ isTmuxServerGoneMessage(message) {
54668
+ const normalized = message.toLowerCase();
54669
+ return normalized.includes("no server running on") || normalized.includes("no sessions") || normalized.includes("lost server") || normalized.includes("can't find session") || normalized.includes("session not found") || normalized.includes("no such session");
54670
+ }
54539
54671
  recoverFromTargetMissingError(message) {
54540
54672
  const normalized = message.toLowerCase();
54541
54673
  if (normalized.includes("window")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmex-cli",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "tmex": "./bin/tmex.js",