tmux-ide 2.9.2 → 2.9.3

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