tmux-ide 2.9.2 → 2.9.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 (263) hide show
  1. package/README.md +1 -1
  2. package/bin/cli.js +2071 -625
  3. package/package.json +14 -5
  4. package/packages/contracts/src/desktop-host.ts +3 -0
  5. package/packages/contracts/src/saved-machines.ts +19 -0
  6. package/packages/contracts/src/terminal-replica.ts +1 -1
  7. package/packages/core/package.json +3 -2
  8. package/packages/core/src/terminal-delivery.ts +51 -3
  9. package/packages/core/src/terminal-fnv64-wasm.ts +4 -3
  10. package/packages/core/src/terminal-replica-hash-cache.ts +129 -31
  11. package/packages/core/src/terminal-replica-owned-row.ts +27 -0
  12. package/packages/core/src/terminal-replica.ts +13 -28
  13. package/packages/daemon/dist/command-center/daemon-fleet-facts-observer.js +96 -10
  14. package/packages/daemon/dist/command-center/resources/saved-machine-route.js +40 -1
  15. package/packages/daemon/dist/command-center/ws-events.js +5 -24
  16. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +8 -0
  17. package/packages/daemon/dist/lib/canonical-daemon.js +28 -2
  18. package/packages/daemon/dist/lib/daemon-embed.js +1 -1
  19. package/packages/daemon/dist/lib/daemon-service-manager.js +122 -0
  20. package/packages/daemon/dist/lib/daemon-service-plan.js +127 -0
  21. package/packages/daemon/dist/lib/daemon-service.js +259 -0
  22. package/packages/daemon/dist/lib/install-origin.js +2 -0
  23. package/packages/daemon/dist/lib/local-fleet-request.js +9 -3
  24. package/packages/daemon/dist/lib/log.js +6 -10
  25. package/packages/daemon/dist/lib/managed-installer-update.js +67 -0
  26. package/packages/daemon/dist/lib/{tui-download-lock.js → private-operation-lock.js} +11 -11
  27. package/packages/daemon/dist/lib/remote-daemon-info.js +3 -2
  28. package/packages/daemon/dist/lib/remote-daemon-lifecycle.js +4 -4
  29. package/packages/daemon/dist/lib/remote-tmux-command.js +10 -0
  30. package/packages/daemon/dist/lib/runtime-namespace.js +28 -16
  31. package/packages/daemon/dist/lib/saved-machines.js +40 -2
  32. package/packages/daemon/dist/lib/ssh-daemon-transport.js +24 -9
  33. package/packages/daemon/dist/lib/tmux-server-owner.js +1 -1
  34. package/packages/daemon/dist/lib/tmux-server-registration.js +21 -3
  35. package/packages/daemon/dist/lib/tui-binary.js +2 -2
  36. package/packages/daemon/dist/lib/update.js +18 -0
  37. package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +24 -1
  38. package/packages/daemon/dist/machines.js +40 -4
  39. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  40. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  41. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  42. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  43. package/packages/daemon/dist/terminal/attachments/direct-websocket.js +4 -1
  44. package/packages/daemon/dist/terminal/attachments/native-runtime.js +22 -13
  45. package/packages/daemon/dist/terminal/mirror/mirror-service.js +101 -3
  46. package/packages/daemon/dist/terminal/mirror/registered-window-guard.js +62 -0
  47. package/packages/daemon/dist/terminal/mirror/session-channel.js +75 -4
  48. package/packages/daemon/dist/terminal/mirror/shared-window-index.js +45 -0
  49. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +11 -1
  50. package/packages/daemon/dist/terminal/session-runtime/multiplexer-backend.js +1 -0
  51. package/packages/daemon/dist/terminal/session-runtime/registry.js +56 -7
  52. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +3 -1
  53. package/packages/daemon/dist/tui/mirror/dialog-model.js +2 -35
  54. package/packages/daemon/dist/tui/mirror/runtime/application-daemon-authority-owner.js +8 -0
  55. package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +20 -2
  56. package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +7 -1
  57. package/packages/daemon/dist/tui/mirror/runtime/application-machine-authority.js +59 -9
  58. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +27 -19
  59. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +23 -18
  60. package/packages/daemon/dist/tui/mirror/runtime/application-machine-startup.js +31 -2
  61. package/packages/daemon/dist/tui/mirror/runtime/application-palette-search-owner.js +7 -3
  62. package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +3 -0
  63. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +146 -2
  64. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +201 -50
  65. package/packages/daemon/dist/tui/mirror/runtime/bounded-performance-record-writer.js +50 -10
  66. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -5
  67. package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +3 -0
  68. package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +8 -2
  69. package/packages/daemon/dist/tui/mirror/terminal-text.js +35 -0
  70. package/packages/daemon/dist/tui/mirror/workspace/connection-feedback.js +5 -3
  71. package/packages/daemon/src/command-center/daemon-fleet-facts-observer.ts +100 -10
  72. package/packages/daemon/src/command-center/resources/saved-machine-route.ts +48 -1
  73. package/packages/daemon/src/command-center/ws-events.ts +5 -25
  74. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +12 -0
  75. package/packages/daemon/src/lib/canonical-daemon.ts +44 -2
  76. package/packages/daemon/src/lib/daemon-embed.ts +1 -0
  77. package/packages/daemon/src/lib/daemon-service-manager.ts +152 -0
  78. package/packages/daemon/src/lib/daemon-service-plan.ts +153 -0
  79. package/packages/daemon/src/lib/daemon-service.ts +301 -0
  80. package/packages/daemon/src/lib/install-origin.ts +5 -0
  81. package/packages/daemon/src/lib/local-fleet-request.ts +19 -3
  82. package/packages/daemon/src/lib/log.ts +13 -9
  83. package/packages/daemon/src/lib/managed-installer-update.ts +79 -0
  84. package/packages/daemon/src/lib/{tui-download-lock.ts → private-operation-lock.ts} +15 -11
  85. package/packages/daemon/src/lib/remote-daemon-info.ts +3 -3
  86. package/packages/daemon/src/lib/remote-daemon-lifecycle.ts +4 -4
  87. package/packages/daemon/src/lib/remote-tmux-command.ts +12 -0
  88. package/packages/daemon/src/lib/runtime-namespace.ts +27 -18
  89. package/packages/daemon/src/lib/saved-machines.ts +40 -1
  90. package/packages/daemon/src/lib/ssh-daemon-transport.ts +25 -7
  91. package/packages/daemon/src/lib/tmux-server-owner.ts +1 -0
  92. package/packages/daemon/src/lib/tmux-server-registration.ts +21 -3
  93. package/packages/daemon/src/lib/tui-binary.ts +2 -2
  94. package/packages/daemon/src/lib/update.ts +24 -0
  95. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +22 -0
  96. package/packages/daemon/src/machines.ts +53 -5
  97. package/packages/daemon/src/terminal/attachments/direct-websocket.ts +8 -2
  98. package/packages/daemon/src/terminal/attachments/native-runtime.ts +28 -13
  99. package/packages/daemon/src/terminal/mirror/mirror-service.ts +105 -3
  100. package/packages/daemon/src/terminal/mirror/registered-window-guard.ts +66 -0
  101. package/packages/daemon/src/terminal/mirror/session-channel.ts +92 -4
  102. package/packages/daemon/src/terminal/mirror/shared-window-index.ts +42 -0
  103. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +12 -1
  104. package/packages/daemon/src/terminal/session-runtime/multiplexer-backend.ts +2 -0
  105. package/packages/daemon/src/terminal/session-runtime/registry.ts +70 -8
  106. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +3 -1
  107. package/packages/daemon/src/tui/mirror/dialog-model.ts +2 -32
  108. package/packages/daemon/src/tui/mirror/runtime/application-daemon-authority-owner.ts +8 -0
  109. package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +19 -2
  110. package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +6 -1
  111. package/packages/daemon/src/tui/mirror/runtime/application-machine-authority.ts +60 -11
  112. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +39 -33
  113. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +31 -23
  114. package/packages/daemon/src/tui/mirror/runtime/application-machine-startup.ts +38 -2
  115. package/packages/daemon/src/tui/mirror/runtime/application-palette-search-owner.ts +7 -4
  116. package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +3 -0
  117. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +1 -1
  118. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +47 -46
  119. package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +165 -2
  120. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +207 -53
  121. package/packages/daemon/src/tui/mirror/runtime/bounded-performance-record-writer.ts +49 -9
  122. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +6 -5
  123. package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +4 -0
  124. package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +8 -3
  125. package/packages/daemon/src/tui/mirror/terminal-text.ts +32 -0
  126. package/packages/daemon/src/tui/mirror/workspace/connection-feedback.ts +6 -3
  127. package/packages/daemon-client/src/fleet-connection-status.ts +10 -1
  128. package/packages/daemon-client/src/workspace-client.ts +4 -1
  129. package/packages/contracts/src/__tests__/actions-contract.test.ts +0 -119
  130. package/packages/contracts/src/__tests__/agent-graph-overlay.test.ts +0 -428
  131. package/packages/contracts/src/__tests__/app-window-mutation.test.ts +0 -51
  132. package/packages/contracts/src/__tests__/app-window-state.test.ts +0 -34
  133. package/packages/contracts/src/__tests__/application-shell.test.ts +0 -645
  134. package/packages/contracts/src/__tests__/automation-operations.test.ts +0 -78
  135. package/packages/contracts/src/__tests__/causal-cell.test.ts +0 -47
  136. package/packages/contracts/src/__tests__/client-view-state.test.ts +0 -115
  137. package/packages/contracts/src/__tests__/cohesion-fixture.test.ts +0 -106
  138. package/packages/contracts/src/__tests__/commands.test.ts +0 -86
  139. package/packages/contracts/src/__tests__/control.test.ts +0 -154
  140. package/packages/contracts/src/__tests__/daemon-events.test.ts +0 -405
  141. package/packages/contracts/src/__tests__/daemon-resources.test.ts +0 -143
  142. package/packages/contracts/src/__tests__/daemon-wire.test.ts +0 -149
  143. package/packages/contracts/src/__tests__/desktop-host.test.ts +0 -269
  144. package/packages/contracts/src/__tests__/desktop-missions.test.ts +0 -215
  145. package/packages/contracts/src/__tests__/experience-conformance.test.ts +0 -153
  146. package/packages/contracts/src/__tests__/experience-kernel-boundary.test.ts +0 -113
  147. package/packages/contracts/src/__tests__/experience-shell.test.ts +0 -65
  148. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v2.json +0 -35
  149. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +0 -35
  150. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v4.json +0 -35
  151. package/packages/contracts/src/__tests__/fixtures/scoped-receipt.ts +0 -62
  152. package/packages/contracts/src/__tests__/fleet-agent-graph.test.ts +0 -231
  153. package/packages/contracts/src/__tests__/fleet-catalog.test.ts +0 -270
  154. package/packages/contracts/src/__tests__/fleet-lifecycle.test.ts +0 -62
  155. package/packages/contracts/src/__tests__/focus-overlay.test.ts +0 -110
  156. package/packages/contracts/src/__tests__/ide-config.test.ts +0 -64
  157. package/packages/contracts/src/__tests__/interaction-evidence.test.ts +0 -288
  158. package/packages/contracts/src/__tests__/interaction-receipt-v2.test.ts +0 -72
  159. package/packages/contracts/src/__tests__/issue-error.test.ts +0 -78
  160. package/packages/contracts/src/__tests__/mission-projections.test.ts +0 -186
  161. package/packages/contracts/src/__tests__/missions.test.ts +0 -272
  162. package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +0 -242
  163. package/packages/contracts/src/__tests__/pane-appearance.test.ts +0 -146
  164. package/packages/contracts/src/__tests__/pane-read-result.test.ts +0 -55
  165. package/packages/contracts/src/__tests__/pane-stream.test.ts +0 -536
  166. package/packages/contracts/src/__tests__/pane-widget-marker.test.ts +0 -327
  167. package/packages/contracts/src/__tests__/performance-metrics.test.ts +0 -73
  168. package/packages/contracts/src/__tests__/performance-qualification.test.ts +0 -137
  169. package/packages/contracts/src/__tests__/rich-card-widget.test.ts +0 -29
  170. package/packages/contracts/src/__tests__/session-runtime.test.ts +0 -512
  171. package/packages/contracts/src/__tests__/startup-readiness.test.ts +0 -409
  172. package/packages/contracts/src/__tests__/terminal-attachment-stream.test.ts +0 -72
  173. package/packages/contracts/src/__tests__/terminal-attachments.test.ts +0 -329
  174. package/packages/contracts/src/__tests__/terminal-delivery.test.ts +0 -88
  175. package/packages/contracts/src/__tests__/terminal-runtime-inventory.test.ts +0 -68
  176. package/packages/contracts/src/__tests__/terminals.test.ts +0 -87
  177. package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +0 -166
  178. package/packages/contracts/src/__tests__/transport-contract-loading.test.ts +0 -91
  179. package/packages/contracts/src/__tests__/visual-tokens.test.ts +0 -224
  180. package/packages/contracts/src/__tests__/window-links.test.ts +0 -243
  181. package/packages/contracts/src/__tests__/workspace-changes-resource.test.ts +0 -317
  182. package/packages/contracts/src/__tests__/workspace-changes-view.test.ts +0 -122
  183. package/packages/contracts/src/__tests__/workspace-config.test.ts +0 -294
  184. package/packages/contracts/src/__tests__/workspace-files-resource.test.ts +0 -261
  185. package/packages/contracts/src/__tests__/workspace-files-tree.test.ts +0 -147
  186. package/packages/contracts/src/__tests__/workspace-pane-creation.test.ts +0 -249
  187. package/packages/contracts/src/__tests__/workspace-promotion.test.ts +0 -94
  188. package/packages/contracts/src/__tests__/workspace-resource-identity.test.ts +0 -90
  189. package/packages/contracts/src/daemon-resource-request.test.ts +0 -192
  190. package/packages/contracts/src/native-operation-identity.test.ts +0 -23
  191. package/packages/contracts/src/workspace-admission.test.ts +0 -21
  192. package/packages/contracts/src/workspace-catalog-resource.test.ts +0 -146
  193. package/packages/core/src/agent-provisioning.test.ts +0 -42
  194. package/packages/core/src/application-shell-session.test.ts +0 -105
  195. package/packages/core/src/interaction-evidence.test.ts +0 -134
  196. package/packages/core/src/interaction-flow.test.ts +0 -66
  197. package/packages/core/src/interaction-receipts.test.ts +0 -671
  198. package/packages/core/src/navigator.test.ts +0 -34
  199. package/packages/core/src/optimistic-projection-conformance.test.ts +0 -75
  200. package/packages/core/src/optimistic-projection.test.ts +0 -118
  201. package/packages/core/src/performance-metrics.test.ts +0 -121
  202. package/packages/core/src/performance-qualification.test.ts +0 -283
  203. package/packages/core/src/saved-machines.test.ts +0 -80
  204. package/packages/core/src/terminal-compact-cold.test.ts +0 -87
  205. package/packages/core/src/terminal-conformance.test.ts +0 -48
  206. package/packages/core/src/terminal-delivery-cache-lifetime.test.ts +0 -167
  207. package/packages/core/src/terminal-delivery-encode-cooperative.test.ts +0 -140
  208. package/packages/core/src/terminal-delivery-representation-hash.test.ts +0 -77
  209. package/packages/core/src/terminal-delivery.test.ts +0 -1471
  210. package/packages/core/src/terminal-fnv64-wasm.test.ts +0 -61
  211. package/packages/core/src/terminal-replica-cold.test.ts +0 -31
  212. package/packages/core/src/terminal-replica-cooperative.test.ts +0 -170
  213. package/packages/core/src/terminal-replica-hash-cache.test.ts +0 -283
  214. package/packages/core/src/terminal-replica.test.ts +0 -347
  215. package/packages/core/src/workspace-selection.test.ts +0 -63
  216. package/packages/daemon/dist/__tests__/support.js +0 -33
  217. package/packages/daemon/dist/lib/__tests__/fixtures/workspace-state-writer.js +0 -58
  218. package/packages/daemon/dist/lib/__tests__/installed-recovery-fixture.js +0 -400
  219. package/packages/daemon/dist/terminal/__tests__/MockPtyAdapter.js +0 -182
  220. package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +0 -98
  221. package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +0 -156
  222. package/packages/daemon/src/tui/mirror/__snapshots__/activity-surface-renderer.test.tsx.snap +0 -42
  223. package/packages/daemon/src/tui/mirror/__snapshots__/changes-terminal-surface-renderer.test.tsx.snap +0 -267
  224. package/packages/daemon/src/tui/mirror/__snapshots__/home-files-surface-renderer.test.tsx.snap +0 -267
  225. package/packages/daemon/src/tui/mirror/__snapshots__/missions-surface-renderer.test.tsx.snap +0 -134
  226. package/packages/daemon/src/tui/mirror/__snapshots__/recipes-gallery-renderer.test.tsx.snap +0 -141
  227. package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +0 -134
  228. package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +0 -87
  229. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/agent-terminal-canvas-renderer.test.tsx.snap +0 -348
  230. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +0 -134
  231. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/command-palette-surface-renderer.test.tsx.snap +0 -255
  232. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/pane-frame-renderer.test.tsx.snap +0 -525
  233. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/terminal-pane-header-polish-renderer.test.tsx.snap +0 -31
  234. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/workbench-shell-renderer.test.tsx.snap +0 -134
  235. package/packages/daemon-client/src/application-shell-session.test.ts +0 -196
  236. package/packages/daemon-client/src/automation-client.test.ts +0 -214
  237. package/packages/daemon-client/src/bootstrap-coordinator.test.ts +0 -145
  238. package/packages/daemon-client/src/connection-supervisor.test.ts +0 -109
  239. package/packages/daemon-client/src/direct-application-shell-transport.test.ts +0 -941
  240. package/packages/daemon-client/src/first-latest-coordinator.test.ts +0 -175
  241. package/packages/daemon-client/src/fleet-dial-scheduler.test.ts +0 -114
  242. package/packages/daemon-client/src/fleet-environments.test.ts +0 -40
  243. package/packages/daemon-client/src/generation-bound-store-timers.test.ts +0 -74
  244. package/packages/daemon-client/src/owner-action-client.test.ts +0 -245
  245. package/packages/daemon-client/src/pane-stream-client.test.ts +0 -2841
  246. package/packages/daemon-client/src/pane-stream-clock-calibration.test.ts +0 -207
  247. package/packages/daemon-client/src/pane-stream-loopback-benchmark.test.ts +0 -51
  248. package/packages/daemon-client/src/push-resource-session.test.ts +0 -509
  249. package/packages/daemon-client/src/resource-replica.test.ts +0 -186
  250. package/packages/daemon-client/src/runtime-resource-ledger.test.ts +0 -18
  251. package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +0 -114
  252. package/packages/daemon-client/src/terminal-fast-lane.test.ts +0 -835
  253. package/packages/daemon-client/src/tmux-server-client.test.ts +0 -279
  254. package/packages/daemon-client/src/tmux-server-interaction-events.test.ts +0 -230
  255. package/packages/daemon-client/src/workspace-catalog-v2.test.ts +0 -109
  256. package/packages/daemon-client/src/workspace-client-boundaries.test.ts +0 -59
  257. package/packages/daemon-client/src/workspace-client-operations.test.ts +0 -96
  258. package/packages/daemon-client/src/workspace-client.test.ts +0 -2912
  259. package/packages/daemon-client/src/workspace-event-supervisor.test.ts +0 -1086
  260. package/packages/daemon-client/src/workspace-pane-client.test.ts +0 -80
  261. package/packages/presentation/src/presentation-contract.test.ts +0 -63
  262. package/packages/tmux-bridge/src/runner.test.ts +0 -614
  263. package/packages/tmux-bridge/src/targeting.test.ts +0 -107
