tmux-ide 2.9.2 → 2.9.4

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 (267) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +121 -140
  3. package/bin/cli.js +2457 -727
  4. package/package.json +23 -8
  5. package/packages/contracts/src/desktop-host.ts +3 -0
  6. package/packages/contracts/src/saved-machines.ts +19 -0
  7. package/packages/contracts/src/terminal-replica.ts +1 -1
  8. package/packages/core/package.json +3 -2
  9. package/packages/core/src/terminal-delivery.ts +51 -3
  10. package/packages/core/src/terminal-fnv64-wasm.ts +4 -3
  11. package/packages/core/src/terminal-replica-hash-cache.ts +129 -31
  12. package/packages/core/src/terminal-replica-owned-row.ts +27 -0
  13. package/packages/core/src/terminal-replica.ts +13 -28
  14. package/packages/daemon/dist/command-center/daemon-fleet-facts-observer.js +96 -10
  15. package/packages/daemon/dist/command-center/resources/saved-machine-route.js +40 -1
  16. package/packages/daemon/dist/command-center/ws-events.js +5 -24
  17. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +8 -0
  18. package/packages/daemon/dist/lib/canonical-daemon.js +28 -2
  19. package/packages/daemon/dist/lib/daemon-embed.js +1 -1
  20. package/packages/daemon/dist/lib/daemon-service-manager.js +122 -0
  21. package/packages/daemon/dist/lib/daemon-service-plan.js +127 -0
  22. package/packages/daemon/dist/lib/daemon-service.js +259 -0
  23. package/packages/daemon/dist/lib/install-origin.js +2 -0
  24. package/packages/daemon/dist/lib/local-fleet-request.js +9 -3
  25. package/packages/daemon/dist/lib/log.js +6 -10
  26. package/packages/daemon/dist/lib/managed-installer-update.js +67 -0
  27. package/packages/daemon/dist/lib/{tui-download-lock.js → private-operation-lock.js} +11 -11
  28. package/packages/daemon/dist/lib/remote-daemon-info.js +3 -2
  29. package/packages/daemon/dist/lib/remote-daemon-lifecycle.js +4 -4
  30. package/packages/daemon/dist/lib/remote-tmux-command.js +10 -0
  31. package/packages/daemon/dist/lib/runtime-namespace.js +28 -16
  32. package/packages/daemon/dist/lib/saved-machines.js +40 -2
  33. package/packages/daemon/dist/lib/ssh-daemon-transport.js +24 -9
  34. package/packages/daemon/dist/lib/tmux-server-owner.js +1 -1
  35. package/packages/daemon/dist/lib/tmux-server-registration.js +21 -3
  36. package/packages/daemon/dist/lib/tui-binary.js +2 -2
  37. package/packages/daemon/dist/lib/update.js +18 -0
  38. package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +24 -1
  39. package/packages/daemon/dist/machines.js +40 -4
  40. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  41. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  42. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  43. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  44. package/packages/daemon/dist/terminal/attachments/direct-websocket.js +4 -1
  45. package/packages/daemon/dist/terminal/attachments/native-runtime.js +22 -13
  46. package/packages/daemon/dist/terminal/mirror/mirror-service.js +101 -3
  47. package/packages/daemon/dist/terminal/mirror/registered-window-guard.js +62 -0
  48. package/packages/daemon/dist/terminal/mirror/session-channel.js +75 -4
  49. package/packages/daemon/dist/terminal/mirror/shared-window-index.js +45 -0
  50. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +11 -1
  51. package/packages/daemon/dist/terminal/session-runtime/multiplexer-backend.js +1 -0
  52. package/packages/daemon/dist/terminal/session-runtime/registry.js +56 -7
  53. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +3 -1
  54. package/packages/daemon/dist/tui/integrations/claude-agent-teams.js +264 -0
  55. package/packages/daemon/dist/tui/mirror/dialog-model.js +2 -35
  56. package/packages/daemon/dist/tui/mirror/runtime/application-daemon-authority-owner.js +8 -0
  57. package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +20 -2
  58. package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +7 -1
  59. package/packages/daemon/dist/tui/mirror/runtime/application-machine-authority.js +59 -9
  60. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +27 -19
  61. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +23 -18
  62. package/packages/daemon/dist/tui/mirror/runtime/application-machine-startup.js +31 -2
  63. package/packages/daemon/dist/tui/mirror/runtime/application-palette-search-owner.js +7 -3
  64. package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +3 -0
  65. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +146 -2
  66. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +201 -50
  67. package/packages/daemon/dist/tui/mirror/runtime/bounded-performance-record-writer.js +50 -10
  68. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -5
  69. package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +3 -0
  70. package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +8 -2
  71. package/packages/daemon/dist/tui/mirror/terminal-text.js +35 -0
  72. package/packages/daemon/dist/tui/mirror/workspace/connection-feedback.js +5 -3
  73. package/packages/daemon/src/command-center/daemon-fleet-facts-observer.ts +100 -10
  74. package/packages/daemon/src/command-center/resources/saved-machine-route.ts +48 -1
  75. package/packages/daemon/src/command-center/ws-events.ts +5 -25
  76. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +12 -0
  77. package/packages/daemon/src/lib/canonical-daemon.ts +44 -2
  78. package/packages/daemon/src/lib/daemon-embed.ts +1 -0
  79. package/packages/daemon/src/lib/daemon-service-manager.ts +152 -0
  80. package/packages/daemon/src/lib/daemon-service-plan.ts +153 -0
  81. package/packages/daemon/src/lib/daemon-service.ts +301 -0
  82. package/packages/daemon/src/lib/install-origin.ts +5 -0
  83. package/packages/daemon/src/lib/local-fleet-request.ts +19 -3
  84. package/packages/daemon/src/lib/log.ts +13 -9
  85. package/packages/daemon/src/lib/managed-installer-update.ts +79 -0
  86. package/packages/daemon/src/lib/{tui-download-lock.ts → private-operation-lock.ts} +15 -11
  87. package/packages/daemon/src/lib/remote-daemon-info.ts +3 -3
  88. package/packages/daemon/src/lib/remote-daemon-lifecycle.ts +4 -4
  89. package/packages/daemon/src/lib/remote-tmux-command.ts +12 -0
  90. package/packages/daemon/src/lib/runtime-namespace.ts +27 -18
  91. package/packages/daemon/src/lib/saved-machines.ts +40 -1
  92. package/packages/daemon/src/lib/ssh-daemon-transport.ts +25 -7
  93. package/packages/daemon/src/lib/tmux-server-owner.ts +1 -0
  94. package/packages/daemon/src/lib/tmux-server-registration.ts +21 -3
  95. package/packages/daemon/src/lib/tui-binary.ts +2 -2
  96. package/packages/daemon/src/lib/update.ts +24 -0
  97. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +22 -0
  98. package/packages/daemon/src/machines.ts +53 -5
  99. package/packages/daemon/src/terminal/attachments/direct-websocket.ts +8 -2
  100. package/packages/daemon/src/terminal/attachments/native-runtime.ts +28 -13
  101. package/packages/daemon/src/terminal/mirror/mirror-service.ts +105 -3
  102. package/packages/daemon/src/terminal/mirror/registered-window-guard.ts +66 -0
  103. package/packages/daemon/src/terminal/mirror/session-channel.ts +92 -4
  104. package/packages/daemon/src/terminal/mirror/shared-window-index.ts +42 -0
  105. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +12 -1
  106. package/packages/daemon/src/terminal/session-runtime/multiplexer-backend.ts +2 -0
  107. package/packages/daemon/src/terminal/session-runtime/registry.ts +70 -8
  108. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +3 -1
  109. package/packages/daemon/src/tui/integrations/claude-agent-teams.ts +334 -0
  110. package/packages/daemon/src/tui/mirror/dialog-model.ts +2 -32
  111. package/packages/daemon/src/tui/mirror/runtime/application-daemon-authority-owner.ts +8 -0
  112. package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +19 -2
  113. package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +6 -1
  114. package/packages/daemon/src/tui/mirror/runtime/application-machine-authority.ts +60 -11
  115. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +39 -33
  116. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +31 -23
  117. package/packages/daemon/src/tui/mirror/runtime/application-machine-startup.ts +38 -2
  118. package/packages/daemon/src/tui/mirror/runtime/application-palette-search-owner.ts +7 -4
  119. package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +3 -0
  120. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +1 -1
  121. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +47 -46
  122. package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +165 -2
  123. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +207 -53
  124. package/packages/daemon/src/tui/mirror/runtime/bounded-performance-record-writer.ts +49 -9
  125. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +6 -5
  126. package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +4 -0
  127. package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +8 -3
  128. package/packages/daemon/src/tui/mirror/terminal-text.ts +32 -0
  129. package/packages/daemon/src/tui/mirror/workspace/connection-feedback.ts +6 -3
  130. package/packages/daemon-client/src/fleet-connection-status.ts +10 -1
  131. package/packages/daemon-client/src/workspace-client.ts +4 -1
  132. package/skill/SKILL.md +30 -30
  133. package/packages/contracts/src/__tests__/actions-contract.test.ts +0 -119
  134. package/packages/contracts/src/__tests__/agent-graph-overlay.test.ts +0 -428
  135. package/packages/contracts/src/__tests__/app-window-mutation.test.ts +0 -51
  136. package/packages/contracts/src/__tests__/app-window-state.test.ts +0 -34
  137. package/packages/contracts/src/__tests__/application-shell.test.ts +0 -645
  138. package/packages/contracts/src/__tests__/automation-operations.test.ts +0 -78
  139. package/packages/contracts/src/__tests__/causal-cell.test.ts +0 -47
  140. package/packages/contracts/src/__tests__/client-view-state.test.ts +0 -115
  141. package/packages/contracts/src/__tests__/cohesion-fixture.test.ts +0 -106
  142. package/packages/contracts/src/__tests__/commands.test.ts +0 -86
  143. package/packages/contracts/src/__tests__/control.test.ts +0 -154
  144. package/packages/contracts/src/__tests__/daemon-events.test.ts +0 -405
  145. package/packages/contracts/src/__tests__/daemon-resources.test.ts +0 -143
  146. package/packages/contracts/src/__tests__/daemon-wire.test.ts +0 -149
  147. package/packages/contracts/src/__tests__/desktop-host.test.ts +0 -269
  148. package/packages/contracts/src/__tests__/desktop-missions.test.ts +0 -215
  149. package/packages/contracts/src/__tests__/experience-conformance.test.ts +0 -153
  150. package/packages/contracts/src/__tests__/experience-kernel-boundary.test.ts +0 -113
  151. package/packages/contracts/src/__tests__/experience-shell.test.ts +0 -65
  152. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v2.json +0 -35
  153. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +0 -35
  154. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v4.json +0 -35
  155. package/packages/contracts/src/__tests__/fixtures/scoped-receipt.ts +0 -62
  156. package/packages/contracts/src/__tests__/fleet-agent-graph.test.ts +0 -231
  157. package/packages/contracts/src/__tests__/fleet-catalog.test.ts +0 -270
  158. package/packages/contracts/src/__tests__/fleet-lifecycle.test.ts +0 -62
  159. package/packages/contracts/src/__tests__/focus-overlay.test.ts +0 -110
  160. package/packages/contracts/src/__tests__/ide-config.test.ts +0 -64
  161. package/packages/contracts/src/__tests__/interaction-evidence.test.ts +0 -288
  162. package/packages/contracts/src/__tests__/interaction-receipt-v2.test.ts +0 -72
  163. package/packages/contracts/src/__tests__/issue-error.test.ts +0 -78
  164. package/packages/contracts/src/__tests__/mission-projections.test.ts +0 -186
  165. package/packages/contracts/src/__tests__/missions.test.ts +0 -272
  166. package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +0 -242
  167. package/packages/contracts/src/__tests__/pane-appearance.test.ts +0 -146
  168. package/packages/contracts/src/__tests__/pane-read-result.test.ts +0 -55
  169. package/packages/contracts/src/__tests__/pane-stream.test.ts +0 -536
  170. package/packages/contracts/src/__tests__/pane-widget-marker.test.ts +0 -327
  171. package/packages/contracts/src/__tests__/performance-metrics.test.ts +0 -73
  172. package/packages/contracts/src/__tests__/performance-qualification.test.ts +0 -137
  173. package/packages/contracts/src/__tests__/rich-card-widget.test.ts +0 -29
  174. package/packages/contracts/src/__tests__/session-runtime.test.ts +0 -512
  175. package/packages/contracts/src/__tests__/startup-readiness.test.ts +0 -409
  176. package/packages/contracts/src/__tests__/terminal-attachment-stream.test.ts +0 -72
  177. package/packages/contracts/src/__tests__/terminal-attachments.test.ts +0 -329
  178. package/packages/contracts/src/__tests__/terminal-delivery.test.ts +0 -88
  179. package/packages/contracts/src/__tests__/terminal-runtime-inventory.test.ts +0 -68
  180. package/packages/contracts/src/__tests__/terminals.test.ts +0 -87
  181. package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +0 -166
  182. package/packages/contracts/src/__tests__/transport-contract-loading.test.ts +0 -91
  183. package/packages/contracts/src/__tests__/visual-tokens.test.ts +0 -224
  184. package/packages/contracts/src/__tests__/window-links.test.ts +0 -243
  185. package/packages/contracts/src/__tests__/workspace-changes-resource.test.ts +0 -317
  186. package/packages/contracts/src/__tests__/workspace-changes-view.test.ts +0 -122
  187. package/packages/contracts/src/__tests__/workspace-config.test.ts +0 -294
  188. package/packages/contracts/src/__tests__/workspace-files-resource.test.ts +0 -261
  189. package/packages/contracts/src/__tests__/workspace-files-tree.test.ts +0 -147
  190. package/packages/contracts/src/__tests__/workspace-pane-creation.test.ts +0 -249
  191. package/packages/contracts/src/__tests__/workspace-promotion.test.ts +0 -94
  192. package/packages/contracts/src/__tests__/workspace-resource-identity.test.ts +0 -90
  193. package/packages/contracts/src/daemon-resource-request.test.ts +0 -192
  194. package/packages/contracts/src/native-operation-identity.test.ts +0 -23
  195. package/packages/contracts/src/workspace-admission.test.ts +0 -21
  196. package/packages/contracts/src/workspace-catalog-resource.test.ts +0 -146
  197. package/packages/core/src/agent-provisioning.test.ts +0 -42
  198. package/packages/core/src/application-shell-session.test.ts +0 -105
  199. package/packages/core/src/interaction-evidence.test.ts +0 -134
  200. package/packages/core/src/interaction-flow.test.ts +0 -66
  201. package/packages/core/src/interaction-receipts.test.ts +0 -671
  202. package/packages/core/src/navigator.test.ts +0 -34
  203. package/packages/core/src/optimistic-projection-conformance.test.ts +0 -75
  204. package/packages/core/src/optimistic-projection.test.ts +0 -118
  205. package/packages/core/src/performance-metrics.test.ts +0 -121
  206. package/packages/core/src/performance-qualification.test.ts +0 -283
  207. package/packages/core/src/saved-machines.test.ts +0 -80
  208. package/packages/core/src/terminal-compact-cold.test.ts +0 -87
  209. package/packages/core/src/terminal-conformance.test.ts +0 -48
  210. package/packages/core/src/terminal-delivery-cache-lifetime.test.ts +0 -167
  211. package/packages/core/src/terminal-delivery-encode-cooperative.test.ts +0 -140
  212. package/packages/core/src/terminal-delivery-representation-hash.test.ts +0 -77
  213. package/packages/core/src/terminal-delivery.test.ts +0 -1471
  214. package/packages/core/src/terminal-fnv64-wasm.test.ts +0 -61
  215. package/packages/core/src/terminal-replica-cold.test.ts +0 -31
  216. package/packages/core/src/terminal-replica-cooperative.test.ts +0 -170
  217. package/packages/core/src/terminal-replica-hash-cache.test.ts +0 -283
  218. package/packages/core/src/terminal-replica.test.ts +0 -347
  219. package/packages/core/src/workspace-selection.test.ts +0 -63
  220. package/packages/daemon/dist/__tests__/support.js +0 -33
  221. package/packages/daemon/dist/lib/__tests__/fixtures/workspace-state-writer.js +0 -58
  222. package/packages/daemon/dist/lib/__tests__/installed-recovery-fixture.js +0 -400
  223. package/packages/daemon/dist/terminal/__tests__/MockPtyAdapter.js +0 -182
  224. package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +0 -98
  225. package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +0 -156
  226. package/packages/daemon/src/tui/mirror/__snapshots__/activity-surface-renderer.test.tsx.snap +0 -42
  227. package/packages/daemon/src/tui/mirror/__snapshots__/changes-terminal-surface-renderer.test.tsx.snap +0 -267
  228. package/packages/daemon/src/tui/mirror/__snapshots__/home-files-surface-renderer.test.tsx.snap +0 -267
  229. package/packages/daemon/src/tui/mirror/__snapshots__/missions-surface-renderer.test.tsx.snap +0 -134
  230. package/packages/daemon/src/tui/mirror/__snapshots__/recipes-gallery-renderer.test.tsx.snap +0 -141
  231. package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +0 -134
  232. package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +0 -87
  233. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/agent-terminal-canvas-renderer.test.tsx.snap +0 -348
  234. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +0 -134
  235. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/command-palette-surface-renderer.test.tsx.snap +0 -255
  236. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/pane-frame-renderer.test.tsx.snap +0 -525
  237. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/terminal-pane-header-polish-renderer.test.tsx.snap +0 -31
  238. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/workbench-shell-renderer.test.tsx.snap +0 -134
  239. package/packages/daemon-client/src/application-shell-session.test.ts +0 -196
  240. package/packages/daemon-client/src/automation-client.test.ts +0 -214
  241. package/packages/daemon-client/src/bootstrap-coordinator.test.ts +0 -145
  242. package/packages/daemon-client/src/connection-supervisor.test.ts +0 -109
  243. package/packages/daemon-client/src/direct-application-shell-transport.test.ts +0 -941
  244. package/packages/daemon-client/src/first-latest-coordinator.test.ts +0 -175
  245. package/packages/daemon-client/src/fleet-dial-scheduler.test.ts +0 -114
  246. package/packages/daemon-client/src/fleet-environments.test.ts +0 -40
  247. package/packages/daemon-client/src/generation-bound-store-timers.test.ts +0 -74
  248. package/packages/daemon-client/src/owner-action-client.test.ts +0 -245
  249. package/packages/daemon-client/src/pane-stream-client.test.ts +0 -2841
  250. package/packages/daemon-client/src/pane-stream-clock-calibration.test.ts +0 -207
  251. package/packages/daemon-client/src/pane-stream-loopback-benchmark.test.ts +0 -51
  252. package/packages/daemon-client/src/push-resource-session.test.ts +0 -509
  253. package/packages/daemon-client/src/resource-replica.test.ts +0 -186
  254. package/packages/daemon-client/src/runtime-resource-ledger.test.ts +0 -18
  255. package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +0 -114
  256. package/packages/daemon-client/src/terminal-fast-lane.test.ts +0 -835
  257. package/packages/daemon-client/src/tmux-server-client.test.ts +0 -279
  258. package/packages/daemon-client/src/tmux-server-interaction-events.test.ts +0 -230
  259. package/packages/daemon-client/src/workspace-catalog-v2.test.ts +0 -109
  260. package/packages/daemon-client/src/workspace-client-boundaries.test.ts +0 -59
  261. package/packages/daemon-client/src/workspace-client-operations.test.ts +0 -96
  262. package/packages/daemon-client/src/workspace-client.test.ts +0 -2912
  263. package/packages/daemon-client/src/workspace-event-supervisor.test.ts +0 -1086
  264. package/packages/daemon-client/src/workspace-pane-client.test.ts +0 -80
  265. package/packages/presentation/src/presentation-contract.test.ts +0 -63
  266. package/packages/tmux-bridge/src/runner.test.ts +0 -614
  267. package/packages/tmux-bridge/src/targeting.test.ts +0 -107
