borgmcp 1.1.15 → 2.0.1

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 (459) 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.map +1 -0
  71. package/dist/codex-app-wake.js +432 -2
  72. package/dist/codex-app-wake.js.map +1 -0
  73. package/dist/codex-launch.d.ts.map +1 -0
  74. package/dist/codex-launch.js +102 -2
  75. package/dist/codex-launch.js.map +1 -0
  76. package/dist/codex-remote.d.ts +6 -1
  77. package/dist/codex-remote.d.ts.map +1 -0
  78. package/dist/codex-remote.js +394 -1
  79. package/dist/codex-remote.js.map +1 -0
  80. package/dist/codex-wake-resolve.d.ts.map +1 -0
  81. package/dist/codex-wake-resolve.js +127 -1
  82. package/dist/codex-wake-resolve.js.map +1 -0
  83. package/dist/config-utils.d.ts +2 -2
  84. package/dist/config-utils.d.ts.map +1 -0
  85. package/dist/config-utils.js +766 -3
  86. package/dist/config-utils.js.map +1 -0
  87. package/dist/config.d.ts +98 -61
  88. package/dist/config.d.ts.map +1 -0
  89. package/dist/config.js +426 -1
  90. package/dist/config.js.map +1 -0
  91. package/dist/console-prefix.d.ts.map +1 -0
  92. package/dist/console-prefix.js +96 -1
  93. package/dist/console-prefix.js.map +1 -0
  94. package/dist/cube-name.d.ts.map +1 -0
  95. package/dist/cube-name.js +67 -1
  96. package/dist/cube-name.js.map +1 -0
  97. package/dist/cubes.d.ts +126 -18
  98. package/dist/cubes.d.ts.map +1 -0
  99. package/dist/cubes.js +412 -4
  100. package/dist/cubes.js.map +1 -0
  101. package/dist/debug.d.ts.map +1 -0
  102. package/dist/debug.js +71 -1
  103. package/dist/debug.js.map +1 -0
  104. package/dist/direct-log.d.ts.map +1 -0
  105. package/dist/direct-log.js +11 -1
  106. package/dist/direct-log.js.map +1 -0
  107. package/dist/docs-sections.d.ts +11 -10
  108. package/dist/docs-sections.d.ts.map +1 -0
  109. package/dist/docs-sections.js +113 -3
  110. package/dist/docs-sections.js.map +1 -0
  111. package/dist/drone-lifecycle.d.ts +5 -21
  112. package/dist/drone-lifecycle.d.ts.map +1 -0
  113. package/dist/drone-lifecycle.js +57 -8
  114. package/dist/drone-lifecycle.js.map +1 -0
  115. package/dist/drone-management.d.ts +29 -0
  116. package/dist/drone-management.d.ts.map +1 -0
  117. package/dist/drone-management.js +109 -0
  118. package/dist/drone-management.js.map +1 -0
  119. package/dist/ensure-mcp-config.d.ts.map +1 -0
  120. package/dist/ensure-mcp-config.js +36 -1
  121. package/dist/ensure-mcp-config.js.map +1 -0
  122. package/dist/evict-drone.d.ts +10 -2
  123. package/dist/evict-drone.d.ts.map +1 -0
  124. package/dist/evict-drone.js +60 -1
  125. package/dist/evict-drone.js.map +1 -0
  126. package/dist/gc-orphan-inboxes.d.ts.map +1 -0
  127. package/dist/gc-orphan-inboxes.js +177 -1
  128. package/dist/gc-orphan-inboxes.js.map +1 -0
  129. package/dist/inbox-monitor.d.ts +1 -1
  130. package/dist/inbox-monitor.d.ts.map +1 -0
  131. package/dist/inbox-monitor.js +856 -3
  132. package/dist/inbox-monitor.js.map +1 -0
  133. package/dist/index.d.ts +6 -4
  134. package/dist/index.d.ts.map +1 -0
  135. package/dist/index.js +1034 -37
  136. package/dist/index.js.map +1 -0
  137. package/dist/launch-all-cmd.d.ts.map +1 -0
  138. package/dist/launch-all-cmd.js +377 -37
  139. package/dist/launch-all-cmd.js.map +1 -0
  140. package/dist/launch-all-command.d.ts.map +1 -0
  141. package/dist/launch-all-command.js +24 -1
  142. package/dist/launch-all-command.js.map +1 -0
  143. package/dist/launch-all-deps.d.ts +3 -8
  144. package/dist/launch-all-deps.d.ts.map +1 -0
  145. package/dist/launch-all-deps.js +99 -1
  146. package/dist/launch-all-deps.js.map +1 -0
  147. package/dist/launch-all-discovery.d.ts +1 -0
  148. package/dist/launch-all-discovery.d.ts.map +1 -0
  149. package/dist/launch-all-discovery.js +90 -4
  150. package/dist/launch-all-discovery.js.map +1 -0
  151. package/dist/launch-all-locks.d.ts.map +1 -0
  152. package/dist/launch-all-locks.js +54 -1
  153. package/dist/launch-all-locks.js.map +1 -0
  154. package/dist/launch-gate.d.ts +2 -2
  155. package/dist/launch-gate.d.ts.map +1 -0
  156. package/dist/launch-gate.js +73 -1
  157. package/dist/launch-gate.js.map +1 -0
  158. package/dist/lifecycle-log-guard.d.ts.map +1 -0
  159. package/dist/lifecycle-log-guard.js +93 -2
  160. package/dist/lifecycle-log-guard.js.map +1 -0
  161. package/dist/list-roles-render.d.ts.map +1 -0
  162. package/dist/list-roles-render.js +40 -6
  163. package/dist/list-roles-render.js.map +1 -0
  164. package/dist/local-log-routing.d.ts +19 -0
  165. package/dist/local-log-routing.d.ts.map +1 -0
  166. package/dist/local-log-routing.js +67 -0
  167. package/dist/local-log-routing.js.map +1 -0
  168. package/dist/local-manage-tool-result.d.ts +8 -0
  169. package/dist/local-manage-tool-result.d.ts.map +1 -0
  170. package/dist/local-manage-tool-result.js +10 -0
  171. package/dist/local-manage-tool-result.js.map +1 -0
  172. package/dist/local-server-cursor.d.ts +34 -0
  173. package/dist/local-server-cursor.d.ts.map +1 -0
  174. package/dist/local-server-cursor.js +151 -0
  175. package/dist/local-server-cursor.js.map +1 -0
  176. package/dist/log-audit.d.ts.map +1 -0
  177. package/dist/log-audit.js +194 -3
  178. package/dist/log-audit.js.map +1 -0
  179. package/dist/log-stream.d.ts +31 -18
  180. package/dist/log-stream.d.ts.map +1 -0
  181. package/dist/log-stream.js +1214 -12
  182. package/dist/log-stream.js.map +1 -0
  183. package/dist/model-presets.d.ts.map +1 -0
  184. package/dist/model-presets.js +19 -1
  185. package/dist/model-presets.js.map +1 -0
  186. package/dist/name-validator.d.ts.map +1 -0
  187. package/dist/name-validator.js +22 -1
  188. package/dist/name-validator.js.map +1 -0
  189. package/dist/opencode-drone.d.ts.map +1 -0
  190. package/dist/opencode-drone.js +438 -4
  191. package/dist/opencode-drone.js.map +1 -0
  192. package/dist/opencode-plugin.d.ts +1 -1
  193. package/dist/opencode-plugin.d.ts.map +1 -0
  194. package/dist/opencode-plugin.js +29 -4
  195. package/dist/opencode-plugin.js.map +1 -0
  196. package/dist/parse-assimilate-args.d.ts +1 -1
  197. package/dist/parse-assimilate-args.d.ts.map +1 -0
  198. package/dist/parse-assimilate-args.js +125 -1
  199. package/dist/parse-assimilate-args.js.map +1 -0
  200. package/dist/parse-launch-all-args.d.ts.map +1 -0
  201. package/dist/parse-launch-all-args.js +70 -1
  202. package/dist/parse-launch-all-args.js.map +1 -0
  203. package/dist/postinstall-banner.d.ts.map +1 -0
  204. package/dist/postinstall-banner.js +27 -2
  205. package/dist/postinstall-banner.js.map +1 -0
  206. package/dist/postinstall.d.ts.map +1 -0
  207. package/dist/postinstall.js +32 -5
  208. package/dist/postinstall.js.map +1 -0
  209. package/dist/readiness-probe.d.ts.map +1 -0
  210. package/dist/readiness-probe.js +9 -1
  211. package/dist/readiness-probe.js.map +1 -0
  212. package/dist/regen-format.d.ts.map +1 -0
  213. package/dist/regen-format.js +545 -13
  214. package/dist/regen-format.js.map +1 -0
  215. package/dist/regen.d.ts.map +1 -0
  216. package/dist/regen.js +141 -5
  217. package/dist/regen.js.map +1 -0
  218. package/dist/remote-client.d.ts +65 -175
  219. package/dist/remote-client.d.ts.map +1 -0
  220. package/dist/remote-client.js +942 -1
  221. package/dist/remote-client.js.map +1 -0
  222. package/dist/reset-local-seat-cmd.d.ts +50 -0
  223. package/dist/reset-local-seat-cmd.d.ts.map +1 -0
  224. package/dist/reset-local-seat-cmd.js +169 -0
  225. package/dist/reset-local-seat-cmd.js.map +1 -0
  226. package/dist/role-match.d.ts.map +1 -0
  227. package/dist/role-match.js +23 -1
  228. package/dist/role-match.js.map +1 -0
  229. package/dist/role-resolver.d.ts.map +1 -0
  230. package/dist/role-resolver.js +60 -1
  231. package/dist/role-resolver.js.map +1 -0
  232. package/dist/roster-render.d.ts.map +1 -0
  233. package/dist/roster-render.js +121 -3
  234. package/dist/roster-render.js.map +1 -0
  235. package/dist/seat-probe.d.ts +32 -13
  236. package/dist/seat-probe.d.ts.map +1 -0
  237. package/dist/seat-probe.js +78 -1
  238. package/dist/seat-probe.js.map +1 -0
  239. package/dist/seat-store.d.ts +76 -0
  240. package/dist/seat-store.d.ts.map +1 -0
  241. package/dist/seat-store.js +309 -0
  242. package/dist/seat-store.js.map +1 -0
  243. package/dist/seats.d.ts +303 -0
  244. package/dist/seats.d.ts.map +1 -0
  245. package/dist/seats.js +551 -0
  246. package/dist/seats.js.map +1 -0
  247. package/dist/self-path.d.ts +27 -0
  248. package/dist/self-path.d.ts.map +1 -0
  249. package/dist/self-path.js +43 -0
  250. package/dist/self-path.js.map +1 -0
  251. package/dist/server-endpoint.d.ts +9 -0
  252. package/dist/server-endpoint.d.ts.map +1 -0
  253. package/dist/server-endpoint.js +38 -0
  254. package/dist/server-endpoint.js.map +1 -0
  255. package/dist/server-errors.d.ts +50 -0
  256. package/dist/server-errors.d.ts.map +1 -0
  257. package/dist/server-errors.js +79 -0
  258. package/dist/server-errors.js.map +1 -0
  259. package/dist/server-handshake.d.ts +197 -0
  260. package/dist/server-handshake.d.ts.map +1 -0
  261. package/dist/server-handshake.js +505 -0
  262. package/dist/server-handshake.js.map +1 -0
  263. package/dist/server-response.d.ts +3 -0
  264. package/dist/server-response.d.ts.map +1 -0
  265. package/dist/server-response.js +67 -0
  266. package/dist/server-response.js.map +1 -0
  267. package/dist/server-trust.d.ts +14 -0
  268. package/dist/server-trust.d.ts.map +1 -0
  269. package/dist/server-trust.js +225 -0
  270. package/dist/server-trust.js.map +1 -0
  271. package/dist/setup-confirm.d.ts +3 -3
  272. package/dist/setup-confirm.d.ts.map +1 -0
  273. package/dist/setup-confirm.js +126 -2
  274. package/dist/setup-confirm.js.map +1 -0
  275. package/dist/setup.d.ts +5 -3
  276. package/dist/setup.d.ts.map +1 -0
  277. package/dist/setup.js +169 -46
  278. package/dist/setup.js.map +1 -0
  279. package/dist/shell-escape.d.ts.map +1 -0
  280. package/dist/shell-escape.js +22 -1
  281. package/dist/shell-escape.js.map +1 -0
  282. package/dist/spawn.d.ts +2 -2
  283. package/dist/spawn.d.ts.map +1 -0
  284. package/dist/spawn.js +29 -10
  285. package/dist/spawn.js.map +1 -0
  286. package/dist/startup-services.d.ts +0 -1
  287. package/dist/startup-services.d.ts.map +1 -0
  288. package/dist/startup-services.js +26 -1
  289. package/dist/startup-services.js.map +1 -0
  290. package/dist/stream-owner.d.ts +6 -2
  291. package/dist/stream-owner.d.ts.map +1 -0
  292. package/dist/stream-owner.js +547 -2
  293. package/dist/stream-owner.js.map +1 -0
  294. package/dist/stream-status.d.ts +1 -1
  295. package/dist/stream-status.d.ts.map +1 -0
  296. package/dist/stream-status.js +268 -3
  297. package/dist/stream-status.js.map +1 -0
  298. package/dist/sync-roles-render.d.ts.map +1 -0
  299. package/dist/sync-roles-render.js +118 -3
  300. package/dist/sync-roles-render.js.map +1 -0
  301. package/dist/sync.d.ts +1 -1
  302. package/dist/sync.d.ts.map +1 -0
  303. package/dist/sync.js +286 -22
  304. package/dist/sync.js.map +1 -0
  305. package/dist/terminal-title.d.ts.map +1 -0
  306. package/dist/terminal-title.js +68 -1
  307. package/dist/terminal-title.js.map +1 -0
  308. package/dist/token-store.d.ts +17 -111
  309. package/dist/token-store.d.ts.map +1 -0
  310. package/dist/token-store.js +81 -1
  311. package/dist/token-store.js.map +1 -0
  312. package/dist/tool-manifest.d.ts +4 -8
  313. package/dist/tool-manifest.d.ts.map +1 -0
  314. package/dist/tool-manifest.js +554 -1
  315. package/dist/tool-manifest.js.map +1 -0
  316. package/dist/tool-scope.d.ts +8 -12
  317. package/dist/tool-scope.d.ts.map +1 -0
  318. package/dist/tool-scope.js +97 -1
  319. package/dist/tool-scope.js.map +1 -0
  320. package/dist/types.d.ts +1 -5
  321. package/dist/types.d.ts.map +1 -0
  322. package/dist/types.js +5 -0
  323. package/dist/types.js.map +1 -0
  324. package/dist/unknown-subcommand.d.ts +1 -1
  325. package/dist/unknown-subcommand.d.ts.map +1 -0
  326. package/dist/unknown-subcommand.js +37 -1
  327. package/dist/unknown-subcommand.js.map +1 -0
  328. package/dist/version.d.ts +1 -1
  329. package/dist/version.d.ts.map +1 -0
  330. package/dist/version.js +78 -2
  331. package/dist/version.js.map +1 -0
  332. package/dist/working-repo.d.ts.map +1 -0
  333. package/dist/working-repo.js +81 -1
  334. package/dist/working-repo.js.map +1 -0
  335. package/dist/worktree-lifecycle.d.ts.map +1 -0
  336. package/dist/worktree-lifecycle.js +205 -2
  337. package/dist/worktree-lifecycle.js.map +1 -0
  338. package/docs/EXTRACTION_PROVENANCE.md +40 -0
  339. package/docs/LOCAL_SERVER.md +103 -0
  340. package/docs/RELEASING.md +120 -0
  341. package/package.json +52 -34
  342. package/src/agent-runtime.ts +69 -0
  343. package/src/assimilate-cmd.ts +1864 -0
  344. package/src/assimilate-deps.ts +444 -0
  345. package/src/assimilate-guard.ts +84 -0
  346. package/src/assimilate-welcome.ts +39 -0
  347. package/src/auth-env.ts +21 -0
  348. package/src/backends/launch-all-pastelist.ts +18 -0
  349. package/src/backends/launch-all-tmux.ts +75 -0
  350. package/src/backends/launch-all-windows.ts +110 -0
  351. package/src/bare-launch-menu.ts +148 -0
  352. package/src/claude-launch-args.ts +35 -0
  353. package/src/claude.ts +561 -0
  354. package/src/cleanup-cmd.ts +581 -0
  355. package/src/clear-rewake-core.ts +34 -0
  356. package/src/clear-rewake.ts +18 -0
  357. package/src/cli-help.ts +116 -0
  358. package/src/cli-platform.ts +127 -0
  359. package/src/cli-tool-approval.ts +716 -0
  360. package/src/codex-app-server.ts +238 -0
  361. package/src/codex-app-wake.ts +512 -0
  362. package/src/codex-launch.ts +140 -0
  363. package/src/codex-remote.ts +500 -0
  364. package/src/codex-wake-resolve.ts +155 -0
  365. package/src/config-utils.ts +811 -0
  366. package/src/config.ts +595 -0
  367. package/src/console-prefix.ts +99 -0
  368. package/src/cube-name.ts +69 -0
  369. package/src/cubes.ts +583 -0
  370. package/src/debug.ts +73 -0
  371. package/src/direct-log.ts +9 -0
  372. package/src/docs-sections.ts +129 -0
  373. package/src/drone-lifecycle.ts +62 -0
  374. package/src/drone-management.ts +162 -0
  375. package/src/ensure-mcp-config.ts +54 -0
  376. package/src/evict-drone.ts +82 -0
  377. package/src/gc-orphan-inboxes.ts +229 -0
  378. package/src/inbox-monitor.ts +1005 -0
  379. package/src/index.ts +1201 -0
  380. package/src/launch-all-cmd.ts +450 -0
  381. package/src/launch-all-command.ts +30 -0
  382. package/src/launch-all-deps.ts +188 -0
  383. package/src/launch-all-discovery.ts +118 -0
  384. package/src/launch-all-locks.ts +74 -0
  385. package/src/launch-gate.ts +80 -0
  386. package/src/lifecycle-log-guard.ts +144 -0
  387. package/src/list-roles-render.ts +52 -0
  388. package/src/local-log-routing.ts +106 -0
  389. package/src/local-manage-tool-result.ts +10 -0
  390. package/src/local-server-cursor.ts +188 -0
  391. package/src/log-audit.ts +207 -0
  392. package/src/log-stream.ts +1505 -0
  393. package/src/model-presets.ts +22 -0
  394. package/src/name-validator.ts +23 -0
  395. package/src/opencode-drone.ts +528 -0
  396. package/src/opencode-plugin.ts +38 -0
  397. package/src/parse-assimilate-args.ts +125 -0
  398. package/src/parse-launch-all-args.ts +100 -0
  399. package/src/postinstall-banner.ts +37 -0
  400. package/src/postinstall.ts +36 -0
  401. package/src/readiness-probe.ts +12 -0
  402. package/src/regen-format.ts +651 -0
  403. package/src/regen.ts +150 -0
  404. package/src/remote-client.ts +1379 -0
  405. package/src/reset-local-seat-cmd.ts +227 -0
  406. package/src/role-match.ts +29 -0
  407. package/src/role-resolver.ts +93 -0
  408. package/src/roster-render.ts +196 -0
  409. package/src/seat-probe.ts +115 -0
  410. package/src/seat-store.ts +333 -0
  411. package/src/seats.ts +741 -0
  412. package/src/self-path.ts +51 -0
  413. package/src/server-endpoint.ts +38 -0
  414. package/src/server-errors.ts +93 -0
  415. package/src/server-handshake.ts +783 -0
  416. package/src/server-response.ts +76 -0
  417. package/src/server-trust.ts +245 -0
  418. package/src/setup-confirm.ts +171 -0
  419. package/src/setup.ts +186 -0
  420. package/src/shell-escape.ts +22 -0
  421. package/src/spawn.ts +31 -0
  422. package/src/startup-services.ts +36 -0
  423. package/src/stream-owner.ts +686 -0
  424. package/src/stream-status.ts +337 -0
  425. package/src/sync-roles-render.ts +163 -0
  426. package/src/sync.ts +363 -0
  427. package/src/terminal-title.ts +76 -0
  428. package/src/token-store.ts +102 -0
  429. package/src/tool-manifest.ts +610 -0
  430. package/src/tool-scope.ts +110 -0
  431. package/src/types.ts +5 -0
  432. package/src/unknown-subcommand.ts +35 -0
  433. package/src/version.ts +82 -0
  434. package/src/working-repo.ts +99 -0
  435. package/src/worktree-lifecycle.ts +254 -0
  436. package/dist/auth-recovery.d.ts +0 -33
  437. package/dist/auth-recovery.js +0 -1
  438. package/dist/auth.d.ts +0 -103
  439. package/dist/auth.js +0 -39
  440. package/dist/device-auth.d.ts +0 -75
  441. package/dist/device-auth.js +0 -1
  442. package/dist/drone-address.d.ts +0 -26
  443. package/dist/drone-address.js +0 -1
  444. package/dist/get-started.d.ts +0 -20
  445. package/dist/get-started.js +0 -2
  446. package/dist/health-beat.d.ts +0 -134
  447. package/dist/health-beat.js +0 -1
  448. package/dist/role-section.d.ts +0 -110
  449. package/dist/role-section.js +0 -8
  450. package/dist/setup-action.d.ts +0 -20
  451. package/dist/setup-action.js +0 -1
  452. package/dist/stale-version-check.d.ts +0 -40
  453. package/dist/stale-version-check.js +0 -1
  454. package/dist/subscription-retry.d.ts +0 -40
  455. package/dist/subscription-retry.js +0 -1
  456. package/dist/templates.d.ts +0 -107
  457. package/dist/templates.js +0 -359
  458. package/dist/token-crypto.d.ts +0 -50
  459. package/dist/token-crypto.js +0 -1
