tmux-ide 2.9.2 → 2.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/README.md +1 -1
  2. package/bin/cli.js +2071 -625
  3. package/package.json +14 -5
  4. package/packages/contracts/src/desktop-host.ts +3 -0
  5. package/packages/contracts/src/saved-machines.ts +19 -0
  6. package/packages/contracts/src/terminal-replica.ts +1 -1
  7. package/packages/core/package.json +3 -2
  8. package/packages/core/src/terminal-delivery.ts +51 -3
  9. package/packages/core/src/terminal-fnv64-wasm.ts +4 -3
  10. package/packages/core/src/terminal-replica-hash-cache.ts +129 -31
  11. package/packages/core/src/terminal-replica-owned-row.ts +27 -0
  12. package/packages/core/src/terminal-replica.ts +13 -28
  13. package/packages/daemon/dist/command-center/daemon-fleet-facts-observer.js +96 -10
  14. package/packages/daemon/dist/command-center/resources/saved-machine-route.js +40 -1
  15. package/packages/daemon/dist/command-center/ws-events.js +5 -24
  16. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +8 -0
  17. package/packages/daemon/dist/lib/canonical-daemon.js +28 -2
  18. package/packages/daemon/dist/lib/daemon-embed.js +1 -1
  19. package/packages/daemon/dist/lib/daemon-service-manager.js +122 -0
  20. package/packages/daemon/dist/lib/daemon-service-plan.js +127 -0
  21. package/packages/daemon/dist/lib/daemon-service.js +259 -0
  22. package/packages/daemon/dist/lib/install-origin.js +2 -0
  23. package/packages/daemon/dist/lib/local-fleet-request.js +9 -3
  24. package/packages/daemon/dist/lib/log.js +6 -10
  25. package/packages/daemon/dist/lib/managed-installer-update.js +67 -0
  26. package/packages/daemon/dist/lib/{tui-download-lock.js → private-operation-lock.js} +11 -11
  27. package/packages/daemon/dist/lib/remote-daemon-info.js +3 -2
  28. package/packages/daemon/dist/lib/remote-daemon-lifecycle.js +4 -4
  29. package/packages/daemon/dist/lib/remote-tmux-command.js +10 -0
  30. package/packages/daemon/dist/lib/runtime-namespace.js +28 -16
  31. package/packages/daemon/dist/lib/saved-machines.js +40 -2
  32. package/packages/daemon/dist/lib/ssh-daemon-transport.js +24 -9
  33. package/packages/daemon/dist/lib/tmux-server-owner.js +1 -1
  34. package/packages/daemon/dist/lib/tmux-server-registration.js +21 -3
  35. package/packages/daemon/dist/lib/tui-binary.js +2 -2
  36. package/packages/daemon/dist/lib/update.js +18 -0
  37. package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +24 -1
  38. package/packages/daemon/dist/machines.js +40 -4
  39. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  40. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  41. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  42. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  43. package/packages/daemon/dist/terminal/attachments/direct-websocket.js +4 -1
  44. package/packages/daemon/dist/terminal/attachments/native-runtime.js +22 -13
  45. package/packages/daemon/dist/terminal/mirror/mirror-service.js +101 -3
  46. package/packages/daemon/dist/terminal/mirror/registered-window-guard.js +62 -0
  47. package/packages/daemon/dist/terminal/mirror/session-channel.js +75 -4
  48. package/packages/daemon/dist/terminal/mirror/shared-window-index.js +45 -0
  49. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +11 -1
  50. package/packages/daemon/dist/terminal/session-runtime/multiplexer-backend.js +1 -0
  51. package/packages/daemon/dist/terminal/session-runtime/registry.js +56 -7
  52. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +3 -1
  53. package/packages/daemon/dist/tui/mirror/dialog-model.js +2 -35
  54. package/packages/daemon/dist/tui/mirror/runtime/application-daemon-authority-owner.js +8 -0
  55. package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +20 -2
  56. package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +7 -1
  57. package/packages/daemon/dist/tui/mirror/runtime/application-machine-authority.js +59 -9
  58. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +27 -19
  59. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +23 -18
  60. package/packages/daemon/dist/tui/mirror/runtime/application-machine-startup.js +31 -2
  61. package/packages/daemon/dist/tui/mirror/runtime/application-palette-search-owner.js +7 -3
  62. package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +3 -0
  63. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +146 -2
  64. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +201 -50
  65. package/packages/daemon/dist/tui/mirror/runtime/bounded-performance-record-writer.js +50 -10
  66. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -5
  67. package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +3 -0
  68. package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +8 -2
  69. package/packages/daemon/dist/tui/mirror/terminal-text.js +35 -0
  70. package/packages/daemon/dist/tui/mirror/workspace/connection-feedback.js +5 -3
  71. package/packages/daemon/src/command-center/daemon-fleet-facts-observer.ts +100 -10
  72. package/packages/daemon/src/command-center/resources/saved-machine-route.ts +48 -1
  73. package/packages/daemon/src/command-center/ws-events.ts +5 -25
  74. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +12 -0
  75. package/packages/daemon/src/lib/canonical-daemon.ts +44 -2
  76. package/packages/daemon/src/lib/daemon-embed.ts +1 -0
  77. package/packages/daemon/src/lib/daemon-service-manager.ts +152 -0
  78. package/packages/daemon/src/lib/daemon-service-plan.ts +153 -0
  79. package/packages/daemon/src/lib/daemon-service.ts +301 -0
  80. package/packages/daemon/src/lib/install-origin.ts +5 -0
  81. package/packages/daemon/src/lib/local-fleet-request.ts +19 -3
  82. package/packages/daemon/src/lib/log.ts +13 -9
  83. package/packages/daemon/src/lib/managed-installer-update.ts +79 -0
  84. package/packages/daemon/src/lib/{tui-download-lock.ts → private-operation-lock.ts} +15 -11
  85. package/packages/daemon/src/lib/remote-daemon-info.ts +3 -3
  86. package/packages/daemon/src/lib/remote-daemon-lifecycle.ts +4 -4
  87. package/packages/daemon/src/lib/remote-tmux-command.ts +12 -0
  88. package/packages/daemon/src/lib/runtime-namespace.ts +27 -18
  89. package/packages/daemon/src/lib/saved-machines.ts +40 -1
  90. package/packages/daemon/src/lib/ssh-daemon-transport.ts +25 -7
  91. package/packages/daemon/src/lib/tmux-server-owner.ts +1 -0
  92. package/packages/daemon/src/lib/tmux-server-registration.ts +21 -3
  93. package/packages/daemon/src/lib/tui-binary.ts +2 -2
  94. package/packages/daemon/src/lib/update.ts +24 -0
  95. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +22 -0
  96. package/packages/daemon/src/machines.ts +53 -5
  97. package/packages/daemon/src/terminal/attachments/direct-websocket.ts +8 -2
  98. package/packages/daemon/src/terminal/attachments/native-runtime.ts +28 -13
  99. package/packages/daemon/src/terminal/mirror/mirror-service.ts +105 -3
  100. package/packages/daemon/src/terminal/mirror/registered-window-guard.ts +66 -0
  101. package/packages/daemon/src/terminal/mirror/session-channel.ts +92 -4
  102. package/packages/daemon/src/terminal/mirror/shared-window-index.ts +42 -0
  103. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +12 -1
  104. package/packages/daemon/src/terminal/session-runtime/multiplexer-backend.ts +2 -0
  105. package/packages/daemon/src/terminal/session-runtime/registry.ts +70 -8
  106. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +3 -1
  107. package/packages/daemon/src/tui/mirror/dialog-model.ts +2 -32
  108. package/packages/daemon/src/tui/mirror/runtime/application-daemon-authority-owner.ts +8 -0
  109. package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +19 -2
  110. package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +6 -1
  111. package/packages/daemon/src/tui/mirror/runtime/application-machine-authority.ts +60 -11
  112. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +39 -33
  113. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +31 -23
  114. package/packages/daemon/src/tui/mirror/runtime/application-machine-startup.ts +38 -2
  115. package/packages/daemon/src/tui/mirror/runtime/application-palette-search-owner.ts +7 -4
  116. package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +3 -0
  117. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +1 -1
  118. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +47 -46
  119. package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +165 -2
  120. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +207 -53
  121. package/packages/daemon/src/tui/mirror/runtime/bounded-performance-record-writer.ts +49 -9
  122. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +6 -5
  123. package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +4 -0
  124. package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +8 -3
  125. package/packages/daemon/src/tui/mirror/terminal-text.ts +32 -0
  126. package/packages/daemon/src/tui/mirror/workspace/connection-feedback.ts +6 -3
  127. package/packages/daemon-client/src/fleet-connection-status.ts +10 -1
  128. package/packages/daemon-client/src/workspace-client.ts +4 -1
  129. package/packages/contracts/src/__tests__/actions-contract.test.ts +0 -119
  130. package/packages/contracts/src/__tests__/agent-graph-overlay.test.ts +0 -428
  131. package/packages/contracts/src/__tests__/app-window-mutation.test.ts +0 -51
  132. package/packages/contracts/src/__tests__/app-window-state.test.ts +0 -34
  133. package/packages/contracts/src/__tests__/application-shell.test.ts +0 -645
  134. package/packages/contracts/src/__tests__/automation-operations.test.ts +0 -78
  135. package/packages/contracts/src/__tests__/causal-cell.test.ts +0 -47
  136. package/packages/contracts/src/__tests__/client-view-state.test.ts +0 -115
  137. package/packages/contracts/src/__tests__/cohesion-fixture.test.ts +0 -106
  138. package/packages/contracts/src/__tests__/commands.test.ts +0 -86
  139. package/packages/contracts/src/__tests__/control.test.ts +0 -154
  140. package/packages/contracts/src/__tests__/daemon-events.test.ts +0 -405
  141. package/packages/contracts/src/__tests__/daemon-resources.test.ts +0 -143
  142. package/packages/contracts/src/__tests__/daemon-wire.test.ts +0 -149
  143. package/packages/contracts/src/__tests__/desktop-host.test.ts +0 -269
  144. package/packages/contracts/src/__tests__/desktop-missions.test.ts +0 -215
  145. package/packages/contracts/src/__tests__/experience-conformance.test.ts +0 -153
  146. package/packages/contracts/src/__tests__/experience-kernel-boundary.test.ts +0 -113
  147. package/packages/contracts/src/__tests__/experience-shell.test.ts +0 -65
  148. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v2.json +0 -35
  149. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +0 -35
  150. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v4.json +0 -35
  151. package/packages/contracts/src/__tests__/fixtures/scoped-receipt.ts +0 -62
  152. package/packages/contracts/src/__tests__/fleet-agent-graph.test.ts +0 -231
  153. package/packages/contracts/src/__tests__/fleet-catalog.test.ts +0 -270
  154. package/packages/contracts/src/__tests__/fleet-lifecycle.test.ts +0 -62
  155. package/packages/contracts/src/__tests__/focus-overlay.test.ts +0 -110
  156. package/packages/contracts/src/__tests__/ide-config.test.ts +0 -64
  157. package/packages/contracts/src/__tests__/interaction-evidence.test.ts +0 -288
  158. package/packages/contracts/src/__tests__/interaction-receipt-v2.test.ts +0 -72
  159. package/packages/contracts/src/__tests__/issue-error.test.ts +0 -78
  160. package/packages/contracts/src/__tests__/mission-projections.test.ts +0 -186
  161. package/packages/contracts/src/__tests__/missions.test.ts +0 -272
  162. package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +0 -242
  163. package/packages/contracts/src/__tests__/pane-appearance.test.ts +0 -146
  164. package/packages/contracts/src/__tests__/pane-read-result.test.ts +0 -55
  165. package/packages/contracts/src/__tests__/pane-stream.test.ts +0 -536
  166. package/packages/contracts/src/__tests__/pane-widget-marker.test.ts +0 -327
  167. package/packages/contracts/src/__tests__/performance-metrics.test.ts +0 -73
  168. package/packages/contracts/src/__tests__/performance-qualification.test.ts +0 -137
  169. package/packages/contracts/src/__tests__/rich-card-widget.test.ts +0 -29
  170. package/packages/contracts/src/__tests__/session-runtime.test.ts +0 -512
  171. package/packages/contracts/src/__tests__/startup-readiness.test.ts +0 -409
  172. package/packages/contracts/src/__tests__/terminal-attachment-stream.test.ts +0 -72
  173. package/packages/contracts/src/__tests__/terminal-attachments.test.ts +0 -329
  174. package/packages/contracts/src/__tests__/terminal-delivery.test.ts +0 -88
  175. package/packages/contracts/src/__tests__/terminal-runtime-inventory.test.ts +0 -68
  176. package/packages/contracts/src/__tests__/terminals.test.ts +0 -87
  177. package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +0 -166
  178. package/packages/contracts/src/__tests__/transport-contract-loading.test.ts +0 -91
  179. package/packages/contracts/src/__tests__/visual-tokens.test.ts +0 -224
  180. package/packages/contracts/src/__tests__/window-links.test.ts +0 -243
  181. package/packages/contracts/src/__tests__/workspace-changes-resource.test.ts +0 -317
  182. package/packages/contracts/src/__tests__/workspace-changes-view.test.ts +0 -122
  183. package/packages/contracts/src/__tests__/workspace-config.test.ts +0 -294
  184. package/packages/contracts/src/__tests__/workspace-files-resource.test.ts +0 -261
  185. package/packages/contracts/src/__tests__/workspace-files-tree.test.ts +0 -147
  186. package/packages/contracts/src/__tests__/workspace-pane-creation.test.ts +0 -249
  187. package/packages/contracts/src/__tests__/workspace-promotion.test.ts +0 -94
  188. package/packages/contracts/src/__tests__/workspace-resource-identity.test.ts +0 -90
  189. package/packages/contracts/src/daemon-resource-request.test.ts +0 -192
  190. package/packages/contracts/src/native-operation-identity.test.ts +0 -23
  191. package/packages/contracts/src/workspace-admission.test.ts +0 -21
  192. package/packages/contracts/src/workspace-catalog-resource.test.ts +0 -146
  193. package/packages/core/src/agent-provisioning.test.ts +0 -42
  194. package/packages/core/src/application-shell-session.test.ts +0 -105
  195. package/packages/core/src/interaction-evidence.test.ts +0 -134
  196. package/packages/core/src/interaction-flow.test.ts +0 -66
  197. package/packages/core/src/interaction-receipts.test.ts +0 -671
  198. package/packages/core/src/navigator.test.ts +0 -34
  199. package/packages/core/src/optimistic-projection-conformance.test.ts +0 -75
  200. package/packages/core/src/optimistic-projection.test.ts +0 -118
  201. package/packages/core/src/performance-metrics.test.ts +0 -121
  202. package/packages/core/src/performance-qualification.test.ts +0 -283
  203. package/packages/core/src/saved-machines.test.ts +0 -80
  204. package/packages/core/src/terminal-compact-cold.test.ts +0 -87
  205. package/packages/core/src/terminal-conformance.test.ts +0 -48
  206. package/packages/core/src/terminal-delivery-cache-lifetime.test.ts +0 -167
  207. package/packages/core/src/terminal-delivery-encode-cooperative.test.ts +0 -140
  208. package/packages/core/src/terminal-delivery-representation-hash.test.ts +0 -77
  209. package/packages/core/src/terminal-delivery.test.ts +0 -1471
  210. package/packages/core/src/terminal-fnv64-wasm.test.ts +0 -61
  211. package/packages/core/src/terminal-replica-cold.test.ts +0 -31
  212. package/packages/core/src/terminal-replica-cooperative.test.ts +0 -170
  213. package/packages/core/src/terminal-replica-hash-cache.test.ts +0 -283
  214. package/packages/core/src/terminal-replica.test.ts +0 -347
  215. package/packages/core/src/workspace-selection.test.ts +0 -63
  216. package/packages/daemon/dist/__tests__/support.js +0 -33
  217. package/packages/daemon/dist/lib/__tests__/fixtures/workspace-state-writer.js +0 -58
  218. package/packages/daemon/dist/lib/__tests__/installed-recovery-fixture.js +0 -400
  219. package/packages/daemon/dist/terminal/__tests__/MockPtyAdapter.js +0 -182
  220. package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +0 -98
  221. package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +0 -156
  222. package/packages/daemon/src/tui/mirror/__snapshots__/activity-surface-renderer.test.tsx.snap +0 -42
  223. package/packages/daemon/src/tui/mirror/__snapshots__/changes-terminal-surface-renderer.test.tsx.snap +0 -267
  224. package/packages/daemon/src/tui/mirror/__snapshots__/home-files-surface-renderer.test.tsx.snap +0 -267
  225. package/packages/daemon/src/tui/mirror/__snapshots__/missions-surface-renderer.test.tsx.snap +0 -134
  226. package/packages/daemon/src/tui/mirror/__snapshots__/recipes-gallery-renderer.test.tsx.snap +0 -141
  227. package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +0 -134
  228. package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +0 -87
  229. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/agent-terminal-canvas-renderer.test.tsx.snap +0 -348
  230. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +0 -134
  231. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/command-palette-surface-renderer.test.tsx.snap +0 -255
  232. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/pane-frame-renderer.test.tsx.snap +0 -525
  233. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/terminal-pane-header-polish-renderer.test.tsx.snap +0 -31
  234. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/workbench-shell-renderer.test.tsx.snap +0 -134
  235. package/packages/daemon-client/src/application-shell-session.test.ts +0 -196
  236. package/packages/daemon-client/src/automation-client.test.ts +0 -214
  237. package/packages/daemon-client/src/bootstrap-coordinator.test.ts +0 -145
  238. package/packages/daemon-client/src/connection-supervisor.test.ts +0 -109
  239. package/packages/daemon-client/src/direct-application-shell-transport.test.ts +0 -941
  240. package/packages/daemon-client/src/first-latest-coordinator.test.ts +0 -175
  241. package/packages/daemon-client/src/fleet-dial-scheduler.test.ts +0 -114
  242. package/packages/daemon-client/src/fleet-environments.test.ts +0 -40
  243. package/packages/daemon-client/src/generation-bound-store-timers.test.ts +0 -74
  244. package/packages/daemon-client/src/owner-action-client.test.ts +0 -245
  245. package/packages/daemon-client/src/pane-stream-client.test.ts +0 -2841
  246. package/packages/daemon-client/src/pane-stream-clock-calibration.test.ts +0 -207
  247. package/packages/daemon-client/src/pane-stream-loopback-benchmark.test.ts +0 -51
  248. package/packages/daemon-client/src/push-resource-session.test.ts +0 -509
  249. package/packages/daemon-client/src/resource-replica.test.ts +0 -186
  250. package/packages/daemon-client/src/runtime-resource-ledger.test.ts +0 -18
  251. package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +0 -114
  252. package/packages/daemon-client/src/terminal-fast-lane.test.ts +0 -835
  253. package/packages/daemon-client/src/tmux-server-client.test.ts +0 -279
  254. package/packages/daemon-client/src/tmux-server-interaction-events.test.ts +0 -230
  255. package/packages/daemon-client/src/workspace-catalog-v2.test.ts +0 -109
  256. package/packages/daemon-client/src/workspace-client-boundaries.test.ts +0 -59
  257. package/packages/daemon-client/src/workspace-client-operations.test.ts +0 -96
  258. package/packages/daemon-client/src/workspace-client.test.ts +0 -2912
  259. package/packages/daemon-client/src/workspace-event-supervisor.test.ts +0 -1086
  260. package/packages/daemon-client/src/workspace-pane-client.test.ts +0 -80
  261. package/packages/presentation/src/presentation-contract.test.ts +0 -63
  262. package/packages/tmux-bridge/src/runner.test.ts +0 -614
  263. package/packages/tmux-bridge/src/targeting.test.ts +0 -107
@@ -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
+ }