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
@@ -10,12 +10,14 @@ import type {
10
10
  TerminalReplicaTombstonePayload,
11
11
  } from "@tmux-ide/contracts";
12
12
  import { consumeCompactReplicaCapability } from "./terminal-compact-capability.ts";
13
+ import { freezeOwnedTerminalReplicaRow as freezeRow } from "./terminal-replica-owned-row.ts";
13
14
  import {
14
15
  hashCanonicalTerminalValue,
15
16
  hashCanonicalTerminalValueCooperatively,
16
17
  hashTerminalReplicaRowCached,
17
18
  hashTerminalReplicaRowCooperatively,
18
19
  isTerminalReplicaRowDeeplyFrozen,
20
+ TerminalReplicaRunEncodingCache,
19
21
  } from "./terminal-replica-hash-cache.ts";
20
22
 
21
23
  // Only rows validated and copied by this module can bypass a repeated copy.
@@ -835,27 +837,11 @@ function blankRow(cols: number): TerminalReplicaRow {
835
837
  }) as unknown as TerminalReplicaRow;
836
838
  }
837
839
 
838
- function freezeRow(row: TerminalReplicaRow): TerminalReplicaRow {
839
- return Object.freeze({
840
- wrapped: row.wrapped,
841
- cells: Object.freeze(
842
- row.cells.map((cell) =>
843
- Object.freeze({
844
- ...cell,
845
- foreground: Object.freeze({ ...cell.foreground }),
846
- background: Object.freeze({ ...cell.background }),
847
- }),
848
- ),
849
- ),
850
- }) as unknown as TerminalReplicaRow;
851
- }
852
-
853
840
  function validateAndFreezeRow(
854
841
  row: TerminalReplicaRow,
855
842
  profile?: MutableTerminalReplicaApplyProfile,
856
843
  ): TerminalReplicaRow {
857
844
  if (VALIDATED_PATCH_ROWS.has(row)) return row;
858
- const cells = new Array(row.cells.length);
859
845
  for (let index = 0; index < row.cells.length; index += 1) {
860
846
  const cell = row.cells[index]!;
861
847
  if (profile) profile.counts.validatedCells += 1;
@@ -863,17 +849,11 @@ function validateAndFreezeRow(
863
849
  throw new TypeError("Malformed terminal replica row");
864
850
  if (cell.width === 0 && (index === 0 || row.cells[index - 1]?.width !== 2))
865
851
  throw new TypeError("Malformed terminal replica row");
866
- cells[index] = Object.freeze({
867
- ...cell,
868
- foreground: Object.freeze({ ...cell.foreground }),
869
- background: Object.freeze({ ...cell.background }),
870
- });
871
- if (profile) profile.counts.frozenCells += 1;
872
852
  }
873
- const frozen = Object.freeze({
874
- wrapped: row.wrapped,
875
- cells: Object.freeze(cells),
876
- }) as unknown as TerminalReplicaRow;
853
+ // Use the same copy owner as seed admission and encoding. Already owned rows
854
+ // retain identity; external rows still receive an immutable detached copy.
855
+ const frozen = freezeRow(row);
856
+ if (profile && frozen !== row) profile.counts.frozenCells += row.cells.length;
877
857
  VALIDATED_PATCH_ROWS.add(frozen);
878
858
  return frozen;
879
859
  }
@@ -1034,11 +1014,13 @@ function hashTerminalReplicaRow(
1034
1014
  row: TerminalReplicaRow,
1035
1015
  profile?: MutableTerminalReplicaApplyProfile,
1036
1016
  instrumentation?: TerminalReplicaApplyOptions["instrumentation"],
1017
+ encodingCache?: TerminalReplicaRunEncodingCache,
1037
1018
  ): string {
1038
1019
  const started = readProfileClock(instrumentation);
1039
1020
  const hash = hashTerminalReplicaRowCached(
1040
1021
  row,
1041
1022
  profile ? () => (profile.counts.rowHashMisses += 1) : undefined,
1023
+ encodingCache,
1042
1024
  );
1043
1025
  addProfileDuration(profile, "rowHash", started, instrumentation);
1044
1026
  return hash;
@@ -1051,12 +1033,13 @@ function hashTerminalReplicaRows(
1051
1033
  ): string {
1052
1034
  const cached = ROW_ARRAY_HASH_CACHE.get(rows);
1053
1035
  if (cached) return cached.hash.toString(16).padStart(16, "0");
1036
+ const encodingCache = rows.length >= 64 ? new TerminalReplicaRunEncodingCache() : undefined;
1054
1037
  let hash = 0n;
1055
1038
  for (const row of rows)
1056
1039
  hash = BigInt.asUintN(
1057
1040
  64,
1058
1041
  hash * ROW_SEQUENCE_BASE +
1059
- BigInt(`0x${hashTerminalReplicaRow(row, profile, instrumentation)}`),
1042
+ BigInt(`0x${hashTerminalReplicaRow(row, profile, instrumentation, encodingCache)}`),
1060
1043
  );
1061
1044
  if (Object.isFrozen(rows) && rows.every(isTerminalReplicaRowDeeplyFrozen))
1062
1045
  ROW_ARRAY_HASH_CACHE.set(rows, { hash, length: rows.length });
@@ -1099,10 +1082,12 @@ export function registerTerminalReplicaRowsDeltaHash(
1099
1082
  );
1100
1083
  hash = BigInt.asUintN(64, hash - contribution);
1101
1084
  }
1085
+ const encodingCache = append.length >= 64 ? new TerminalReplicaRunEncodingCache() : undefined;
1102
1086
  for (const row of append)
1103
1087
  hash = BigInt.asUintN(
1104
1088
  64,
1105
- hash * ROW_SEQUENCE_BASE + BigInt(`0x${hashTerminalReplicaRow(row)}`),
1089
+ hash * ROW_SEQUENCE_BASE +
1090
+ BigInt(`0x${hashTerminalReplicaRow(row, undefined, undefined, encodingCache)}`),
1106
1091
  );
1107
1092
  if (Object.isFrozen(next) && next.every(isTerminalReplicaRowDeeplyFrozen))
1108
1093
  ROW_ARRAY_HASH_CACHE.set(next, { hash, length: next.length });
@@ -459,16 +459,7 @@ export function createDefaultFleetFactsReaders() {
459
459
  return null;
460
460
  }
461
461
  };
462
- return {
463
- readSessions: async (signal) => {
464
- const raw = await execute(SESSION_COMPOSITION_TMUX_ARGS, signal);
465
- return raw === null ? null : parseSessionCompositionFacts(raw);
466
- },
467
- readAgents: async (signal) => {
468
- const raw = await execute(AGENT_STATE_TMUX_ARGS, signal);
469
- return raw === null ? null : parseAgentStateFacts(raw);
470
- },
471
- };
462
+ return createSharedFleetFactsReaders(execute);
472
463
  }