@@ -0,0 +1,686 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { promises as fs } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
7
+ const STREAM_LOCKS_DIR = path.join(homedir(), '.config', 'borgmcp', 'stream-locks');
8
+ const OWNER_FILE = 'owner.json';
9
+ const TAKEOVER_FILE = 'takeover.json';
10
+ const SCHEMA_VERSION = 1;
11
+
12
+ export const STREAM_OWNER_STALE_MS = 70_000;
13
+ /** Grace window for mkdir→owner.json initialization before an empty lock is reclaimable. */
14
+ export const STREAM_OWNER_INIT_STALE_MS = 5_000;
15
+ export const STREAM_OWNER_TAKEOVER_STALE_MS = 5_000;
16
+
17
+ const processNonce = randomUUID();
18
+ const processStartedAt = new Date().toISOString();
19
+
20
+ export interface StreamOwnerRecord {
21
+ schemaVersion: number;
22
+ pid: number;
23
+ processNonce: string;
24
+ cwd: string;
25
+ startedAt: string;
26
+ heartbeatAt: string;
27
+ }
28
+
29
+ export interface StreamOwnershipSnapshot {
30
+ state: 'owner' | 'owned-by-other-process' | 'initializing' | 'orphaned-initialization' | 'unowned';
31
+ pid?: number;
32
+ processNonce?: string;
33
+ cwd?: string;
34
+ startedAt?: string;
35
+ heartbeatAt?: string;
36
+ ageMs?: number;
37
+ lockPath?: string;
38
+ /** Opened-directory identity used to bind inspection to later takeover. */
39
+ lockDev?: number;
40
+ lockIno?: number;
41
+ }
42
+
43
+ export interface StreamLease {
44
+ lockPath: string;
45
+ record: StreamOwnerRecord;
46
+ refresh(): Promise<boolean>;
47
+ release(): Promise<void>;
48
+ }
49
+
50
+ export interface StreamOwnerDeps {
51
+ now?: () => Date;
52
+ pid?: number;
53
+ cwd?: string;
54
+ locksDir?: string;
55
+ processNonce?: string;
56
+ processStartedAt?: string;
57
+ isPidAlive?: (pid: number) => boolean;
58
+ beforeTakeoverVerify?: (takeoverPath: string) => Promise<void>;
59
+ beforeLeaseRefreshMutation?: (lockPath: string) => Promise<void>;
60
+ beforeLeaseReleaseMutation?: (lockPath: string) => Promise<void>;
61
+ /** Initialization/refresh writer seam for failure-path regression tests. */
62
+ writeRecord?: (lockPath: string, record: StreamOwnerRecord) => Promise<void>;
63
+ }
64
+
65
+ export function streamLockPath(
66
+ cubeId: string,
67
+ droneId: string,
68
+ locksDir = STREAM_LOCKS_DIR
69
+ ): string {
70
+ assertUuid('cubeId', cubeId);
71
+ assertUuid('droneId', droneId);
72
+ return path.join(locksDir, cubeId, `${droneId}.lock`);
73
+ }
74
+
75
+ export async function acquireStreamLease(
76
+ cubeId: string,
77
+ droneId: string,
78
+ staleMs = STREAM_OWNER_STALE_MS,
79
+ deps: StreamOwnerDeps = {}
80
+ ): Promise<StreamLease | null> {
81
+ const lockPath = streamLockPath(cubeId, droneId, deps.locksDir);
82
+ await fs.mkdir(path.dirname(lockPath), { recursive: true, mode: 0o700 });
83
+
84
+ if (!(await recoverTakeoverClaim(lockPath, staleMs, deps))) return null;
85
+
86
+ const lease = await tryCreateLease(lockPath, deps);
87
+ if (lease) return lease;
88
+
89
+ const snapshot = await readOwnershipSnapshot(cubeId, droneId, deps);
90
+ if (snapshot.state === 'unowned') {
91
+ return tryCreateLease(lockPath, deps);
92
+ }
93
+ if (snapshot.state === 'initializing') {
94
+ return null;
95
+ }
96
+ if (snapshot.state === 'orphaned-initialization') {
97
+ if (!(await moveStaleLockAside(lockPath, snapshot, staleMs, deps))) return null;
98
+ return tryCreateLease(lockPath, deps);
99
+ }
100
+ if (snapshot.state !== 'owned-by-other-process') {
101
+ return null;
102
+ }
103
+
104
+ const stale = (snapshot.ageMs ?? 0) > staleMs;
105
+ const pidDead =
106
+ typeof snapshot.pid === 'number' &&
107
+ !isPidAlive(snapshot.pid, deps);
108
+ if (!stale && !pidDead) {
109
+ return null;
110
+ }
111
+
112
+ if (!(await moveStaleLockAside(lockPath, snapshot, staleMs, deps))) {
113
+ return null;
114
+ }
115
+ return tryCreateLease(lockPath, deps);
116
+ }
117
+
118
+ export async function readOwnershipSnapshot(
119
+ cubeId: string,
120
+ droneId: string,
121
+ deps: StreamOwnerDeps = {}
122
+ ): Promise<StreamOwnershipSnapshot> {
123
+ const lockPath = streamLockPath(cubeId, droneId, deps.locksDir);
124
+ const inspected = await readBoundOwner(lockPath);
125
+ if (!inspected) return { state: 'unowned', lockPath };
126
+ const { raw, stat: lockStat } = inspected;
127
+ if (raw === null) {
128
+ const now = (deps.now ?? (() => new Date()))();
129
+ const ageMs = Math.max(0, now.getTime() - lockStat.mtimeMs);
130
+ return {
131
+ state: ageMs >= STREAM_OWNER_INIT_STALE_MS
132
+ ? 'orphaned-initialization'
133
+ : 'initializing',
134
+ ageMs,
135
+ lockPath,
136
+ lockDev: lockStat.dev,
137
+ lockIno: lockStat.ino,
138
+ };
139
+ }
140
+
141
+ let parsed: any;
142
+ try {
143
+ parsed = JSON.parse(raw);
144
+ } catch {
145
+ return {
146
+ state: 'owned-by-other-process',
147
+ lockPath,
148
+ ageMs: Number.POSITIVE_INFINITY,
149
+ lockDev: lockStat.dev,
150
+ lockIno: lockStat.ino,
151
+ };
152
+ }
153
+
154
+ if (!isRecord(parsed)) {
155
+ return {
156
+ state: 'owned-by-other-process',
157
+ lockPath,
158
+ ageMs: Number.POSITIVE_INFINITY,
159
+ lockDev: lockStat.dev,
160
+ lockIno: lockStat.ino,
161
+ };
162
+ }
163
+
164
+ const now = (deps.now ?? (() => new Date()))();
165
+ const heartbeatMs = Date.parse(parsed.heartbeatAt);
166
+ const ageMs = Number.isFinite(heartbeatMs) ? now.getTime() - heartbeatMs : Number.POSITIVE_INFINITY;
167
+ const ownPid = deps.pid ?? process.pid;
168
+ const ownNonce = deps.processNonce ?? processNonce;
169
+ const state =
170
+ parsed.pid === ownPid && parsed.processNonce === ownNonce
171
+ ? 'owner'
172
+ : 'owned-by-other-process';
173
+ return {
174
+ state,
175
+ pid: parsed.pid,
176
+ processNonce: parsed.processNonce,
177
+ cwd: parsed.cwd,
178
+ startedAt: parsed.startedAt,
179
+ heartbeatAt: parsed.heartbeatAt,
180
+ ageMs,
181
+ lockPath,
182
+ lockDev: lockStat.dev,
183
+ lockIno: lockStat.ino,
184
+ };
185
+ }
186
+
187
+ async function tryCreateLease(
188
+ lockPath: string,
189
+ deps: StreamOwnerDeps
190
+ ): Promise<StreamLease | null> {
191
+ if (await pathExists(takeoverPath(lockPath))) return null;
192
+ try {
193
+ await fs.mkdir(lockPath, { mode: 0o700 });
194
+ } catch (err: any) {
195
+ if (err?.code === 'EEXIST') return null;
196
+ throw err;
197
+ }
198
+
199
+ const record = makeRecord(deps);
200
+ try {
201
+ await (deps.writeRecord ?? writeRecord)(lockPath, record);
202
+ if (await pathExists(takeoverPath(lockPath))) {
203
+ await cleanupFailedInitialization(lockPath, record).catch(() => {});
204
+ return null;
205
+ }
206
+ } catch (err) {
207
+ await cleanupFailedInitialization(lockPath, record).catch(() => {});
208
+ throw err;
209
+ }
210
+ const created = await readBoundOwner(lockPath);
211
+ const createdRecord = created?.raw ? parseOwnershipRecord(created.raw) : null;
212
+ if (
213
+ !created || !createdRecord ||
214
+ !sameOwner(record, createdRecord)
215
+ ) {
216
+ await cleanupFailedInitialization(lockPath, record).catch(() => {});
217
+ throw new Error('Borg stream lease initialization lost ownership');
218
+ }
219
+ return makeLease(
220
+ lockPath,
221
+ record,
222
+ { dev: created.stat.dev, ino: created.stat.ino },
223
+ deps,
224
+ );
225
+ }
226
+
227
+ /**
228
+ * Remove a mkdir-without-owner partial state after initialization fails. Rename
229
+ * first, then verify identity: if a racer installed a different valid owner in
230
+ * the canonical path, restore/preserve it rather than recursively deleting it.
231
+ */
232
+ async function cleanupFailedInitialization(
233
+ lockPath: string,
234
+ attempted: StreamOwnerRecord
235
+ ): Promise<void> {
236
+ const cleanupPath = `${lockPath}.failed-${attempted.processNonce}-${Date.now()}`;
237
+ try {
238
+ await fs.rename(lockPath, cleanupPath);
239
+ } catch (err: any) {
240
+ if (err?.code === 'ENOENT') return;
241
+ throw err;
242
+ }
243
+ const current = await readOwnershipRecord(cleanupPath);
244
+ if (current && (current.pid !== attempted.pid || current.processNonce !== attempted.processNonce)) {
245
+ try {
246
+ await fs.rename(cleanupPath, lockPath);
247
+ } catch (err: any) {
248
+ if (err?.code !== 'EEXIST') throw err;
249
+ // A canonical successor won the race. Keep its lock; the displaced
250
+ // record cannot own the canonical lease and is safe to remove.
251
+ await fs.rm(cleanupPath, { recursive: true, force: true });
252
+ }
253
+ return;
254
+ }
255
+ await fs.rm(cleanupPath, { recursive: true, force: true });
256
+ }
257
+
258
+ async function moveStaleLockAside(
259
+ lockPath: string,
260
+ snapshot: StreamOwnershipSnapshot,
261
+ staleMs: number,
262
+ deps: StreamOwnerDeps
263
+ ): Promise<boolean> {
264
+ const claimPath = takeoverPath(lockPath);
265
+ try {
266
+ await fs.rename(lockPath, claimPath);
267
+ } catch (err: any) {
268
+ if (err?.code === 'ENOENT') return false;
269
+ if (err?.code === 'EEXIST' || err?.code === 'ENOTEMPTY') return false;
270
+ throw err;
271
+ }
272
+
273
+ const claimStat = await fs.stat(claimPath).catch(() => null);
274
+ if (!claimStat || !snapshotMatchesIdentity(snapshot, claimStat)) {
275
+ try {
276
+ await fs.rename(claimPath, lockPath);
277
+ } catch (error: any) {
278
+ if (error?.code !== 'EEXIST' && error?.code !== 'ENOTEMPTY') throw error;
279
+ // The renamed directory was a successor, not the inspected lease. It is
280
+ // authoritative unless another canonical successor already appeared.
281
+ await preserveDisplacedLock(claimPath, lockPath);
282
+ }
283
+ return false;
284
+ }
285
+
286
+ const claimant = {
287
+ schemaVersion: 1,
288
+ pid: deps.pid ?? process.pid,
289
+ processNonce: deps.processNonce ?? processNonce,
290
+ claimedAt: (deps.now ?? (() => new Date()))().toISOString(),
291
+ };
292
+ await fs.writeFile(
293
+ path.join(claimPath, TAKEOVER_FILE),
294
+ JSON.stringify(claimant, null, 2) + '\n',
295
+ { flag: 'wx', mode: 0o600 },
296
+ );
297
+ await deps.beforeTakeoverVerify?.(claimPath);
298
+ const verified = await readOwnershipRecord(claimPath);
299
+ const verifiedStat = await fs.stat(claimPath).catch(() => null);
300
+ if (!isStillReclaimable(snapshot, verified, verifiedStat, staleMs, deps)) {
301
+ await fs.unlink(path.join(claimPath, TAKEOVER_FILE)).catch(() => {});
302
+ try {
303
+ await fs.rename(claimPath, lockPath);
304
+ } catch (err: any) {
305
+ if (err?.code !== 'EEXIST') throw err;
306
+ await fs.rm(claimPath, { recursive: true, force: true });
307
+ }
308
+ return false;
309
+ }
310
+
311
+ await fs.rm(claimPath, { recursive: true, force: true });
312
+ return true;
313
+ }
314
+
315
+ function takeoverPath(lockPath: string): string {
316
+ return `${lockPath}.takeover`;
317
+ }
318
+
319
+ async function pathExists(filePath: string): Promise<boolean> {
320
+ try {
321
+ await fs.access(filePath);
322
+ return true;
323
+ } catch (error: any) {
324
+ if (error?.code === 'ENOENT') return false;
325
+ throw error;
326
+ }
327
+ }
328
+
329
+ async function recoverTakeoverClaim(
330
+ lockPath: string,
331
+ staleMs: number,
332
+ deps: StreamOwnerDeps,
333
+ ): Promise<boolean> {
334
+ const claimPath = takeoverPath(lockPath);
335
+ let claimStat;
336
+ try {
337
+ claimStat = await fs.stat(claimPath);
338
+ } catch (error: any) {
339
+ if (error?.code === 'ENOENT') return true;
340
+ throw error;
341
+ }
342
+
343
+ const now = (deps.now ?? (() => new Date()))();
344
+ let claimant: { pid: number; processNonce: string; claimedAt: string } | null = null;
345
+ try {
346
+ const parsed = JSON.parse(await fs.readFile(path.join(claimPath, TAKEOVER_FILE), 'utf8'));
347
+ if (
348
+ parsed?.schemaVersion === 1 &&
349
+ Number.isSafeInteger(parsed.pid) && parsed.pid > 0 &&
350
+ isSafeLeaseText(parsed.processNonce, 128) &&
351
+ isIsoTimestamp(parsed.claimedAt)
352
+ ) claimant = parsed;
353
+ } catch {
354
+ // A claimant can die after rename but before publishing its marker. The
355
+ // directory mtime supplies a bounded initialization grace period.
356
+ }
357
+
358
+ const claimAge = claimant
359
+ ? now.getTime() - Date.parse(claimant.claimedAt)
360
+ : now.getTime() - claimStat.mtimeMs;
361
+ if (
362
+ claimant && Number.isFinite(claimAge) &&
363
+ claimAge <= STREAM_OWNER_TAKEOVER_STALE_MS &&
364
+ isPidAlive(claimant.pid, deps)
365
+ ) return false;
366
+ if (!claimant && claimAge <= STREAM_OWNER_TAKEOVER_STALE_MS) return false;
367
+
368
+ const owner = await readOwnershipRecord(claimPath);
369
+ const ownerHeartbeat = owner ? Date.parse(owner.heartbeatAt) : Number.NaN;
370
+ const ownerAge = Number.isFinite(ownerHeartbeat)
371
+ ? now.getTime() - ownerHeartbeat
372
+ : Number.POSITIVE_INFINITY;
373
+ const ownerReclaimable = !owner || ownerAge > staleMs || !isPidAlive(owner.pid, deps);
374
+ if (ownerReclaimable) {
375
+ await fs.rm(claimPath, { recursive: true, force: true });
376
+ return true;
377
+ }
378
+
379
+ await fs.unlink(path.join(claimPath, TAKEOVER_FILE)).catch(() => {});
380
+ try {
381
+ await fs.rename(claimPath, lockPath);
382
+ } catch (error: any) {
383
+ if (error?.code !== 'EEXIST' && error?.code !== 'ENOTEMPTY') throw error;
384
+ // A canonical successor already owns the name. Preserve it and discard
385
+ // only the displaced, noncanonical claim directory.
386
+ await fs.rm(claimPath, { recursive: true, force: true });
387
+ }
388
+ return false;
389
+ }
390
+
391
+ function isStillReclaimable(
392
+ snapshot: StreamOwnershipSnapshot,
393
+ current: StreamOwnerRecord | null,
394
+ currentLockStat: { dev: number; ino: number } | null,
395
+ staleMs: number,
396
+ deps: StreamOwnerDeps
397
+ ): boolean {
398
+ if (!currentLockStat || !snapshotMatchesIdentity(snapshot, currentLockStat)) return false;
399
+ if (!current) {
400
+ if (snapshot.state === 'orphaned-initialization') return true;
401
+ return snapshot.ageMs === Number.POSITIVE_INFINITY;
402
+ }
403
+ if (
404
+ snapshot.pid !== current.pid ||
405
+ snapshot.processNonce !== current.processNonce ||
406
+ snapshot.heartbeatAt !== current.heartbeatAt
407
+ ) {
408
+ return false;
409
+ }
410
+ const now = (deps.now ?? (() => new Date()))();
411
+ const heartbeatMs = Date.parse(current.heartbeatAt);
412
+ const ageMs = Number.isFinite(heartbeatMs)
413
+ ? now.getTime() - heartbeatMs
414
+ : Number.POSITIVE_INFINITY;
415
+ const stale = ageMs > staleMs;
416
+ const pidDead =
417
+ !isPidAlive(current.pid, deps);
418
+ return stale || pidDead;
419
+ }
420
+
421
+ function snapshotMatchesIdentity(
422
+ snapshot: StreamOwnershipSnapshot,
423
+ stat: { dev: number; ino: number },
424
+ ): boolean {
425
+ return snapshot.lockDev !== undefined && snapshot.lockIno !== undefined &&
426
+ snapshot.lockDev === stat.dev && snapshot.lockIno === stat.ino;
427
+ }
428
+
429
+ async function preserveDisplacedLock(claimPath: string, lockPath: string): Promise<void> {
430
+ try {
431
+ await fs.rename(claimPath, lockPath);
432
+ } catch (error: any) {
433
+ if (error?.code !== 'EEXIST' && error?.code !== 'ENOTEMPTY') throw error;
434
+ // Never delete an identity we did not prove stale. Leave it under its
435
+ // unique recovery name for conservative operator/process recovery.
436
+ }
437
+ }
438
+
439
+ function isPidAlive(pid: number, deps: StreamOwnerDeps): boolean {
440
+ if (deps.isPidAlive) return deps.isPidAlive(pid);
441
+ // Tests that inject synthetic owner PIDs but no process probe retain the
442
+ // historical "assume live" behavior. Production uses the real process PID
443
+ // and therefore verifies dead owners immediately.
444
+ if (deps.pid !== undefined) return true;
445
+ try {
446
+ process.kill(pid, 0);
447
+ return true;
448
+ } catch (error) {
449
+ return (error as NodeJS.ErrnoException).code === 'EPERM';
450
+ }
451
+ }
452
+
453
+ function makeLease(
454
+ lockPath: string,
455
+ record: StreamOwnerRecord,
456
+ identity: { dev: number; ino: number },
457
+ deps: StreamOwnerDeps
458
+ ): StreamLease {
459
+ let ownedRecord = record;
460
+ return {
461
+ lockPath,
462
+ record,
463
+ async refresh(): Promise<boolean> {
464
+ await deps.beforeLeaseRefreshMutation?.(lockPath);
465
+ const next = { ...ownedRecord, heartbeatAt: (deps.now ?? (() => new Date()))().toISOString() };
466
+ const refreshed = await mutateClaimedLease(
467
+ lockPath,
468
+ identity,
469
+ ownedRecord,
470
+ deps,
471
+ async (claimPath) => {
472
+ await (deps.writeRecord ?? writeRecord)(claimPath, next);
473
+ return 'restore';
474
+ },
475
+ );
476
+ if (!refreshed) return false;
477
+ ownedRecord = next;
478
+ this.record = next;
479
+ return true;
480
+ },
481
+ async release(): Promise<void> {
482
+ await deps.beforeLeaseReleaseMutation?.(lockPath);
483
+ await mutateClaimedLease(
484
+ lockPath,
485
+ identity,
486
+ ownedRecord,
487
+ deps,
488
+ async () => 'remove',
489
+ );
490
+ },
491
+ };
492
+ }
493
+
494
+ async function mutateClaimedLease(
495
+ lockPath: string,
496
+ identity: { dev: number; ino: number },
497
+ expected: StreamOwnerRecord,
498
+ deps: StreamOwnerDeps,
499
+ mutation: (claimPath: string) => Promise<'restore' | 'remove'>,
500
+ ): Promise<boolean> {
501
+ const claimPath = takeoverPath(lockPath);
502
+ try {
503
+ await fs.rename(lockPath, claimPath);
504
+ } catch (error: any) {
505
+ if (error?.code === 'ENOENT' || error?.code === 'EEXIST' || error?.code === 'ENOTEMPTY') {
506
+ return false;
507
+ }
508
+ throw error;
509
+ }
510
+
511
+ const movedStat = await fs.stat(claimPath).catch(() => null);
512
+ if (!movedStat || !sameIdentity(identity, movedStat)) {
513
+ await restoreClaimedLease(claimPath, lockPath);
514
+ return false;
515
+ }
516
+
517
+ const claimant = {
518
+ schemaVersion: 1,
519
+ pid: deps.pid ?? process.pid,
520
+ processNonce: deps.processNonce ?? processNonce,
521
+ claimedAt: (deps.now ?? (() => new Date()))().toISOString(),
522
+ };
523
+ await fs.writeFile(
524
+ path.join(claimPath, TAKEOVER_FILE),
525
+ JSON.stringify(claimant, null, 2) + '\n',
526
+ { flag: 'wx', mode: 0o600 },
527
+ );
528
+
529
+ const inspected = await readBoundOwner(claimPath);
530
+ const current = inspected?.raw ? parseOwnershipRecord(inspected.raw) : null;
531
+ if (
532
+ !inspected || !sameIdentity(identity, inspected.stat) || !current ||
533
+ !sameOwner(expected, current)
534
+ ) {
535
+ await fs.unlink(path.join(claimPath, TAKEOVER_FILE)).catch(() => {});
536
+ await restoreClaimedLease(claimPath, lockPath);
537
+ return false;
538
+ }
539
+
540
+ const disposition = await mutation(claimPath);
541
+ if (disposition === 'remove') {
542
+ await fs.rm(claimPath, { recursive: true, force: true });
543
+ return true;
544
+ }
545
+
546
+ await fs.unlink(path.join(claimPath, TAKEOVER_FILE)).catch(() => {});
547
+ return restoreClaimedLease(claimPath, lockPath);
548
+ }
549
+
550
+ async function restoreClaimedLease(claimPath: string, lockPath: string): Promise<boolean> {
551
+ for (let attempt = 0; attempt < 20; attempt += 1) {
552
+ try {
553
+ await fs.rename(claimPath, lockPath);
554
+ return true;
555
+ } catch (error: any) {
556
+ if (error?.code !== 'EEXIST' && error?.code !== 'ENOTEMPTY') {
557
+ if (error?.code === 'ENOENT') return false;
558
+ throw error;
559
+ }
560
+ // A creator that began just before the claim appeared must observe the
561
+ // fixed claim and clean its own initialization. Give it a bounded chance
562
+ // to do so without ever deleting that unverified canonical directory.
563
+ await new Promise((resolve) => setTimeout(resolve, 5));
564
+ }
565
+ }
566
+ return false;
567
+ }
568
+
569
+ function sameOwner(left: StreamOwnerRecord, right: StreamOwnerRecord): boolean {
570
+ return left.schemaVersion === right.schemaVersion &&
571
+ left.pid === right.pid &&
572
+ left.processNonce === right.processNonce &&
573
+ left.cwd === right.cwd &&
574
+ left.startedAt === right.startedAt &&
575
+ left.heartbeatAt === right.heartbeatAt;
576
+ }
577
+
578
+ async function readOwnershipRecord(lockPath: string): Promise<StreamOwnerRecord | null> {
579
+ try {
580
+ const raw = await fs.readFile(path.join(lockPath, OWNER_FILE), 'utf8');
581
+ return parseOwnershipRecord(raw);
582
+ } catch {
583
+ return null;
584
+ }
585
+ }
586
+
587
+ function parseOwnershipRecord(raw: string): StreamOwnerRecord | null {
588
+ try {
589
+ const parsed = JSON.parse(raw);
590
+ return isRecord(parsed) ? parsed : null;
591
+ } catch {
592
+ return null;
593
+ }
594
+ }
595
+
596
+ async function readBoundOwner(lockPath: string): Promise<{
597
+ raw: string | null;
598
+ stat: { dev: number; ino: number; mtimeMs: number };
599
+ } | null> {
600
+ for (let attempt = 0; attempt < 3; attempt += 1) {
601
+ let handle;
602
+ try {
603
+ handle = await fs.open(lockPath, 'r');
604
+ } catch (error: any) {
605
+ if (error?.code === 'ENOENT') return null;
606
+ throw error;
607
+ }
608
+ try {
609
+ const openedStat = await handle.stat();
610
+ let raw: string | null;
611
+ try {
612
+ raw = await fs.readFile(path.join(lockPath, OWNER_FILE), 'utf8');
613
+ } catch (error: any) {
614
+ if (error?.code !== 'ENOENT') throw error;
615
+ raw = null;
616
+ }
617
+ const canonicalStat = await fs.stat(lockPath).catch((error: any) => {
618
+ if (error?.code === 'ENOENT') return null;
619
+ throw error;
620
+ });
621
+ const finalOpenedStat = await handle.stat();
622
+ if (canonicalStat && sameIdentity(openedStat, finalOpenedStat) &&
623
+ sameIdentity(openedStat, canonicalStat)) {
624
+ return { raw, stat: canonicalStat };
625
+ }
626
+ } finally {
627
+ await handle.close();
628
+ }
629
+ }
630
+ return null;
631
+ }
632
+
633
+ function sameIdentity(
634
+ left: { dev: number; ino: number },
635
+ right: { dev: number; ino: number },
636
+ ): boolean {
637
+ return left.dev === right.dev && left.ino === right.ino;
638
+ }
639
+
640
+ async function writeRecord(lockPath: string, record: StreamOwnerRecord): Promise<void> {
641
+ const ownerPath = path.join(lockPath, OWNER_FILE);
642
+ const tmpPath = path.join(lockPath, `${OWNER_FILE}.${record.processNonce}.tmp`);
643
+ await fs.writeFile(tmpPath, JSON.stringify(record, null, 2) + '\n', {
644
+ mode: 0o600,
645
+ });
646
+ await fs.rename(tmpPath, ownerPath);
647
+ }
648
+
649
+ function makeRecord(deps: StreamOwnerDeps): StreamOwnerRecord {
650
+ const now = deps.now ?? (() => new Date());
651
+ return {
652
+ schemaVersion: SCHEMA_VERSION,
653
+ pid: deps.pid ?? process.pid,
654
+ processNonce: deps.processNonce ?? processNonce,
655
+ cwd: deps.cwd ?? process.cwd(),
656
+ startedAt: deps.processStartedAt ?? processStartedAt,
657
+ heartbeatAt: now().toISOString(),
658
+ };
659
+ }
660
+
661
+ function isRecord(value: any): value is StreamOwnerRecord {
662
+ return (
663
+ value !== null &&
664
+ typeof value === 'object' &&
665
+ value.schemaVersion === SCHEMA_VERSION &&
666
+ Number.isSafeInteger(value.pid) &&
667
+ value.pid > 0 &&
668
+ isSafeLeaseText(value.processNonce, 128) &&
669
+ isSafeLeaseText(value.cwd, 4096) &&
670
+ isIsoTimestamp(value.startedAt) &&
671
+ isIsoTimestamp(value.heartbeatAt)
672
+ );
673
+ }
674
+
675
+ function isSafeLeaseText(value: unknown, maxLength: number): value is string {
676
+ return typeof value === 'string' && value.length > 0 && value.length <= maxLength &&
677
+ !/[\u0000-\u001f\u007f]/.test(value);
678
+ }
679
+
680
+ function isIsoTimestamp(value: unknown): value is string {
681
+ return isSafeLeaseText(value, 64) && Number.isFinite(Date.parse(value));
682
+ }
683
+
684
+ function assertUuid(label: string, value: string): void {
685
+ if (!UUID_RE.test(value)) throw new Error(`Invalid ${label}: ${value}`);
686
+ }