borgmcp 1.1.15 → 2.0.2

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 (466) hide show
  1. package/CONTRIBUTING.md +15 -0
  2. package/LICENSE +201 -33
  3. package/NOTICE +2 -0
  4. package/README.md +85 -36
  5. package/SECURITY.md +7 -0
  6. package/dist/agent-runtime.d.ts.map +1 -0
  7. package/dist/agent-runtime.js +63 -1
  8. package/dist/agent-runtime.js.map +1 -0
  9. package/dist/assimilate-cmd.d.ts +96 -17
  10. package/dist/assimilate-cmd.d.ts.map +1 -0
  11. package/dist/assimilate-cmd.js +1396 -48
  12. package/dist/assimilate-cmd.js.map +1 -0
  13. package/dist/assimilate-deps.d.ts.map +1 -0
  14. package/dist/assimilate-deps.js +384 -3
  15. package/dist/assimilate-deps.js.map +1 -0
  16. package/dist/assimilate-guard.d.ts +5 -7
  17. package/dist/assimilate-guard.d.ts.map +1 -0
  18. package/dist/assimilate-guard.js +60 -2
  19. package/dist/assimilate-guard.js.map +1 -0
  20. package/dist/assimilate-welcome.d.ts +1 -1
  21. package/dist/assimilate-welcome.d.ts.map +1 -0
  22. package/dist/assimilate-welcome.js +32 -2
  23. package/dist/assimilate-welcome.js.map +1 -0
  24. package/dist/auth-env.d.ts +7 -50
  25. package/dist/auth-env.d.ts.map +1 -0
  26. package/dist/auth-env.js +22 -1
  27. package/dist/auth-env.js.map +1 -0
  28. package/dist/backends/launch-all-pastelist.d.ts.map +1 -0
  29. package/dist/backends/launch-all-pastelist.js +11 -4
  30. package/dist/backends/launch-all-pastelist.js.map +1 -0
  31. package/dist/backends/launch-all-tmux.d.ts.map +1 -0
  32. package/dist/backends/launch-all-tmux.js +49 -1
  33. package/dist/backends/launch-all-tmux.js.map +1 -0
  34. package/dist/backends/launch-all-windows.d.ts.map +1 -0
  35. package/dist/backends/launch-all-windows.js +86 -12
  36. package/dist/backends/launch-all-windows.js.map +1 -0
  37. package/dist/bare-launch-menu.d.ts.map +1 -0
  38. package/dist/bare-launch-menu.js +83 -6
  39. package/dist/bare-launch-menu.js.map +1 -0
  40. package/dist/claude-launch-args.d.ts.map +1 -0
  41. package/dist/claude-launch-args.js +34 -1
  42. package/dist/claude-launch-args.js.map +1 -0
  43. package/dist/claude.d.ts.map +1 -0
  44. package/dist/claude.js +486 -14
  45. package/dist/claude.js.map +1 -0
  46. package/dist/cleanup-cmd.d.ts +5 -5
  47. package/dist/cleanup-cmd.d.ts.map +1 -0
  48. package/dist/cleanup-cmd.js +453 -14
  49. package/dist/cleanup-cmd.js.map +1 -0
  50. package/dist/clear-rewake-core.d.ts.map +1 -0
  51. package/dist/clear-rewake-core.js +23 -2
  52. package/dist/clear-rewake-core.js.map +1 -0
  53. package/dist/clear-rewake.d.ts.map +1 -0
  54. package/dist/clear-rewake.js +19 -1
  55. package/dist/clear-rewake.js.map +1 -0
  56. package/dist/cli-help.d.ts +5 -0
  57. package/dist/cli-help.d.ts.map +1 -0
  58. package/dist/cli-help.js +104 -56
  59. package/dist/cli-help.js.map +1 -0
  60. package/dist/cli-platform.d.ts.map +1 -0
  61. package/dist/cli-platform.js +106 -4
  62. package/dist/cli-platform.js.map +1 -0
  63. package/dist/cli-tool-approval.d.ts +84 -0
  64. package/dist/cli-tool-approval.d.ts.map +1 -0
  65. package/dist/cli-tool-approval.js +583 -0
  66. package/dist/cli-tool-approval.js.map +1 -0
  67. package/dist/codex-app-server.d.ts.map +1 -0
  68. package/dist/codex-app-server.js +228 -4
  69. package/dist/codex-app-server.js.map +1 -0
  70. package/dist/codex-app-wake.d.ts +9 -11
  71. package/dist/codex-app-wake.d.ts.map +1 -0
  72. package/dist/codex-app-wake.js +436 -2
  73. package/dist/codex-app-wake.js.map +1 -0
  74. package/dist/codex-launch.d.ts.map +1 -0
  75. package/dist/codex-launch.js +102 -2
  76. package/dist/codex-launch.js.map +1 -0
  77. package/dist/codex-remote.d.ts +6 -1
  78. package/dist/codex-remote.d.ts.map +1 -0
  79. package/dist/codex-remote.js +394 -1
  80. package/dist/codex-remote.js.map +1 -0
  81. package/dist/codex-wake-resolve.d.ts +4 -4
  82. package/dist/codex-wake-resolve.d.ts.map +1 -0
  83. package/dist/codex-wake-resolve.js +127 -1
  84. package/dist/codex-wake-resolve.js.map +1 -0
  85. package/dist/config-utils.d.ts +2 -2
  86. package/dist/config-utils.d.ts.map +1 -0
  87. package/dist/config-utils.js +766 -3
  88. package/dist/config-utils.js.map +1 -0
  89. package/dist/config.d.ts +98 -61
  90. package/dist/config.d.ts.map +1 -0
  91. package/dist/config.js +426 -1
  92. package/dist/config.js.map +1 -0
  93. package/dist/console-prefix.d.ts.map +1 -0
  94. package/dist/console-prefix.js +96 -1
  95. package/dist/console-prefix.js.map +1 -0
  96. package/dist/cube-name.d.ts.map +1 -0
  97. package/dist/cube-name.js +67 -1
  98. package/dist/cube-name.js.map +1 -0
  99. package/dist/cubes.d.ts +126 -18
  100. package/dist/cubes.d.ts.map +1 -0
  101. package/dist/cubes.js +412 -4
  102. package/dist/cubes.js.map +1 -0
  103. package/dist/debug.d.ts.map +1 -0
  104. package/dist/debug.js +71 -1
  105. package/dist/debug.js.map +1 -0
  106. package/dist/direct-log.d.ts.map +1 -0
  107. package/dist/direct-log.js +11 -1
  108. package/dist/direct-log.js.map +1 -0
  109. package/dist/docs-sections.d.ts +11 -10
  110. package/dist/docs-sections.d.ts.map +1 -0
  111. package/dist/docs-sections.js +113 -3
  112. package/dist/docs-sections.js.map +1 -0
  113. package/dist/drone-lifecycle.d.ts +5 -21
  114. package/dist/drone-lifecycle.d.ts.map +1 -0
  115. package/dist/drone-lifecycle.js +57 -8
  116. package/dist/drone-lifecycle.js.map +1 -0
  117. package/dist/drone-management.d.ts +29 -0
  118. package/dist/drone-management.d.ts.map +1 -0
  119. package/dist/drone-management.js +109 -0
  120. package/dist/drone-management.js.map +1 -0
  121. package/dist/ensure-mcp-config.d.ts.map +1 -0
  122. package/dist/ensure-mcp-config.js +36 -1
  123. package/dist/ensure-mcp-config.js.map +1 -0
  124. package/dist/evict-drone.d.ts +10 -2
  125. package/dist/evict-drone.d.ts.map +1 -0
  126. package/dist/evict-drone.js +60 -1
  127. package/dist/evict-drone.js.map +1 -0
  128. package/dist/gc-orphan-inboxes.d.ts.map +1 -0
  129. package/dist/gc-orphan-inboxes.js +177 -1
  130. package/dist/gc-orphan-inboxes.js.map +1 -0
  131. package/dist/inbox-monitor.d.ts +1 -1
  132. package/dist/inbox-monitor.d.ts.map +1 -0
  133. package/dist/inbox-monitor.js +856 -3
  134. package/dist/inbox-monitor.js.map +1 -0
  135. package/dist/index.d.ts +6 -4
  136. package/dist/index.d.ts.map +1 -0
  137. package/dist/index.js +1034 -37
  138. package/dist/index.js.map +1 -0
  139. package/dist/launch-all-cmd.d.ts.map +1 -0
  140. package/dist/launch-all-cmd.js +377 -37
  141. package/dist/launch-all-cmd.js.map +1 -0
  142. package/dist/launch-all-command.d.ts.map +1 -0
  143. package/dist/launch-all-command.js +24 -1
  144. package/dist/launch-all-command.js.map +1 -0
  145. package/dist/launch-all-deps.d.ts +3 -8
  146. package/dist/launch-all-deps.d.ts.map +1 -0
  147. package/dist/launch-all-deps.js +99 -1
  148. package/dist/launch-all-deps.js.map +1 -0
  149. package/dist/launch-all-discovery.d.ts +1 -0
  150. package/dist/launch-all-discovery.d.ts.map +1 -0
  151. package/dist/launch-all-discovery.js +90 -4
  152. package/dist/launch-all-discovery.js.map +1 -0
  153. package/dist/launch-all-locks.d.ts.map +1 -0
  154. package/dist/launch-all-locks.js +54 -1
  155. package/dist/launch-all-locks.js.map +1 -0
  156. package/dist/launch-gate.d.ts +2 -2
  157. package/dist/launch-gate.d.ts.map +1 -0
  158. package/dist/launch-gate.js +73 -1
  159. package/dist/launch-gate.js.map +1 -0
  160. package/dist/lifecycle-log-guard.d.ts.map +1 -0
  161. package/dist/lifecycle-log-guard.js +93 -2
  162. package/dist/lifecycle-log-guard.js.map +1 -0
  163. package/dist/list-roles-render.d.ts.map +1 -0
  164. package/dist/list-roles-render.js +40 -6
  165. package/dist/list-roles-render.js.map +1 -0
  166. package/dist/local-log-routing.d.ts +19 -0
  167. package/dist/local-log-routing.d.ts.map +1 -0
  168. package/dist/local-log-routing.js +67 -0
  169. package/dist/local-log-routing.js.map +1 -0
  170. package/dist/local-manage-tool-result.d.ts +8 -0
  171. package/dist/local-manage-tool-result.d.ts.map +1 -0
  172. package/dist/local-manage-tool-result.js +10 -0
  173. package/dist/local-manage-tool-result.js.map +1 -0
  174. package/dist/local-server-cursor.d.ts +34 -0
  175. package/dist/local-server-cursor.d.ts.map +1 -0
  176. package/dist/local-server-cursor.js +151 -0
  177. package/dist/local-server-cursor.js.map +1 -0
  178. package/dist/log-audit.d.ts.map +1 -0
  179. package/dist/log-audit.js +194 -3
  180. package/dist/log-audit.js.map +1 -0
  181. package/dist/log-stream.d.ts +35 -18
  182. package/dist/log-stream.d.ts.map +1 -0
  183. package/dist/log-stream.js +1270 -12
  184. package/dist/log-stream.js.map +1 -0
  185. package/dist/model-presets.d.ts.map +1 -0
  186. package/dist/model-presets.js +19 -1
  187. package/dist/model-presets.js.map +1 -0
  188. package/dist/name-validator.d.ts.map +1 -0
  189. package/dist/name-validator.js +22 -1
  190. package/dist/name-validator.js.map +1 -0
  191. package/dist/opencode-drone.d.ts.map +1 -0
  192. package/dist/opencode-drone.js +438 -4
  193. package/dist/opencode-drone.js.map +1 -0
  194. package/dist/opencode-plugin.d.ts +1 -1
  195. package/dist/opencode-plugin.d.ts.map +1 -0
  196. package/dist/opencode-plugin.js +29 -4
  197. package/dist/opencode-plugin.js.map +1 -0
  198. package/dist/parse-assimilate-args.d.ts +1 -1
  199. package/dist/parse-assimilate-args.d.ts.map +1 -0
  200. package/dist/parse-assimilate-args.js +125 -1
  201. package/dist/parse-assimilate-args.js.map +1 -0
  202. package/dist/parse-launch-all-args.d.ts.map +1 -0
  203. package/dist/parse-launch-all-args.js +70 -1
  204. package/dist/parse-launch-all-args.js.map +1 -0
  205. package/dist/postinstall-banner.d.ts.map +1 -0
  206. package/dist/postinstall-banner.js +27 -2
  207. package/dist/postinstall-banner.js.map +1 -0
  208. package/dist/postinstall.d.ts.map +1 -0
  209. package/dist/postinstall.js +32 -5
  210. package/dist/postinstall.js.map +1 -0
  211. package/dist/readiness-probe.d.ts.map +1 -0
  212. package/dist/readiness-probe.js +9 -1
  213. package/dist/readiness-probe.js.map +1 -0
  214. package/dist/regen-format.d.ts.map +1 -0
  215. package/dist/regen-format.js +545 -13
  216. package/dist/regen-format.js.map +1 -0
  217. package/dist/regen.d.ts.map +1 -0
  218. package/dist/regen.js +141 -5
  219. package/dist/regen.js.map +1 -0
  220. package/dist/remote-client.d.ts +88 -173
  221. package/dist/remote-client.d.ts.map +1 -0
  222. package/dist/remote-client.js +997 -1
  223. package/dist/remote-client.js.map +1 -0
  224. package/dist/reset-local-seat-cmd.d.ts +50 -0
  225. package/dist/reset-local-seat-cmd.d.ts.map +1 -0
  226. package/dist/reset-local-seat-cmd.js +169 -0
  227. package/dist/reset-local-seat-cmd.js.map +1 -0
  228. package/dist/role-match.d.ts.map +1 -0
  229. package/dist/role-match.js +23 -1
  230. package/dist/role-match.js.map +1 -0
  231. package/dist/role-resolver.d.ts.map +1 -0
  232. package/dist/role-resolver.js +60 -1
  233. package/dist/role-resolver.js.map +1 -0
  234. package/dist/roster-render.d.ts.map +1 -0
  235. package/dist/roster-render.js +121 -3
  236. package/dist/roster-render.js.map +1 -0
  237. package/dist/seat-probe.d.ts +32 -13
  238. package/dist/seat-probe.d.ts.map +1 -0
  239. package/dist/seat-probe.js +78 -1
  240. package/dist/seat-probe.js.map +1 -0
  241. package/dist/seat-store.d.ts +76 -0
  242. package/dist/seat-store.d.ts.map +1 -0
  243. package/dist/seat-store.js +309 -0
  244. package/dist/seat-store.js.map +1 -0
  245. package/dist/seats.d.ts +375 -0
  246. package/dist/seats.d.ts.map +1 -0
  247. package/dist/seats.js +670 -0
  248. package/dist/seats.js.map +1 -0
  249. package/dist/self-path.d.ts +27 -0
  250. package/dist/self-path.d.ts.map +1 -0
  251. package/dist/self-path.js +43 -0
  252. package/dist/self-path.js.map +1 -0
  253. package/dist/server-endpoint.d.ts +9 -0
  254. package/dist/server-endpoint.d.ts.map +1 -0
  255. package/dist/server-endpoint.js +38 -0
  256. package/dist/server-endpoint.js.map +1 -0
  257. package/dist/server-errors.d.ts +50 -0
  258. package/dist/server-errors.d.ts.map +1 -0
  259. package/dist/server-errors.js +79 -0
  260. package/dist/server-errors.js.map +1 -0
  261. package/dist/server-handshake.d.ts +197 -0
  262. package/dist/server-handshake.d.ts.map +1 -0
  263. package/dist/server-handshake.js +533 -0
  264. package/dist/server-handshake.js.map +1 -0
  265. package/dist/server-response.d.ts +3 -0
  266. package/dist/server-response.d.ts.map +1 -0
  267. package/dist/server-response.js +67 -0
  268. package/dist/server-response.js.map +1 -0
  269. package/dist/server-trust.d.ts +14 -0
  270. package/dist/server-trust.d.ts.map +1 -0
  271. package/dist/server-trust.js +225 -0
  272. package/dist/server-trust.js.map +1 -0
  273. package/dist/session-continuity.d.ts +33 -0
  274. package/dist/session-continuity.d.ts.map +1 -0
  275. package/dist/session-continuity.js +220 -0
  276. package/dist/session-continuity.js.map +1 -0
  277. package/dist/setup-confirm.d.ts +3 -3
  278. package/dist/setup-confirm.d.ts.map +1 -0
  279. package/dist/setup-confirm.js +126 -2
  280. package/dist/setup-confirm.js.map +1 -0
  281. package/dist/setup.d.ts +5 -3
  282. package/dist/setup.d.ts.map +1 -0
  283. package/dist/setup.js +169 -46
  284. package/dist/setup.js.map +1 -0
  285. package/dist/shell-escape.d.ts.map +1 -0
  286. package/dist/shell-escape.js +22 -1
  287. package/dist/shell-escape.js.map +1 -0
  288. package/dist/spawn.d.ts +2 -2
  289. package/dist/spawn.d.ts.map +1 -0
  290. package/dist/spawn.js +29 -10
  291. package/dist/spawn.js.map +1 -0
  292. package/dist/startup-services.d.ts +0 -1
  293. package/dist/startup-services.d.ts.map +1 -0
  294. package/dist/startup-services.js +26 -1
  295. package/dist/startup-services.js.map +1 -0
  296. package/dist/stream-owner.d.ts +6 -2
  297. package/dist/stream-owner.d.ts.map +1 -0
  298. package/dist/stream-owner.js +547 -2
  299. package/dist/stream-owner.js.map +1 -0
  300. package/dist/stream-status.d.ts +1 -1
  301. package/dist/stream-status.d.ts.map +1 -0
  302. package/dist/stream-status.js +268 -3
  303. package/dist/stream-status.js.map +1 -0
  304. package/dist/sync-roles-render.d.ts.map +1 -0
  305. package/dist/sync-roles-render.js +118 -3
  306. package/dist/sync-roles-render.js.map +1 -0
  307. package/dist/sync.d.ts +1 -1
  308. package/dist/sync.d.ts.map +1 -0
  309. package/dist/sync.js +286 -22
  310. package/dist/sync.js.map +1 -0
  311. package/dist/terminal-title.d.ts.map +1 -0
  312. package/dist/terminal-title.js +68 -1
  313. package/dist/terminal-title.js.map +1 -0
  314. package/dist/token-store.d.ts +17 -111
  315. package/dist/token-store.d.ts.map +1 -0
  316. package/dist/token-store.js +81 -1
  317. package/dist/token-store.js.map +1 -0
  318. package/dist/tool-manifest.d.ts +4 -8
  319. package/dist/tool-manifest.d.ts.map +1 -0
  320. package/dist/tool-manifest.js +554 -1
  321. package/dist/tool-manifest.js.map +1 -0
  322. package/dist/tool-scope.d.ts +8 -12
  323. package/dist/tool-scope.d.ts.map +1 -0
  324. package/dist/tool-scope.js +97 -1
  325. package/dist/tool-scope.js.map +1 -0
  326. package/dist/types.d.ts +1 -5
  327. package/dist/types.d.ts.map +1 -0
  328. package/dist/types.js +5 -0
  329. package/dist/types.js.map +1 -0
  330. package/dist/unknown-subcommand.d.ts +1 -1
  331. package/dist/unknown-subcommand.d.ts.map +1 -0
  332. package/dist/unknown-subcommand.js +37 -1
  333. package/dist/unknown-subcommand.js.map +1 -0
  334. package/dist/version.d.ts +1 -1
  335. package/dist/version.d.ts.map +1 -0
  336. package/dist/version.js +78 -2
  337. package/dist/version.js.map +1 -0
  338. package/dist/working-repo.d.ts.map +1 -0
  339. package/dist/working-repo.js +81 -1
  340. package/dist/working-repo.js.map +1 -0
  341. package/dist/worktree-lifecycle.d.ts.map +1 -0
  342. package/dist/worktree-lifecycle.js +205 -2
  343. package/dist/worktree-lifecycle.js.map +1 -0
  344. package/docs/EXTRACTION_PROVENANCE.md +42 -0
  345. package/docs/LOCAL_SERVER.md +103 -0
  346. package/docs/RELEASING.md +129 -0
  347. package/package.json +52 -34
  348. package/src/agent-runtime.ts +69 -0
  349. package/src/assimilate-cmd.ts +1864 -0
  350. package/src/assimilate-deps.ts +444 -0
  351. package/src/assimilate-guard.ts +84 -0
  352. package/src/assimilate-welcome.ts +39 -0
  353. package/src/auth-env.ts +21 -0
  354. package/src/backends/launch-all-pastelist.ts +18 -0
  355. package/src/backends/launch-all-tmux.ts +75 -0
  356. package/src/backends/launch-all-windows.ts +110 -0
  357. package/src/bare-launch-menu.ts +148 -0
  358. package/src/claude-launch-args.ts +35 -0
  359. package/src/claude.ts +561 -0
  360. package/src/cleanup-cmd.ts +581 -0
  361. package/src/clear-rewake-core.ts +34 -0
  362. package/src/clear-rewake.ts +18 -0
  363. package/src/cli-help.ts +116 -0
  364. package/src/cli-platform.ts +127 -0
  365. package/src/cli-tool-approval.ts +716 -0
  366. package/src/codex-app-server.ts +238 -0
  367. package/src/codex-app-wake.ts +524 -0
  368. package/src/codex-launch.ts +140 -0
  369. package/src/codex-remote.ts +500 -0
  370. package/src/codex-wake-resolve.ts +155 -0
  371. package/src/config-utils.ts +811 -0
  372. package/src/config.ts +595 -0
  373. package/src/console-prefix.ts +99 -0
  374. package/src/cube-name.ts +69 -0
  375. package/src/cubes.ts +583 -0
  376. package/src/debug.ts +73 -0
  377. package/src/direct-log.ts +9 -0
  378. package/src/docs-sections.ts +129 -0
  379. package/src/drone-lifecycle.ts +62 -0
  380. package/src/drone-management.ts +162 -0
  381. package/src/ensure-mcp-config.ts +54 -0
  382. package/src/evict-drone.ts +82 -0
  383. package/src/gc-orphan-inboxes.ts +229 -0
  384. package/src/inbox-monitor.ts +1005 -0
  385. package/src/index.ts +1201 -0
  386. package/src/launch-all-cmd.ts +450 -0
  387. package/src/launch-all-command.ts +30 -0
  388. package/src/launch-all-deps.ts +188 -0
  389. package/src/launch-all-discovery.ts +118 -0
  390. package/src/launch-all-locks.ts +74 -0
  391. package/src/launch-gate.ts +80 -0
  392. package/src/lifecycle-log-guard.ts +144 -0
  393. package/src/list-roles-render.ts +52 -0
  394. package/src/local-log-routing.ts +106 -0
  395. package/src/local-manage-tool-result.ts +10 -0
  396. package/src/local-server-cursor.ts +188 -0
  397. package/src/log-audit.ts +207 -0
  398. package/src/log-stream.ts +1567 -0
  399. package/src/model-presets.ts +22 -0
  400. package/src/name-validator.ts +23 -0
  401. package/src/opencode-drone.ts +528 -0
  402. package/src/opencode-plugin.ts +38 -0
  403. package/src/parse-assimilate-args.ts +125 -0
  404. package/src/parse-launch-all-args.ts +100 -0
  405. package/src/postinstall-banner.ts +37 -0
  406. package/src/postinstall.ts +36 -0
  407. package/src/readiness-probe.ts +12 -0
  408. package/src/regen-format.ts +651 -0
  409. package/src/regen.ts +150 -0
  410. package/src/remote-client.ts +1457 -0
  411. package/src/reset-local-seat-cmd.ts +227 -0
  412. package/src/role-match.ts +29 -0
  413. package/src/role-resolver.ts +93 -0
  414. package/src/roster-render.ts +196 -0
  415. package/src/seat-probe.ts +115 -0
  416. package/src/seat-store.ts +333 -0
  417. package/src/seats.ts +911 -0
  418. package/src/self-path.ts +51 -0
  419. package/src/server-endpoint.ts +38 -0
  420. package/src/server-errors.ts +95 -0
  421. package/src/server-handshake.ts +816 -0
  422. package/src/server-response.ts +76 -0
  423. package/src/server-trust.ts +245 -0
  424. package/src/session-continuity.ts +280 -0
  425. package/src/setup-confirm.ts +171 -0
  426. package/src/setup.ts +186 -0
  427. package/src/shell-escape.ts +22 -0
  428. package/src/spawn.ts +31 -0
  429. package/src/startup-services.ts +36 -0
  430. package/src/stream-owner.ts +686 -0
  431. package/src/stream-status.ts +337 -0
  432. package/src/sync-roles-render.ts +163 -0
  433. package/src/sync.ts +363 -0
  434. package/src/terminal-title.ts +76 -0
  435. package/src/token-store.ts +102 -0
  436. package/src/tool-manifest.ts +610 -0
  437. package/src/tool-scope.ts +110 -0
  438. package/src/types.ts +5 -0
  439. package/src/unknown-subcommand.ts +35 -0
  440. package/src/version.ts +82 -0
  441. package/src/working-repo.ts +99 -0
  442. package/src/worktree-lifecycle.ts +254 -0
  443. package/dist/auth-recovery.d.ts +0 -33
  444. package/dist/auth-recovery.js +0 -1
  445. package/dist/auth.d.ts +0 -103
  446. package/dist/auth.js +0 -39
  447. package/dist/device-auth.d.ts +0 -75
  448. package/dist/device-auth.js +0 -1
  449. package/dist/drone-address.d.ts +0 -26
  450. package/dist/drone-address.js +0 -1
  451. package/dist/get-started.d.ts +0 -20
  452. package/dist/get-started.js +0 -2
  453. package/dist/health-beat.d.ts +0 -134
  454. package/dist/health-beat.js +0 -1
  455. package/dist/role-section.d.ts +0 -110
  456. package/dist/role-section.js +0 -8
  457. package/dist/setup-action.d.ts +0 -20
  458. package/dist/setup-action.js +0 -1
  459. package/dist/stale-version-check.d.ts +0 -40
  460. package/dist/stale-version-check.js +0 -1
  461. package/dist/subscription-retry.d.ts +0 -40
  462. package/dist/subscription-retry.js +0 -1
  463. package/dist/templates.d.ts +0 -107
  464. package/dist/templates.js +0 -359
  465. package/dist/token-crypto.d.ts +0 -50
  466. package/dist/token-crypto.js +0 -1
