@team-agent/installer 0.2.11 → 0.3.0

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 (326) hide show
  1. package/Cargo.lock +744 -0
  2. package/Cargo.toml +34 -0
  3. package/crates/team-agent/Cargo.toml +33 -0
  4. package/crates/team-agent/src/cli/adapters.rs +1343 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +554 -0
  6. package/crates/team-agent/src/cli/emit.rs +1077 -0
  7. package/crates/team-agent/src/cli/helpers.rs +88 -0
  8. package/crates/team-agent/src/cli/leader.rs +216 -0
  9. package/crates/team-agent/src/cli/mod.rs +1141 -0
  10. package/crates/team-agent/src/cli/profile.rs +306 -0
  11. package/crates/team-agent/src/cli/send.rs +215 -0
  12. package/crates/team-agent/src/cli/status.rs +179 -0
  13. package/crates/team-agent/src/cli/status_port.rs +502 -0
  14. package/crates/team-agent/src/cli/tests/base.rs +616 -0
  15. package/crates/team-agent/src/cli/tests/compile.rs +96 -0
  16. package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
  21. package/crates/team-agent/src/cli/tests/mod.rs +97 -0
  22. package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
  23. package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
  25. package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
  26. package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
  27. package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
  28. package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
  29. package/crates/team-agent/src/cli/types.rs +605 -0
  30. package/crates/team-agent/src/compiler/tests.rs +701 -0
  31. package/crates/team-agent/src/compiler.rs +489 -0
  32. package/crates/team-agent/src/coordinator/backoff.rs +153 -0
  33. package/crates/team-agent/src/coordinator/health.rs +436 -0
  34. package/crates/team-agent/src/coordinator/mod.rs +80 -0
  35. package/crates/team-agent/src/coordinator/orphan.rs +179 -0
  36. package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
  37. package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
  38. package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
  39. package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
  40. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
  41. package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
  42. package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
  43. package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
  44. package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
  45. package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
  46. package/crates/team-agent/src/coordinator/tick.rs +2032 -0
  47. package/crates/team-agent/src/coordinator/types.rs +584 -0
  48. package/crates/team-agent/src/db/migration.rs +716 -0
  49. package/crates/team-agent/src/db/mod.rs +23 -0
  50. package/crates/team-agent/src/db/schema.rs +378 -0
  51. package/crates/team-agent/src/event_log.rs +375 -0
  52. package/crates/team-agent/src/fake_worker.rs +253 -0
  53. package/crates/team-agent/src/leader/helpers.rs +190 -0
  54. package/crates/team-agent/src/leader/inject.rs +33 -0
  55. package/crates/team-agent/src/leader/lease.rs +1063 -0
  56. package/crates/team-agent/src/leader/mod.rs +99 -0
  57. package/crates/team-agent/src/leader/owner_bind.rs +292 -0
  58. package/crates/team-agent/src/leader/rediscover/tests.rs +525 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1099 -0
  60. package/crates/team-agent/src/leader/start.rs +273 -0
  61. package/crates/team-agent/src/leader/takeover.rs +235 -0
  62. package/crates/team-agent/src/leader/tests/basics.rs +183 -0
  63. package/crates/team-agent/src/leader/tests/byte_findings.rs +234 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +271 -0
  66. package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
  67. package/crates/team-agent/src/leader/tests/lease_claim.rs +253 -0
  68. package/crates/team-agent/src/leader/tests/mod.rs +125 -0
  69. package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
  70. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
  71. package/crates/team-agent/src/leader/types.rs +487 -0
  72. package/crates/team-agent/src/lib.rs +85 -0
  73. package/crates/team-agent/src/lifecycle/display.rs +228 -0
  74. package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
  75. package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
  76. package/crates/team-agent/src/lifecycle/launch.rs +1833 -0
  77. package/crates/team-agent/src/lifecycle/mod.rs +62 -0
  78. package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
  79. package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
  80. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
  81. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
  82. package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
  83. package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
  84. package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
  85. package/crates/team-agent/src/lifecycle/restart.rs +76 -0
  86. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
  88. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
  89. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +933 -0
  90. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
  91. package/crates/team-agent/src/lifecycle/tests.rs +27 -0
  92. package/crates/team-agent/src/lifecycle/types.rs +685 -0
  93. package/crates/team-agent/src/main.rs +41 -0
  94. package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
  95. package/crates/team-agent/src/mcp_server/mod.rs +183 -0
  96. package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
  97. package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
  98. package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
  99. package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
  100. package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
  101. package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
  102. package/crates/team-agent/src/mcp_server/tests/wire.rs +159 -0
  103. package/crates/team-agent/src/mcp_server/tests.rs +38 -0
  104. package/crates/team-agent/src/mcp_server/tools.rs +603 -0
  105. package/crates/team-agent/src/mcp_server/types.rs +421 -0
  106. package/crates/team-agent/src/mcp_server/wire.rs +388 -0
  107. package/crates/team-agent/src/message_store.rs +767 -0
  108. package/crates/team-agent/src/messaging/activity.rs +433 -0
  109. package/crates/team-agent/src/messaging/delivery.rs +542 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +340 -0
  112. package/crates/team-agent/src/messaging/mod.rs +147 -0
  113. package/crates/team-agent/src/messaging/peers.rs +32 -0
  114. package/crates/team-agent/src/messaging/results.rs +537 -0
  115. package/crates/team-agent/src/messaging/scheduler.rs +344 -0
  116. package/crates/team-agent/src/messaging/selftest.rs +100 -0
  117. package/crates/team-agent/src/messaging/send.rs +582 -0
  118. package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
  119. package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
  121. package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
  122. package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
  123. package/crates/team-agent/src/messaging/trust.rs +192 -0
  124. package/crates/team-agent/src/messaging/types.rs +355 -0
  125. package/crates/team-agent/src/messaging/watchers.rs +591 -0
  126. package/crates/team-agent/src/model/enums.rs +311 -0
  127. package/crates/team-agent/src/model/errors.rs +17 -0
  128. package/crates/team-agent/src/model/ids.rs +155 -0
  129. package/crates/team-agent/src/model/mod.rs +22 -0
  130. package/crates/team-agent/src/model/paths.rs +228 -0
  131. package/crates/team-agent/src/model/permissions.rs +567 -0
  132. package/crates/team-agent/src/model/routing.rs +340 -0
  133. package/crates/team-agent/src/model/spec.rs +680 -0
  134. package/crates/team-agent/src/model/task_graph.rs +380 -0
  135. package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
  136. package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
  137. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
  138. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
  139. package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
  140. package/crates/team-agent/src/model/yaml/tests.rs +288 -0
  141. package/crates/team-agent/src/model/yaml.rs +800 -0
  142. package/crates/team-agent/src/packaging/install.rs +305 -0
  143. package/crates/team-agent/src/packaging/migrate.rs +30 -0
  144. package/crates/team-agent/src/packaging/mod.rs +82 -0
  145. package/crates/team-agent/src/packaging/repair.rs +24 -0
  146. package/crates/team-agent/src/packaging/tests.rs +829 -0
  147. package/crates/team-agent/src/packaging/types.rs +369 -0
  148. package/crates/team-agent/src/provider/adapter.rs +801 -0
  149. package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
  150. package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
  151. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
  152. package/crates/team-agent/src/provider/classify.rs +456 -0
  153. package/crates/team-agent/src/provider/faults.rs +136 -0
  154. package/crates/team-agent/src/provider/helpers.rs +41 -0
  155. package/crates/team-agent/src/provider/mod.rs +53 -0
  156. package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
  157. package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
  158. package/crates/team-agent/src/provider/tests/classify.rs +240 -0
  159. package/crates/team-agent/src/provider/tests/faults.rs +120 -0
  160. package/crates/team-agent/src/provider/tests/idle.rs +208 -0
  161. package/crates/team-agent/src/provider/tests/wire.rs +213 -0
  162. package/crates/team-agent/src/provider/tests.rs +31 -0
  163. package/crates/team-agent/src/provider/types.rs +424 -0
  164. package/crates/team-agent/src/state/identity.rs +656 -0
  165. package/crates/team-agent/src/state/mod.rs +58 -0
  166. package/crates/team-agent/src/state/owner_gate.rs +423 -0
  167. package/crates/team-agent/src/state/persist.rs +712 -0
  168. package/crates/team-agent/src/state/projection.rs +657 -0
  169. package/crates/team-agent/src/state/selector.rs +105 -0
  170. package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
  171. package/crates/team-agent/src/tmux_backend/tests.rs +586 -0
  172. package/crates/team-agent/src/tmux_backend.rs +758 -0
  173. package/crates/team-agent/src/transport/test_support.rs +252 -0
  174. package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
  175. package/crates/team-agent/src/transport/tests/mod.rs +199 -0
  176. package/crates/team-agent/src/transport/tests/wire.rs +527 -0
  177. package/crates/team-agent/src/transport.rs +774 -0
  178. package/npm/install.mjs +90 -106
  179. package/package.json +15 -13
  180. package/crates/team-agent-core/Cargo.toml +0 -12
  181. package/crates/team-agent-core/src/lib.rs +0 -332
  182. package/crates/team-agent-core/src/main.rs +0 -152
  183. package/pyproject.toml +0 -18
  184. package/scripts/install.py +0 -88
  185. package/scripts/run_regression_tests.py +0 -83
  186. package/src/team_agent/__init__.py +0 -3
  187. package/src/team_agent/__main__.py +0 -5
  188. package/src/team_agent/_legacy_pane_discovery.py +0 -186
  189. package/src/team_agent/abnormal_track.py +0 -253
  190. package/src/team_agent/approvals/__init__.py +0 -65
  191. package/src/team_agent/approvals/constants.py +0 -6
  192. package/src/team_agent/approvals/parsing.py +0 -176
  193. package/src/team_agent/approvals/runtime_prompts.py +0 -171
  194. package/src/team_agent/approvals/status.py +0 -176
  195. package/src/team_agent/cli/__init__.py +0 -137
  196. package/src/team_agent/cli/commands.py +0 -481
  197. package/src/team_agent/cli/e2e.py +0 -202
  198. package/src/team_agent/cli/helpers.py +0 -226
  199. package/src/team_agent/cli/parser.py +0 -540
  200. package/src/team_agent/compiler.py +0 -334
  201. package/src/team_agent/coordinator/__init__.py +0 -53
  202. package/src/team_agent/coordinator/__main__.py +0 -119
  203. package/src/team_agent/coordinator/lifecycle.py +0 -411
  204. package/src/team_agent/coordinator/metadata.py +0 -61
  205. package/src/team_agent/coordinator/paths.py +0 -17
  206. package/src/team_agent/diagnose/__init__.py +0 -48
  207. package/src/team_agent/diagnose/checks.py +0 -101
  208. package/src/team_agent/diagnose/comms.py +0 -213
  209. package/src/team_agent/diagnose/health.py +0 -241
  210. package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
  211. package/src/team_agent/diagnose/preflight.py +0 -194
  212. package/src/team_agent/diagnose/quick_start.py +0 -324
  213. package/src/team_agent/display/__init__.py +0 -92
  214. package/src/team_agent/display/adaptive.py +0 -511
  215. package/src/team_agent/display/backend.py +0 -46
  216. package/src/team_agent/display/close.py +0 -154
  217. package/src/team_agent/display/ghostty.py +0 -77
  218. package/src/team_agent/display/rebuild.py +0 -102
  219. package/src/team_agent/display/tiling.py +0 -156
  220. package/src/team_agent/display/worker_window.py +0 -114
  221. package/src/team_agent/display/workspace.py +0 -382
  222. package/src/team_agent/errors.py +0 -10
  223. package/src/team_agent/events.py +0 -84
  224. package/src/team_agent/fake_worker.py +0 -80
  225. package/src/team_agent/idle_predicate.py +0 -218
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -114
  228. package/src/team_agent/launch/__init__.py +0 -41
  229. package/src/team_agent/launch/bootstrap.py +0 -85
  230. package/src/team_agent/launch/config.py +0 -106
  231. package/src/team_agent/launch/core.py +0 -301
  232. package/src/team_agent/launch/requirements.py +0 -57
  233. package/src/team_agent/leader/__init__.py +0 -926
  234. package/src/team_agent/leader_binding.py +0 -183
  235. package/src/team_agent/lifecycle/__init__.py +0 -5
  236. package/src/team_agent/lifecycle/agents.py +0 -278
  237. package/src/team_agent/lifecycle/operations.py +0 -411
  238. package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
  239. package/src/team_agent/lifecycle/start.py +0 -363
  240. package/src/team_agent/mcp_server/__init__.py +0 -42
  241. package/src/team_agent/mcp_server/__main__.py +0 -7
  242. package/src/team_agent/mcp_server/contracts.py +0 -148
  243. package/src/team_agent/mcp_server/normalize.py +0 -257
  244. package/src/team_agent/mcp_server/server.py +0 -150
  245. package/src/team_agent/mcp_server/tools.py +0 -352
  246. package/src/team_agent/message_store/__init__.py +0 -23
  247. package/src/team_agent/message_store/agent_health.py +0 -113
  248. package/src/team_agent/message_store/core.py +0 -497
  249. package/src/team_agent/message_store/leader_notification_log.py +0 -198
  250. package/src/team_agent/message_store/result_watchers.py +0 -251
  251. package/src/team_agent/message_store/schema.py +0 -308
  252. package/src/team_agent/message_store/schema_migration.py +0 -448
  253. package/src/team_agent/messaging/__init__.py +0 -1
  254. package/src/team_agent/messaging/activity_detector.py +0 -262
  255. package/src/team_agent/messaging/delivery.py +0 -504
  256. package/src/team_agent/messaging/deps.py +0 -247
  257. package/src/team_agent/messaging/idle_alerts.py +0 -423
  258. package/src/team_agent/messaging/internal_delivery.py +0 -46
  259. package/src/team_agent/messaging/leader.py +0 -497
  260. package/src/team_agent/messaging/leader_api_errors.py +0 -216
  261. package/src/team_agent/messaging/leader_panes.py +0 -673
  262. package/src/team_agent/messaging/owner_bypass.py +0 -29
  263. package/src/team_agent/messaging/result_delivery.py +0 -539
  264. package/src/team_agent/messaging/results.py +0 -447
  265. package/src/team_agent/messaging/scheduler.py +0 -450
  266. package/src/team_agent/messaging/send.py +0 -532
  267. package/src/team_agent/messaging/session_drift.py +0 -94
  268. package/src/team_agent/messaging/tmux_io.py +0 -506
  269. package/src/team_agent/messaging/tmux_prompt.py +0 -338
  270. package/src/team_agent/messaging/trust_auto_answer.py +0 -52
  271. package/src/team_agent/orchestrator/__init__.py +0 -376
  272. package/src/team_agent/orchestrator/plan.py +0 -122
  273. package/src/team_agent/orchestrator/state.py +0 -128
  274. package/src/team_agent/paths.py +0 -45
  275. package/src/team_agent/permissions.py +0 -123
  276. package/src/team_agent/profiles/__init__.py +0 -82
  277. package/src/team_agent/profiles/constants.py +0 -19
  278. package/src/team_agent/profiles/core.py +0 -407
  279. package/src/team_agent/profiles/helpers.py +0 -69
  280. package/src/team_agent/profiles/provider_env.py +0 -188
  281. package/src/team_agent/profiles/smoke.py +0 -201
  282. package/src/team_agent/provider_cli/__init__.py +0 -43
  283. package/src/team_agent/provider_cli/adapter.py +0 -172
  284. package/src/team_agent/provider_cli/base.py +0 -48
  285. package/src/team_agent/provider_cli/claude.py +0 -503
  286. package/src/team_agent/provider_cli/codex.py +0 -336
  287. package/src/team_agent/provider_cli/copilot.py +0 -8
  288. package/src/team_agent/provider_cli/fake.py +0 -39
  289. package/src/team_agent/provider_cli/gemini.py +0 -95
  290. package/src/team_agent/provider_cli/opencode.py +0 -8
  291. package/src/team_agent/provider_cli/prompt.py +0 -62
  292. package/src/team_agent/provider_cli/registry.py +0 -18
  293. package/src/team_agent/provider_cli/unsupported.py +0 -32
  294. package/src/team_agent/provider_state/README.md +0 -78
  295. package/src/team_agent/provider_state/__init__.py +0 -91
  296. package/src/team_agent/provider_state/claude.py +0 -86
  297. package/src/team_agent/provider_state/codex.py +0 -84
  298. package/src/team_agent/provider_state/common.py +0 -207
  299. package/src/team_agent/provider_state/registry.py +0 -118
  300. package/src/team_agent/providers.py +0 -163
  301. package/src/team_agent/quality_gates.py +0 -104
  302. package/src/team_agent/restart/__init__.py +0 -34
  303. package/src/team_agent/restart/orchestration.py +0 -554
  304. package/src/team_agent/restart/selection.py +0 -89
  305. package/src/team_agent/restart/snapshot.py +0 -70
  306. package/src/team_agent/routing.py +0 -84
  307. package/src/team_agent/runtime.py +0 -1243
  308. package/src/team_agent/rust_core.py +0 -327
  309. package/src/team_agent/sessions/__init__.py +0 -25
  310. package/src/team_agent/sessions/capture.py +0 -144
  311. package/src/team_agent/sessions/inventory.py +0 -44
  312. package/src/team_agent/sessions/resume.py +0 -135
  313. package/src/team_agent/simple_yaml.py +0 -236
  314. package/src/team_agent/spec.py +0 -370
  315. package/src/team_agent/state.py +0 -693
  316. package/src/team_agent/status/__init__.py +0 -63
  317. package/src/team_agent/status/approvals.py +0 -52
  318. package/src/team_agent/status/compact.py +0 -158
  319. package/src/team_agent/status/constants.py +0 -18
  320. package/src/team_agent/status/inbox.py +0 -58
  321. package/src/team_agent/status/peek.py +0 -117
  322. package/src/team_agent/status/queries.py +0 -199
  323. package/src/team_agent/task_graph.py +0 -80
  324. package/src/team_agent/terminal.py +0 -57
  325. package/src/team_agent/wake.py +0 -58
  326. package/src/team_agent/watch/__init__.py +0 -145