473
464
  export const SESSION_COMPOSITION_TMUX_ARGS = [
474
465
  "list-panes",
@@ -500,3 +491,98 @@ export const AGENT_STATE_TMUX_ARGS = [
500
491
  export async function readAgentStateFacts(signal) {
501
492
  return createDefaultFleetFactsReaders().readAgents(signal);
502
493
  }
494
+ /** Agent columns are excluded from the session topology projection. */
495
+ export const FLEET_FACTS_TMUX_ARGS = [
496
+ "list-panes",
497
+ "-a",
498
+ "-F",
499
+ `${SESSION_COMPOSITION_TMUX_ARGS[3]}\t#{@agent_state}\t#{pane_current_command}`,
500
+ ];
501
+ /** Share only an outstanding read; completed snapshots are never cached. */
502
+ export function createSharedFleetFactsReaders(run, isAbsentServer = () => false) {
503
+ let pending = null;
504
+ const read = async (signal) => {
505
+ if (signal?.aborted)
506
+ return null;
507
+ if (!pending) {
508
+ const controller = new AbortController();
509
+ const entry = {
510
+ controller,
511
+ readers: 0,
512
+ result: Promise.resolve().then(() => run(FLEET_FACTS_TMUX_ARGS, controller.signal)),
513
+ };
514
+ entry.result = entry.result.finally(() => {
515
+ if (pending === entry)
516
+ pending = null;
517
+ });
518
+ pending = entry;
519
+ }
520
+ const entry = pending;
521
+ entry.readers += 1;
522
+ let released = false;
523
+ const release = () => {
524
+ if (released)
525
+ return;
526
+ released = true;
527
+ entry.readers -= 1;
528
+ if (entry.readers === 0) {
529
+ if (pending === entry)
530
+ pending = null;
531
+ entry.controller.abort();
532
+ }
533
+ };
534
+ signal?.addEventListener("abort", release, { once: true });
535
+ try {
536
+ // Keep the promise outstanding for a noncooperative runner so the
537
+ // observer's per-kind quarantine remains effective after cancellation.
538
+ const raw = await entry.result;
539
+ return signal?.aborted ? null : raw;
540
+ }
541
+ catch (error) {
542
+ if (signal?.aborted)
543
+ return null;
544
+ throw error;
545
+ }
546
+ finally {
547
+ signal?.removeEventListener("abort", release);
548
+ release();
549
+ }
550
+ };
551
+ return {
552
+ readSessions: async (signal) => {
553
+ try {
554
+ const raw = await read(signal);
555
+ return raw === null
556
+ ? null
557
+ : parseSessionCompositionFacts(raw
558
+ .split("\n")
559
+ .map((line) => line.split("\t").slice(0, 11).join("\t"))
560
+ .join("\n"));
561
+ }
562
+ catch (error) {
563
+ // Socket absence is an authoritative empty catalog, but must not
564
+ // manufacture agent turn completions from a failed agent observation.
565
+ return isAbsentServer(error) ? parseSessionCompositionFacts("") : null;
566
+ }
567
+ },
568
+ readAgents: async (signal) => {
569
+ try {
570
+ const raw = await read(signal);
571
+ return raw === null
572
+ ? null
573
+ : parseAgentStateFacts(raw
574
+ .split("\n")
575
+ .map((line) => {
576
+ const fields = line.split("\t");
577
+ if (fields.length !== 13)
578
+ return "";
579
+ return [fields[0], fields[6], fields[9], fields[11], fields[12]].join("\t");
580
+ })
581
+ .join("\n"));
582
+ }
583
+ catch {
584
+ return null;
585
+ }
586
+ },
587
+ };
588
+ }
@@ -1,12 +1,17 @@
1
1
  import { bodyLimit } from "hono/body-limit";
2
2
  import { z } from "zod";
3
3
  import { SavedMachineRegistrySchema } from "@tmux-ide/contracts";
4
- import { loadSavedMachines, mergeSavedMachines } from "../../lib/saved-machines.js";
4
+ import { SavedMachineMutationSchema } from "@tmux-ide/contracts/saved-machines";
5
+ import { loadSavedMachines, mergeSavedMachines, updateSavedMachines, } from "../../lib/saved-machines.js";
5
6
  import { ownerAuthorityGate } from "../owner-authority.js";
6
7
  const Request = z.strictObject({
7
8
  expectedInstanceId: z.uuid(),
8
9
  registry: SavedMachineRegistrySchema,
9
10
  });
11
+ const MutationRequest = z.strictObject({
12
+ expectedInstanceId: z.uuid(),
13
+ change: SavedMachineMutationSchema,
14
+ });
10
15
  export function mountSavedMachineRoute(app, options) {
11
16
  const authorize = ownerAuthorityGate(options.ownerToken, {
12
17
  whenOwnerless: "unavailable",
@@ -47,4 +52,38 @@ export function mountSavedMachineRoute(app, options) {
47
52
  }, 409);
48
53
  }
49
54
  });
55
+ app.patch("/api/resources/saved-machines", bodyLimit({ maxSize: 1024 }), async (c) => {
56
+ const gate = authorize(c);
57
+ if (gate)
58
+ return gate;
59
+ c.header("Cache-Control", "no-store");
60
+ let parsed;
61
+ try {
62
+ parsed = MutationRequest.safeParse(await c.req.json());
63
+ }
64
+ catch {
65
+ return c.json({ error: "Invalid machine mutation" }, 400);
66
+ }
67
+ if (!parsed.success)
68
+ return c.json({ error: "Invalid machine mutation" }, 400);
69
+ if (parsed.data.expectedInstanceId !== options.daemon.instanceId)
70
+ return c.json({ error: "Daemon generation changed" }, 409);
71
+ const change = parsed.data.change;
72
+ const { id, operation } = change;
73
+ try {
74
+ const registry = (options.update ?? updateSavedMachines)(operation === "remove"
75
+ ? { type: "remove", id }
76
+ : {
77
+ type: "update",
78
+ id,
79
+ patch: change.operation === "edit" ? change.patch : { enabled: operation === "enable" },
80
+ });
81
+ return c.json({ daemon: options.daemon, registry });
82
+ }
83
+ catch {
84
+ return c.json({
85
+ error: "Machine not found, edit conflicts with another profile, or registry unavailable; nothing changed",
86
+ }, 409);
87
+ }
88
+ });
50
89
  }