@@ -0,0 +1,450 @@
1
+ // gh#556 Part 2 — `borg launch-all [cube]` orchestrator (spec §3, §6, §7).
2
+ //
3
+ // runLaunchAll: resolve target cube → sweep stale locks → discover candidates →
4
+ // live-skip → (dry-run / >6 confirm) → select backend → dispatch → reconcile
5
+ // roster → summary. Returns 0 on success, 1 on hard failure.
6
+
7
+ import type { LaunchAllArgs } from './parse-launch-all-args.js';
8
+ import type { LaunchAllDeps } from './launch-all-deps.js';
9
+ import { discoverDroneCandidates, type DroneCandidate } from './launch-all-discovery.js';
10
+ import type { SeatStatus } from './seat-probe.js';
11
+ import { resolveBorgPath } from './launch-all-command.js';
12
+ import { sweepStaleLocks, isLockLive } from './launch-all-locks.js';
13
+ import { runTmuxBackend } from './backends/launch-all-tmux.js';
14
+ import { runWindowsBackend } from './backends/launch-all-windows.js';
15
+ import { runPastelistBackend } from './backends/launch-all-pastelist.js';
16
+
17
+ type Backend = 'tmux' | 'windows' | 'pastelist';
18
+
19
+ const TMUX_INSTALL_HINT =
20
+ 'borg launch-all: tmux not found.\n' +
21
+ ' macOS: brew install tmux\n' +
22
+ ' Debian: sudo apt install tmux\n' +
23
+ ' Fedora: sudo dnf install tmux\n';
24
+
25
+ function checkTmuxAvailable(deps: LaunchAllDeps): boolean {
26
+ try {
27
+ deps.runSync('tmux', ['-V']);
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ function isWSL(deps: LaunchAllDeps): boolean {
35
+ try {
36
+ return /microsoft|wsl/i.test(deps.runSync('uname', ['-r']));
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ /** /^drone-\d+$/ → a drone label, not a role name (governs --only tier-2 note). */
43
+ function looksLikeDroneLabel(only: string): boolean {
44
+ return /^drone-\d+$/i.test(only) || only.toLowerCase() === 'drone';
45
+ }
46
+
47
+ /** Resolve the target cubeId + display name (spec §3.1). */
48
+ async function resolveTargetCube(
49
+ args: LaunchAllArgs,
50
+ deps: LaunchAllDeps
51
+ ): Promise<{ cubeId: string; name: string } | { error: string }> {
52
+ if (args.cubeName !== undefined) {
53
+ const identities = await deps.readAllProjectIdentities();
54
+ const matches = identities.filter((e) => e.cube.name === args.cubeName);
55
+ if (matches.length === 0) {
56
+ return { error: `no cube named '${args.cubeName}' found among this machine's saved seats — has any drone assimilated into it?` };
57
+ }
58
+ // gh#850: distinct cubes can share a name (same name across accounts/
59
+ // environments, or a stale seat). Silently taking matches[0] could launch
60
+ // the wrong fleet, so when the name is ambiguous, surface each match's
61
+ // cubeId + the project that holds the seat and refuse to guess.
62
+ if (matches.length > 1) {
63
+ const list = matches
64
+ .map((m) => ` ${m.cube.cubeId} (seat in ${m.projectPath})`)
65
+ .join('\n');
66
+ return {
67
+ error:
68
+ `'${args.cubeName}' is ambiguous — ${matches.length} saved seats on this machine share that name:\n${list}\n` +
69
+ 'cd into the intended project and re-run without --cube-name (resolves the active cube), ' +
70
+ 'or clear the stale seat(s) by running `borg reset-local-seat` from the worktree that holds each.',
71
+ };
72
+ }
73
+ return { cubeId: matches[0].cube.cubeId, name: args.cubeName };
74
+ }
75
+ const active = await deps.getActiveCube();
76
+ if (!active) {
77
+ return {
78
+ error:
79
+ 'no active cube in this directory; run `borg assimilate` first, or pass a cube name explicitly',
80
+ };
81
+ }
82
+ return { cubeId: active.cubeId, name: active.name };
83
+ }
84
+
85
+ /** Backend selection (spec §4.1): native-Windows / explicit / tmux-preflight / auto. */
86
+ function selectBackend(args: LaunchAllArgs, deps: LaunchAllDeps): { backend: Backend } | { hardFail: string } {
87
+ const explicit = args.flags.mode;
88
+ const nativeWindows = deps.platform() === 'win32' && !isWSL(deps);
89
+ if (nativeWindows) {
90
+ deps.stderr(
91
+ 'native Windows is not supported for interactive launch; using pastelist mode instead ' +
92
+ '(WSL + tmux is the recommended Windows path)\n'
93
+ );
94
+ return { backend: 'pastelist' };
95
+ }
96
+ if (explicit === 'windows') return { backend: 'windows' };
97
+ if (explicit === 'pastelist') return { backend: 'pastelist' };
98
+
99
+ const tmuxAvail = checkTmuxAvailable(deps);
100
+ if (explicit === 'tmux') {
101
+ return tmuxAvail ? { backend: 'tmux' } : { hardFail: TMUX_INSTALL_HINT };
102
+ }
103
+ // auto (no explicit mode)
104
+ if (tmuxAvail) return { backend: 'tmux' };
105
+ deps.stderr(TMUX_INSTALL_HINT + 'Falling back to pastelist mode (paste the commands below).\n');
106
+ return { backend: 'pastelist' };
107
+ }
108
+
109
+ /** attach mode for tmux (spec §4.1 attach behavior). */
110
+ function resolveAttachMode(args: LaunchAllArgs, deps: LaunchAllDeps): 'attach' | 'switch' | 'none' {
111
+ if (args.flags.noAttach) return 'none';
112
+ if (!deps.isTTY()) return 'none';
113
+ if (deps.getEnv('TMUX')) return 'switch';
114
+ return 'attach';
115
+ }
116
+
117
+ function printCheatSheet(sessionName: string, deps: LaunchAllDeps): void {
118
+ deps.stdout(` tmux attach -t ${sessionName} # re-attach later\n`);
119
+ deps.stdout(` tmux list-windows -t ${sessionName} # list all drone windows\n`);
120
+ deps.stdout(` tmux kill-session -t ${sessionName} # stop all drones\n`);
121
+ }
122
+
123
+ function sanitizeSessionName(cubeName: string): string {
124
+ return `borg-${cubeName.replace(/[^a-zA-Z0-9_-]/g, '-')}`;
125
+ }
126
+
127
+ /** Roster reconciliation (spec §7.2). Returns droneId → 'verified'|'unconfirmed'. */
128
+ async function reconcileRoster(
129
+ deps: LaunchAllDeps,
130
+ token: string,
131
+ apiUrl: string,
132
+ serverTrustIdentity: string | undefined,
133
+ launchStartISO: string,
134
+ launchedDroneIds: string[],
135
+ now: () => number = Date.now,
136
+ sleep: (ms: number) => Promise<void> = (ms) => new Promise((r) => setTimeout(r, ms))
137
+ ): Promise<Map<string, 'verified' | 'unconfirmed'>> {
138
+ const result = new Map<string, 'verified' | 'unconfirmed'>();
139
+ for (const id of launchedDroneIds) result.set(id, 'unconfirmed');
140
+ const deadline = now() + 10_000;
141
+ // Bounded: 20 polls max (10s / 500ms), AND the time deadline — whichever first.
142
+ // The poll cap guarantees termination even if a (test-)injected clock is static.
143
+ for (let i = 0; i < 20; i++) {
144
+ if (now() >= deadline) break;
145
+ let roster: { drones: Array<{ id: string; seen_since?: boolean }> };
146
+ try {
147
+ roster = await deps.getRoster(
148
+ token,
149
+ apiUrl,
150
+ launchStartISO,
151
+ serverTrustIdentity,
152
+ );
153
+ } catch (err) {
154
+ // gh#850: the roster-reconcile token can rotate mid-launch — authedFetch
155
+ // throws "Authentication required" on a terminal 401. A bare `break` left
156
+ // the operator staring at all-'unconfirmed' with no reason. Surface WHY
157
+ // confirmation stopped (token rotation is the common cause) so they know
158
+ // the launches likely succeeded even though we couldn't confirm them.
159
+ const reason = err instanceof Error ? err.message : String(err);
160
+ const tokenRotated = /Authentication required/i.test(reason);
161
+ deps.stderr(
162
+ `roster confirmation skipped (${tokenRotated ? 'token rotated mid-launch' : reason}); ` +
163
+ 'launched drones may still be live — re-check with `borg_roster`.\n'
164
+ );
165
+ break;
166
+ }
167
+ for (const drone of roster.drones) {
168
+ if (result.get(drone.id) === 'unconfirmed' && drone.seen_since === true) {
169
+ result.set(drone.id, 'verified');
170
+ }
171
+ }
172
+ if ([...result.values()].every((v) => v === 'verified')) break;
173
+ await sleep(500);
174
+ }
175
+ return result;
176
+ }
177
+
178
+ /**
179
+ * Default ms to wait BETWEEN each drone launch, so a fleet's agents don't all
180
+ * bootstrap at once and trip the per-user/IP rate limiter. Override per-run with
181
+ * `--launch-delay <ms>` or persistently with `$BORG_LAUNCH_DELAY_MS`; 0 disables.
182
+ */
183
+ export const DEFAULT_LAUNCH_DELAY_MS = 2000;
184
+
185
+ /** Resolve the inter-launch stagger: flag > env > default (each must be a non-negative integer to win). */
186
+ export function resolveLaunchDelayMs(flag: number | undefined, env: string | undefined): number {
187
+ if (flag !== undefined && Number.isInteger(flag) && flag >= 0) return flag;
188
+ // Trim BEFORE the empty check: `Number(' ')` is 0, which would otherwise pass
189
+ // validation and SILENTLY DISABLE the stagger — defeating the rate-limit
190
+ // protection this feature exists to provide. Whitespace-only → fall to default.
191
+ const trimmed = env === undefined ? '' : env.trim();
192
+ const n = trimmed === '' ? NaN : Number(trimmed);
193
+ if (Number.isInteger(n) && n >= 0) return n;
194
+ return DEFAULT_LAUNCH_DELAY_MS;
195
+ }
196
+
197
+ export interface RunLaunchAllOptions {
198
+ /** Injectable clock/sleep for deterministic reconciliation tests. */
199
+ now?: () => number;
200
+ sleep?: (ms: number) => Promise<void>;
201
+ nowISO?: () => string;
202
+ borgPath?: string;
203
+ }
204
+
205
+ export async function runLaunchAll(
206
+ args: LaunchAllArgs,
207
+ deps: LaunchAllDeps,
208
+ opts: RunLaunchAllOptions = {}
209
+ ): Promise<number> {
210
+ const now = opts.now ?? Date.now;
211
+ const nowISO = opts.nowISO ?? (() => new Date().toISOString());
212
+ const borgPath = opts.borgPath ?? resolveBorgPath();
213
+ const sleep = opts.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
214
+ const launchDelayMs = resolveLaunchDelayMs(args.flags.launchDelayMs, deps.getEnv('BORG_LAUNCH_DELAY_MS'));
215
+
216
+ // 1. resolve target cube
217
+ const resolved = await resolveTargetCube(args, deps);
218
+ if ('error' in resolved) {
219
+ deps.stderr(`borg launch-all: ${resolved.error}\n`);
220
+ return 1;
221
+ }
222
+ const { cubeId, name: cubeName } = resolved;
223
+
224
+ // 2. sweep stale locks before discovery
225
+ sweepStaleLocks(deps, cubeId, now());
226
+
227
+ // 3. discover candidates
228
+ const discovered = await discoverDroneCandidates({ targetCubeId: cubeId, only: args.flags.only }, deps);
229
+ if (discovered.length === 0) {
230
+ if (args.flags.only !== undefined) {
231
+ deps.stdout(`No worktrees matched --only '${args.flags.only}' for cube '${cubeName}'\n`);
232
+ if (!looksLikeDroneLabel(args.flags.only)) {
233
+ deps.stderr(
234
+ `note: --only '${args.flags.only}' is matched best-effort by drone label; role-name matching ` +
235
+ `needs a drone session and is not available here.\n`
236
+ );
237
+ }
238
+ } else {
239
+ deps.stdout(
240
+ `No worktrees found for cube '${cubeName}' — have you run \`borg assimilate --worktree\` to create any drone seats?\n`
241
+ );
242
+ }
243
+ return 0;
244
+ }
245
+
246
+ // 4. live-drone skip (lock-file), unless --force
247
+ const lockLaunchable: DroneCandidate[] = [];
248
+ for (const c of discovered) {
249
+ const lock = isLockLive(deps, cubeId, c.worktreeDir, now());
250
+ if (lock.live && !args.flags.force) {
251
+ deps.stderr(`skipping ${c.droneLabel} (${c.worktreeDir}): appears live. Use --force to re-launch.\n`);
252
+ continue;
253
+ }
254
+ if (lock.live && args.flags.force) {
255
+ deps.stderr(`--force: re-launching ${c.droneLabel} (${c.worktreeDir}); the running session's token will be displaced.\n`);
256
+ }
257
+ lockLaunchable.push(c);
258
+ }
259
+ if (lockLaunchable.length === 0) {
260
+ deps.stdout(`All ${discovered.length} drone(s) for cube '${cubeName}' appear live; nothing to launch (use --force to re-launch).\n`);
261
+ return 0;
262
+ }
263
+
264
+ // 4b. server-liveness skip — drop seats the server reports EVICTED (gone).
265
+ // Reuses the gh#882 per-seat probe (each seat's OWN token → 410
266
+ // DRONE_EVICTED). Relaunching an evicted seat silently re-mints a fresh
267
+ // drone (the resurrection bug). So SKIP evicted; LAUNCH 'live' +
268
+ // 'indeterminate'.
269
+ //
270
+ // Launch-vs-delete asymmetry (vs `borg cleanup`, which fails SAFE): a
271
+ // LAUNCH is constructive — a transient probe failure must NOT silently
272
+ // omit a real seat (that is a new silent-stall class), so 'indeterminate'
273
+ // fails OPEN (launch anyway, with a note). Only an AUTHORITATIVE 410
274
+ // skips. --force does NOT override this skip: an evicted seat is
275
+ // server-authoritative gone, whereas --force only re-launches a
276
+ // LOCK-live (seemingly-running) session.
277
+ const launchable: DroneCandidate[] = [];
278
+ let evictedCount = 0;
279
+ let rejectedCount = 0;
280
+ let trustMismatchCount = 0;
281
+ let credentialRejectedCount = 0;
282
+ for (const c of lockLaunchable) {
283
+ let status: SeatStatus;
284
+ try {
285
+ status = await deps.probeSeat(c.sessionToken, c.apiUrl, c.serverTrustIdentity);
286
+ } catch {
287
+ status = 'indeterminate';
288
+ }
289
+ if (status === 'evicted') {
290
+ evictedCount += 1;
291
+ deps.stderr(
292
+ `skipping ${c.droneLabel} (${c.worktreeDir}): seat no longer in cube (evicted) — ` +
293
+ `run \`borg cleanup --prune\` to remove the worktree, or \`borg assimilate\` to re-seat fresh.\n`
294
+ );
295
+ continue;
296
+ }
297
+ if (status === 'rejected') {
298
+ rejectedCount += 1;
299
+ deps.stderr(
300
+ `skipping ${c.droneLabel} (${c.worktreeDir}): saved seat no longer accepted (revoked or ` +
301
+ `taken over). Recover from that worktree: ` +
302
+ `1) \`borg reset-local-seat --host ${c.apiUrl}\` (add --yes non-interactively) to clear ` +
303
+ `ONLY that worktree's saved seat; ` +
304
+ `2) ask the server operator to mint a fresh scoped invitation (the server stays running); ` +
305
+ `3) \`borg assimilate --host ${c.apiUrl} --enroll\`.\n`
306
+ );
307
+ continue;
308
+ }
309
+ // SR-seven (b): trust-mismatch is TERMINAL — a pinned-identity change is not
310
+ // fixed by launching a doomed drone, so SKIP (never fail-open).
311
+ if (status === 'trust-mismatch') {
312
+ trustMismatchCount += 1;
313
+ deps.stderr(
314
+ `skipping ${c.droneLabel} (${c.worktreeDir}): could not verify the server identity ` +
315
+ `(pinned trust changed) — this is terminal. Confirm ${c.apiUrl} is the expected server; ` +
316
+ 'if it was re-initialized, restore the expected identity before relaunching.\n'
317
+ );
318
+ continue;
319
+ }
320
+ // SR-seven (b): credential-rejected is a cause-accurate SKIP (not a fail-open
321
+ // launch) — the saved credential no longer authenticates; re-enroll first.
322
+ if (status === 'credential-rejected') {
323
+ credentialRejectedCount += 1;
324
+ deps.stderr(
325
+ `skipping ${c.droneLabel} (${c.worktreeDir}): saved credential was rejected (not a takeover) — ` +
326
+ `re-enroll from that worktree with \`borg assimilate --host ${c.apiUrl} --enroll\`.\n`
327
+ );
328
+ continue;
329
+ }
330
+ // CR5: every non-authoritative cause fails OPEN (launch anyway) with a
331
+ // cause-accurate note. Only the authoritative/terminal causes above skip.
332
+ if (status === 'unreachable') {
333
+ deps.stderr(
334
+ `note: could not reach ${c.droneLabel}'s server to confirm its seat (network/timeout) — launching anyway.\n`
335
+ );
336
+ } else if (status === 'endpoint-mismatch') {
337
+ deps.stderr(
338
+ `note: ${c.droneLabel}'s server did not recognize the drone endpoint (possible client/server version mismatch) — launching anyway.\n`
339
+ );
340
+ } else if (status === 'server-failure') {
341
+ deps.stderr(
342
+ `note: ${c.droneLabel}'s server returned an error while confirming its seat (transient) — launching anyway.\n`
343
+ );
344
+ } else if (status === 'indeterminate') {
345
+ deps.stderr(
346
+ `note: could not confirm ${c.droneLabel}'s seat is live (network/transient) — launching anyway.\n`
347
+ );
348
+ }
349
+ launchable.push(c);
350
+ }
351
+ if (launchable.length === 0) {
352
+ // Accurate cause counts — an all-rejected sweep must NOT claim "evicted"
353
+ // (and vice-versa). Only name a cause when at least one seat hit it.
354
+ const causes: string[] = [];
355
+ if (evictedCount > 0) {
356
+ causes.push(`${evictedCount} evicted`);
357
+ }
358
+ if (rejectedCount > 0) {
359
+ causes.push(`${rejectedCount} with a saved seat no longer accepted (revoked/taken over)`);
360
+ }
361
+ if (trustMismatchCount > 0) {
362
+ causes.push(`${trustMismatchCount} with a changed (terminal) server identity`);
363
+ }
364
+ if (credentialRejectedCount > 0) {
365
+ causes.push(`${credentialRejectedCount} with a rejected saved credential`);
366
+ }
367
+ const causeText = causes.length > 0 ? ` (${causes.join(', ')})` : '';
368
+ deps.stdout(
369
+ `All ${lockLaunchable.length} discovered drone(s) for cube '${cubeName}' are not launchable` +
370
+ `${causeText}; nothing to launch.\n`
371
+ );
372
+ return 0;
373
+ }
374
+
375
+ // 5. --dry-run
376
+ if (args.flags.dryRun) {
377
+ deps.stdout(`borg launch-all (dry-run): would launch ${launchable.length} drone(s) for cube '${cubeName}':\n`);
378
+ for (const c of launchable) deps.stdout(` ${c.droneLabel} ${c.worktreeDir}\n`);
379
+ return 0;
380
+ }
381
+
382
+ // 6. >6 confirmation
383
+ if (launchable.length > 6 && !args.flags.yes) {
384
+ const ans = await deps.prompt(`About to launch ${launchable.length} drones for cube '${cubeName}'. Continue? [y/N]: `);
385
+ if (ans.trim().toLowerCase() !== 'y') {
386
+ deps.stdout('Aborted.\n');
387
+ return 0;
388
+ }
389
+ }
390
+
391
+ // 7. backend selection
392
+ const sel = selectBackend(args, deps);
393
+ if ('hardFail' in sel) {
394
+ deps.stderr(sel.hardFail);
395
+ return 1;
396
+ }
397
+ const sessionName = sanitizeSessionName(cubeName);
398
+ const launchStartISO = nowISO();
399
+
400
+ // 8. dispatch
401
+ try {
402
+ if (sel.backend === 'tmux') {
403
+ const attachMode = resolveAttachMode(args, deps);
404
+ await runTmuxBackend(launchable, { sessionName, borgPath, attachMode, launchedAtISO: launchStartISO, launchDelayMs, sleep }, deps);
405
+ if (attachMode === 'none') {
406
+ if (!deps.isTTY()) {
407
+ deps.stderr(`Launching in detached mode — stdout is non-TTY. Attach manually with: tmux attach -t ${sessionName}\n`);
408
+ }
409
+ printCheatSheet(sessionName, deps);
410
+ }
411
+ } else if (sel.backend === 'windows') {
412
+ await runWindowsBackend(launchable, { borgPath, platform: deps.platform(), launchedAtISO: launchStartISO, launchDelayMs, sleep }, deps);
413
+ } else {
414
+ runPastelistBackend(launchable, borgPath, deps);
415
+ return 0; // pastelist: nothing to reconcile (operator pastes manually)
416
+ }
417
+ } catch (e) {
418
+ deps.stderr(`borg launch-all: ${e instanceof Error ? e.message : String(e)}\n`);
419
+ return 1;
420
+ }
421
+
422
+ // 9. roster reconciliation (best-effort; uses the OLD saved token from the first candidate)
423
+ const reconToken = launchable[0].sessionToken;
424
+ const reconApiUrl = launchable[0].apiUrl;
425
+ const reconServerTrustIdentity = launchable[0].serverTrustIdentity;
426
+ let statuses: Map<string, 'verified' | 'unconfirmed'> | null = null;
427
+ if (reconToken && reconApiUrl) {
428
+ statuses = await reconcileRoster(
429
+ deps,
430
+ reconToken,
431
+ reconApiUrl,
432
+ reconServerTrustIdentity,
433
+ launchStartISO,
434
+ launchable.map((c) => c.droneId),
435
+ opts.now,
436
+ opts.sleep
437
+ );
438
+ } else {
439
+ deps.stderr('roster reconciliation skipped — no session token available\n');
440
+ }
441
+
442
+ // 10. summary
443
+ deps.stdout(`\nborg launch-all: launched ${launchable.length} drones for cube '${cubeName}'\n\n`);
444
+ for (const c of launchable) {
445
+ const status = statuses ? (statuses.get(c.droneId) === 'verified' ? 'VERIFIED' : 'unconfirmed (may still be joining)') : 'launched';
446
+ deps.stdout(` ${c.droneLabel} ${c.worktreeDir} ${status}\n`);
447
+ }
448
+ deps.stdout(`\nAttach: tmux attach -t ${sessionName}\n`);
449
+ return 0;
450
+ }
@@ -0,0 +1,30 @@
1
+ // gh#556 Part 2 — the per-drone shell command + borg-binary resolution (spec §5).
2
+
3
+ import { shellEscape } from './shell-escape.js';
4
+
5
+ /**
6
+ * The borg binary that invoked launch-all (spec §5.1). `process.argv[1]` is the
7
+ * absolute path to the running borg script — deterministic, independent of $PATH
8
+ * inside the spawned window's shell (npm link / global / local .bin all work).
9
+ */
10
+ export function resolveBorgPath(): string {
11
+ return process.argv[1];
12
+ }
13
+
14
+ /**
15
+ * The shell command run inside each worktree's window/tab.
16
+ * `keepOpenOnFail` wraps a `|| read` pause so a failed assimilate doesn't close
17
+ * the tmux window before the operator reads the error (tmux convenience only;
18
+ * the pastelist backend omits it — the operator owns their own shell).
19
+ */
20
+ export function buildLaunchCommand(
21
+ worktreeDir: string,
22
+ borgPath: string,
23
+ opts: { keepOpenOnFail?: boolean } = {}
24
+ ): string {
25
+ const base = `cd ${shellEscape(worktreeDir)} && ${shellEscape(borgPath)} assimilate --here`;
26
+ if (opts.keepOpenOnFail) {
27
+ return `${base} || { echo "borg assimilate failed — press Enter to close"; read _; }`;
28
+ }
29
+ return base;
30
+ }
@@ -0,0 +1,188 @@
1
+ // gh#556 Part 2 — `borg launch-all [cube]` dependency seam.
2
+ //
3
+ // LaunchAllDeps is the full injectable seam list (per spec §10) so every
4
+ // pure module (discovery / parser / orchestrator / backends) is unit-testable
5
+ // with vi.fn() stubs. buildDefaultLaunchAllDeps() (added in launch-all-cmd
6
+ // wiring / Phase 4) wires the real-IO production modules.
7
+
8
+ import { spawnSync } from 'node:child_process';
9
+ import {
10
+ existsSync,
11
+ mkdirSync,
12
+ readFileSync,
13
+ writeFileSync,
14
+ unlinkSync,
15
+ statSync,
16
+ readdirSync,
17
+ } from 'node:fs';
18
+ import { homedir as osHomedir } from 'node:os';
19
+ import { createInterface } from 'node:readline/promises';
20
+ import type { ActiveCube } from './cubes.js';
21
+ import {
22
+ readAllProjectIdentities as cubesReadAllProjectIdentities,
23
+ getProjectCliPreferenceForPath,
24
+ findProjectRoot,
25
+ getActiveCube,
26
+ } from './cubes.js';
27
+ import { getRoster, getCube } from './remote-client.js';
28
+ import { defaultProbeSeat, type SeatStatus } from './seat-probe.js';
29
+
30
+ /** Subprocess runner — sync, returns stdout, THROWS on non-zero exit or ENOENT. */
31
+ export type RunSyncFn = (cmd: string, args: string[]) => string;
32
+
33
+ export interface LaunchAllDeps {
34
+ /** Subprocess — sync, throws on non-zero exit (git, tmux -V, ...). */
35
+ runSync: (cmd: string, args: string[], opts?: { cwd?: string }) => string;
36
+ /** Subprocess — sync, returns exit code WITHOUT throwing (tmux has-session). */
37
+ runSyncExitCode: (cmd: string, args: string[]) => number;
38
+ /**
39
+ * Interactive subprocess with INHERITED stdio (terminal handover) — used for
40
+ * `tmux attach-session` / `switch-client`, where a captured stdout would not
41
+ * render the TUI. Spec §10's capture-runSync cannot interactively attach; this
42
+ * seam completes that (real: spawnSync stdio:'inherit').
43
+ */
44
+ attachInteractive: (cmd: string, args: string[]) => void;
45
+ /** Absolute path of the current working directory. */
46
+ cwd: () => string;
47
+ /** True iff the path exists on disk. */
48
+ pathExists: (p: string) => boolean;
49
+ /** $HOME / os.homedir(). */
50
+ homedir: () => string;
51
+ /** mkdir -p (recursive; no chmod of existing parents). */
52
+ mkdirp: (dir: string) => void;
53
+ /** Read a file; returns null on ENOENT (never throws for absence). */
54
+ readFileOpt: (p: string) => string | null;
55
+ /** Write a file (mode default 0o600). */
56
+ writeFile: (p: string, content: string, mode?: number) => void;
57
+ /** Unlink a file; does NOT throw on ENOENT. */
58
+ unlinkOpt: (p: string) => void;
59
+ /** mtime in ms, or null if absent. */
60
+ statMtime: (p: string) => number | null;
61
+ /** Directory entries, or [] if absent. */
62
+ listDir: (p: string) => string[];
63
+ /** Roster call (wraps getRoster from remote-client.ts). */
64
+ getRoster: (
65
+ token: string,
66
+ apiUrl: string,
67
+ since?: string,
68
+ serverTrustIdentity?: string,
69
+ ) => Promise<{ drones: Array<{ id: string; seen_since?: boolean }> }>;
70
+ /** getCube for --only tier-2 role-name resolution (best-effort). */
71
+ getCube: (
72
+ apiUrl: string,
73
+ token: string,
74
+ cubeId: string
75
+ ) => Promise<{ id: string; name: string; roles: Array<{ id: string; name: string }> }>;
76
+ /**
77
+ * Probe ONE saved seat's server-side liveness using ITS OWN token (gh#877
78
+ * reuse via seat-probe.ts). Lets launch-all skip evicted seats instead
79
+ * of relaunching them (which silently re-mints a fresh drone — resurrection).
80
+ */
81
+ probeSeat: (
82
+ sessionToken: string,
83
+ apiUrl: string,
84
+ serverTrustIdentity?: string,
85
+ ) => Promise<SeatStatus>;
86
+ /** Saved CLI preference for a worktree path (launch.json). */
87
+ getCliPreferenceForPath: (projectPath: string) => Promise<'claude' | 'codex' | 'opencode' | null>;
88
+ /** All persisted project identities from cubes.json. */
89
+ readAllProjectIdentities: () => Promise<Array<{ projectPath: string; cube: ActiveCube }>>;
90
+ /** findProjectRoot (cubes.ts export). */
91
+ findProjectRoot: (dir: string) => string;
92
+ /** Active cube for the cwd (cubes.ts getActiveCube), null if none. */
93
+ getActiveCube: () => Promise<ActiveCube | null>;
94
+ /** Interactive confirmation prompt. */
95
+ prompt: (message: string) => Promise<string>;
96
+ /** TTY check (stdin). */
97
+ isTTY: () => boolean;
98
+ /** Environment variable accessor (e.g. $BORG_TERMINAL, $TMUX). */
99
+ getEnv: (name: string) => string | undefined;
100
+ /** process.platform (injectable for native-Windows/WSL backend-selection tests). */
101
+ platform: () => NodeJS.Platform;
102
+ /** stderr writer. */
103
+ stderr: (line: string) => void;
104
+ /** stdout writer. */
105
+ stdout: (line: string) => void;
106
+ }
107
+
108
+ /** Real-IO factory wiring production modules (spec §10). Test code stubs LaunchAllDeps directly. */
109
+ export function buildDefaultLaunchAllDeps(): LaunchAllDeps {
110
+ return {
111
+ runSync: (cmd, args, opts) => {
112
+ const r = spawnSync(cmd, args, { encoding: 'utf-8', cwd: opts?.cwd });
113
+ if (r.error) throw r.error;
114
+ if (r.status !== 0) {
115
+ throw new Error(`${cmd} exited ${r.status}: ${(r.stderr ?? '').toString().trim()}`);
116
+ }
117
+ return (r.stdout ?? '').toString();
118
+ },
119
+ runSyncExitCode: (cmd, args) => spawnSync(cmd, args, { encoding: 'utf-8' }).status ?? 1,
120
+ attachInteractive: (cmd, args) => {
121
+ spawnSync(cmd, args, { stdio: 'inherit' });
122
+ },
123
+ cwd: () => process.cwd(),
124
+ pathExists: (p) => existsSync(p),
125
+ homedir: () => osHomedir(),
126
+ mkdirp: (dir) => {
127
+ mkdirSync(dir, { recursive: true });
128
+ },
129
+ readFileOpt: (p) => {
130
+ try {
131
+ return readFileSync(p, 'utf-8');
132
+ } catch {
133
+ return null;
134
+ }
135
+ },
136
+ writeFile: (p, content, mode) => {
137
+ writeFileSync(p, content, { mode: mode ?? 0o600 });
138
+ },
139
+ unlinkOpt: (p) => {
140
+ try {
141
+ unlinkSync(p);
142
+ } catch {
143
+ /* ENOENT ignored */
144
+ }
145
+ },
146
+ statMtime: (p) => {
147
+ try {
148
+ return statSync(p).mtimeMs;
149
+ } catch {
150
+ return null;
151
+ }
152
+ },
153
+ listDir: (p) => {
154
+ try {
155
+ return readdirSync(p);
156
+ } catch {
157
+ return [];
158
+ }
159
+ },
160
+ getRoster: (token, apiUrl, since, serverTrustIdentity) =>
161
+ getRoster(token, apiUrl, since, serverTrustIdentity),
162
+ // getCube uses the drone session token via authedFetch (cubeId-only); apiUrl/token unused.
163
+ getCube: (_apiUrl, _token, cubeId) => getCube(cubeId),
164
+ probeSeat: (sessionToken, apiUrl, serverTrustIdentity) =>
165
+ defaultProbeSeat(sessionToken, apiUrl, serverTrustIdentity),
166
+ getCliPreferenceForPath: (projectPath) => getProjectCliPreferenceForPath(projectPath),
167
+ readAllProjectIdentities: () => cubesReadAllProjectIdentities(),
168
+ findProjectRoot: (dir) => findProjectRoot(dir),
169
+ getActiveCube: () => getActiveCube(),
170
+ prompt: async (message) => {
171
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
172
+ try {
173
+ return (await rl.question(message)).trim();
174
+ } finally {
175
+ rl.close();
176
+ }
177
+ },
178
+ isTTY: () => process.stdin.isTTY === true,
179
+ getEnv: (name) => process.env[name],
180
+ platform: () => process.platform,
181
+ stderr: (line) => {
182
+ process.stderr.write(line);
183
+ },
184
+ stdout: (line) => {
185
+ process.stdout.write(line);
186
+ },
187
+ };
188
+ }