tmux-ide 2.9.2 → 2.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +121 -140
  3. package/bin/cli.js +2457 -727
  4. package/package.json +23 -8
  5. package/packages/contracts/src/desktop-host.ts +3 -0
  6. package/packages/contracts/src/saved-machines.ts +19 -0
  7. package/packages/contracts/src/terminal-replica.ts +1 -1
  8. package/packages/core/package.json +3 -2
  9. package/packages/core/src/terminal-delivery.ts +51 -3
  10. package/packages/core/src/terminal-fnv64-wasm.ts +4 -3
  11. package/packages/core/src/terminal-replica-hash-cache.ts +129 -31
  12. package/packages/core/src/terminal-replica-owned-row.ts +27 -0
  13. package/packages/core/src/terminal-replica.ts +13 -28
  14. package/packages/daemon/dist/command-center/daemon-fleet-facts-observer.js +96 -10
  15. package/packages/daemon/dist/command-center/resources/saved-machine-route.js +40 -1
  16. package/packages/daemon/dist/command-center/ws-events.js +5 -24
  17. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +8 -0
  18. package/packages/daemon/dist/lib/canonical-daemon.js +28 -2
  19. package/packages/daemon/dist/lib/daemon-embed.js +1 -1
  20. package/packages/daemon/dist/lib/daemon-service-manager.js +122 -0
  21. package/packages/daemon/dist/lib/daemon-service-plan.js +127 -0
  22. package/packages/daemon/dist/lib/daemon-service.js +259 -0
  23. package/packages/daemon/dist/lib/install-origin.js +2 -0
  24. package/packages/daemon/dist/lib/local-fleet-request.js +9 -3
  25. package/packages/daemon/dist/lib/log.js +6 -10
  26. package/packages/daemon/dist/lib/managed-installer-update.js +67 -0
  27. package/packages/daemon/dist/lib/{tui-download-lock.js → private-operation-lock.js} +11 -11
  28. package/packages/daemon/dist/lib/remote-daemon-info.js +3 -2
  29. package/packages/daemon/dist/lib/remote-daemon-lifecycle.js +4 -4
  30. package/packages/daemon/dist/lib/remote-tmux-command.js +10 -0
  31. package/packages/daemon/dist/lib/runtime-namespace.js +28 -16
  32. package/packages/daemon/dist/lib/saved-machines.js +40 -2
  33. package/packages/daemon/dist/lib/ssh-daemon-transport.js +24 -9
  34. package/packages/daemon/dist/lib/tmux-server-owner.js +1 -1
  35. package/packages/daemon/dist/lib/tmux-server-registration.js +21 -3
  36. package/packages/daemon/dist/lib/tui-binary.js +2 -2
  37. package/packages/daemon/dist/lib/update.js +18 -0
  38. package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +24 -1
  39. package/packages/daemon/dist/machines.js +40 -4
  40. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  41. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  42. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  43. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  44. package/packages/daemon/dist/terminal/attachments/direct-websocket.js +4 -1
  45. package/packages/daemon/dist/terminal/attachments/native-runtime.js +22 -13
  46. package/packages/daemon/dist/terminal/mirror/mirror-service.js +101 -3
  47. package/packages/daemon/dist/terminal/mirror/registered-window-guard.js +62 -0
  48. package/packages/daemon/dist/terminal/mirror/session-channel.js +75 -4
  49. package/packages/daemon/dist/terminal/mirror/shared-window-index.js +45 -0
  50. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +11 -1
  51. package/packages/daemon/dist/terminal/session-runtime/multiplexer-backend.js +1 -0
  52. package/packages/daemon/dist/terminal/session-runtime/registry.js +56 -7
  53. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +3 -1
  54. package/packages/daemon/dist/tui/integrations/claude-agent-teams.js +264 -0
  55. package/packages/daemon/dist/tui/mirror/dialog-model.js +2 -35
  56. package/packages/daemon/dist/tui/mirror/runtime/application-daemon-authority-owner.js +8 -0
  57. package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +20 -2
  58. package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +7 -1
  59. package/packages/daemon/dist/tui/mirror/runtime/application-machine-authority.js +59 -9
  60. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +27 -19
  61. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +23 -18
  62. package/packages/daemon/dist/tui/mirror/runtime/application-machine-startup.js +31 -2
  63. package/packages/daemon/dist/tui/mirror/runtime/application-palette-search-owner.js +7 -3
  64. package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +3 -0
  65. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +146 -2
  66. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +201 -50
  67. package/packages/daemon/dist/tui/mirror/runtime/bounded-performance-record-writer.js +50 -10
  68. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -5
  69. package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +3 -0
  70. package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +8 -2
  71. package/packages/daemon/dist/tui/mirror/terminal-text.js +35 -0
  72. package/packages/daemon/dist/tui/mirror/workspace/connection-feedback.js +5 -3
  73. package/packages/daemon/src/command-center/daemon-fleet-facts-observer.ts +100 -10
  74. package/packages/daemon/src/command-center/resources/saved-machine-route.ts +48 -1
  75. package/packages/daemon/src/command-center/ws-events.ts +5 -25
  76. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +12 -0
  77. package/packages/daemon/src/lib/canonical-daemon.ts +44 -2
  78. package/packages/daemon/src/lib/daemon-embed.ts +1 -0
  79. package/packages/daemon/src/lib/daemon-service-manager.ts +152 -0
  80. package/packages/daemon/src/lib/daemon-service-plan.ts +153 -0
  81. package/packages/daemon/src/lib/daemon-service.ts +301 -0
  82. package/packages/daemon/src/lib/install-origin.ts +5 -0
  83. package/packages/daemon/src/lib/local-fleet-request.ts +19 -3
  84. package/packages/daemon/src/lib/log.ts +13 -9
  85. package/packages/daemon/src/lib/managed-installer-update.ts +79 -0
  86. package/packages/daemon/src/lib/{tui-download-lock.ts → private-operation-lock.ts} +15 -11
  87. package/packages/daemon/src/lib/remote-daemon-info.ts +3 -3
  88. package/packages/daemon/src/lib/remote-daemon-lifecycle.ts +4 -4
  89. package/packages/daemon/src/lib/remote-tmux-command.ts +12 -0
  90. package/packages/daemon/src/lib/runtime-namespace.ts +27 -18
  91. package/packages/daemon/src/lib/saved-machines.ts +40 -1
  92. package/packages/daemon/src/lib/ssh-daemon-transport.ts +25 -7
  93. package/packages/daemon/src/lib/tmux-server-owner.ts +1 -0
  94. package/packages/daemon/src/lib/tmux-server-registration.ts +21 -3
  95. package/packages/daemon/src/lib/tui-binary.ts +2 -2
  96. package/packages/daemon/src/lib/update.ts +24 -0
  97. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +22 -0
  98. package/packages/daemon/src/machines.ts +53 -5
  99. package/packages/daemon/src/terminal/attachments/direct-websocket.ts +8 -2
  100. package/packages/daemon/src/terminal/attachments/native-runtime.ts +28 -13
  101. package/packages/daemon/src/terminal/mirror/mirror-service.ts +105 -3
  102. package/packages/daemon/src/terminal/mirror/registered-window-guard.ts +66 -0
  103. package/packages/daemon/src/terminal/mirror/session-channel.ts +92 -4
  104. package/packages/daemon/src/terminal/mirror/shared-window-index.ts +42 -0
  105. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +12 -1
  106. package/packages/daemon/src/terminal/session-runtime/multiplexer-backend.ts +2 -0
  107. package/packages/daemon/src/terminal/session-runtime/registry.ts +70 -8
  108. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +3 -1
  109. package/packages/daemon/src/tui/integrations/claude-agent-teams.ts +334 -0
  110. package/packages/daemon/src/tui/mirror/dialog-model.ts +2 -32
  111. package/packages/daemon/src/tui/mirror/runtime/application-daemon-authority-owner.ts +8 -0
  112. package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +19 -2
  113. package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +6 -1
  114. package/packages/daemon/src/tui/mirror/runtime/application-machine-authority.ts +60 -11
  115. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +39 -33
  116. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +31 -23
  117. package/packages/daemon/src/tui/mirror/runtime/application-machine-startup.ts +38 -2
  118. package/packages/daemon/src/tui/mirror/runtime/application-palette-search-owner.ts +7 -4
  119. package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +3 -0
  120. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +1 -1
  121. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +47 -46
  122. package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +165 -2
  123. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +207 -53
  124. package/packages/daemon/src/tui/mirror/runtime/bounded-performance-record-writer.ts +49 -9
  125. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +6 -5
  126. package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +4 -0
  127. package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +8 -3
  128. package/packages/daemon/src/tui/mirror/terminal-text.ts +32 -0
  129. package/packages/daemon/src/tui/mirror/workspace/connection-feedback.ts +6 -3
  130. package/packages/daemon-client/src/fleet-connection-status.ts +10 -1
  131. package/packages/daemon-client/src/workspace-client.ts +4 -1
  132. package/skill/SKILL.md +30 -30
  133. package/packages/contracts/src/__tests__/actions-contract.test.ts +0 -119
  134. package/packages/contracts/src/__tests__/agent-graph-overlay.test.ts +0 -428
  135. package/packages/contracts/src/__tests__/app-window-mutation.test.ts +0 -51
  136. package/packages/contracts/src/__tests__/app-window-state.test.ts +0 -34
  137. package/packages/contracts/src/__tests__/application-shell.test.ts +0 -645
  138. package/packages/contracts/src/__tests__/automation-operations.test.ts +0 -78
  139. package/packages/contracts/src/__tests__/causal-cell.test.ts +0 -47
  140. package/packages/contracts/src/__tests__/client-view-state.test.ts +0 -115
  141. package/packages/contracts/src/__tests__/cohesion-fixture.test.ts +0 -106
  142. package/packages/contracts/src/__tests__/commands.test.ts +0 -86
  143. package/packages/contracts/src/__tests__/control.test.ts +0 -154
  144. package/packages/contracts/src/__tests__/daemon-events.test.ts +0 -405
  145. package/packages/contracts/src/__tests__/daemon-resources.test.ts +0 -143
  146. package/packages/contracts/src/__tests__/daemon-wire.test.ts +0 -149
  147. package/packages/contracts/src/__tests__/desktop-host.test.ts +0 -269
  148. package/packages/contracts/src/__tests__/desktop-missions.test.ts +0 -215
  149. package/packages/contracts/src/__tests__/experience-conformance.test.ts +0 -153
  150. package/packages/contracts/src/__tests__/experience-kernel-boundary.test.ts +0 -113
  151. package/packages/contracts/src/__tests__/experience-shell.test.ts +0 -65
  152. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v2.json +0 -35
  153. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +0 -35
  154. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v4.json +0 -35
  155. package/packages/contracts/src/__tests__/fixtures/scoped-receipt.ts +0 -62
  156. package/packages/contracts/src/__tests__/fleet-agent-graph.test.ts +0 -231
  157. package/packages/contracts/src/__tests__/fleet-catalog.test.ts +0 -270
  158. package/packages/contracts/src/__tests__/fleet-lifecycle.test.ts +0 -62
  159. package/packages/contracts/src/__tests__/focus-overlay.test.ts +0 -110
  160. package/packages/contracts/src/__tests__/ide-config.test.ts +0 -64
  161. package/packages/contracts/src/__tests__/interaction-evidence.test.ts +0 -288
  162. package/packages/contracts/src/__tests__/interaction-receipt-v2.test.ts +0 -72
  163. package/packages/contracts/src/__tests__/issue-error.test.ts +0 -78
  164. package/packages/contracts/src/__tests__/mission-projections.test.ts +0 -186
  165. package/packages/contracts/src/__tests__/missions.test.ts +0 -272
  166. package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +0 -242
  167. package/packages/contracts/src/__tests__/pane-appearance.test.ts +0 -146
  168. package/packages/contracts/src/__tests__/pane-read-result.test.ts +0 -55
  169. package/packages/contracts/src/__tests__/pane-stream.test.ts +0 -536
  170. package/packages/contracts/src/__tests__/pane-widget-marker.test.ts +0 -327
  171. package/packages/contracts/src/__tests__/performance-metrics.test.ts +0 -73
  172. package/packages/contracts/src/__tests__/performance-qualification.test.ts +0 -137
  173. package/packages/contracts/src/__tests__/rich-card-widget.test.ts +0 -29
  174. package/packages/contracts/src/__tests__/session-runtime.test.ts +0 -512
  175. package/packages/contracts/src/__tests__/startup-readiness.test.ts +0 -409
  176. package/packages/contracts/src/__tests__/terminal-attachment-stream.test.ts +0 -72
  177. package/packages/contracts/src/__tests__/terminal-attachments.test.ts +0 -329
  178. package/packages/contracts/src/__tests__/terminal-delivery.test.ts +0 -88
  179. package/packages/contracts/src/__tests__/terminal-runtime-inventory.test.ts +0 -68
  180. package/packages/contracts/src/__tests__/terminals.test.ts +0 -87
  181. package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +0 -166
  182. package/packages/contracts/src/__tests__/transport-contract-loading.test.ts +0 -91
  183. package/packages/contracts/src/__tests__/visual-tokens.test.ts +0 -224
  184. package/packages/contracts/src/__tests__/window-links.test.ts +0 -243
  185. package/packages/contracts/src/__tests__/workspace-changes-resource.test.ts +0 -317
  186. package/packages/contracts/src/__tests__/workspace-changes-view.test.ts +0 -122
  187. package/packages/contracts/src/__tests__/workspace-config.test.ts +0 -294
  188. package/packages/contracts/src/__tests__/workspace-files-resource.test.ts +0 -261
  189. package/packages/contracts/src/__tests__/workspace-files-tree.test.ts +0 -147
  190. package/packages/contracts/src/__tests__/workspace-pane-creation.test.ts +0 -249
  191. package/packages/contracts/src/__tests__/workspace-promotion.test.ts +0 -94
  192. package/packages/contracts/src/__tests__/workspace-resource-identity.test.ts +0 -90
  193. package/packages/contracts/src/daemon-resource-request.test.ts +0 -192
  194. package/packages/contracts/src/native-operation-identity.test.ts +0 -23
  195. package/packages/contracts/src/workspace-admission.test.ts +0 -21
  196. package/packages/contracts/src/workspace-catalog-resource.test.ts +0 -146
  197. package/packages/core/src/agent-provisioning.test.ts +0 -42
  198. package/packages/core/src/application-shell-session.test.ts +0 -105
  199. package/packages/core/src/interaction-evidence.test.ts +0 -134
  200. package/packages/core/src/interaction-flow.test.ts +0 -66
  201. package/packages/core/src/interaction-receipts.test.ts +0 -671
  202. package/packages/core/src/navigator.test.ts +0 -34
  203. package/packages/core/src/optimistic-projection-conformance.test.ts +0 -75
  204. package/packages/core/src/optimistic-projection.test.ts +0 -118
  205. package/packages/core/src/performance-metrics.test.ts +0 -121
  206. package/packages/core/src/performance-qualification.test.ts +0 -283
  207. package/packages/core/src/saved-machines.test.ts +0 -80
  208. package/packages/core/src/terminal-compact-cold.test.ts +0 -87
  209. package/packages/core/src/terminal-conformance.test.ts +0 -48
  210. package/packages/core/src/terminal-delivery-cache-lifetime.test.ts +0 -167
  211. package/packages/core/src/terminal-delivery-encode-cooperative.test.ts +0 -140
  212. package/packages/core/src/terminal-delivery-representation-hash.test.ts +0 -77
  213. package/packages/core/src/terminal-delivery.test.ts +0 -1471
  214. package/packages/core/src/terminal-fnv64-wasm.test.ts +0 -61
  215. package/packages/core/src/terminal-replica-cold.test.ts +0 -31
  216. package/packages/core/src/terminal-replica-cooperative.test.ts +0 -170
  217. package/packages/core/src/terminal-replica-hash-cache.test.ts +0 -283
  218. package/packages/core/src/terminal-replica.test.ts +0 -347
  219. package/packages/core/src/workspace-selection.test.ts +0 -63
  220. package/packages/daemon/dist/__tests__/support.js +0 -33
  221. package/packages/daemon/dist/lib/__tests__/fixtures/workspace-state-writer.js +0 -58
  222. package/packages/daemon/dist/lib/__tests__/installed-recovery-fixture.js +0 -400
  223. package/packages/daemon/dist/terminal/__tests__/MockPtyAdapter.js +0 -182
  224. package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +0 -98
  225. package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +0 -156
  226. package/packages/daemon/src/tui/mirror/__snapshots__/activity-surface-renderer.test.tsx.snap +0 -42
  227. package/packages/daemon/src/tui/mirror/__snapshots__/changes-terminal-surface-renderer.test.tsx.snap +0 -267
  228. package/packages/daemon/src/tui/mirror/__snapshots__/home-files-surface-renderer.test.tsx.snap +0 -267
  229. package/packages/daemon/src/tui/mirror/__snapshots__/missions-surface-renderer.test.tsx.snap +0 -134
  230. package/packages/daemon/src/tui/mirror/__snapshots__/recipes-gallery-renderer.test.tsx.snap +0 -141
  231. package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +0 -134
  232. package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +0 -87
  233. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/agent-terminal-canvas-renderer.test.tsx.snap +0 -348
  234. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +0 -134
  235. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/command-palette-surface-renderer.test.tsx.snap +0 -255
  236. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/pane-frame-renderer.test.tsx.snap +0 -525
  237. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/terminal-pane-header-polish-renderer.test.tsx.snap +0 -31
  238. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/workbench-shell-renderer.test.tsx.snap +0 -134
  239. package/packages/daemon-client/src/application-shell-session.test.ts +0 -196
  240. package/packages/daemon-client/src/automation-client.test.ts +0 -214
  241. package/packages/daemon-client/src/bootstrap-coordinator.test.ts +0 -145
  242. package/packages/daemon-client/src/connection-supervisor.test.ts +0 -109
  243. package/packages/daemon-client/src/direct-application-shell-transport.test.ts +0 -941
  244. package/packages/daemon-client/src/first-latest-coordinator.test.ts +0 -175
  245. package/packages/daemon-client/src/fleet-dial-scheduler.test.ts +0 -114
  246. package/packages/daemon-client/src/fleet-environments.test.ts +0 -40
  247. package/packages/daemon-client/src/generation-bound-store-timers.test.ts +0 -74
  248. package/packages/daemon-client/src/owner-action-client.test.ts +0 -245
  249. package/packages/daemon-client/src/pane-stream-client.test.ts +0 -2841
  250. package/packages/daemon-client/src/pane-stream-clock-calibration.test.ts +0 -207
  251. package/packages/daemon-client/src/pane-stream-loopback-benchmark.test.ts +0 -51
  252. package/packages/daemon-client/src/push-resource-session.test.ts +0 -509
  253. package/packages/daemon-client/src/resource-replica.test.ts +0 -186
  254. package/packages/daemon-client/src/runtime-resource-ledger.test.ts +0 -18
  255. package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +0 -114
  256. package/packages/daemon-client/src/terminal-fast-lane.test.ts +0 -835
  257. package/packages/daemon-client/src/tmux-server-client.test.ts +0 -279
  258. package/packages/daemon-client/src/tmux-server-interaction-events.test.ts +0 -230
  259. package/packages/daemon-client/src/workspace-catalog-v2.test.ts +0 -109
  260. package/packages/daemon-client/src/workspace-client-boundaries.test.ts +0 -59
  261. package/packages/daemon-client/src/workspace-client-operations.test.ts +0 -96
  262. package/packages/daemon-client/src/workspace-client.test.ts +0 -2912
  263. package/packages/daemon-client/src/workspace-event-supervisor.test.ts +0 -1086
  264. package/packages/daemon-client/src/workspace-pane-client.test.ts +0 -80
  265. package/packages/presentation/src/presentation-contract.test.ts +0 -63
  266. package/packages/tmux-bridge/src/runner.test.ts +0 -614
  267. package/packages/tmux-bridge/src/targeting.test.ts +0 -107