@@ -1,6 +1,6 @@
1
1
  import { createBoundedControlWriter } from "./bounded-control-writer.js";
2
2
  import { discoverSessions, buildOverviews, buildProjectDetail } from "./discovery.js";
3
- import { DaemonFleetFactsObserver, AGENT_STATE_TMUX_ARGS, parseAgentStateFacts, parseSessionCompositionFacts, createDefaultFleetFactsReaders, SESSION_COMPOSITION_TMUX_ARGS, } from "./daemon-fleet-facts-observer.js";
3
+ import { DaemonFleetFactsObserver, createDefaultFleetFactsReaders, createSharedFleetFactsReaders, } from "./daemon-fleet-facts-observer.js";
4
4
  import { agentIdForPaneStamp } from "./resources/application-shell.js";
5
5
  import { projectRegistryEmitter } from "../lib/project-registry.js";
6
6
  import { getDefaultWorkspaceRegistry, isTmuxServerUnavailableError, } from "../lib/workspace-registry.js";
@@ -256,30 +256,11 @@ export function setFleetFactsObserverDiagnostics(diagnostics) {
256
256
  /** Pin fleet invalidations to the same tmux authority as catalog HTTP reads. */
257
257
  export function setFleetFactsTmuxRunner(runTmux) {
258
258
  stopFleetFactsObserver();
259
- sessionCompositionReaderOverride = runTmux
260
- ? async (signal) => {
261
- try {
262
- return parseSessionCompositionFacts(await runTmux(SESSION_COMPOSITION_TMUX_ARGS, signal));
263
- }
264
- catch (error) {
265
- // A daemon may legitimately precede the first tmux server. Socket
266
- // absence is an authoritative empty fleet baseline, not a failed
267
- // observation: catalog clients must be allowed to render their
268
- // actionable first-run state while the observer waits for tmux.
269
- return isTmuxServerUnavailableError(error) ? parseSessionCompositionFacts("") : null;
270
- }
271
- }
272
- : null;
273
- agentStateReaderOverride = runTmux
274
- ? async (signal) => {
275
- try {
276
- return parseAgentStateFacts(await runTmux(AGENT_STATE_TMUX_ARGS, signal));
277
- }
278
- catch {
279
- return null;
280
- }
281
- }
259
+ const readers = runTmux
260
+ ? createSharedFleetFactsReaders(runTmux, isTmuxServerUnavailableError)
282
261
  : null;
262
+ sessionCompositionReaderOverride = readers?.readSessions ?? null;
263
+ agentStateReaderOverride = readers?.readAgents ?? null;
283
264
  }
284
265
  function acquireGlobalObserver(kind) {
285
266
  let releaseAuthority;
@@ -133,6 +133,14 @@ async function replaceOlderCanonicalDaemon(deps, info, timeoutMs, expectedProduc
133
133
  reason: "identity-mismatch",
134
134
  });
135
135
  }