@@ -152,16 +152,16 @@ export function getLogStreamFailures(): readonly StreamName[] {
152
152
  function markStreamFailed(name: StreamName, error: unknown): void {
153
153
  if (streamState[name].failed) return;
154
154
  streamState[name].failed = true;
155
- const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
155
+ const detail = sanitizeLogMessage(
156
+ error instanceof Error ? `${error.name}: ${error.message}` : String(error),
157
+ secrets,
158
+ budget,
159
+ );
156
160
  const warning = `[log.ts] ${name} write failed (${detail}); further records are retained in memory only\n`;
157
161
  const other: StreamName = name === "stdout" ? "stderr" : "stdout";
158
- if (!streamState[other].failed) {
159
- try {
160
- process[other].write(warning);
161
- } catch {
162
- streamState[other].failed = true;
163
- }
164
- }
162
+ // The fallback can fail asynchronously too. Install the same error handling
163
+ // before writing it; failed flags prevent a warning loop if both streams fail.
164
+ if (!streamState[other].failed) writeToStream(other, warning);
165
165
  }
166
166
 
167
167
  function writeToStream(name: StreamName, line: string): void {
@@ -221,7 +221,11 @@ function writeStructuredLog(
221
221
  // Avoid recursion via the logger; write directly to stderr.
222
222
  writeToStream(
223
223
  "stderr",
224
- `[log.ts] subscriber threw: ${err instanceof Error ? err.message : String(err)}\n`,
224
+ `[log.ts] subscriber threw: ${sanitizeLogMessage(
225
+ err instanceof Error ? err.message : String(err),
226
+ secrets,
227
+ budget,
228
+ )}\n`,
225
229
  );
226
230
  }
227
231
  }
@@ -0,0 +1,79 @@
1
+ import { mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { tmpdir } from "node:os";
4
+ import { execFileSync } from "node:child_process";
5
+
6
+ export const MANAGED_INSTALLER_URL = "https://tmux-ide.com/install.sh";
7
+
8
+ /** Only the active release of a marked installation may update its prefix. */
9
+ export function managedInstallerPrefix(cliPath: string): string | null {
10
+ const match =
11
+ /^(.*)\/share\/tmux-ide\/releases\/(install-[A-Za-z0-9]{6})\/npm\/lib\/node_modules\/tmux-ide\/bin$/.exec(
12
+ cliPath,
13
+ );
14
+ if (!match) return null;
15
+ const prefix = match[1] || "/";
16
+ const root = join(prefix, "share/tmux-ide");
17
+ const release = join(root, "releases", match[2]!);
18
+ try {
19
+ if (realpathSync(join(root, "current")) !== release) return null;
20
+ if (realpathSync(release) !== release) return null;
21
+ if (readFileSync(join(root, "installer-v1"), "utf8") !== "1\n") return null;
22
+ if (readFileSync(join(release, ".installer-release-v1"), "utf8") !== "1\n") return null;
23
+ if (
24
+ !readFileSync(join(prefix, "bin/tmux-ide"), "utf8")
25
+ .split("\n")
26
+ .includes("# tmux-ide universal installer v1")
27
+ )
28
+ return null;
29
+ if (
30
+ JSON.parse(readFileSync(join(dirname(cliPath), "package.json"), "utf8")).name !== "tmux-ide"
31
+ )
32
+ return null;
33
+ return prefix;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /** Download completely before execution; the canonical installer owns activation and rollback. */
40
+ export function runManagedInstallerUpdate(
41
+ prefix: string,
42
+ channel: string,
43
+ json: boolean,
44
+ execute: typeof execFileSync = execFileSync,
45
+ ): void {
46
+ const temporary = mkdtempSync(join(tmpdir(), "tmux-ide-update-"));
47
+ const script = join(temporary, "install.sh");
48
+ try {
49
+ execute(
50
+ "curl",
51
+ [
52
+ "--fail",
53
+ "--location",
54
+ "--silent",
55
+ "--show-error",
56
+ "--proto",
57
+ "=https",
58
+ "--proto-redir",
59
+ "=https",
60
+ "--connect-timeout",
61
+ "15",
62
+ "--max-time",
63
+ "180",
64
+ "--output",
65
+ script,
66
+ MANAGED_INSTALLER_URL,
67
+ ],
68
+ { stdio: ["ignore", 2, 2] },
69
+ );
70
+ if (!readFileSync(script, "utf8").startsWith("#!/bin/sh\n"))
71
+ throw new Error("Installer download was not a shell script; installation unchanged");
72
+ execute("/bin/sh", ["-n", script], { stdio: ["ignore", 2, 2] });
73
+ execute("/bin/sh", [script, "--prefix", prefix, "--version", channel], {
74
+ stdio: json ? ["ignore", 2, 2] : "inherit",
75
+ });
76
+ } finally {
77
+ rmSync(temporary, { recursive: true, force: true });
78
+ }
79
+ }
@@ -26,7 +26,7 @@ function trusted(path: string, directory: boolean) {
26
26
  (value.mode & 0o777) !== (directory ? 0o700 : 0o600) ||
27
27
  (!directory && value.nlink !== 1)
28
28
  )
29
- throw new Error("unsafe TUI download lock");
29
+ throw new Error("unsafe private operation lock");
30
30
  return value;
31
31
  }
32
32
  function readOwner(path: string): { pid: number; ino: number; dev: number } {
@@ -35,15 +35,15 @@ function readOwner(path: string): { pid: number; ino: number; dev: number } {
35
35
  try {
36
36
  const actual = fstatSync(fd);
37
37
  if (actual.ino !== before.ino || actual.dev !== before.dev || actual.size > 32) {
38
- throw new Error("unsafe TUI download lock");
38
+ throw new Error("unsafe private operation lock");
39
39
  }
40
40
  const bytes = Buffer.alloc(33);
41
41
  const length = readSync(fd, bytes, 0, bytes.length, 0);
42
42
  const text = bytes.subarray(0, length).toString("utf8");
43
- if (!/^[1-9][0-9]*\n$/.test(text)) throw new Error("invalid TUI download lock owner");
43
+ if (!/^[1-9][0-9]*\n$/.test(text)) throw new Error("invalid private operation lock owner");
44
44
  const pid = Number(text.trim());
45
45
  if (!Number.isSafeInteger(pid) || pid > 2147483647)
46
- throw new Error("invalid TUI download lock owner");
46
+ throw new Error("invalid private operation lock owner");
47
47
  return { pid, ino: actual.ino, dev: actual.dev };
48
48
  } finally {
49
49
  closeSync(fd);
@@ -88,7 +88,7 @@ function cleanStaging(
88
88
  try {
89
89
  const current = trusted(path, true);
90
90
  if (current.ino !== expected.ino || current.dev !== expected.dev)
91
- throw new Error("TUI download staging lock changed");
91
+ throw new Error("private operation staging lock changed");
92
92
  try {
93
93
  unlinkSync(join(path, name));
94
94
  } catch (error) {
@@ -102,7 +102,10 @@ function cleanStaging(
102
102
  }
103
103
 
104
104
  /** Publish a populated private directory atomically; age never proves owner death. */
105
- export async function acquireTuiDownloadLock(lock: string, waitMs: number): Promise<() => void> {
105
+ export async function acquirePrivateOperationLock(
106
+ lock: string,
107
+ waitMs: number,
108
+ ): Promise<() => void> {
106
109
  const deadline = Date.now() + waitMs;
107
110
  const nonce = randomUUID();
108
111
  const name = `owner-${nonce}`;
@@ -116,7 +119,7 @@ export async function acquireTuiDownloadLock(lock: string, waitMs: number): Prom
116
119
  writeFileSync(join(staging, name), `${process.pid}\n`, { flag: "wx", mode: 0o600 });
117
120
  while (true) {
118
121
  if (attempted && Date.now() >= deadline)
119
- throw new Error("timed out waiting for another TUI download");
122
+ throw new Error("timed out waiting for another private operation");
120
123
  attempted = true;
121
124
  try {
122
125
  const existing = lstatSync(lock);
@@ -154,7 +157,7 @@ export async function acquireTuiDownloadLock(lock: string, waitMs: number): Prom
154
157
  }
155
158
  if (names.length !== 1 || !/^owner-[0-9a-f-]{36}$/.test(names[0]!)) {
156
159
  // Empty is only a retirement gap; the next atomic rename can claim it.
157
- if (names.length !== 0) throw new Error("invalid TUI download lock inventory");
160
+ if (names.length !== 0) throw new Error("invalid private operation lock inventory");
158
161
  } else {
159
162
  const owner = readOwner(join(lock, names[0]!));
160
163
  if (dead(owner.pid)) {
@@ -164,7 +167,7 @@ export async function acquireTuiDownloadLock(lock: string, waitMs: number): Prom
164
167
  current.dev !== owner.dev ||
165
168
  current.pid !== owner.pid
166
169
  ) {
167
- throw new Error("TUI download lock changed");
170
+ throw new Error("private operation lock changed");
168
171
  }
169
172
  retire(lock, names[0]!, entry);
170
173
  }
@@ -182,7 +185,7 @@ export async function acquireTuiDownloadLock(lock: string, waitMs: number): Prom
182
185
  current.dev !== owner.dev ||
183
186
  current.pid !== owner.pid
184
187
  ) {
185
- throw new Error("TUI download lock changed");
188
+ throw new Error("private operation lock changed");
186
189
  }
187
190
  try {
188
191
  unlinkSync(lock);
@@ -194,7 +197,8 @@ export async function acquireTuiDownloadLock(lock: string, waitMs: number): Prom
194
197
  } catch (error) {
195
198
  if (code(error) !== "ENOENT") throw error;
196
199
  }
197
- if (Date.now() >= deadline) throw new Error("timed out waiting for another TUI download");
200
+ if (Date.now() >= deadline)
201
+ throw new Error("timed out waiting for another private operation");
198
202
  await new Promise((resolve) =>
199
203
  setTimeout(resolve, Math.min(50, Math.max(0, deadline - Date.now()))),
200
204
  );
@@ -1,4 +1,4 @@
1
- import { type CanonicalDaemonInfo } from "@tmux-ide/contracts";
1
+ import { DAEMON_WIRE_PROTOCOL_VERSION, type CanonicalDaemonInfo } from "@tmux-ide/contracts";
2
2
  import {
3
3
  canonicalDaemonUrl,
4
4
  isCanonicalDaemonAlive,
@@ -35,9 +35,9 @@ export async function readRemoteDaemonHandshake(
35
35
  "daemon-missing",
36
36
  );
37
37
  const parsed = RemoteDaemonHandshakeSchema.safeParse({ version: 1, daemon: info });
38
- if (!parsed.success)
38
+ if (!parsed.success || info.protocolVersion !== DAEMON_WIRE_PROTOCOL_VERSION)
39
39
  throw new RemoteDaemonInfoError(
40
- "The daemon does not support authenticated SSH discovery.",
40
+ "The daemon does not support this authenticated SSH discovery protocol.",
41
41
  "incompatible",
42
42
  );
43
43
  try {
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { SavedMachineSchema } from "@tmux-ide/contracts/saved-machines";
3
+ import { remoteTmuxIdeCommand } from "./remote-tmux-command.ts";
3
4
 
4
5
  /** Only explicit user actions call this; catalog discovery and retries never do. */
5
6
  export async function startInstalledRemoteDaemon(
@@ -21,12 +22,11 @@ export async function startInstalledRemoteDaemon(
21
22
  "BatchMode=yes",
22
23
  "-o",
23
24
  "ConnectTimeout=10",
25
+ "-o",
26
+ "ForkAfterAuthentication=no",
24
27
  "--",
25
28
  alias,
26
- "tmux-ide",
27
- "update",
28
- "--daemon",
29
- "--json",
29
+ remoteTmuxIdeCommand("start"),
30
30
  ],
31
31
  { stdio: ["ignore", "pipe", "pipe"] },
32
32
  );
@@ -0,0 +1,12 @@
1
+ import { shellEscape } from "./shell.ts";
2
+
3
+ /** Fixed commands only. SSH normally omits interactive shell PATH setup. */
4
+ export function remoteTmuxIdeCommand(operation: "discover" | "start"): string {
5
+ const args = operation === "discover" ? "remote-daemon-info --json" : "update --daemon --json";
6
+ // Respect the host's existing installation preference, then try the installer
7
+ // and common package-manager prefixes. Never source shell profiles.
8
+ const script =
9
+ 'export PATH="${PATH:-/usr/bin:/bin}:$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/home/linuxbrew/.linuxbrew/bin:$HOME/.npm-global/bin"; ' +
10
+ `exec tmux-ide ${args}`;
11
+ return `/bin/sh -c ${shellEscape(script)}`;
12
+ }
@@ -81,12 +81,11 @@ function absolutePath(value: string, cwd: string, key: string): string {
81
81
  return path;
82
82
  }
83
83
 
84
- function pathEntryExists(path: string): boolean {
84
+ function pathEntryStat(path: string) {
85
85
  try {
86
- lstatSync(path);
87
- return true;
86
+ return lstatSync(path);
88
87
  } catch (error) {
89
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
88
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
90
89
  throw error;
91
90
  }
92
91
  }
@@ -94,21 +93,23 @@ function pathEntryExists(path: string): boolean {
94
93
  function pathIdentity(path: string): string {
95
94
  let cursor = resolve(path);
96
95
  const suffix: string[] = [];
97
- while (!pathEntryExists(cursor)) {
96
+ let stat = pathEntryStat(cursor);
97
+ while (!stat) {
98
98
  const parent = dirname(cursor);
99
99
  if (parent === cursor) break;
100
100
  suffix.unshift(basename(cursor));
101
101
  cursor = parent;
102
+ stat = pathEntryStat(cursor);
102
103
  }
103
- if (pathEntryExists(cursor) && lstatSync(cursor).isSocket()) {
104
+ if (stat?.isSocket()) {
104
105
  return resolve(captureUnixSocketIdentity(cursor).path, ...suffix);
105
106
  }
106
- return resolve(pathEntryExists(cursor) ? realpathSync(cursor) : cursor, ...suffix);
107
+ return resolve(stat ? realpathSync(cursor) : cursor, ...suffix);
107
108
  }
108
109
 
109
- function isInsideOrEqual(path: string, parent: string): boolean {
110
- const child = pathIdentity(path);
111
- const root = pathIdentity(parent);
110
+ function isInsideOrEqual(path: string, parent: string, identity = pathIdentity): boolean {
111
+ const child = identity(path);
112
+ const root = identity(parent);
112
113
  const offset = relative(root, child);
113
114
  return offset === "" || (!offset.startsWith(`..${sep}`) && offset !== "..");
114
115
  }
@@ -124,6 +125,17 @@ function runtimeMode(env: NodeJS.ProcessEnv): RuntimeMode {
124
125
  export function resolveRuntimeNamespace(
125
126
  options: RuntimeNamespaceResolutionOptions = {},
126
127
  ): RuntimeNamespace {
128
+ // Reuse a path's identity only within this synchronous resolution. Every
129
+ // subsequent resolution must see new ancestors and replaced symlinks.
130
+ let identities: Map<string, string> | undefined;
131
+ const identity = (path: string): string => {
132
+ const cached = identities?.get(path);
133
+ if (cached !== undefined) return cached;
134
+ const resolved = pathIdentity(path);
135
+ (identities ??= new Map()).set(path, resolved);
136
+ return resolved;
137
+ };
138
+ const within = (path: string, parent: string) => isInsideOrEqual(path, parent, identity);
127
139
  const env = options.env ?? process.env;
128
140
  const userHome = options.userHome ?? homedir();
129
141
  const cwd = options.cwd ?? process.cwd();
@@ -205,16 +217,13 @@ export function resolveRuntimeNamespace(
205
217
  if (isolated && tmuxSocket.kind === "name" && tmuxSocket.name === "default") {
206
218
  throw new TypeError(`${mode} runtime requires a non-default ${TMUX_SOCKET_NAME_ENV}`);
207
219
  }
208
- if (isolated && isInsideOrEqual(stateHome, canonicalHome)) {
220
+ if (isolated && within(stateHome, canonicalHome)) {
209
221
  throw new TypeError(`${mode} runtime cannot use the canonical tmux-ide state home`);
210
222
  }
211
- if (
212
- isolated &&
213
- (isInsideOrEqual(registryDir, canonicalHome) || isInsideOrEqual(daemonInfoDir, canonicalHome))
214
- ) {
223
+ if (isolated && (within(registryDir, canonicalHome) || within(daemonInfoDir, canonicalHome))) {
215
224
  throw new TypeError(`${mode} runtime cannot use canonical registry or daemon state`);
216
225
  }
217
- if (isolated && tmuxSocket.kind === "path" && isInsideOrEqual(tmuxSocket.path, canonicalHome)) {
226
+ if (isolated && tmuxSocket.kind === "path" && within(tmuxSocket.path, canonicalHome)) {
218
227
  throw new TypeError(`${mode} runtime cannot use a tmux socket inside canonical state`);
219
228
  }
220
229
  if (isolated && cleanupToken === null) {
@@ -229,7 +238,7 @@ export function resolveRuntimeNamespace(
229
238
  (key === "TMUX_IDE_CONFIG" || key === "TMUX_IDE_SETTINGS_DIR"
230
239
  ? env[key]
231
240
  : nonEmpty(env, key)) ?? fallback;
232
- if (development && (!isAbsolute(value) || !isInsideOrEqual(value, development.root)))
241
+ if (development && (!isAbsolute(value) || !within(value, development.root)))
233
242
  throw new TypeError(`development ${key} escapes instance`);
234
243
  return value;
235
244
  };
@@ -267,7 +276,7 @@ export function resolveRuntimeNamespace(
267
276
  if (nonEmpty(env, key)) scoped(key, "");
268
277
  if (env.TMUX) {
269
278
  const socket = /^(.*),[0-9]+,[0-9]+$/u.exec(env.TMUX)?.[1];
270
- if (!socket || pathIdentity(socket) !== pathIdentity(join(runtimeDir, "tmux.sock")))
279
+ if (!socket || identity(socket) !== identity(join(runtimeDir, "tmux.sock")))
271
280
  throw new TypeError("development rejects inherited foreign TMUX authority");
272
281
  }
273
282
  }
@@ -9,8 +9,9 @@ import {
9
9
  renameSync,
10
10
  unlinkSync,
11
11
  writeFileSync,
12
+ watch,
12
13
  } from "node:fs";
13
- import { dirname, join } from "node:path";
14
+ import { basename, dirname, join } from "node:path";
14
15
  import { SavedMachineRegistrySchema, type SavedMachineRegistry } from "@tmux-ide/contracts";
15
16
  import { changeSavedMachines, type SavedMachineChange } from "@tmux-ide/core";
16
17
  import { resolveRuntimeNamespace } from "./runtime-namespace.ts";
@@ -38,6 +39,44 @@ export function loadSavedMachines(path = savedMachinesPath()): SavedMachineRegis
38
39
  }
39
40
  }
40
41
 
42
+ /** Watch the directory so atomic registry replacement does not detach the watcher. */
43
+ export function watchSavedMachines(
44
+ onChange: (registry: SavedMachineRegistry) => void,
45
+ onError: () => void,
46
+ path = savedMachinesPath(),
47
+ ): () => void {
48
+ let stopped = false;
49
+ let pending: ReturnType<typeof setTimeout> | null = null;
50
+ const refresh = () => {
51
+ pending = null;
52
+ if (stopped) return;
53
+ try {
54
+ onChange(loadSavedMachines(path));
55
+ } catch {
56
+ // Keep the last valid routes while an editor is midway through a write.
57
+ onError();
58
+ }
59
+ };
60
+ // A first-run app can subscribe before the daemon creates its registry.
61
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
62
+ const watcher = watch(dirname(path), (_event, file) => {
63
+ if (stopped || (file !== null && file.toString() !== basename(path))) return;
64
+ if (pending) clearTimeout(pending);
65
+ pending = setTimeout(refresh, 50);
66
+ pending.unref();
67
+ });
68
+ watcher.unref();
69
+ watcher.on("error", onError);
70
+ // Close the read-before-subscribe race at startup.
71
+ pending = setTimeout(refresh, 0);
72
+ pending.unref();
73
+ return () => {
74
+ stopped = true;
75
+ if (pending) clearTimeout(pending);
76
+ watcher.close();
77
+ };
78
+ }
79
+
41
80
  /** The daemon owns writes. Synchronous read/reduce/rename cannot interleave within it. */
42
81
  export function updateSavedMachines(
43
82
  change: SavedMachineChange,
@@ -3,6 +3,7 @@ import { spawn, type ChildProcess } from "node:child_process";
3
3
  import { createServer } from "node:net";
4
4
  import type { Readable } from "node:stream";
5
5
  import { z } from "zod";
6
+ import { remoteTmuxIdeCommand } from "./remote-tmux-command.ts";
6
7
  import { createSshDaemonRelay, type SshDaemonRelay } from "./ssh-daemon-relay.ts";
7
8
  import {
8
9
  CanonicalDaemonInfoSchema,
@@ -49,12 +50,30 @@ export class SshConnectionError extends Error {
49
50
  super(`SSH daemon connection: ${message}`);
50
51
  }
51
52
  get retryable(): boolean {
52
- return this.code === "unavailable";
53
+ // A supervisor may temporarily remove the record while replacing its daemon.
54
+ return this.code === "unavailable" || this.code === "daemon-missing";
53
55
  }
54
56
  }
55
57
  function failure(message: string, code: FleetConnectionFailureCode = "unavailable"): Error {
56
58
  return new SshConnectionError(message, code);
57
59
  }
60
+
61
+ /** Classify known OpenSSH errors without publishing remote stderr or credentials. */
62
+ function observeSshFailure(child: SshTransportChild): () => FleetConnectionFailureCode {
63
+ let tail = "";
64
+ let code: FleetConnectionFailureCode = "unavailable";
65
+ child.stderr?.on("data", (chunk: Buffer) => {
66
+ tail = (tail + chunk.toString("utf8")).slice(-4096);
67
+ if (/Host key verification failed|REMOTE HOST IDENTIFICATION HAS CHANGED/i.test(tail))
68
+ code = "ssh-host-key";
69
+ else if (/Permission denied \((?:publickey|password|keyboard-interactive)[^)]*\)/i.test(tail))
70
+ code = "ssh-authentication";
71
+ });
72
+ child.once("close", () => {
73
+ tail = "";
74
+ });
75
+ return () => code;
76
+ }
58
77
  const stoppedChildren = new WeakSet<SshTransportChild>();
59
78
  function stop(child: SshTransportChild): void {
60
79
  if (stoppedChildren.has(child) || child.exitCode !== null || child.signalCode !== null) return;
@@ -153,6 +172,7 @@ const defaults: SshDaemonTransportDependencies = {
153
172
  probe: probeSshDaemonIdentity,
154
173
  };
155
174
  function discover(child: SshTransportChild, signal: AbortSignal): Promise<RemoteDaemon> {
175
+ const sshFailure = observeSshFailure(child);
156
176
  return new Promise((resolve, reject) => {
157
177
  const chunks: Buffer[] = [];
158
178
  let bytes = 0;
@@ -177,8 +197,6 @@ function discover(child: SshTransportChild, signal: AbortSignal): Promise<Remote
177
197
  }
178
198
  chunks.push(Buffer.from(chunk));
179
199
  });
180
- // Drain without capturing: SSH banners/errors may contain confidential remote information.
181
- child.stderr?.resume();
182
200
  child.once("error", () => finish(failure("could not start OpenSSH")));
183
201
  child.once("close", (code) => {
184
202
  if (settled) return;
@@ -186,6 +204,7 @@ function discover(child: SshTransportChild, signal: AbortSignal): Promise<Remote
186
204
  finish(
187
205
  failure(
188
206
  "discovery failed; check SSH authentication, host trust, and remote tmux-ide installation",
207
+ code === 127 ? "remote-cli-missing" : sshFailure(),
189
208
  ),
190
209
  );
191
210
  return;
@@ -266,9 +285,7 @@ export async function openSshDaemonTransport(
266
285
  "ForkAfterAuthentication=no",
267
286
  "--",
268
287
  options.alias,
269
- "tmux-ide",
270
- "remote-daemon-info",
271
- "--json",
288
+ remoteTmuxIdeCommand("discover"),
272
289
  ]);
273
290
  const daemon = await discover(child, signal);
274
291
  const port = await cancellable(dependencies.allocatePort(), signal);
@@ -307,7 +324,7 @@ export async function openSshDaemonTransport(
307
324
  options.alias,
308
325
  ]);
309
326
  child.stdout?.resume();
310
- child.stderr?.resume();
327
+ const sshFailure = observeSshFailure(child);
311
328
  child.once("exit", dispose);
312
329
  const tunnelClosed = new Promise<void>((resolve) => {
313
330
  child!.once("close", () => {
@@ -342,6 +359,7 @@ export async function openSshDaemonTransport(
342
359
  }
343
360
  throw failure(
344
361
  "tunnel could not authenticate the expected daemon before cancellation or timeout",
362
+ sshFailure(),
345
363
  );
346
364
  } catch (error) {
347
365
  dispose();
@@ -268,6 +268,7 @@ export async function createNativeTmuxServerOwner(options: NativeTmuxServerOwner
268
268
  return multiplexer.mutateWindowLink(
269
269
  { operationId, expectedDaemonInstanceId: generation, intent },
270
270
  (session, action) => sessionRuntimeRegistry.executeWindowLinkAction(session, action),
271
+ timing,
271
272
  );
272
273
  }
273
274
  if (intent.verb === "workspace.pane.resize") {
@@ -77,11 +77,29 @@ export function createTmuxServerProbe(
77
77
  )
78
78
  ).stdout.trimEnd();
79
79
  try {
80
- const first = await read(args);
80
+ // A direct socket can be pinned before contacting tmux. Named servers
81
+ // and aliases still need discovery followed by an explicit-path proof.
82
+ let pinned: ReturnType<typeof captureUnixSocketIdentity> | null = null;
83
+ if (selector.kind === "path") {
84
+ try {
85
+ pinned = captureUnixSocketIdentity(selector.path);
86
+ } catch {
87
+ // Preserve discovery for aliases which are not direct socket paths.
88
+ }
89
+ }
90
+ const first = await read(pinned ? ["-S", pinned.path] : args);
81
91
  const match = /^(.*)\|([1-9][0-9]*)\|([1-9][0-9]*)$/u.exec(first);
82
92
  if (!match) return null;
83
- const socket = captureUnixSocketIdentity(match[1]!);
84
- if ((await read(["-S", socket.path])) !== first) return null;
93
+ const reportedSocket = captureUnixSocketIdentity(match[1]!);
94
+ const socket = pinned ?? reportedSocket;
95
+ if (pinned) {
96
+ if (
97
+ reportedSocket.path !== pinned.path ||
98
+ reportedSocket.dev !== pinned.dev ||
99
+ reportedSocket.ino !== pinned.ino
100
+ )
101
+ return null;
102
+ } else if ((await read(["-S", socket.path])) !== first) return null;
85
103
  revalidateUnixSocketIdentity(socket);
86
104
  return {
87
105
  nativeServerIdentity: { pid: match[2]!, startTime: match[3]! },
@@ -28,7 +28,7 @@ import {
28
28
  } from "node:fs";
29
29
  import { dirname, join } from "node:path";
30
30
  import { gunzipSync } from "node:zlib";
31
- import { acquireTuiDownloadLock } from "./tui-download-lock.ts";
31
+ import { acquirePrivateOperationLock } from "./private-operation-lock.ts";
32
32
  import { getCurrentVersion } from "./update-check.ts";
33
33
 
34
34
  /** The `<os>-<arch>` tags we publish a prebuilt TUI binary for. */
@@ -360,7 +360,7 @@ export async function downloadTuiBinary(
360
360
  timeoutMs: opts.timeoutMs ?? TUI_DOWNLOAD_TIMEOUT_MS,
361
361
  };
362
362
  mkdirSync(dirname(dest), { recursive: true });
363
- const releaseLock = await acquireTuiDownloadLock(
363
+ const releaseLock = await acquirePrivateOperationLock(
364
364
  downloadLockPath(dest),
365
365
  limits.timeoutMs * 2 + 5_000,
366
366
  );
@@ -1,3 +1,8 @@
1
+ import {
2
+ managedInstallerPrefix,
3
+ runManagedInstallerUpdate,
4
+ MANAGED_INSTALLER_URL,
5
+ } from "./managed-installer-update.ts";
1
6
  import { resolveRuntimeNamespace } from "./runtime-namespace.ts";
2
7
  import { parseStrictSemver } from "./semver.ts";
3
8
  /** Origin-aware update planning; unsupported origins never fall back to npm. */
@@ -28,6 +33,7 @@ export interface UpdatePlan {
28
33
  args?: string[];
29
34
  guidance?: string;
30
35
  proposedCommand?: string;
36
+ installerPrefix?: string;
31
37
  }
32
38
  export const UPDATE_COMMANDS: Record<PackageManager, string> = {
33
39
  npm: "npm install -g tmux-ide@latest",
@@ -62,7 +68,21 @@ export function planUpdate(input: {
62
68
  reason: `global ${method} layout (${input.cliPath})`,
63
69
  };
64
70
  }
71
+ if (method === "installer") {
72
+ const prefix = managedInstallerPrefix(input.cliPath);
73
+ if (prefix) {
74
+ return {
75
+ method,
76
+ channel,
77
+ installerPrefix: prefix,
78
+ command: `installer ${MANAGED_INSTALLER_URL} --prefix ${JSON.stringify(prefix)} --version ${channel}`,
79
+ reason: `verified active managed installation (${prefix})`,
80
+ };
81
+ }
82
+ }
65
83
  const guidance: Record<Exclude<InstallOrigin, PackageManager>, string> = {
84
+ installer:
85
+ "Rerun https://tmux-ide.com/install.sh with the same --prefix to update atomically and retain rollback. This managed release must not be modified by npm in place.",
66
86
  dev: "Update this checkout with git pull, then follow its build instructions.",
67
87
  homebrew: "Update with brew upgrade tmux-ide (using the tap/formula you installed).",
68
88
  yarn: `Update this Yarn global installation with yarn global add tmux-ide@${channel}.`,
@@ -167,6 +187,10 @@ export function runUpdate(
167
187
  });
168
188
  executed = true;
169
189
  }
190
+ if (!dryRun && plan.installerPrefix && plan.channel) {
191
+ runManagedInstallerUpdate(plan.installerPrefix, plan.channel, json, dependencies.execute);
192
+ executed = true;
193
+ }
170
194
  if (json) output(JSON.stringify({ ...plan, current, latest, dryRun, executed }));
171
195
  return plan;
172
196
  }