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
@@ -2,13 +2,22 @@ import type { Hono } from "hono";
2
2
  import { bodyLimit } from "hono/body-limit";
3
3
  import { z } from "zod";
4
4
  import { SavedMachineRegistrySchema, type DaemonInstanceIdentity } from "@tmux-ide/contracts";
5
- import { loadSavedMachines, mergeSavedMachines } from "../../lib/saved-machines.ts";
5
+ import { SavedMachineMutationSchema } from "@tmux-ide/contracts/saved-machines";
6
+ import {
7
+ loadSavedMachines,
8
+ mergeSavedMachines,
9
+ updateSavedMachines,
10
+ } from "../../lib/saved-machines.ts";
6
11
  import { ownerAuthorityGate } from "../owner-authority.ts";
7
12
 
8
13
  const Request = z.strictObject({
9
14
  expectedInstanceId: z.uuid(),
10
15
  registry: SavedMachineRegistrySchema,
11
16
  });
17
+ const MutationRequest = z.strictObject({
18
+ expectedInstanceId: z.uuid(),
19
+ change: SavedMachineMutationSchema,
20
+ });
12
21
  export function mountSavedMachineRoute(
13
22
  app: Hono,
14
23
  options: {
@@ -16,6 +25,7 @@ export function mountSavedMachineRoute(
16
25
  ownerToken: string | null;
17
26
  read?: typeof loadSavedMachines;
18
27
  merge?: typeof mergeSavedMachines;
28
+ update?: typeof updateSavedMachines;
19
29
  },
20
30
  ) {
21
31
  const authorize = ownerAuthorityGate(options.ownerToken, {
@@ -56,4 +66,41 @@ export function mountSavedMachineRoute(
56
66
  );
57
67
  }
58
68
  });
69
+ app.patch("/api/resources/saved-machines", bodyLimit({ maxSize: 1024 }), async (c) => {
70
+ const gate = authorize(c);
71
+ if (gate) return gate;
72
+ c.header("Cache-Control", "no-store");
73
+ let parsed;
74
+ try {
75
+ parsed = MutationRequest.safeParse(await c.req.json());
76
+ } catch {
77
+ return c.json({ error: "Invalid machine mutation" }, 400);
78
+ }
79
+ if (!parsed.success) return c.json({ error: "Invalid machine mutation" }, 400);
80
+ if (parsed.data.expectedInstanceId !== options.daemon.instanceId)
81
+ return c.json({ error: "Daemon generation changed" }, 409);
82
+ const change = parsed.data.change;
83
+ const { id, operation } = change;
84
+ try {
85
+ const registry = (options.update ?? updateSavedMachines)(
86
+ operation === "remove"
87
+ ? { type: "remove", id }
88
+ : {
89
+ type: "update",
90
+ id,
91
+ patch:
92
+ change.operation === "edit" ? change.patch : { enabled: operation === "enable" },
93
+ },
94
+ );
95
+ return c.json({ daemon: options.daemon, registry });
96
+ } catch {
97
+ return c.json(
98
+ {
99
+ error:
100
+ "Machine not found, edit conflicts with another profile, or registry unavailable; nothing changed",
101
+ },
102
+ 409,
103
+ );
104
+ }
105
+ });
59
106
  }
@@ -17,11 +17,8 @@ import { discoverSessions, buildOverviews, buildProjectDetail } from "./discover
17
17
  import type { AgentTurnCompletion } from "./agent-status-watch.ts";
18
18
  import {
19
19
  DaemonFleetFactsObserver,
20
- AGENT_STATE_TMUX_ARGS,
21
- parseAgentStateFacts,
22
- parseSessionCompositionFacts,
23
20
  createDefaultFleetFactsReaders,
24
- SESSION_COMPOSITION_TMUX_ARGS,
21
+ createSharedFleetFactsReaders,
25
22
  type DaemonFleetFactsObserverOptions,
26
23
  type DaemonFleetFactsObserverDiagnostic,
27
24
  } from "./daemon-fleet-facts-observer.ts";