136
+ const supervisor = latest.info.provenance?.supervisor;
137
+ if (!latest.info.supervisionId && (supervisor === "systemd" || supervisor === "launchd")) {
138
+ throw new DaemonBootstrapError("incompatible", `The older canonical daemon is managed by ${supervisor} without a supervisor reservation. ` +
139
+ "Stop and disable the old service, reserve the stopped namespace with " +
140
+ "tmux-ide daemon reserve-supervisor <id>, then configure the service to run " +
141
+ "tmux-ide --headless --supervised <id> before restarting it. " +
142
+ "The existing daemon has been left running.", { reason: "canonical-record-invalid" });
143
+ }
136
144
  await deps.shutdownOlderOwner(info);
137
145
  const deadline = deps.now() + timeoutMs;
138
146
  while (deps.now() < deadline) {
@@ -10,7 +10,12 @@ const MAX_DAEMON_INFO_BYTES = 64 * 1024;
10
10
  const MAX_DAEMON_CLAIM_BYTES = 4 * 1024;
11
11
  const activeClaims = new Map();
12
12
  export function getCanonicalDaemonInfoPath() {
13
- return runtimeOwnedPath(join(resolveRuntimeNamespace().daemonInfoDir, DAEMON_INFO_FILE));
13
+ const namespace = resolveRuntimeNamespace();
14
+ const path = join(namespace.daemonInfoDir, DAEMON_INFO_FILE);
15
+ // Resolution already validates ordinary and isolated namespaces. Only
16
+ // development adds a per-path containment check; do not resolve twice on
17
+ // every ordinary daemon read. Each call still resolves the current namespace.
18
+ return namespace.development ? runtimeOwnedPath(path) : path;
14
19
  }
15
20
  export function getCanonicalDaemonClaimPath() {
16
21
  return join(dirname(getCanonicalDaemonInfoPath()), DAEMON_CLAIM_DIR);
@@ -472,10 +477,31 @@ export function assertCanonicalDaemonSupervision(supervisionId) {
472
477
  throw new Error("Matching supervisor reservation required before startup");
473
478
  }
474
479
  /** Explicit installation only; startup never calls this to recreate a reservation. */
480
+ /** Sanitized reservation guidance; never includes credentials or filesystem errors. */
481
+ export class CanonicalDaemonReservationError extends Error {
482
+ constructor(message) {
483
+ super(`Supervisor reservation refused: ${message}`);
484
+ this.name = "CanonicalDaemonReservationError";
485
+ }
486
+ }
487
+ function reservationRefusal(attempt) {
488
+ if (attempt.status === "busy")
489
+ return new CanonicalDaemonReservationError(`another lifecycle operation holds the namespace (PID ${attempt.owner.pid}). Wait for it to finish, then retry.`);
490
+ const state = inspectCanonicalDaemonInfo();
491
+ if (state.status === "valid")
492
+ return new CanonicalDaemonReservationError(`the recorded ${state.info.supervisionId ? "supervised" : "unsupervised"} daemon (PID ${state.info.pid}) has not been released. Inspect it with tmux-ide daemon info --json, then stop it through its owning process or service before reserving.`);
493
+ if (state.status === "reserved")
494
+ return new CanonicalDaemonReservationError(`this namespace is reserved for supervisor ${state.reservation.supervisionId}. Remove that service and release its reservation before assigning a different ID.`);
495
+ if (state.status === "invalid")
496
+ return new CanonicalDaemonReservationError(`the namespace record cannot be trusted (${state.reason}). Run tmux-ide daemon info --json for diagnosis; repair ownership or permissions before retrying.`);
497
+ return new CanonicalDaemonReservationError("the namespace changed or its lifecycle claim is unavailable. Run tmux-ide daemon info --json, then retry after the owning operation finishes.");
498
+ }
475
499
  export function reserveCanonicalDaemonSupervision(supervisionId) {
500
+ if (!DaemonSupervisionIdSchema.safeParse(supervisionId).success)
501
+ throw new CanonicalDaemonReservationError("use a 1–128 character ID starting with a letter or digit and containing only letters, digits, periods, underscores or hyphens.");
476
502
  const attempt = tryAcquireCanonicalDaemonClaim({ kind: "reserve", supervisionId });
477
503
  if (attempt.status !== "acquired")
478
- throw new Error("Cannot reserve supervised daemon namespace");
504
+ throw reservationRefusal(attempt);
479
505
  const claim = attempt.claim;
480
506
  try {
481
507
  const before = inspectCanonicalDaemonInfo();
@@ -989,7 +989,7 @@ async function startEmbeddedDaemonGeneration(opts) {
989
989
  if (!sessionRuntimeRegistry)
990
990
  throw new Error("Session runtime unavailable");
991
991
  return sessionRuntimeRegistry.executeWindowLinkAction(session, action);
992
- });
992
+ }, timing);
993
993
  }