@@ -0,0 +1,301 @@
1
+ import {
2
+ accessSync,
3
+ constants,
4
+ openSync,
5
+ closeSync,
6
+ fstatSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ unlinkSync,
10
+ writeFileSync,
11
+ renameSync,
12
+ rmSync,
13
+ realpathSync,
14
+ linkSync,
15
+ } from "node:fs";
16
+ import { randomUUID } from "node:crypto";
17
+ import { dirname, join } from "node:path";
18
+ import { homedir } from "node:os";
19
+ import { z } from "zod";
20
+ import {
21
+ getCanonicalDaemonInfoPath,
22
+ inspectCanonicalDaemonInfo,
23
+ probeCanonicalDaemonIdentity,
24
+ releaseCanonicalDaemonSupervision,
25
+ reserveCanonicalDaemonSupervision,
26
+ type CanonicalDaemonInfoState,
27
+ } from "./canonical-daemon.ts";
28
+ import { createDaemonServiceManager } from "./daemon-service-manager.ts";
29
+ import { planDaemonService, type DaemonServicePlan } from "./daemon-service-plan.ts";
30
+ import { resolveRuntimeNamespace } from "./runtime-namespace.ts";
31
+ import { IdeError } from "./errors.ts";
32
+ import { acquirePrivateOperationLock } from "./private-operation-lock.ts";
33
+
34
+ const RecordSchema = z.strictObject({
35
+ version: z.literal(1),
36
+ removing: z.boolean().optional(),
37
+ executable: z.string().min(1),
38
+ contents: z
39
+ .string()
40
+ .min(1)
41
+ .max(64 * 1024),
42
+ });
43
+
44
+ export interface DaemonServiceDependencies {
45
+ plan(executable: string): DaemonServicePlan;
46
+ manager: typeof createDaemonServiceManager;
47
+ inspect: typeof inspectCanonicalDaemonInfo;
48
+ reserve: typeof reserveCanonicalDaemonSupervision;
49
+ release: typeof releaseCanonicalDaemonSupervision;
50
+ probe: typeof probeCanonicalDaemonIdentity;
51
+ recordPath: string;
52
+ waitMs: number;
53
+ }
54
+
55
+ function defaults(): DaemonServiceDependencies {
56
+ const namespace = resolveRuntimeNamespace();
57
+ return {
58
+ plan: (executable) =>
59
+ planDaemonService({
60
+ platform: process.platform,
61
+ uid: process.getuid?.() ?? 0,
62
+ home: homedir(),
63
+ configHome: process.env.XDG_CONFIG_HOME || undefined,
64
+ executable,
65
+ path: process.env.PATH || "/usr/bin:/bin",
66
+ namespace,
67
+ }),
68
+ manager: createDaemonServiceManager,
69
+ inspect: inspectCanonicalDaemonInfo,
70
+ reserve: reserveCanonicalDaemonSupervision,
71
+ release: releaseCanonicalDaemonSupervision,
72
+ probe: probeCanonicalDaemonIdentity,
73
+ recordPath: join(dirname(getCanonicalDaemonInfoPath()), "service.json"),
74
+ waitMs: 30_000,
75
+ };
76
+ }
77
+
78
+ function refused(message: string): never {
79
+ throw new IdeError(message, { code: "DAEMON_SERVICE_REFUSED" });
80
+ }
81
+
82
+ function ownedFile(path: string): string | null {
83
+ let descriptor: number | undefined;
84
+ try {
85
+ descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
86
+ const stat = fstatSync(descriptor);
87
+ if (
88
+ !stat.isFile() ||
89
+ stat.uid !== process.getuid?.() ||
90
+ (stat.mode & 0o022) !== 0 ||
91
+ stat.size > 128 * 1024
92
+ )
93
+ refused(`Service file is not a bounded, user-owned regular file: ${path}`);
94
+ return readFileSync(descriptor, "utf8");
95
+ } catch (error) {
96
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
97
+ throw error;
98
+ } finally {
99
+ if (descriptor !== undefined) closeSync(descriptor);
100
+ }
101
+ }
102
+
103
+ function boundTo(state: CanonicalDaemonInfoState, id: string): boolean {
104
+ return state.status === "reserved"
105
+ ? state.reservation.supervisionId === id
106
+ : state.status === "valid" && state.info.supervisionId === id;
107
+ }
108
+
109
+ /** Explicit OS supervision. Never starts a detached daemon or signals a guessed PID. */
110
+ export async function manageDaemonService(
111
+ action: "install" | "status" | "restart" | "remove",
112
+ executable?: string,
113
+ deps: DaemonServiceDependencies = defaults(),
114
+ ) {
115
+ const previousRecord = ownedFile(deps.recordPath);
116
+ const record = previousRecord === null ? null : RecordSchema.parse(JSON.parse(previousRecord));
117
+ if (record?.removing && action !== "remove" && action !== "status")
118
+ refused("Service removal is incomplete; finish remove before installing or restarting");
119
+ if (!record && action !== "install") {
120
+ if (action === "status") return { status: "not-installed" as const };
121
+ refused("No managed service is installed in this daemon namespace");
122
+ }
123
+ if (action === "install" && !executable)
124
+ refused("Service installation requires an absolute stable CLI launcher path");
125
+ if (record && executable && record.executable !== executable)
126
+ refused(
127
+ "A different launcher is already registered; remove the stopped service before changing it",
128
+ );
129
+ const plan = deps.plan(record?.executable ?? executable!);
130
+ if (plan.recordPath !== deps.recordPath) refused("Service namespace changed during resolution");
131
+ const contents = record?.contents ?? plan.contents;
132
+ const manager = deps.manager(plan);
133
+ const assertFiles = (allowMissingUnit = false) => {
134
+ const unit = ownedFile(plan.unitPath);
135
+ if (unit !== contents && !((record?.removing || allowMissingUnit) && unit === null))
136
+ refused("Service definition is missing or modified; refusing to control it");
137
+ if (record && ownedFile(plan.recordPath) !== previousRecord)
138
+ refused("Service ownership record changed");
139
+ };
140
+ const inspectManager = async () => {
141
+ const state = await manager.inspect();
142
+ // systemd reports its generated search-path symlink for units enabled by
143
+ // absolute path. Resolve that link to the owned definition; the definition
144
+ // itself still has to pass our no-follow private-file and content checks.
145
+ if (
146
+ state.loaded &&
147
+ (!state.definitionPath || realpathSync(state.definitionPath) !== realpathSync(plan.unitPath))
148
+ )
149
+ refused("The service manager loaded a different definition; refusing to control it");
150
+ return state;
151
+ };
152
+ const ready = async (previousInstance?: string) => {
153
+ const deadline = Date.now() + deps.waitMs;
154
+ do {
155
+ const state = deps.inspect();
156
+ if (!boundTo(state, plan.supervisionId))
157
+ refused("Service reservation changed during startup");
158
+ if (state.status === "valid" && state.info.instanceId !== previousInstance) {
159
+ const identity = await deps.probe(state.info);
160
+ const service = await inspectManager();
161
+ if (
162
+ identity &&
163
+ service.active &&
164
+ service.pid === state.info.pid &&
165
+ identity.pid === state.info.pid &&
166
+ identity.instanceId === state.info.instanceId &&
167
+ identity.startedAt === state.info.startedAt &&
168
+ identity.protocolVersion === state.info.protocolVersion
169
+ )
170
+ return {
171
+ status: "running" as const,
172
+ manager: plan.manager,
173
+ target: plan.target,
174
+ pid: state.info.pid,
175
+ instanceId: state.info.instanceId,
176
+ };
177
+ }
178
+ await new Promise((resolve) => setTimeout(resolve, 100));
179
+ } while (Date.now() < deadline);
180
+ refused(
181
+ "Service did not publish a verified daemon before the deadline; inspect service logs, then retry restart or remove",
182
+ );
183
+ };
184
+ await manager.available();
185
+ const service = await inspectManager();
186
+ const allowMissingUnit = action === "remove" && !service.loaded;
187
+ if (record) assertFiles(allowMissingUnit);
188
+ if (action === "status") {
189
+ const state = deps.inspect();
190
+ const identity =
191
+ service.active && state.status === "valid" && boundTo(state, plan.supervisionId)
192
+ ? await deps.probe(state.info)
193
+ : null;
194
+ const verified =
195
+ identity !== null &&
196
+ state.status === "valid" &&
197
+ service.pid === state.info.pid &&
198
+ identity.pid === state.info.pid &&
199
+ identity.instanceId === state.info.instanceId &&
200
+ identity.startedAt === state.info.startedAt &&
201
+ identity.protocolVersion === state.info.protocolVersion;
202
+ return {
203
+ status: verified
204
+ ? ("running" as const)
205
+ : service.active
206
+ ? ("active-unverified" as const)
207
+ : ("stopped" as const),
208
+ manager: plan.manager,
209
+ target: plan.target,
210
+ pid: service.pid,
211
+ reservationMatches: boundTo(state, plan.supervisionId),
212
+ unitPath: plan.unitPath,
213
+ };
214
+ }
215
+ if (!record && (service.loaded || ownedFile(plan.unitPath) !== null))
216
+ refused("A service definition already exists without this namespace's ownership record");
217
+ if (
218
+ record &&
219
+ !boundTo(deps.inspect(), plan.supervisionId) &&
220
+ !(record.removing && action === "remove" && deps.inspect().status === "missing")
221
+ )
222
+ refused("Service reservation is missing or belongs to another owner");
223
+ if (!record) {
224
+ accessSync(plan.executable, constants.X_OK);
225
+ // Canonical reservation admission rejects live, uncertain and foreign owners.
226
+ deps.reserve(plan.supervisionId);
227
+ }
228
+ const lock = `${plan.recordPath}.lock`;
229
+ const releaseLock = await acquirePrivateOperationLock(lock, 1000);
230
+ let wroteRecord = false;
231
+ let wroteUnit = false;
232
+ let activationAttempted = false;
233
+ try {
234
+ if (record) assertFiles(allowMissingUnit);
235
+ if (action === "install" && !record) {
236
+ writeFileSync(
237
+ plan.recordPath,
238
+ JSON.stringify({ version: 1, executable: plan.executable, contents }) + "\n",
239
+ { flag: "wx", mode: 0o600 },
240
+ );
241
+ wroteRecord = true;
242
+ mkdirSync(dirname(plan.unitPath), { recursive: true, mode: 0o700 });
243
+ const unitStage = `${plan.unitPath}.${randomUUID()}.tmp`;
244
+ writeFileSync(unitStage, contents, { flag: "wx", mode: 0o600 });
245
+ try {
246
+ linkSync(unitStage, plan.unitPath);
247
+ } finally {
248
+ unlinkSync(unitStage);
249
+ }
250
+ wroteUnit = true;
251
+ activationAttempted = true;
252
+ await manager.install();
253
+ return await ready();
254
+ }
255
+ if (action === "remove") {
256
+ if (service.loaded) await manager.stop();
257
+ let stopped = await inspectManager();
258
+ const stopDeadline = Date.now() + deps.waitMs;
259
+ while ((stopped.active || stopped.pid !== null) && Date.now() < stopDeadline) {
260
+ await new Promise((resolve) => setTimeout(resolve, 100));
261
+ stopped = await inspectManager();
262
+ }
263
+ if (stopped.active || stopped.pid !== null)
264
+ refused("Service manager has not confirmed the daemon stopped; reservation retained");
265
+ assertFiles(allowMissingUnit);
266
+ const removalRecord = JSON.stringify({ ...record!, removing: true }) + "\n";
267
+ const temporaryRecord = `${plan.recordPath}.${randomUUID()}.tmp`;
268
+ writeFileSync(temporaryRecord, removalRecord, { flag: "wx", mode: 0o600 });
269
+ try {
270
+ if (ownedFile(plan.recordPath) !== previousRecord)
271
+ refused("Service ownership record changed during removal");
272
+ renameSync(temporaryRecord, plan.recordPath);
273
+ } finally {
274
+ rmSync(temporaryRecord, { force: true });
275
+ }
276
+ if (ownedFile(plan.unitPath) !== null) unlinkSync(plan.unitPath);
277
+ await manager.reload();
278
+ if ((await inspectManager()).loaded)
279
+ refused("Service remains loaded after removal; reservation retained");
280
+ if (deps.inspect().status !== "missing") deps.release(plan.supervisionId);
281
+ unlinkSync(plan.recordPath);
282
+ return { status: "removed" as const, target: plan.target };
283
+ }
284
+ const before = deps.inspect();
285
+ if (action === "install" && service.active) return await ready();
286
+ if (service.loaded) await manager.restart(service.pid !== null);
287
+ else await manager.install();
288
+ return await ready(before.status === "valid" ? before.info.instanceId : undefined);
289
+ } catch (error) {
290
+ // Never discard ownership after an uncertain OS activation. The retained
291
+ // record/reservation gives the explicit remove/restart commands a recovery path.
292
+ if (!activationAttempted && wroteRecord) {
293
+ if (wroteUnit && ownedFile(plan.unitPath) === contents) unlinkSync(plan.unitPath);
294
+ unlinkSync(plan.recordPath);
295
+ deps.release(plan.supervisionId);
296
+ }
297
+ throw error;
298
+ } finally {
299
+ releaseLock();
300
+ }
301
+ }
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, realpathSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  export type InstallOrigin =
5
+ | "installer"
5
6
  | "npm"