@@ -431,28 +428,11 @@ export function setFleetFactsTmuxRunner(
431
428
  runTmux: ((args: readonly string[], signal?: AbortSignal) => string | Promise<string>) | null,
432
429
  ): void {
433
430
  stopFleetFactsObserver();
434
- sessionCompositionReaderOverride = runTmux
435
- ? async (signal) => {
436
- try {
437
- return parseSessionCompositionFacts(await runTmux(SESSION_COMPOSITION_TMUX_ARGS, signal));
438
- } catch (error) {
439
- // A daemon may legitimately precede the first tmux server. Socket
440
- // absence is an authoritative empty fleet baseline, not a failed
441
- // observation: catalog clients must be allowed to render their
442
- // actionable first-run state while the observer waits for tmux.
443
- return isTmuxServerUnavailableError(error) ? parseSessionCompositionFacts("") : null;
444
- }
445
- }
446
- : null;
447
- agentStateReaderOverride = runTmux
448
- ? async (signal) => {
449
- try {
450
- return parseAgentStateFacts(await runTmux(AGENT_STATE_TMUX_ARGS, signal));
451
- } catch {
452
- return null;
453
- }
454
- }
431
+ const readers = runTmux
432
+ ? createSharedFleetFactsReaders(runTmux, isTmuxServerUnavailableError)
455
433
  : null;
434
+ sessionCompositionReaderOverride = readers?.readSessions ?? null;
435
+ agentStateReaderOverride = readers?.readAgents ?? null;
456
436
  }
457
437
 
458
438
  interface ResourceObservationHandle {
@@ -236,6 +236,18 @@ async function replaceOlderCanonicalDaemon(
236
236
  reason: "identity-mismatch",
237
237
  });
238
238
  }
239
+ const supervisor = latest.info.provenance?.supervisor;
240
+ if (!latest.info.supervisionId && (supervisor === "systemd" || supervisor === "launchd")) {
241
+ throw new DaemonBootstrapError(
242
+ "incompatible",
243
+ `The older canonical daemon is managed by ${supervisor} without a supervisor reservation. ` +
244
+ "Stop and disable the old service, reserve the stopped namespace with " +
245
+ "tmux-ide daemon reserve-supervisor <id>, then configure the service to run " +
246
+ "tmux-ide --headless --supervised <id> before restarting it. " +
247
+ "The existing daemon has been left running.",
248
+ { reason: "canonical-record-invalid" },
249
+ );
250
+ }
239
251
  await deps.shutdownOlderOwner(info);
240
252
  const deadline = deps.now() + timeoutMs;
241
253
  while (deps.now() < deadline) {
@@ -115,7 +115,12 @@ export type CanonicalDaemonInfoState =
115
115
  };
116
116
 
117
117
  export function getCanonicalDaemonInfoPath(): string {
118
- return runtimeOwnedPath(join(resolveRuntimeNamespace().daemonInfoDir, DAEMON_INFO_FILE));
118
+ const namespace = resolveRuntimeNamespace();
119
+ const path = join(namespace.daemonInfoDir, DAEMON_INFO_FILE);
120
+ // Resolution already validates ordinary and isolated namespaces. Only
121
+ // development adds a per-path containment check; do not resolve twice on
122
+ // every ordinary daemon read. Each call still resolves the current namespace.
123
+ return namespace.development ? runtimeOwnedPath(path) : path;
119
124
  }
120
125
 