994
994
  if (intent.verb === "workspace.pane.resize") {
995
995
  return workspaceMultiplexer.mutateResize({ operationId, expectedDaemonInstanceId: instanceId, intent }, (session) => {
@@ -0,0 +1,122 @@
1
+ import { execFile } from "node:child_process";
2
+ const runCommand = (file, args) => new Promise((resolve) => {
3
+ execFile(file, [...args], {
4
+ encoding: "utf8",
5
+ timeout: 15_000,
6
+ maxBuffer: 256 * 1024,
7
+ env: { ...process.env, LC_ALL: "C", LANG: "C" },
8
+ }, (error, stdout, stderr) => resolve({
9
+ code: error ? (typeof error.code === "number" ? error.code : null) : 0,
10
+ stdout,
11
+ stderr,
12
+ }));
13
+ });
14
+ export class DaemonServiceManagerError extends Error {
15
+ operation;
16
+ manager;
17
+ code = "DAEMON_SERVICE_MANAGER_FAILED";
18
+ constructor(operation, manager) {
19
+ super(`${manager} user service ${operation} failed; check the user manager and service logs`);
20
+ this.operation = operation;
21
+ this.manager = manager;
22
+ this.name = "DaemonServiceManagerError";
23
+ }
24
+ }
25
+ /** Bounded argv-only OS boundary. Never expose manager output (it may contain environment data). */
26
+ export function createDaemonServiceManager(plan, run = runCommand) {
27
+ const launchd = plan.manager === "launchd";
28
+ const file = launchd ? "/bin/launchctl" : "systemctl";
29
+ const domain = plan.target.slice(0, plan.target.lastIndexOf("/"));
30
+ const command = async (operation, args) => {
31
+ const result = await run(file, launchd ? args : ["--user", ...args]);
32
+ if (result.code !== 0)
33
+ throw new DaemonServiceManagerError(operation, plan.manager);
34
+ return result;
35
+ };
36
+ return {
37
+ async available() {
38
+ await command("availability check", launchd ? ["print-disabled", domain] : ["show-environment"]);
39
+ },
40
+ async inspect() {
41
+ const result = await run(file, launchd
42
+ ? ["print", plan.target]
43
+ : [
44
+ "--user",
45
+ "show",
46
+ plan.target,
47
+ "--property=LoadState,ActiveState,MainPID,FragmentPath",
48
+ ]);
49
+ const absent = { loaded: false, active: false, pid: null, definitionPath: null };
50
+ if (launchd) {
51
+ if (result.code === 113 && result.stderr.includes("Could not find service"))
52
+ return absent;
53
+ if (result.code !== 0)
54
+ throw new DaemonServiceManagerError("inspection", plan.manager);
55
+ const field = (name) => result.stdout.match(new RegExp(`^\\t${name} = (.*)$`, "m"))?.[1];
56
+ const path = field("path");
57
+ const pidText = field("pid");
58
+ const state = field("state");
59
+ if (!path || !state || (pidText !== undefined && !/^[1-9][0-9]*$/u.test(pidText)))
60
+ throw new DaemonServiceManagerError("inspection format", plan.manager);
61
+ return {
62
+ loaded: true,
63
+ active: state === "running",
64
+ pid: pidText ? Number(pidText) : null,
65
+ definitionPath: path,
66
+ };
67
+ }
68
+ const fields = new Map();
69
+ for (const line of result.stdout.trim().split("\n")) {
70
+ const index = line.indexOf("=");
71
+ if (index < 1 || fields.has(line.slice(0, index)))
72
+ throw new DaemonServiceManagerError("inspection format", plan.manager);
73
+ fields.set(line.slice(0, index), line.slice(index + 1));
74
+ }
75
+ if (fields.get("LoadState") === "not-found" && (result.code === 0 || result.code === 4))
76
+ return absent;
77
+ if (result.code !== 0 || fields.get("LoadState") !== "loaded" || !fields.has("ActiveState"))
78
+ throw new DaemonServiceManagerError("inspection", plan.manager);
79
+ const pidText = fields.get("MainPID");
80
+ const path = fields.get("FragmentPath");
81
+ if (!pidText || !/^(0|[1-9][0-9]*)$/u.test(pidText) || !path)
82
+ throw new DaemonServiceManagerError("inspection format", plan.manager);
83
+ return {
84
+ loaded: true,
85
+ active: fields.get("ActiveState") === "active",
86
+ pid: Number(pidText) || null,
87
+ definitionPath: path,
88
+ };
89
+ },
90
+ async install() {
91
+ if (launchd)
92
+ await command("bootstrap", ["bootstrap", domain, plan.unitPath]);
93
+ else {
94
+ await command("reload", ["daemon-reload"]);
95
+ // The manager may have a different XDG_CONFIG_HOME from the invoking
96
+ // shell. Enable the owned file explicitly so it is linked into its search path.
97
+ await command("enable/start", ["enable", "--now", plan.unitPath]);
98
+ }
99
+ },
100
+ async restart(hasProcess = true) {
101
+ // An explicit recovery after a failed launcher must also clear systemd's
102
+ // start-rate counter. The lifecycle owner has already verified this unit's
103
+ // private definition and reservation; never reset all user units.
104
+ if (!launchd && !hasProcess)
105
+ await command("reset failed state", ["reset-failed", plan.target]);
106
+ // KeepAlive relaunches after a graceful TERM. kickstart -k would force-kill
107
+ // the daemon instead of allowing canonical ownership to be released.
108
+ await command("restart", launchd
109
+ ? hasProcess
110
+ ? ["kill", "SIGTERM", plan.target]
111
+ : ["kickstart", plan.target]
112
+ : ["restart", plan.target]);
113
+ },
114
+ async stop() {
115
+ await command("stop/remove", launchd ? ["bootout", plan.target] : ["disable", "--now", plan.target]);
116
+ },
117
+ async reload() {
118
+ if (!launchd)
119
+ await command("reload", ["daemon-reload"]);
120
+ },
121
+ };
122
+ }
@@ -0,0 +1,127 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isAbsolute, join } from "node:path";
3
+ import { runtimeNamespaceEnvironment } from "./runtime-namespace.js";
4
+ function safeValue(value, name) {
5
+ if (!value || [...value].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
6
+ throw new TypeError(`${name} must be nonempty and contain no control characters`);
7
+ return value;
8
+ }
9
+ function absolute(value, name) {
10
+ safeValue(value, name);
11
+ if (!isAbsolute(value))
12
+ throw new TypeError(`${name} must be an absolute path`);
13
+ return value;
14
+ }
15
+ function xml(value) {
16
+ return value.replace(/[&<>"']/gu, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&apos;" })[char]);
17
+ }
18
+ // systemd applies specifier expansion to paths, environment values and ExecStart.
19
+ // ExecStart additionally expands dollars unless the ':' executable prefix is used.
20
+ function systemd(value) {
21
+ return `"${value.replace(/%/gu, "%%").replace(/\\/gu, "\\\\").replace(/"/gu, '\\"')}"`;
22
+ }
23
+ /** Pure rendering only. Reservation and OS-manager mutations belong to the lifecycle owner. */
24
+ export function planDaemonService(options) {
25
+ const { namespace } = options;
26
+ if (options.platform !== "darwin" && options.platform !== "linux")
27
+ throw new TypeError("Daemon services support macOS launchd and Linux systemd user managers");
28
+ if (!Number.isSafeInteger(options.uid) || options.uid <= 0)
29
+ throw new TypeError("Daemon services require a non-root user");
30
+ if (namespace.mode === "development")
31
+ throw new TypeError("Development instances own their lifecycle; use dev:instance instead");
32
+ const home = absolute(options.home, "Home directory");
33
+ const executable = absolute(options.executable, "Stable CLI executable");
34
+ // Keep the supplied stable launcher path. realpath would pin installer services
35
+ // to a retired version instead of following the next verified activation.
36
+ const identity = createHash("sha256")
37
+ .update(absolute(namespace.daemonInfoDir, "Daemon state directory"))
38
+ .digest("hex")
39
+ .slice(0, 24);
40
+ const supervisionId = `tmux-ide.${identity}`;
41
+ const stdoutPath = join(namespace.daemonInfoDir, "service.stdout.log");
42
+ const stderrPath = join(namespace.daemonInfoDir, "service.stderr.log");
43
+ const environment = {
44
+ HOME: home,
45
+ PATH: safeValue(options.path, "Service PATH"),
46
+ ...runtimeNamespaceEnvironment(namespace),
47
+ TMUX_IDE_CONFIG: namespace.configPath,
48
+ TMUX_IDE_SETTINGS_DIR: namespace.settingsDir,
49
+ TMUX_IDE_CLAUDE_DIR: namespace.claudeDir,
50
+ TMUX_IDE_CLAUDE_SETTINGS: namespace.claudeSettingsPath,
51
+ TMUX_IDE_CLAUDE_HOOK_PATH: namespace.claudeHookPath,
52
+ TMUX_IDE_OPENCODE_DIR: namespace.opencodeDir,
53
+ };
54
+ for (const [name, value] of Object.entries(environment))
55
+ safeValue(value, name);
56
+ const args = [executable, "--headless", "--supervised", supervisionId];
57
+ const shared = {
58
+ supervisionId,
59
+ executable,
60
+ recordPath: join(namespace.daemonInfoDir, "service.json"),
61
+ stdoutPath,
62
+ stderrPath,
63
+ };
64
+ if (options.platform === "darwin") {
65
+ const label = `com.${supervisionId}`;
66
+ const string = (value) => `<string>${xml(value)}</string>`;
67
+ return Object.freeze({
68
+ ...shared,
69
+ manager: "launchd",
70
+ target: `gui/${options.uid}/${label}`,
71
+ unitPath: join(home, "Library", "LaunchAgents", `${label}.plist`),
72
+ contents: `<?xml version="1.0" encoding="UTF-8"?>
73
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
74
+ <plist version="1.0"><dict>
75
+ <key>Label</key>${string(label)}
76
+ <key>ProgramArguments</key><array>${args.map(string).join("")}</array>
77
+ <key>EnvironmentVariables</key><dict>${Object.entries(environment)
78
+ .map(([key, value]) => `<key>${xml(key)}</key>${string(value)}`)
79
+ .join("")}</dict>
80
+ <key>WorkingDirectory</key>${string(home)}
81
+ <key>RunAtLoad</key><true/>
82
+ <key>KeepAlive</key><true/>
83
+ <key>ThrottleInterval</key><integer>3</integer>
84
+ <key>ExitTimeOut</key><integer>30</integer>
85
+ <key>AbandonProcessGroup</key><true/>
86
+ <key>StandardOutPath</key>${string(stdoutPath)}
87
+ <key>StandardErrorPath</key>${string(stderrPath)}
88
+ </dict></plist>
89
+ `,
90
+ });
91
+ }
92
+ const configHome = absolute(options.configHome ?? join(home, ".config"), "Config directory");
93
+ const target = `${supervisionId}.service`;
94
+ // WorkingDirectory is a raw path, not an ExecStart-style quoted word.
95
+ // The equivalent '/.' suffix preserves trailing spaces/backslashes without
96
+ // allowing the unit parser to trim them or treat them as line continuation.
97
+ const workingDirectory = `${home.replace(/%/gu, "%%")}/.`;
98
+ return Object.freeze({
99
+ ...shared,
100
+ manager: "systemd",
101
+ target,
102
+ unitPath: join(configHome, "systemd", "user", target),
103
+ contents: `[Unit]
104
+ Description=tmux-ide canonical user daemon
105
+ StartLimitIntervalSec=60
106
+ StartLimitBurst=10
107
+
108
+ [Service]
109
+ Type=exec
110
+ ExecStart=:${args.map(systemd).join(" ")}
111
+ WorkingDirectory=${workingDirectory}
112
+ ${Object.entries(environment)
113
+ .map(([key, value]) => `Environment=${systemd(`${key}=${value}`)}`)
114
+ .join("\n")}
115
+ Restart=always
116
+ RestartSec=3
117
+ TimeoutStopSec=30
118
+ # tmux pane work has an independent lifetime; never kill the whole service cgroup.
119
+ KillMode=process
120
+ StandardOutput=journal
121
+ StandardError=journal
122
+
123
+ [Install]
124
+ WantedBy=default.target
125
+ `,
126
+ });
127
+ }