@@ -0,0 +1,259 @@
1
+ import { accessSync, constants, openSync, closeSync, fstatSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, renameSync, rmSync, realpathSync, linkSync, } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
3
+ import { dirname, join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { z } from "zod";
6
+ import { getCanonicalDaemonInfoPath, inspectCanonicalDaemonInfo, probeCanonicalDaemonIdentity, releaseCanonicalDaemonSupervision, reserveCanonicalDaemonSupervision, } from "./canonical-daemon.js";
7
+ import { createDaemonServiceManager } from "./daemon-service-manager.js";
8
+ import { planDaemonService } from "./daemon-service-plan.js";
9
+ import { resolveRuntimeNamespace } from "./runtime-namespace.js";
10
+ import { IdeError } from "./errors.js";
11
+ import { acquirePrivateOperationLock } from "./private-operation-lock.js";
12
+ const RecordSchema = z.strictObject({
13
+ version: z.literal(1),
14
+ removing: z.boolean().optional(),
15
+ executable: z.string().min(1),
16
+ contents: z
17
+ .string()
18
+ .min(1)
19
+ .max(64 * 1024),
20
+ });
21
+ function defaults() {
22
+ const namespace = resolveRuntimeNamespace();
23
+ return {
24
+ plan: (executable) => planDaemonService({
25
+ platform: process.platform,
26
+ uid: process.getuid?.() ?? 0,
27
+ home: homedir(),
28
+ configHome: process.env.XDG_CONFIG_HOME || undefined,
29
+ executable,
30
+ path: process.env.PATH || "/usr/bin:/bin",
31
+ namespace,
32
+ }),
33
+ manager: createDaemonServiceManager,
34
+ inspect: inspectCanonicalDaemonInfo,
35
+ reserve: reserveCanonicalDaemonSupervision,
36
+ release: releaseCanonicalDaemonSupervision,
37
+ probe: probeCanonicalDaemonIdentity,
38
+ recordPath: join(dirname(getCanonicalDaemonInfoPath()), "service.json"),
39
+ waitMs: 30_000,
40
+ };
41
+ }
42
+ function refused(message) {
43
+ throw new IdeError(message, { code: "DAEMON_SERVICE_REFUSED" });
44
+ }
45
+ function ownedFile(path) {
46
+ let descriptor;
47
+ try {
48
+ descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
49
+ const stat = fstatSync(descriptor);
50
+ if (!stat.isFile() ||
51
+ stat.uid !== process.getuid?.() ||
52
+ (stat.mode & 0o022) !== 0 ||
53
+ stat.size > 128 * 1024)
54
+ refused(`Service file is not a bounded, user-owned regular file: ${path}`);
55
+ return readFileSync(descriptor, "utf8");
56
+ }
57
+ catch (error) {
58
+ if (error.code === "ENOENT")
59
+ return null;
60
+ throw error;
61
+ }
62
+ finally {
63
+ if (descriptor !== undefined)
64
+ closeSync(descriptor);
65
+ }
66
+ }
67
+ function boundTo(state, id) {
68
+ return state.status === "reserved"
69
+ ? state.reservation.supervisionId === id
70
+ : state.status === "valid" && state.info.supervisionId === id;
71
+ }
72
+ /** Explicit OS supervision. Never starts a detached daemon or signals a guessed PID. */
73
+ export async function manageDaemonService(action, executable, deps = defaults()) {
74
+ const previousRecord = ownedFile(deps.recordPath);
75
+ const record = previousRecord === null ? null : RecordSchema.parse(JSON.parse(previousRecord));
76
+ if (record?.removing && action !== "remove" && action !== "status")
77
+ refused("Service removal is incomplete; finish remove before installing or restarting");
78
+ if (!record && action !== "install") {
79
+ if (action === "status")
80
+ return { status: "not-installed" };
81
+ refused("No managed service is installed in this daemon namespace");
82
+ }
83
+ if (action === "install" && !executable)
84
+ refused("Service installation requires an absolute stable CLI launcher path");
85
+ if (record && executable && record.executable !== executable)
86
+ refused("A different launcher is already registered; remove the stopped service before changing it");
87
+ const plan = deps.plan(record?.executable ?? executable);
88
+ if (plan.recordPath !== deps.recordPath)
89
+ refused("Service namespace changed during resolution");
90
+ const contents = record?.contents ?? plan.contents;
91
+ const manager = deps.manager(plan);
92
+ const assertFiles = (allowMissingUnit = false) => {
93
+ const unit = ownedFile(plan.unitPath);
94
+ if (unit !== contents && !((record?.removing || allowMissingUnit) && unit === null))
95
+ refused("Service definition is missing or modified; refusing to control it");
96
+ if (record && ownedFile(plan.recordPath) !== previousRecord)
97
+ refused("Service ownership record changed");
98
+ };
99
+ const inspectManager = async () => {
100
+ const state = await manager.inspect();
101
+ // systemd reports its generated search-path symlink for units enabled by
102
+ // absolute path. Resolve that link to the owned definition; the definition
103
+ // itself still has to pass our no-follow private-file and content checks.
104
+ if (state.loaded &&
105
+ (!state.definitionPath || realpathSync(state.definitionPath) !== realpathSync(plan.unitPath)))
106
+ refused("The service manager loaded a different definition; refusing to control it");
107
+ return state;
108
+ };
109
+ const ready = async (previousInstance) => {
110
+ const deadline = Date.now() + deps.waitMs;
111
+ do {
112
+ const state = deps.inspect();
113
+ if (!boundTo(state, plan.supervisionId))
114
+ refused("Service reservation changed during startup");
115
+ if (state.status === "valid" && state.info.instanceId !== previousInstance) {
116
+ const identity = await deps.probe(state.info);
117
+ const service = await inspectManager();
118
+ if (identity &&
119
+ service.active &&
120
+ service.pid === state.info.pid &&
121
+ identity.pid === state.info.pid &&
122
+ identity.instanceId === state.info.instanceId &&
123
+ identity.startedAt === state.info.startedAt &&
124
+ identity.protocolVersion === state.info.protocolVersion)
125
+ return {
126
+ status: "running",
127
+ manager: plan.manager,
128
+ target: plan.target,
129
+ pid: state.info.pid,
130
+ instanceId: state.info.instanceId,
131
+ };
132
+ }
133
+ await new Promise((resolve) => setTimeout(resolve, 100));
134
+ } while (Date.now() < deadline);
135
+ refused("Service did not publish a verified daemon before the deadline; inspect service logs, then retry restart or remove");
136
+ };
137
+ await manager.available();
138
+ const service = await inspectManager();
139
+ const allowMissingUnit = action === "remove" && !service.loaded;
140
+ if (record)
141
+ assertFiles(allowMissingUnit);
142
+ if (action === "status") {
143
+ const state = deps.inspect();
144
+ const identity = service.active && state.status === "valid" && boundTo(state, plan.supervisionId)
145
+ ? await deps.probe(state.info)
146
+ : null;
147
+ const verified = identity !== null &&
148
+ state.status === "valid" &&
149
+ service.pid === state.info.pid &&
150
+ identity.pid === state.info.pid &&
151
+ identity.instanceId === state.info.instanceId &&
152
+ identity.startedAt === state.info.startedAt &&
153
+ identity.protocolVersion === state.info.protocolVersion;
154
+ return {
155
+ status: verified
156
+ ? "running"
157
+ : service.active
158
+ ? "active-unverified"
159
+ : "stopped",
160
+ manager: plan.manager,
161
+ target: plan.target,
162
+ pid: service.pid,
163
+ reservationMatches: boundTo(state, plan.supervisionId),
164
+ unitPath: plan.unitPath,
165
+ };
166
+ }
167
+ if (!record && (service.loaded || ownedFile(plan.unitPath) !== null))
168
+ refused("A service definition already exists without this namespace's ownership record");
169
+ if (record &&
170
+ !boundTo(deps.inspect(), plan.supervisionId) &&
171
+ !(record.removing && action === "remove" && deps.inspect().status === "missing"))
172
+ refused("Service reservation is missing or belongs to another owner");
173
+ if (!record) {
174
+ accessSync(plan.executable, constants.X_OK);
175
+ // Canonical reservation admission rejects live, uncertain and foreign owners.
176
+ deps.reserve(plan.supervisionId);
177
+ }
178
+ const lock = `${plan.recordPath}.lock`;
179
+ const releaseLock = await acquirePrivateOperationLock(lock, 1000);
180
+ let wroteRecord = false;
181
+ let wroteUnit = false;
182
+ let activationAttempted = false;
183
+ try {
184
+ if (record)
185
+ assertFiles(allowMissingUnit);
186
+ if (action === "install" && !record) {
187
+ writeFileSync(plan.recordPath, JSON.stringify({ version: 1, executable: plan.executable, contents }) + "\n", { flag: "wx", mode: 0o600 });
188
+ wroteRecord = true;
189
+ mkdirSync(dirname(plan.unitPath), { recursive: true, mode: 0o700 });
190
+ const unitStage = `${plan.unitPath}.${randomUUID()}.tmp`;
191
+ writeFileSync(unitStage, contents, { flag: "wx", mode: 0o600 });
192
+ try {
193
+ linkSync(unitStage, plan.unitPath);
194
+ }
195
+ finally {
196
+ unlinkSync(unitStage);
197
+ }
198
+ wroteUnit = true;
199
+ activationAttempted = true;
200
+ await manager.install();
201
+ return await ready();
202
+ }
203
+ if (action === "remove") {
204
+ if (service.loaded)
205
+ await manager.stop();
206
+ let stopped = await inspectManager();
207
+ const stopDeadline = Date.now() + deps.waitMs;
208
+ while ((stopped.active || stopped.pid !== null) && Date.now() < stopDeadline) {
209
+ await new Promise((resolve) => setTimeout(resolve, 100));
210
+ stopped = await inspectManager();
211
+ }
212
+ if (stopped.active || stopped.pid !== null)
213
+ refused("Service manager has not confirmed the daemon stopped; reservation retained");
214
+ assertFiles(allowMissingUnit);
215
+ const removalRecord = JSON.stringify({ ...record, removing: true }) + "\n";
216
+ const temporaryRecord = `${plan.recordPath}.${randomUUID()}.tmp`;
217
+ writeFileSync(temporaryRecord, removalRecord, { flag: "wx", mode: 0o600 });
218
+ try {
219
+ if (ownedFile(plan.recordPath) !== previousRecord)
220
+ refused("Service ownership record changed during removal");
221
+ renameSync(temporaryRecord, plan.recordPath);
222
+ }
223
+ finally {
224
+ rmSync(temporaryRecord, { force: true });
225
+ }
226
+ if (ownedFile(plan.unitPath) !== null)
227
+ unlinkSync(plan.unitPath);
228
+ await manager.reload();
229
+ if ((await inspectManager()).loaded)
230
+ refused("Service remains loaded after removal; reservation retained");
231
+ if (deps.inspect().status !== "missing")
232
+ deps.release(plan.supervisionId);
233
+ unlinkSync(plan.recordPath);
234
+ return { status: "removed", target: plan.target };
235
+ }
236
+ const before = deps.inspect();
237
+ if (action === "install" && service.active)
238
+ return await ready();
239
+ if (service.loaded)
240
+ await manager.restart(service.pid !== null);
241
+ else
242
+ await manager.install();
243
+ return await ready(before.status === "valid" ? before.info.instanceId : undefined);
244
+ }
245
+ catch (error) {
246
+ // Never discard ownership after an uncertain OS activation. The retained
247
+ // record/reservation gives the explicit remove/restart commands a recovery path.
248
+ if (!activationAttempted && wroteRecord) {
249
+ if (wroteUnit && ownedFile(plan.unitPath) === contents)
250
+ unlinkSync(plan.unitPath);
251
+ unlinkSync(plan.recordPath);
252
+ deps.release(plan.supervisionId);
253
+ }
254
+ throw error;
255
+ }
256
+ finally {
257
+ releaseLock();
258
+ }
259
+ }
@@ -3,6 +3,8 @@ import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  /** Conservative layout evidence; an arbitrary node_modules directory is not a global install. */
5
5
  export function detectPackageManager(path) {
6
+ if (/\/share\/tmux-ide\/releases\/install-[^/]+\/npm\/lib\/node_modules\/tmux-ide\/bin$/.test(path))
7
+ return "installer";
6
8
  if (/\/(?:Cellar|Caskroom)\//.test(path))
7
9
  return "homebrew";
8
10
  if (/\/_npx\//.test(path))
@@ -1,22 +1,28 @@
1
- import { SavedMachineRegistrySchema } from "@tmux-ide/contracts/saved-machines";
1
+ import { SavedMachineRegistrySchema, SavedMachineMutationSchema, } from "@tmux-ide/contracts/saved-machines";
2
2
  import { canonicalDaemonUrl, isCanonicalDaemonAlive, readCanonicalDaemonInfo, } from "./canonical-daemon.js";
3
3
  /** Intentional local registry mutation. Never uses selected-machine authority. */
4
4
  export async function saveMachineProfiles(machines) {
5
5
  const registry = SavedMachineRegistrySchema.parse({ version: 1, machines });
6
+ return writeLocalMachineRegistry("POST", { registry });
7
+ }
8
+ export async function mutateMachineProfile(change) {
9
+ return writeLocalMachineRegistry("PATCH", { change: SavedMachineMutationSchema.parse(change) });
10
+ }
11
+ async function writeLocalMachineRegistry(method, payload) {
6
12
  const daemon = readCanonicalDaemonInfo();
7
13
  if (!daemon?.authToken || !(await isCanonicalDaemonAlive(daemon)))
8
14
  throw new Error("Start a local tmux-ide daemon to save machine profiles.");
9
15
  try {
10
16
  const response = await fetch(canonicalDaemonUrl("http", daemon.bindHostname, daemon.port) +
11
17
  "/api/resources/saved-machines", {
12
- method: "POST",
18
+ method,
13
19
  redirect: "error",
14
20
  signal: AbortSignal.timeout(5000),
15
21
  headers: {
16
22
  Authorization: `Bearer ${daemon.authToken}`,
17
23
  "Content-Type": "application/json",
18
24
  },
19
- body: JSON.stringify({ expectedInstanceId: daemon.instanceId, registry }),
25
+ body: JSON.stringify({ expectedInstanceId: daemon.instanceId, ...payload }),
20
26
  });
21
27
  if (!response.ok) {
22
28
  await response.body?.cancel();
@@ -86,17 +86,13 @@ function markStreamFailed(name, error) {
86
86
  if (streamState[name].failed)
87
87
  return;
88
88
  streamState[name].failed = true;
89
- const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
89
+ const detail = sanitizeLogMessage(error instanceof Error ? `${error.name}: ${error.message}` : String(error), secrets, budget);
90
90
  const warning = `[log.ts] ${name} write failed (${detail}); further records are retained in memory only\n`;
91
91
  const other = name === "stdout" ? "stderr" : "stdout";
92
- if (!streamState[other].failed) {
93
- try {
94
- process[other].write(warning);
95
- }
96
- catch {
97
- streamState[other].failed = true;
98
- }
99
- }
92
+ // The fallback can fail asynchronously too. Install the same error handling
93
+ // before writing it; failed flags prevent a warning loop if both streams fail.
94
+ if (!streamState[other].failed)
95
+ writeToStream(other, warning);
100
96
  }
101
97
  function writeToStream(name, line) {
102
98
  const state = streamState[name];
@@ -150,7 +146,7 @@ function writeStructuredLog(level, component, message, data) {
150
146
  }
151
147
  catch (err) {
152
148
  // Avoid recursion via the logger; write directly to stderr.
153
- writeToStream("stderr", `[log.ts] subscriber threw: ${err instanceof Error ? err.message : String(err)}\n`);
149
+ writeToStream("stderr", `[log.ts] subscriber threw: ${sanitizeLogMessage(err instanceof Error ? err.message : String(err), secrets, budget)}\n`);
154
150
  }
155
151
  }
156
152
  // Stream form for humans tailing the process. The data field is
@@ -0,0 +1,67 @@
1
+ import { mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { tmpdir } from "node:os";
4
+ import { execFileSync } from "node:child_process";
5
+ export const MANAGED_INSTALLER_URL = "https://tmux-ide.com/install.sh";
6
+ /** Only the active release of a marked installation may update its prefix. */
7
+ export function managedInstallerPrefix(cliPath) {
8
+ const match = /^(.*)\/share\/tmux-ide\/releases\/(install-[A-Za-z0-9]{6})\/npm\/lib\/node_modules\/tmux-ide\/bin$/.exec(cliPath);
9
+ if (!match)
10
+ return null;
11
+ const prefix = match[1] || "/";
12
+ const root = join(prefix, "share/tmux-ide");
13
+ const release = join(root, "releases", match[2]);
14
+ try {
15
+ if (realpathSync(join(root, "current")) !== release)
16
+ return null;
17
+ if (realpathSync(release) !== release)
18
+ return null;
19
+ if (readFileSync(join(root, "installer-v1"), "utf8") !== "1\n")
20
+ return null;
21
+ if (readFileSync(join(release, ".installer-release-v1"), "utf8") !== "1\n")
22
+ return null;
23
+ if (!readFileSync(join(prefix, "bin/tmux-ide"), "utf8")
24
+ .split("\n")
25
+ .includes("# tmux-ide universal installer v1"))
26
+ return null;
27
+ if (JSON.parse(readFileSync(join(dirname(cliPath), "package.json"), "utf8")).name !== "tmux-ide")
28
+ return null;
29
+ return prefix;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ /** Download completely before execution; the canonical installer owns activation and rollback. */
36
+ export function runManagedInstallerUpdate(prefix, channel, json, execute = execFileSync) {
37
+ const temporary = mkdtempSync(join(tmpdir(), "tmux-ide-update-"));
38
+ const script = join(temporary, "install.sh");
39
+ try {
40
+ execute("curl", [
41
+ "--fail",
42
+ "--location",
43
+ "--silent",
44
+ "--show-error",
45
+ "--proto",
46
+ "=https",
47
+ "--proto-redir",
48
+ "=https",
49
+ "--connect-timeout",
50
+ "15",
51
+ "--max-time",
52
+ "180",
53
+ "--output",
54
+ script,
55
+ MANAGED_INSTALLER_URL,
56
+ ], { stdio: ["ignore", 2, 2] });
57
+ if (!readFileSync(script, "utf8").startsWith("#!/bin/sh\n"))
58
+ throw new Error("Installer download was not a shell script; installation unchanged");
59
+ execute("/bin/sh", ["-n", script], { stdio: ["ignore", 2, 2] });
60
+ execute("/bin/sh", [script, "--prefix", prefix, "--version", channel], {
61
+ stdio: json ? ["ignore", 2, 2] : "inherit",
62
+ });
63
+ }
64
+ finally {
65
+ rmSync(temporary, { recursive: true, force: true });
66
+ }
67
+ }
@@ -10,7 +10,7 @@ function trusted(path, directory) {
10
10
  value.uid !== process.getuid?.() ||
11
11
  (value.mode & 0o777) !== (directory ? 0o700 : 0o600) ||
12
12
  (!directory && value.nlink !== 1))
13
- throw new Error("unsafe TUI download lock");
13
+ throw new Error("unsafe private operation lock");
14
14
  return value;
15
15
  }
16
16
  function readOwner(path) {
@@ -19,16 +19,16 @@ function readOwner(path) {
19
19
  try {
20
20
  const actual = fstatSync(fd);
21
21
  if (actual.ino !== before.ino || actual.dev !== before.dev || actual.size > 32) {
22
- throw new Error("unsafe TUI download lock");
22
+ throw new Error("unsafe private operation lock");
23
23
  }
24
24
  const bytes = Buffer.alloc(33);
25
25
  const length = readSync(fd, bytes, 0, bytes.length, 0);
26
26
  const text = bytes.subarray(0, length).toString("utf8");
27
27
  if (!/^[1-9][0-9]*\n$/.test(text))
28
- throw new Error("invalid TUI download lock owner");
28
+ throw new Error("invalid private operation lock owner");
29
29
  const pid = Number(text.trim());
30
30
  if (!Number.isSafeInteger(pid) || pid > 2147483647)
31
- throw new Error("invalid TUI download lock owner");
31
+ throw new Error("invalid private operation lock owner");
32
32
  return { pid, ino: actual.ino, dev: actual.dev };
33
33
  }
34
34
  finally {
@@ -76,7 +76,7 @@ function cleanStaging(path, name, expected, failed) {
76
76
  try {
77
77
  const current = trusted(path, true);
78
78
  if (current.ino !== expected.ino || current.dev !== expected.dev)
79
- throw new Error("TUI download staging lock changed");
79
+ throw new Error("private operation staging lock changed");
80
80
  try {
81
81
  unlinkSync(join(path, name));
82
82
  }
@@ -93,7 +93,7 @@ function cleanStaging(path, name, expected, failed) {
93
93
  }
94
94
  }
95
95
  /** Publish a populated private directory atomically; age never proves owner death. */
96
- export async function acquireTuiDownloadLock(lock, waitMs) {
96
+ export async function acquirePrivateOperationLock(lock, waitMs) {
97
97
  const deadline = Date.now() + waitMs;
98
98
  const nonce = randomUUID();
99
99
  const name = `owner-${nonce}`;
@@ -107,7 +107,7 @@ export async function acquireTuiDownloadLock(lock, waitMs) {
107
107
  writeFileSync(join(staging, name), `${process.pid}\n`, { flag: "wx", mode: 0o600 });
108
108
  while (true) {
109
109
  if (attempted && Date.now() >= deadline)
110
- throw new Error("timed out waiting for another TUI download");
110
+ throw new Error("timed out waiting for another private operation");
111
111
  attempted = true;
112
112
  try {
113
113
  const existing = lstatSync(lock);
@@ -152,7 +152,7 @@ export async function acquireTuiDownloadLock(lock, waitMs) {
152
152
  if (names.length !== 1 || !/^owner-[0-9a-f-]{36}$/.test(names[0])) {
153
153
  // Empty is only a retirement gap; the next atomic rename can claim it.
154
154
  if (names.length !== 0)
155
- throw new Error("invalid TUI download lock inventory");
155
+ throw new Error("invalid private operation lock inventory");
156
156
  }
157
157
  else {
158
158
  const owner = readOwner(join(lock, names[0]));
@@ -161,7 +161,7 @@ export async function acquireTuiDownloadLock(lock, waitMs) {
161
161
  if (current.ino !== owner.ino ||
162
162
  current.dev !== owner.dev ||
163
163
  current.pid !== owner.pid) {
164
- throw new Error("TUI download lock changed");
164
+ throw new Error("private operation lock changed");
165
165
  }
166
166
  retire(lock, names[0], entry);
167
167
  }
@@ -178,7 +178,7 @@ export async function acquireTuiDownloadLock(lock, waitMs) {
178
178
  if (current.ino !== owner.ino ||
179
179
  current.dev !== owner.dev ||
180
180
  current.pid !== owner.pid) {
181
- throw new Error("TUI download lock changed");
181
+ throw new Error("private operation lock changed");
182
182
  }
183
183
  try {
184
184
  unlinkSync(lock);
@@ -195,7 +195,7 @@ export async function acquireTuiDownloadLock(lock, waitMs) {
195
195
  throw error;
196
196
  }
197
197
  if (Date.now() >= deadline)
198
- throw new Error("timed out waiting for another TUI download");
198
+ throw new Error("timed out waiting for another private operation");
199
199
  await new Promise((resolve) => setTimeout(resolve, Math.min(50, Math.max(0, deadline - Date.now()))));
200
200
  }
201
201
  }
@@ -1,3 +1,4 @@
1
+ import { DAEMON_WIRE_PROTOCOL_VERSION } from "@tmux-ide/contracts";
1
2
  import { canonicalDaemonUrl, isCanonicalDaemonAlive, readCanonicalDaemonInfo, } from "./canonical-daemon.js";
2
3
  import { probeSshDaemonIdentity, RemoteDaemonHandshakeSchema, RemoteDaemonHandshakeFailureSchema, } from "./ssh-daemon-transport.js";
3
4
  class RemoteDaemonInfoError extends Error {
@@ -13,8 +14,8 @@ export async function readRemoteDaemonHandshake(dependencies = {}) {
13
14
  if (!info || !(await (dependencies.isAlive ?? isCanonicalDaemonAlive)(info)))
14
15
  throw new RemoteDaemonInfoError("No running tmux-ide daemon. Start tmux-ide on this machine first.", "daemon-missing");
15
16
  const parsed = RemoteDaemonHandshakeSchema.safeParse({ version: 1, daemon: info });
16
- if (!parsed.success)
17
- throw new RemoteDaemonInfoError("The daemon does not support authenticated SSH discovery.", "incompatible");
17
+ if (!parsed.success || info.protocolVersion !== DAEMON_WIRE_PROTOCOL_VERSION)
18
+ throw new RemoteDaemonInfoError("The daemon does not support this authenticated SSH discovery protocol.", "incompatible");
18
19
  try {
19
20
  if (!(await probeSshDaemonIdentity(canonicalDaemonUrl("http", info.bindHostname, info.port), parsed.data.daemon, AbortSignal.timeout(3_000), dependencies.request ?? fetch)))
20
21
  throw new Error("Identity changed");
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { SavedMachineSchema } from "@tmux-ide/contracts/saved-machines";
3
+ import { remoteTmuxIdeCommand } from "./remote-tmux-command.js";
3
4
  /** Only explicit user actions call this; catalog discovery and retries never do. */
4
5
  export async function startInstalledRemoteDaemon(alias, options = {}) {
5
6
  SavedMachineSchema.shape.sshTarget.parse(alias);
@@ -16,12 +17,11 @@ export async function startInstalledRemoteDaemon(alias, options = {}) {
16
17
  "BatchMode=yes",
17
18
  "-o",
18
19
  "ConnectTimeout=10",
20
+ "-o",
21
+ "ForkAfterAuthentication=no",
19
22
  "--",
20
23
  alias,
21
- "tmux-ide",
22
- "update",
23
- "--daemon",
24
- "--json",
24
+ remoteTmuxIdeCommand("start"),
25
25
  ], { stdio: ["ignore", "pipe", "pipe"] });
26
26
  let settled = false;
27
27
  let force;
@@ -0,0 +1,10 @@
1
+ import { shellEscape } from "./shell.js";
2
+ /** Fixed commands only. SSH normally omits interactive shell PATH setup. */
3
+ export function remoteTmuxIdeCommand(operation) {
4
+ const args = operation === "discover" ? "remote-daemon-info --json" : "update --daemon --json";
5
+ // Respect the host's existing installation preference, then try the installer
6
+ // and common package-manager prefixes. Never source shell profiles.
7
+ const script = 'export PATH="${PATH:-/usr/bin:/bin}:$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/home/linuxbrew/.linuxbrew/bin:$HOME/.npm-global/bin"; ' +
8
+ `exec tmux-ide ${args}`;
9
+ return `/bin/sh -c ${shellEscape(script)}`;
10
+ }
@@ -37,35 +37,36 @@ function absolutePath(value, cwd, key) {
37
37
  throw new TypeError(`${key} must resolve to an absolute path`);
38
38
  return path;
39
39
  }
40
- function pathEntryExists(path) {
40
+ function pathEntryStat(path) {
41
41
  try {
42
- lstatSync(path);
43
- return true;
42
+ return lstatSync(path);
44
43
  }
45
44
  catch (error) {
46
45
  if (error.code === "ENOENT")
47
- return false;
46
+ return undefined;
48
47
  throw error;
49
48
  }
50
49
  }
51
50
  function pathIdentity(path) {
52
51
  let cursor = resolve(path);
53
52
  const suffix = [];
54
- while (!pathEntryExists(cursor)) {
53
+ let stat = pathEntryStat(cursor);
54
+ while (!stat) {
55
55
  const parent = dirname(cursor);
56
56
  if (parent === cursor)
57
57
  break;
58
58
  suffix.unshift(basename(cursor));
59
59
  cursor = parent;
60
+ stat = pathEntryStat(cursor);
60
61
  }
61
- if (pathEntryExists(cursor) && lstatSync(cursor).isSocket()) {
62
+ if (stat?.isSocket()) {
62
63
  return resolve(captureUnixSocketIdentity(cursor).path, ...suffix);
63
64
  }
64
- return resolve(pathEntryExists(cursor) ? realpathSync(cursor) : cursor, ...suffix);
65
+ return resolve(stat ? realpathSync(cursor) : cursor, ...suffix);
65
66
  }
66
- function isInsideOrEqual(path, parent) {
67
- const child = pathIdentity(path);
68
- const root = pathIdentity(parent);
67
+ function isInsideOrEqual(path, parent, identity = pathIdentity) {
68
+ const child = identity(path);
69
+ const root = identity(parent);
69
70
  const offset = relative(root, child);
70
71
  return offset === "" || (!offset.startsWith(`..${sep}`) && offset !== "..");
71
72
  }
@@ -77,6 +78,18 @@ function runtimeMode(env) {
77
78
  return raw;
78
79
  }
79
80
  export function resolveRuntimeNamespace(options = {}) {
81
+ // Reuse a path's identity only within this synchronous resolution. Every
82
+ // subsequent resolution must see new ancestors and replaced symlinks.
83
+ let identities;
84
+ const identity = (path) => {
85
+ const cached = identities?.get(path);
86
+ if (cached !== undefined)
87
+ return cached;
88
+ const resolved = pathIdentity(path);
89
+ (identities ??= new Map()).set(path, resolved);
90
+ return resolved;
91
+ };
92
+ const within = (path, parent) => isInsideOrEqual(path, parent, identity);
80
93
  const env = options.env ?? process.env;
81
94
  const userHome = options.userHome ?? homedir();
82
95
  const cwd = options.cwd ?? process.cwd();
@@ -141,14 +154,13 @@ export function resolveRuntimeNamespace(options = {}) {
141
154
  if (isolated && tmuxSocket.kind === "name" && tmuxSocket.name === "default") {
142
155
  throw new TypeError(`${mode} runtime requires a non-default ${TMUX_SOCKET_NAME_ENV}`);
143
156
  }
144
- if (isolated && isInsideOrEqual(stateHome, canonicalHome)) {
157
+ if (isolated && within(stateHome, canonicalHome)) {
145
158
  throw new TypeError(`${mode} runtime cannot use the canonical tmux-ide state home`);
146
159
  }
147
- if (isolated &&
148
- (isInsideOrEqual(registryDir, canonicalHome) || isInsideOrEqual(daemonInfoDir, canonicalHome))) {
160
+ if (isolated && (within(registryDir, canonicalHome) || within(daemonInfoDir, canonicalHome))) {
149
161
  throw new TypeError(`${mode} runtime cannot use canonical registry or daemon state`);
150
162
  }
151
- if (isolated && tmuxSocket.kind === "path" && isInsideOrEqual(tmuxSocket.path, canonicalHome)) {
163
+ if (isolated && tmuxSocket.kind === "path" && within(tmuxSocket.path, canonicalHome)) {
152
164
  throw new TypeError(`${mode} runtime cannot use a tmux socket inside canonical state`);
153
165
  }
154
166
  if (isolated && cleanupToken === null) {
@@ -161,7 +173,7 @@ export function resolveRuntimeNamespace(options = {}) {
161
173
  const value = (key === "TMUX_IDE_CONFIG" || key === "TMUX_IDE_SETTINGS_DIR"
162
174
  ? env[key]
163
175
  : nonEmpty(env, key)) ?? fallback;
164
- if (development && (!isAbsolute(value) || !isInsideOrEqual(value, development.root)))
176
+ if (development && (!isAbsolute(value) || !within(value, development.root)))
165
177
  throw new TypeError(`development ${key} escapes instance`);
166
178
  return value;
167
179
  };
@@ -191,7 +203,7 @@ export function resolveRuntimeNamespace(options = {}) {
191
203
  scoped(key, "");
192
204
  if (env.TMUX) {
193
205
  const socket = /^(.*),[0-9]+,[0-9]+$/u.exec(env.TMUX)?.[1];
194
- if (!socket || pathIdentity(socket) !== pathIdentity(join(runtimeDir, "tmux.sock")))
206
+ if (!socket || identity(socket) !== identity(join(runtimeDir, "tmux.sock")))
195
207
  throw new TypeError("development rejects inherited foreign TMUX authority");
196
208
  }
197
209
  }