121
126
  export function getCanonicalDaemonClaimPath(): string {
@@ -687,11 +692,48 @@ export function assertCanonicalDaemonSupervision(supervisionId: string): void {
687
692
  }
688
693
 
689
694
  /** Explicit installation only; startup never calls this to recreate a reservation. */
695
+ /** Sanitized reservation guidance; never includes credentials or filesystem errors. */
696
+ export class CanonicalDaemonReservationError extends Error {
697
+ constructor(message: string) {
698
+ super(`Supervisor reservation refused: ${message}`);
699
+ this.name = "CanonicalDaemonReservationError";
700
+ }
701
+ }
702
+
703
+ function reservationRefusal(
704
+ attempt: Exclude<CanonicalDaemonClaimAttempt, { status: "acquired" }>,
705
+ ): CanonicalDaemonReservationError {
706
+ if (attempt.status === "busy")
707
+ return new CanonicalDaemonReservationError(
708
+ `another lifecycle operation holds the namespace (PID ${attempt.owner.pid}). Wait for it to finish, then retry.`,
709
+ );
710
+ const state = inspectCanonicalDaemonInfo();
711
+ if (state.status === "valid")
712
+ return new CanonicalDaemonReservationError(
713
+ `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.`,
714
+ );
715
+ if (state.status === "reserved")
716
+ return new CanonicalDaemonReservationError(
717
+ `this namespace is reserved for supervisor ${state.reservation.supervisionId}. Remove that service and release its reservation before assigning a different ID.`,
718
+ );
719
+ if (state.status === "invalid")
720
+ return new CanonicalDaemonReservationError(
721
+ `the namespace record cannot be trusted (${state.reason}). Run tmux-ide daemon info --json for diagnosis; repair ownership or permissions before retrying.`,
722
+ );
723
+ return new CanonicalDaemonReservationError(
724
+ "the namespace changed or its lifecycle claim is unavailable. Run tmux-ide daemon info --json, then retry after the owning operation finishes.",
725
+ );
726
+ }
727
+
690
728
  export function reserveCanonicalDaemonSupervision(
691
729
  supervisionId: string,
692
730
  ): CanonicalDaemonReservation {
731
+ if (!DaemonSupervisionIdSchema.safeParse(supervisionId).success)
732
+ throw new CanonicalDaemonReservationError(
733
+ "use a 1–128 character ID starting with a letter or digit and containing only letters, digits, periods, underscores or hyphens.",
734
+ );
693
735
  const attempt = tryAcquireCanonicalDaemonClaim({ kind: "reserve", supervisionId });
694
- if (attempt.status !== "acquired") throw new Error("Cannot reserve supervised daemon namespace");
736
+ if (attempt.status !== "acquired") throw reservationRefusal(attempt);
695
737
  const claim = attempt.claim;
696
738
  try {
697
739
  const before = inspectCanonicalDaemonInfo();
@@ -1436,6 +1436,7 @@ async function startEmbeddedDaemonGeneration(
1436
1436
  if (!sessionRuntimeRegistry) throw new Error("Session runtime unavailable");
1437
1437
  return sessionRuntimeRegistry.executeWindowLinkAction(session, action);
1438
1438
  },
1439
+ timing,
1439
1440
  );
1440
1441
  }
1441
1442
  if (intent.verb === "workspace.pane.resize") {
@@ -0,0 +1,152 @@
1
+ import { execFile } from "node:child_process";
2
+ import type { DaemonServicePlan } from "./daemon-service-plan.ts";
3
+
4
+ export interface DaemonServiceState {
5
+ readonly loaded: boolean;
6
+ readonly active: boolean;
7
+ readonly pid: number | null;
8
+ readonly definitionPath: string | null;
9
+ }
10
+
11
+ export type ServiceCommand = (
12
+ file: string,
13
+ args: readonly string[],
14
+ ) => Promise<{ code: number | null; stdout: string; stderr: string }>;
15
+
16
+ const runCommand: ServiceCommand = (file, args) =>
17
+ new Promise((resolve) => {
18
+ execFile(
19
+ file,
20
+ [...args],
21
+ {
22
+ encoding: "utf8",
23
+ timeout: 15_000,
24
+ maxBuffer: 256 * 1024,
25
+ env: { ...process.env, LC_ALL: "C", LANG: "C" },
26
+ },
27
+ (error, stdout, stderr) =>
28
+ resolve({
29
+ code: error ? (typeof error.code === "number" ? error.code : null) : 0,
30
+ stdout,
31
+ stderr,
32
+ }),
33
+ );
34
+ });
35
+
36
+ export class DaemonServiceManagerError extends Error {
37
+ readonly code = "DAEMON_SERVICE_MANAGER_FAILED";
38
+ constructor(
39
+ readonly operation: string,
40
+ readonly manager: string,
41
+ ) {
42
+ super(`${manager} user service ${operation} failed; check the user manager and service logs`);
43
+ this.name = "DaemonServiceManagerError";
44
+ }
45
+ }
46
+
47
+ /** Bounded argv-only OS boundary. Never expose manager output (it may contain environment data). */
48
+ export function createDaemonServiceManager(plan: DaemonServicePlan, run = runCommand) {
49
+ const launchd = plan.manager === "launchd";
50
+ const file = launchd ? "/bin/launchctl" : "systemctl";
51
+ const domain = plan.target.slice(0, plan.target.lastIndexOf("/"));
52
+ const command = async (operation: string, args: readonly string[]) => {
53
+ const result = await run(file, launchd ? args : ["--user", ...args]);
54
+ if (result.code !== 0) throw new DaemonServiceManagerError(operation, plan.manager);
55
+ return result;
56
+ };
57
+ return {
58
+ async available(): Promise<void> {
59
+ await command(
60
+ "availability check",
61
+ launchd ? ["print-disabled", domain] : ["show-environment"],
62
+ );
63
+ },
64
+ async inspect(): Promise<DaemonServiceState> {
65
+ const result = await run(
66
+ file,
67
+ launchd
68
+ ? ["print", plan.target]
69
+ : [
70
+ "--user",
71
+ "show",
72
+ plan.target,
73
+ "--property=LoadState,ActiveState,MainPID,FragmentPath",
74
+ ],
75
+ );
76
+ const absent = { loaded: false, active: false, pid: null, definitionPath: null } as const;
77
+ if (launchd) {
78
+ if (result.code === 113 && result.stderr.includes("Could not find service")) return absent;
79
+ if (result.code !== 0) throw new DaemonServiceManagerError("inspection", plan.manager);
80
+ const field = (name: string) =>
81
+ result.stdout.match(new RegExp(`^\\t${name} = (.*)$`, "m"))?.[1];
82
+ const path = field("path");
83
+ const pidText = field("pid");
84
+ const state = field("state");
85
+ if (!path || !state || (pidText !== undefined && !/^[1-9][0-9]*$/u.test(pidText)))
86
+ throw new DaemonServiceManagerError("inspection format", plan.manager);
87
+ return {
88
+ loaded: true,
89
+ active: state === "running",
90
+ pid: pidText ? Number(pidText) : null,
91
+ definitionPath: path,
92
+ };
93
+ }
94
+ const fields = new Map<string, string>();
95
+ for (const line of result.stdout.trim().split("\n")) {
96
+ const index = line.indexOf("=");
97
+ if (index < 1 || fields.has(line.slice(0, index)))
98
+ throw new DaemonServiceManagerError("inspection format", plan.manager);
99
+ fields.set(line.slice(0, index), line.slice(index + 1));
100
+ }
101
+ if (fields.get("LoadState") === "not-found" && (result.code === 0 || result.code === 4))
102
+ return absent;
103
+ if (result.code !== 0 || fields.get("LoadState") !== "loaded" || !fields.has("ActiveState"))
104
+ throw new DaemonServiceManagerError("inspection", plan.manager);
105
+ const pidText = fields.get("MainPID");
106
+ const path = fields.get("FragmentPath");
107
+ if (!pidText || !/^(0|[1-9][0-9]*)$/u.test(pidText) || !path)
108
+ throw new DaemonServiceManagerError("inspection format", plan.manager);
109
+ return {
110
+ loaded: true,
111
+ active: fields.get("ActiveState") === "active",
112
+ pid: Number(pidText) || null,
113
+ definitionPath: path,
114
+ };
115
+ },
116
+ async install(): Promise<void> {
117
+ if (launchd) await command("bootstrap", ["bootstrap", domain, plan.unitPath]);
118
+ else {
119
+ await command("reload", ["daemon-reload"]);
120
+ // The manager may have a different XDG_CONFIG_HOME from the invoking
121
+ // shell. Enable the owned file explicitly so it is linked into its search path.
122
+ await command("enable/start", ["enable", "--now", plan.unitPath]);
123
+ }
124
+ },
125
+ async restart(hasProcess = true): Promise<void> {
126
+ // An explicit recovery after a failed launcher must also clear systemd's
127
+ // start-rate counter. The lifecycle owner has already verified this unit's
128
+ // private definition and reservation; never reset all user units.
129
+ if (!launchd && !hasProcess)
130
+ await command("reset failed state", ["reset-failed", plan.target]);
131
+ // KeepAlive relaunches after a graceful TERM. kickstart -k would force-kill
132
+ // the daemon instead of allowing canonical ownership to be released.
133
+ await command(
134
+ "restart",
135
+ launchd
136
+ ? hasProcess
137
+ ? ["kill", "SIGTERM", plan.target]
138
+ : ["kickstart", plan.target]
139
+ : ["restart", plan.target],
140
+ );
141
+ },
142
+ async stop(): Promise<void> {
143
+ await command(
144
+ "stop/remove",
145
+ launchd ? ["bootout", plan.target] : ["disable", "--now", plan.target],
146
+ );
147
+ },
148
+ async reload(): Promise<void> {
149
+ if (!launchd) await command("reload", ["daemon-reload"]);
150
+ },
151
+ };
152
+ }
@@ -0,0 +1,153 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isAbsolute, join } from "node:path";
3
+ import { runtimeNamespaceEnvironment, type RuntimeNamespace } from "./runtime-namespace.ts";
4
+
5
+ export interface DaemonServicePlan {
6
+ readonly manager: "launchd" | "systemd";
7
+ readonly supervisionId: string;
8
+ readonly target: string;
9
+ readonly unitPath: string;
10
+ readonly recordPath: string;
11
+ readonly executable: string;
12
+ readonly contents: string;
13
+ readonly stdoutPath: string;
14
+ readonly stderrPath: string;
15
+ }
16
+
17
+ function safeValue(value: string, name: string): string {
18
+ if (!value || [...value].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
19
+ throw new TypeError(`${name} must be nonempty and contain no control characters`);
20
+ return value;
21
+ }
22
+
23
+ function absolute(value: string, name: string): string {
24
+ safeValue(value, name);
25
+ if (!isAbsolute(value)) throw new TypeError(`${name} must be an absolute path`);
26
+ return value;
27
+ }
28
+
29
+ function xml(value: string): string {
30
+ return value.replace(
31
+ /[&<>"']/gu,
32
+ (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&apos;" })[char]!,
33
+ );
34
+ }
35
+
36
+ // systemd applies specifier expansion to paths, environment values and ExecStart.
37
+ // ExecStart additionally expands dollars unless the ':' executable prefix is used.
38
+ function systemd(value: string): string {
39
+ return `"${value.replace(/%/gu, "%%").replace(/\\/gu, "\\\\").replace(/"/gu, '\\"')}"`;
40
+ }
41
+
42
+ /** Pure rendering only. Reservation and OS-manager mutations belong to the lifecycle owner. */
43
+ export function planDaemonService(options: {
44
+ platform: string;
45
+ uid: number;
46
+ home: string;
47
+ configHome?: string;
48
+ executable: string;
49
+ path: string;
50
+ namespace: RuntimeNamespace;
51
+ }): DaemonServicePlan {
52
+ const { namespace } = options;
53
+ if (options.platform !== "darwin" && options.platform !== "linux")
54
+ throw new TypeError("Daemon services support macOS launchd and Linux systemd user managers");
55
+ if (!Number.isSafeInteger(options.uid) || options.uid <= 0)
56
+ throw new TypeError("Daemon services require a non-root user");
57
+ if (namespace.mode === "development")
58
+ throw new TypeError("Development instances own their lifecycle; use dev:instance instead");
59
+ const home = absolute(options.home, "Home directory");
60
+ const executable = absolute(options.executable, "Stable CLI executable");
61
+ // Keep the supplied stable launcher path. realpath would pin installer services
62
+ // to a retired version instead of following the next verified activation.
63
+ const identity = createHash("sha256")
64
+ .update(absolute(namespace.daemonInfoDir, "Daemon state directory"))
65
+ .digest("hex")
66
+ .slice(0, 24);
67
+ const supervisionId = `tmux-ide.${identity}`;
68
+ const stdoutPath = join(namespace.daemonInfoDir, "service.stdout.log");
69
+ const stderrPath = join(namespace.daemonInfoDir, "service.stderr.log");
70
+ const environment = {
71
+ HOME: home,
72
+ PATH: safeValue(options.path, "Service PATH"),
73
+ ...runtimeNamespaceEnvironment(namespace),
74
+ TMUX_IDE_CONFIG: namespace.configPath,
75
+ TMUX_IDE_SETTINGS_DIR: namespace.settingsDir,
76
+ TMUX_IDE_CLAUDE_DIR: namespace.claudeDir,
77
+ TMUX_IDE_CLAUDE_SETTINGS: namespace.claudeSettingsPath,
78
+ TMUX_IDE_CLAUDE_HOOK_PATH: namespace.claudeHookPath,
79
+ TMUX_IDE_OPENCODE_DIR: namespace.opencodeDir,
80
+ };
81
+ for (const [name, value] of Object.entries(environment)) safeValue(value, name);
82
+ const args = [executable, "--headless", "--supervised", supervisionId];
83
+ const shared = {
84
+ supervisionId,
85
+ executable,
86
+ recordPath: join(namespace.daemonInfoDir, "service.json"),
87
+ stdoutPath,
88
+ stderrPath,
89
+ };
90
+ if (options.platform === "darwin") {
91
+ const label = `com.${supervisionId}`;
92
+ const string = (value: string) => `<string>${xml(value)}</string>`;
93
+ return Object.freeze({
94
+ ...shared,
95
+ manager: "launchd",
96
+ target: `gui/${options.uid}/${label}`,
97
+ unitPath: join(home, "Library", "LaunchAgents", `${label}.plist`),
98
+ contents: `<?xml version="1.0" encoding="UTF-8"?>
99
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
100
+ <plist version="1.0"><dict>
101
+ <key>Label</key>${string(label)}
102
+ <key>ProgramArguments</key><array>${args.map(string).join("")}</array>
103
+ <key>EnvironmentVariables</key><dict>${Object.entries(environment)
104
+ .map(([key, value]) => `<key>${xml(key)}</key>${string(value)}`)
105
+ .join("")}</dict>
106
+ <key>WorkingDirectory</key>${string(home)}
107
+ <key>RunAtLoad</key><true/>
108
+ <key>KeepAlive</key><true/>
109
+ <key>ThrottleInterval</key><integer>3</integer>
110
+ <key>ExitTimeOut</key><integer>30</integer>
111
+ <key>AbandonProcessGroup</key><true/>
112
+ <key>StandardOutPath</key>${string(stdoutPath)}
113
+ <key>StandardErrorPath</key>${string(stderrPath)}
114
+ </dict></plist>
115
+ `,
116
+ });
117
+ }
118
+ const configHome = absolute(options.configHome ?? join(home, ".config"), "Config directory");
119
+ const target = `${supervisionId}.service`;
120
+ // WorkingDirectory is a raw path, not an ExecStart-style quoted word.
121
+ // The equivalent '/.' suffix preserves trailing spaces/backslashes without
122
+ // allowing the unit parser to trim them or treat them as line continuation.
123
+ const workingDirectory = `${home.replace(/%/gu, "%%")}/.`;
124
+ return Object.freeze({
125
+ ...shared,
126
+ manager: "systemd",
127
+ target,
128
+ unitPath: join(configHome, "systemd", "user", target),
129
+ contents: `[Unit]
130
+ Description=tmux-ide canonical user daemon
131
+ StartLimitIntervalSec=60
132
+ StartLimitBurst=10
133
+
134
+ [Service]
135
+ Type=exec
136
+ ExecStart=:${args.map(systemd).join(" ")}
137
+ WorkingDirectory=${workingDirectory}
138
+ ${Object.entries(environment)
139
+ .map(([key, value]) => `Environment=${systemd(`${key}=${value}`)}`)
140
+ .join("\n")}
141
+ Restart=always
142
+ RestartSec=3
143
+ TimeoutStopSec=30
144
+ # tmux pane work has an independent lifetime; never kill the whole service cgroup.
145
+ KillMode=process
146
+ StandardOutput=journal
147
+ StandardError=journal
148
+
149
+ [Install]
150
+ WantedBy=default.target
151
+ `,
152
+ });
153
+ }