@@ -0,0 +1,758 @@
1
+ //! Concrete tmux `Transport` backend (SKELETON) — the real executor that runs `tmux <argv>`.
2
+ //!
3
+ //! step 9 shipped the [`crate::transport::Transport`] trait + the pure tmux argv-builders
4
+ //! (`tmux_spawn_argv`, `tmux_capture_argv`, `tmux_send_keys_argv`, `tmux_inject_text_argv`,
5
+ //! `tmux_query_argv`, `tmux_cancel_mode_argv`) but NO concrete backend that actually executes them.
6
+ //! This is that backend: each `Transport` method builds its argv via those builders, runs it through
7
+ //! a [`CommandRunner`] seam, and parses the tmux output into the trait's typed return.
8
+ //!
9
+ //! THE SEAM: [`CommandRunner`] is the single OS edge. [`RealCommandRunner`] runs
10
+ //! `std::process::Command::new("tmux") …`; tests inject a recording/canned runner so the argv
11
+ //! construction + output parsing are unit-testable in-process, while the real subprocess execution
12
+ //! stays the `#[ignore]` real-machine boundary (acceptance framework).
13
+ //!
14
+ //! §10: the implementation must be panic-free (porter adds the deny + bodies; this skeleton is
15
+ //! `unimplemented!()`). MUST-NOT-13: a transport backend has no provider-client dependency.
16
+ #![allow(dead_code)]
17
+
18
+ use std::collections::BTreeMap;
19
+ use std::hash::{Hash, Hasher};
20
+ use std::io::{Read, Write};
21
+ use std::path::{Path, PathBuf};
22
+ use std::process::Stdio;
23
+ use std::time::{Duration, Instant};
24
+
25
+ use crate::model::enums::PaneLiveness;
26
+ use crate::transport::{
27
+ normalize_capture, tmux_capture_argv, tmux_empty_inject_argv, tmux_inject_text_argv,
28
+ tmux_query_argv, tmux_send_keys_argv, tmux_spawn_argv, AttachOutcome, BackendKind,
29
+ CaptureRange, CapturedText, InjectPayload, InjectReport, InjectStage, InjectVerification, Key,
30
+ PaneField, PaneId, PaneInfo, PaneMode, SessionName, SetEnvOutcome, SpawnResult, SubmitVerification,
31
+ Target, Transport, TransportError, TurnVerification, WindowName,
32
+ };
33
+
34
+ /// Result of running an external command — the typed output of the OS edge.
35
+ #[derive(Debug, Clone)]
36
+ pub struct CommandOutput {
37
+ /// process exit status was success (code 0).
38
+ pub success: bool,
39
+ /// exit code if the process exited normally.
40
+ pub code: Option<i32>,
41
+ pub stdout: String,
42
+ pub stderr: String,
43
+ }
44
+
45
+ /// The single OS-edge seam: run an argv vector and return its output.
46
+ /// Real impl spawns `std::process::Command`; tests inject canned/recording output so the
47
+ /// argv-construction + output-parsing of [`TmuxBackend`] is testable without a live tmux server.
48
+ pub trait CommandRunner: Send + Sync {
49
+ fn run(&self, argv: &[String]) -> Result<CommandOutput, std::io::Error>;
50
+
51
+ fn run_with_stdin(
52
+ &self,
53
+ argv: &[String],
54
+ stdin: &str,
55
+ ) -> Result<CommandOutput, std::io::Error> {
56
+ let _ = stdin;
57
+ self.run(argv)
58
+ }
59
+ }
60
+
61
+ /// Production runner: `std::process::Command::new(argv[0]).args(argv[1..]).output()`.
62
+ pub struct RealCommandRunner;
63
+
64
+ const COMMAND_TIMEOUT: Duration = Duration::from_secs(5);
65
+
66
+ impl CommandRunner for RealCommandRunner {
67
+ fn run(&self, argv: &[String]) -> Result<CommandOutput, std::io::Error> {
68
+ self.run_inner(argv, None)
69
+ }
70
+
71
+ fn run_with_stdin(
72
+ &self,
73
+ argv: &[String],
74
+ stdin: &str,
75
+ ) -> Result<CommandOutput, std::io::Error> {
76
+ self.run_inner(argv, Some(stdin))
77
+ }
78
+ }
79
+
80
+ impl RealCommandRunner {
81
+ fn run_inner(
82
+ &self,
83
+ argv: &[String],
84
+ stdin_text: Option<&str>,
85
+ ) -> Result<CommandOutput, std::io::Error> {
86
+ let Some(program) = argv.first() else {
87
+ return Err(std::io::Error::new(
88
+ std::io::ErrorKind::InvalidInput,
89
+ "empty argv",
90
+ ));
91
+ };
92
+ let mut child = std::process::Command::new(program)
93
+ .args(argv.iter().skip(1))
94
+ .stdin(if stdin_text.is_some() { Stdio::piped() } else { Stdio::null() })
95
+ .stdout(Stdio::piped())
96
+ .stderr(Stdio::piped())
97
+ .spawn()?;
98
+ if let Some(text) = stdin_text {
99
+ let mut stdin = child.stdin.take().ok_or_else(|| {
100
+ std::io::Error::other("stdin pipe missing")
101
+ })?;
102
+ stdin.write_all(text.as_bytes())?;
103
+ }
104
+ let stdout = child.stdout.take().ok_or_else(|| {
105
+ std::io::Error::other("stdout pipe missing")
106
+ })?;
107
+ let stderr = child.stderr.take().ok_or_else(|| {
108
+ std::io::Error::other("stderr pipe missing")
109
+ })?;
110
+ let stdout_thread = std::thread::spawn(move || read_pipe(stdout));
111
+ let stderr_thread = std::thread::spawn(move || read_pipe(stderr));
112
+ let deadline = Instant::now() + COMMAND_TIMEOUT;
113
+ let status = loop {
114
+ if let Some(status) = child.try_wait()? {
115
+ break status;
116
+ }
117
+ if Instant::now() >= deadline {
118
+ child.kill()?;
119
+ child.wait()?;
120
+ let _ = join_pipe_reader(stdout_thread)?;
121
+ let _ = join_pipe_reader(stderr_thread)?;
122
+ return Err(std::io::Error::new(
123
+ std::io::ErrorKind::TimedOut,
124
+ format!("{program} exceeded 5s timeout"),
125
+ ));
126
+ }
127
+ std::thread::sleep(Duration::from_millis(25));
128
+ };
129
+ let stdout = join_pipe_reader(stdout_thread)?;
130
+ let stderr = join_pipe_reader(stderr_thread)?;
131
+ Ok(CommandOutput {
132
+ success: status.success(),
133
+ code: status.code(),
134
+ stdout,
135
+ stderr,
136
+ })
137
+ }
138
+ }
139
+
140
+ fn read_pipe<R: Read>(mut reader: R) -> Result<String, std::io::Error> {
141
+ let mut bytes = Vec::new();
142
+ reader.read_to_end(&mut bytes)?;
143
+ Ok(String::from_utf8_lossy(&bytes).to_string())
144
+ }
145
+
146
+ fn join_pipe_reader(
147
+ handle: std::thread::JoinHandle<Result<String, std::io::Error>>,
148
+ ) -> Result<String, std::io::Error> {
149
+ handle
150
+ .join()
151
+ .map_err(|_| std::io::Error::other("pipe reader thread panicked"))?
152
+ }
153
+
154
+ /// Concrete tmux backend: builds argv via the `transport::tmux_*_argv` builders, runs them through
155
+ /// the [`CommandRunner`], and parses tmux output into the [`Transport`] typed returns.
156
+ ///
157
+ /// CP-1: a workspace-bound backend carries a PER-TEAM tmux socket (`socket = Some("ta-<hash>")`) so a
158
+ /// dying shared `default` server can no longer tear the team down. The socket is injected at the RUN
159
+ /// CHOKEPOINT ([`TmuxBackend::tmux_argv`]) — the `transport::tmux_*_argv` builders stay socket-free.
160
+ pub struct TmuxBackend {
161
+ runner: Box<dyn CommandRunner>,
162
+ /// `Some(name)` for a per-team socket -> every `tmux` argv gets `-L <name>` injected after the
163
+ /// leading "tmux" token; `None` (default) -> bare `tmux` on the shared default socket.
164
+ socket: Option<String>,
165
+ }
166
+
167
+ impl TmuxBackend {
168
+ /// Backend bound to the real `tmux` subprocess on the SHARED default socket (no `-L`).
169
+ /// Non-team callers + existing argv/unit tests stay unaffected.
170
+ pub fn new() -> Self {
171
+ Self { runner: Box::new(RealCommandRunner), socket: None }
172
+ }
173
+
174
+ /// CP-1 team backend: bound to the real `tmux` subprocess on a PER-WORKSPACE socket, derived
175
+ /// deterministically from the canonicalized workspace path so the leader CLI, the daemon, and
176
+ /// every later op (spawn / inject / has_session / kill) hit the SAME `tmux -L <socket>` server.
177
+ pub fn for_workspace(workspace: &Path) -> Self {
178
+ Self {
179
+ runner: Box::new(RealCommandRunner),
180
+ socket: Some(socket_name_for_workspace(workspace)),
181
+ }
182
+ }
183
+
184
+ /// Backend with an injected runner (tests: canned/recording tmux output). Shared default socket.
185
+ pub fn with_runner(runner: Box<dyn CommandRunner>) -> Self {
186
+ Self { runner, socket: None }
187
+ }
188
+
189
+ /// Backend with an injected runner bound to a per-workspace socket (tests: assert the `-L` is in
190
+ /// the recorded argv for a workspace-bound backend).
191
+ pub fn with_runner_for_workspace(runner: Box<dyn CommandRunner>, workspace: &Path) -> Self {
192
+ Self { runner, socket: Some(socket_name_for_workspace(workspace)) }
193
+ }
194
+
195
+ /// THE RUN CHOKEPOINT: every executed `tmux` argv is funneled through here. When a per-team
196
+ /// socket is set, inject `-L <socket>` right after the leading "tmux" token; otherwise pass argv
197
+ /// through unchanged. Non-`tmux` argv (e.g. the spawned provider command) is never rewritten.
198
+ fn tmux_argv(&self, argv: &[String]) -> Vec<String> {
199
+ match &self.socket {
200
+ Some(socket) if argv.first().map(String::as_str) == Some("tmux") => {
201
+ let mut out = Vec::with_capacity(argv.len() + 2);
202
+ out.push("tmux".to_string());
203
+ out.push("-L".to_string());
204
+ out.push(socket.clone());
205
+ out.extend(argv.iter().skip(1).cloned());
206
+ out
207
+ }
208
+ _ => argv.to_vec(),
209
+ }
210
+ }
211
+
212
+ /// `tmux -L <socket> kill-server` (CP-1 cleanup): best-effort teardown of the per-team server on
213
+ /// shutdown so per-team sockets do not orphan. No-op (and never errors) for a default-socket
214
+ /// backend, and a "no server" failure is ignored.
215
+ pub fn kill_server(&self) {
216
+ if self.socket.is_none() {
217
+ return;
218
+ }
219
+ let argv = self.tmux_argv(&[
220
+ "tmux".to_string(),
221
+ "kill-server".to_string(),
222
+ ]);
223
+ let _ = self.runner.run(&argv);
224
+ }
225
+ }
226
+
227
+ /// CP-1 socket name: SHORT + DETERMINISTIC per canonical workspace path. `ta-` + 12 hex chars of a
228
+ /// stable FNV-1a hash over the canonicalized path. AF_UNIX `sun_path` is ~104 chars and the socket
229
+ /// lives at `/tmp/tmux-<uid>/<name>`, so we must NOT use the (~88-char) session name. §10: a
230
+ /// canonicalize failure falls back to the raw path (never panics).
231
+ pub(crate) fn socket_name_for_workspace(workspace: &Path) -> String {
232
+ let canonical = workspace
233
+ .canonicalize()
234
+ .unwrap_or_else(|_| workspace.to_path_buf());
235
+ let mut hasher = Fnv1a::default();
236
+ canonical.as_os_str().hash(&mut hasher);
237
+ format!("ta-{:012x}", hasher.finish() & 0xffff_ffff_ffff)
238
+ }
239
+
240
+ /// Deterministic FNV-1a (64-bit) — std `DefaultHasher` is NOT stable across releases, so a fixed
241
+ /// FNV keeps the socket identical for the CLI, the daemon, and every later op on the same workspace.
242
+ struct Fnv1a(u64);
243
+
244
+ impl Default for Fnv1a {
245
+ fn default() -> Self {
246
+ Self(0xcbf2_9ce4_8422_2325)
247
+ }
248
+ }
249
+
250
+ impl Hasher for Fnv1a {
251
+ fn finish(&self) -> u64 {
252
+ self.0
253
+ }
254
+
255
+ fn write(&mut self, bytes: &[u8]) {
256
+ for &b in bytes {
257
+ self.0 ^= u64::from(b);
258
+ self.0 = self.0.wrapping_mul(0x0000_0100_0000_01b3);
259
+ }
260
+ }
261
+ }
262
+
263
+ impl Default for TmuxBackend {
264
+ fn default() -> Self {
265
+ Self::new()
266
+ }
267
+ }
268
+
269
+ impl TmuxBackend {
270
+ fn spawn(
271
+ &self,
272
+ session: &SessionName,
273
+ window: &WindowName,
274
+ argv: &[String],
275
+ cwd: &Path,
276
+ env: &BTreeMap<String, String>,
277
+ first: bool,
278
+ ) -> Result<SpawnResult, TransportError> {
279
+ let command = shell_command(argv, cwd, env);
280
+ let spawn_argv = tmux_spawn_argv(session, window, &command, first);
281
+ self.run_spawn(&spawn_argv)?;
282
+ let pane_argv = vec![
283
+ "tmux".to_string(),
284
+ "display-message".to_string(),
285
+ "-p".to_string(),
286
+ "-t".to_string(),
287
+ format!("{}:{}", session.as_str(), window.as_str()),
288
+ "#{pane_id}".to_string(),
289
+ ];
290
+ let output = self.run_spawn(&pane_argv)?;
291
+ let pane = output.stdout.trim();
292
+ let pane_id = if pane.is_empty() { "%0" } else { pane };
293
+ Ok(SpawnResult {
294
+ pane_id: PaneId::new(pane_id),
295
+ session: session.clone(),
296
+ window: window.clone(),
297
+ child_pid: None,
298
+ })
299
+ }
300
+
301
+ fn run_ok(&self, argv: &[String]) -> Result<(), TransportError> {
302
+ let argv = self.tmux_argv(argv);
303
+ let output = self.runner.run(&argv)?;
304
+ if output.success {
305
+ Ok(())
306
+ } else {
307
+ Err(subprocess_error(argv, output))
308
+ }
309
+ }
310
+
311
+ fn run_spawn(&self, argv: &[String]) -> Result<CommandOutput, TransportError> {
312
+ let argv = self.tmux_argv(argv);
313
+ let output = self.runner.run(&argv).map_err(|source| TransportError::Spawn {
314
+ backend: BackendKind::Tmux,
315
+ source,
316
+ })?;
317
+ if output.success {
318
+ Ok(output)
319
+ } else {
320
+ Err(subprocess_error(argv, output))
321
+ }
322
+ }
323
+
324
+ fn run_inject_stage(
325
+ &self,
326
+ argv: &[String],
327
+ stage: InjectStage,
328
+ ) -> Result<(), TransportError> {
329
+ let argv = self.tmux_argv(argv);
330
+ let output = self.runner.run(&argv).map_err(|source| TransportError::Inject {
331
+ stage,
332
+ source,
333
+ })?;
334
+ if output.success {
335
+ Ok(())
336
+ } else {
337
+ Err(subprocess_error(argv, output))
338
+ }
339
+ }
340
+
341
+ fn run_inject_stage_with_stdin(
342
+ &self,
343
+ argv: &[String],
344
+ stage: InjectStage,
345
+ stdin: &str,
346
+ ) -> Result<(), TransportError> {
347
+ let argv = self.tmux_argv(argv);
348
+ let output = self.runner.run_with_stdin(&argv, stdin).map_err(|source| TransportError::Inject {
349
+ stage,
350
+ source,
351
+ })?;
352
+ if output.success {
353
+ Ok(())
354
+ } else {
355
+ Err(subprocess_error(argv, output))
356
+ }
357
+ }
358
+ }
359
+
360
+ fn subprocess_error(argv: Vec<String>, output: CommandOutput) -> TransportError {
361
+ TransportError::Subprocess {
362
+ argv,
363
+ code: output.code,
364
+ stderr: output.stderr,
365
+ }
366
+ }
367
+
368
+ fn pane_from_target(target: &Target) -> PaneId {
369
+ match target {
370
+ Target::Pane(pane) => pane.clone(),
371
+ Target::SessionWindow { session, window } => {
372
+ PaneId::new(format!("{}:{}", session.as_str(), window.as_str()))
373
+ }
374
+ }
375
+ }
376
+
377
+ fn target_name(target: &Target) -> String {
378
+ match target {
379
+ Target::Pane(pane) => pane.as_str().to_string(),
380
+ Target::SessionWindow { session, window } => {
381
+ format!("{}:{}", session.as_str(), window.as_str())
382
+ }
383
+ }
384
+ }
385
+
386
+ fn inject_stage_for_argv(argv: &[String]) -> InjectStage {
387
+ match argv.get(1).map(String::as_str) {
388
+ Some("set-buffer") => InjectStage::SetBuffer,
389
+ Some("load-buffer") => InjectStage::LoadBuffer,
390
+ Some("paste-buffer") => InjectStage::PasteBuffer,
391
+ Some("delete-buffer") => InjectStage::DeleteBuffer,
392
+ Some("send-keys") => InjectStage::Submit,
393
+ _ => InjectStage::Submit,
394
+ }
395
+ }
396
+
397
+ fn pane_mode_from_raw(raw: Option<String>) -> Option<PaneMode> {
398
+ match raw.as_deref().map(str::trim) {
399
+ Some("") | Some("0") => None,
400
+ Some("copy-mode") => Some(PaneMode::Copy),
401
+ Some("tree-mode") => Some(PaneMode::Tree),
402
+ Some("view-mode") => Some(PaneMode::View),
403
+ Some("client-mode") => Some(PaneMode::Client),
404
+ _ => Some(PaneMode::Unknown),
405
+ }
406
+ }
407
+
408
+ fn buffer_name_for_text(text: &str) -> String {
409
+ const PREFIX: &str = "[team-agent-token:";
410
+ match text.find(PREFIX) {
411
+ Some(prefix_start) => {
412
+ let token_start = prefix_start.saturating_add(PREFIX.len());
413
+ let Some(rest) = text.get(token_start..) else {
414
+ return "team-agent-buf".to_string();
415
+ };
416
+ let Some(token_end) = rest.find(']') else {
417
+ return "team-agent-buf".to_string();
418
+ };
419
+ let Some(token) = rest.get(..token_end).filter(|s| !s.is_empty()) else {
420
+ return "team-agent-buf".to_string();
421
+ };
422
+ format!("team-agent-send-{token}")
423
+ }
424
+ None => "team-agent-buf".to_string(),
425
+ }
426
+ }
427
+
428
+ fn inject_verification_for_payload(payload: &InjectPayload) -> InjectVerification {
429
+ match payload {
430
+ InjectPayload::Empty => InjectVerification::EmptyTextSendKeys,
431
+ InjectPayload::Text(text) if text.contains("[team-agent-token:") => {
432
+ InjectVerification::CaptureContainsToken
433
+ }
434
+ InjectPayload::Text(_) => InjectVerification::NoToken,
435
+ }
436
+ }
437
+
438
+ fn submit_verification_for_key(key: Key) -> SubmitVerification {
439
+ match key {
440
+ Key::Enter => SubmitVerification::EnterSentWithoutPlaceholderCheck,
441
+ other => SubmitVerification::KeySentAfterVisibleToken { key: other },
442
+ }
443
+ }
444
+
445
+ fn shell_command(argv: &[String], cwd: &Path, env: &BTreeMap<String, String>) -> String {
446
+ let mut parts = Vec::new();
447
+ parts.push("cd".to_string());
448
+ parts.push(shell_quote(&cwd.to_string_lossy()));
449
+ parts.push("&&".to_string());
450
+ for (key, value) in env {
451
+ parts.push(format!("{key}={}", shell_quote(value)));
452
+ }
453
+ parts.push("exec".to_string());
454
+ parts.extend(argv.iter().map(|arg| shell_quote(arg)));
455
+ parts.join(" ")
456
+ }
457
+
458
+ fn shell_quote(raw: &str) -> String {
459
+ if raw.is_empty() {
460
+ return "''".to_string();
461
+ }
462
+ if raw
463
+ .chars()
464
+ .all(|c| c.is_ascii_alphanumeric() || matches!(c, '/' | '.' | '_' | '-' | ':' | '='))
465
+ {
466
+ return raw.to_string();
467
+ }
468
+ let mut quoted = String::from("'");
469
+ for ch in raw.chars() {
470
+ if ch == '\'' {
471
+ quoted.push_str("'\\''");
472
+ } else {
473
+ quoted.push(ch);
474
+ }
475
+ }
476
+ quoted.push('\'');
477
+ quoted
478
+ }
479
+
480
+ impl Transport for TmuxBackend {
481
+ fn kind(&self) -> BackendKind {
482
+ BackendKind::Tmux
483
+ }
484
+
485
+ fn spawn_first(
486
+ &self,
487
+ session: &SessionName,
488
+ window: &WindowName,
489
+ argv: &[String],
490
+ cwd: &Path,
491
+ env: &BTreeMap<String, String>,
492
+ ) -> Result<SpawnResult, TransportError> {
493
+ self.spawn(session, window, argv, cwd, env, true)
494
+ }
495
+
496
+ fn spawn_into(
497
+ &self,
498
+ session: &SessionName,
499
+ window: &WindowName,
500
+ argv: &[String],
501
+ cwd: &Path,
502
+ env: &BTreeMap<String, String>,
503
+ ) -> Result<SpawnResult, TransportError> {
504
+ self.spawn(session, window, argv, cwd, env, false)
505
+ }
506
+
507
+ fn inject(
508
+ &self,
509
+ target: &Target,
510
+ payload: &InjectPayload,
511
+ submit: Key,
512
+ bracketed: bool,
513
+ ) -> Result<InjectReport, TransportError> {
514
+ let pane = pane_from_target(target);
515
+ match payload {
516
+ InjectPayload::Empty => {
517
+ let argv = tmux_empty_inject_argv(&pane, submit);
518
+ self.run_ok(&argv)?;
519
+ }
520
+ InjectPayload::Text(text) => {
521
+ let buffer = buffer_name_for_text(text);
522
+ for argv in tmux_inject_text_argv(&pane, &buffer, text, bracketed) {
523
+ let stage = inject_stage_for_argv(&argv);
524
+ if stage == InjectStage::LoadBuffer {
525
+ self.run_inject_stage_with_stdin(&argv, stage, text)?;
526
+ } else {
527
+ self.run_inject_stage(&argv, stage)?;
528
+ }
529
+ }
530
+ let submit_argv = tmux_send_keys_argv(&pane, &[submit]);
531
+ self.run_inject_stage(&submit_argv, InjectStage::Submit)?;
532
+ }
533
+ }
534
+ Ok(InjectReport {
535
+ stage_reached: InjectStage::Submit,
536
+ inject_verification: inject_verification_for_payload(payload),
537
+ submit_verification: submit_verification_for_key(submit),
538
+ turn_verification: match payload {
539
+ InjectPayload::Empty => TurnVerification::NotRequired,
540
+ InjectPayload::Text(_) => TurnVerification::NotYetObserved,
541
+ },
542
+ attempts: 1,
543
+ })
544
+ }
545
+
546
+ fn send_keys(&self, target: &Target, keys: &[Key]) -> Result<(), TransportError> {
547
+ let pane = pane_from_target(target);
548
+ if keys.contains(&Key::CancelMode) {
549
+ if let Some(mode) = pane_mode_from_raw(self.query(target, PaneField::PaneMode)?) {
550
+ let argv = crate::transport::tmux_cancel_mode_argv(&pane, mode);
551
+ return self.run_ok(&argv);
552
+ }
553
+ return Ok(());
554
+ }
555
+ let argv = tmux_send_keys_argv(&pane, keys);
556
+ self.run_ok(&argv)
557
+ }
558
+
559
+ fn capture(
560
+ &self,
561
+ target: &Target,
562
+ range: CaptureRange,
563
+ ) -> Result<CapturedText, TransportError> {
564
+ let pane = pane_from_target(target);
565
+ let argv = self.tmux_argv(&tmux_capture_argv(&pane, range));
566
+ let output = self.runner.run(&argv).map_err(|source| TransportError::Capture { source })?;
567
+ if !output.success {
568
+ return Err(subprocess_error(argv, output));
569
+ }
570
+ Ok(CapturedText {
571
+ text: normalize_capture(&output.stdout),
572
+ range,
573
+ })
574
+ }
575
+
576
+ fn query(
577
+ &self,
578
+ target: &Target,
579
+ field: PaneField,
580
+ ) -> Result<Option<String>, TransportError> {
581
+ let pane = pane_from_target(target);
582
+ let argv = self.tmux_argv(&tmux_query_argv(&pane, field));
583
+ let output = self.runner.run(&argv)?;
584
+ if !output.success {
585
+ return Ok(None);
586
+ }
587
+ Ok(Some(output.stdout.trim().to_string()))
588
+ }
589
+
590
+ fn liveness(&self, pane: &PaneId) -> Result<PaneLiveness, TransportError> {
591
+ let argv = self.tmux_argv(&[
592
+ "tmux".to_string(),
593
+ "display-message".to_string(),
594
+ "-p".to_string(),
595
+ "-t".to_string(),
596
+ pane.as_str().to_string(),
597
+ "#{pane_id}".to_string(),
598
+ ]);
599
+ let output = self.runner.run(&argv)?;
600
+ if output.success {
601
+ return Ok(PaneLiveness::Live);
602
+ }
603
+ if output.stderr.to_ascii_lowercase().contains("can't find pane") {
604
+ Ok(PaneLiveness::Dead)
605
+ } else {
606
+ Ok(PaneLiveness::Unknown)
607
+ }
608
+ }
609
+
610
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
611
+ const TMUX_PANE_FORMAT: &str = "#{pane_id}\t#{session_name}\t#{window_index}\t#{window_name}\t#{pane_index}\t#{pane_tty}\t#{pane_current_command}\t#{pane_active}\t#{pane_current_path}\t#{session_attached}\t#{pane_in_mode}";
612
+ let argv = self.tmux_argv(&[
613
+ "tmux".to_string(),
614
+ "list-panes".to_string(),
615
+ "-a".to_string(),
616
+ "-F".to_string(),
617
+ TMUX_PANE_FORMAT.to_string(),
618
+ ]);
619
+ let output = self.runner.run(&argv)?;
620
+ if !output.success {
621
+ return Ok(Vec::new());
622
+ }
623
+ let mut panes = Vec::new();
624
+ for line in output.stdout.lines().filter(|line| !line.is_empty()) {
625
+ if let Some(pane) = parse_pane_info_line(line) {
626
+ panes.push(pane);
627
+ }
628
+ }
629
+ Ok(panes)
630
+ }
631
+
632
+ fn has_session(&self, session: &SessionName) -> Result<bool, TransportError> {
633
+ let argv = self.tmux_argv(&[
634
+ "tmux".to_string(),
635
+ "has-session".to_string(),
636
+ "-t".to_string(),
637
+ session.as_str().to_string(),
638
+ ]);
639
+ let output = self.runner.run(&argv)?;
640
+ Ok(output.success)
641
+ }
642
+
643
+ fn list_windows(
644
+ &self,
645
+ session: &SessionName,
646
+ ) -> Result<Vec<WindowName>, TransportError> {
647
+ // golden runtime.py:1023-1029 `_tmux_window_exists`: `tmux list-windows -t <s> -F #{window_name}`;
648
+ // returncode != 0 -> false (here: an empty window set), else the window names by line.
649
+ let argv = self.tmux_argv(&[
650
+ "tmux".to_string(),
651
+ "list-windows".to_string(),
652
+ "-t".to_string(),
653
+ session.as_str().to_string(),
654
+ "-F".to_string(),
655
+ "#{window_name}".to_string(),
656
+ ]);
657
+ let output = self.runner.run(&argv)?;
658
+ if !output.success {
659
+ return Ok(Vec::new());
660
+ }
661
+ Ok(output
662
+ .stdout
663
+ .lines()
664
+ .filter(|line| !line.is_empty())
665
+ .map(WindowName::new)
666
+ .collect())
667
+ }
668
+
669
+ fn set_session_env(
670
+ &self,
671
+ session: &SessionName,
672
+ key: &str,
673
+ value: &str,
674
+ ) -> Result<SetEnvOutcome, TransportError> {
675
+ let argv = vec![
676
+ "tmux".to_string(),
677
+ "set-environment".to_string(),
678
+ "-t".to_string(),
679
+ session.as_str().to_string(),
680
+ key.to_string(),
681
+ value.to_string(),
682
+ ];
683
+ self.run_ok(&argv)?;
684
+ Ok(SetEnvOutcome::Applied)
685
+ }
686
+
687
+ fn kill_session(&self, session: &SessionName) -> Result<(), TransportError> {
688
+ let argv = vec![
689
+ "tmux".to_string(),
690
+ "kill-session".to_string(),
691
+ "-t".to_string(),
692
+ session.as_str().to_string(),
693
+ ];
694
+ self.run_ok(&argv)
695
+ }
696
+
697
+ fn kill_window(&self, target: &Target) -> Result<(), TransportError> {
698
+ let argv = vec![
699
+ "tmux".to_string(),
700
+ "kill-window".to_string(),
701
+ "-t".to_string(),
702
+ target_name(target),
703
+ ];
704
+ self.run_ok(&argv)
705
+ }
706
+
707
+ fn attach_session(
708
+ &self,
709
+ session: &SessionName,
710
+ ) -> Result<AttachOutcome, TransportError> {
711
+ let argv = [
712
+ "tmux".to_string(),
713
+ "attach-session".to_string(),
714
+ "-t".to_string(),
715
+ session.as_str().to_string(),
716
+ ];
717
+ self.run_ok(&argv)?;
718
+ Ok(AttachOutcome::Attached)
719
+ }
720
+ }
721
+
722
+ fn parse_pane_info_line(line: &str) -> Option<PaneInfo> {
723
+ let fields = line.split('\t').collect::<Vec<_>>();
724
+ if fields.len() < 11 {
725
+ return None;
726
+ }
727
+ Some(PaneInfo {
728
+ pane_id: PaneId::new(fields[0]),
729
+ session: SessionName::new(fields[1]),
730
+ window_index: parse_optional_u32(fields[2]),
731
+ window_name: non_empty(fields[3]).map(WindowName::new),
732
+ pane_index: parse_optional_u32(fields[4]),
733
+ tty: non_empty(fields[5]).map(str::to_string),
734
+ current_command: non_empty(fields[6]).map(str::to_string),
735
+ active: fields[7] == "1",
736
+ current_path: non_empty(fields[8]).map(PathBuf::from),
737
+ pane_pid: None,
738
+ leader_env: BTreeMap::new(),
739
+ })
740
+ }
741
+
742
+ fn parse_optional_u32(raw: &str) -> Option<u32> {
743
+ if raw.is_empty() {
744
+ return None;
745
+ }
746
+ raw.parse::<u32>().ok()
747
+ }
748
+
749
+ fn non_empty(raw: &str) -> Option<&str> {
750
+ if raw.is_empty() {
751
+ None
752
+ } else {
753
+ Some(raw)
754
+ }
755
+ }
756
+
757
+ #[cfg(test)]
758
+ mod tests;