6
7
  | "pnpm"
7
8
  | "bun"
@@ -12,6 +13,10 @@ export type InstallOrigin =
12
13
  | "unknown";
13
14
  /** Conservative layout evidence; an arbitrary node_modules directory is not a global install. */
14
15
  export function detectPackageManager(path: string): Exclude<InstallOrigin, "dev"> {
16
+ if (
17
+ /\/share\/tmux-ide\/releases\/install-[^/]+\/npm\/lib\/node_modules\/tmux-ide\/bin$/.test(path)
18
+ )
19
+ return "installer";
15
20
  if (/\/(?:Cellar|Caskroom)\//.test(path)) return "homebrew";
16
21
  if (/\/_npx\//.test(path)) return "npx";
17
22
  if (/\/(?:\.yarn|yarn)\//.test(path)) return "yarn";
@@ -1,4 +1,9 @@
1
- import { SavedMachineRegistrySchema, type SavedMachine } from "@tmux-ide/contracts/saved-machines";
1
+ import {
2
+ SavedMachineRegistrySchema,
3
+ type SavedMachine,
4
+ SavedMachineMutationSchema,
5
+ type SavedMachineMutation,
6
+ } from "@tmux-ide/contracts/saved-machines";
2
7
  import {
3
8
  canonicalDaemonUrl,
4
9
  isCanonicalDaemonAlive,
@@ -8,6 +13,17 @@ import {
8
13
  /** Intentional local registry mutation. Never uses selected-machine authority. */
9
14
  export async function saveMachineProfiles(machines: readonly SavedMachine[]) {
10
15
  const registry = SavedMachineRegistrySchema.parse({ version: 1, machines });
16
+ return writeLocalMachineRegistry("POST", { registry });
17
+ }
18
+
19
+ export async function mutateMachineProfile(change: SavedMachineMutation) {
20
+ return writeLocalMachineRegistry("PATCH", { change: SavedMachineMutationSchema.parse(change) });
21
+ }
22
+
23
+ async function writeLocalMachineRegistry(
24
+ method: "POST" | "PATCH",
25
+ payload: Record<string, unknown>,
26
+ ) {
11
27
  const daemon = readCanonicalDaemonInfo();
12
28
  if (!daemon?.authToken || !(await isCanonicalDaemonAlive(daemon)))
13
29
  throw new Error("Start a local tmux-ide daemon to save machine profiles.");
@@ -16,14 +32,14 @@ export async function saveMachineProfiles(machines: readonly SavedMachine[]) {
16
32
  canonicalDaemonUrl("http", daemon.bindHostname, daemon.port) +
17
33
  "/api/resources/saved-machines",
18
34
  {
19
- method: "POST",
35
+ method,
20
36
  redirect: "error",
21
37
  signal: AbortSignal.timeout(5000),
22
38
  headers: {
23
39
  Authorization: `Bearer ${daemon.authToken}`,
24
40
  "Content-Type": "application/json",
25
41
  },
26
- body: JSON.stringify({ expectedInstanceId: daemon.instanceId, registry }),
42
+ body: JSON.stringify({ expectedInstanceId: daemon.instanceId, ...payload }),
27
43
  },
28
44
  );
29
45
  if (!response.ok) {
@@ -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
+ }