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
package/src/sync.ts ADDED
@@ -0,0 +1,363 @@
1
+ /**
2
+ * `borg sync` — worktree lifecycle management subcommand (gh#33).
3
+ *
4
+ * Reconciled to the per-worktree `wt-<suffix>` branch model (PR-B,
5
+ * ruling ea643b33). Replaces the previous main-centric semantics
6
+ * ("idle = on main"; "post-merge = checkout main") — under the approved
7
+ * gh#33 model `main` is NEVER a working branch in any worktree; it is
8
+ * purely the integration target. Every worktree works on a named
9
+ * `wt-<suffix>` branch and `borg sync` keeps it current with
10
+ * origin/main, returns to it after a feature branch merges, and absorbs
11
+ * upstream into an in-progress feature branch — never touching `main`
12
+ * as a checkout.
13
+ *
14
+ * All git mutation/decision logic is delegated to
15
+ * `src/worktree-lifecycle.ts` (the seam PR-A added:
16
+ * `adoptWorktree`, `syncWorktree`, `cleanupMerged`, `isMerged`,
17
+ * `localBranchExists`) so the never-discard guards (dirty / unmerged
18
+ * HEAD / unmerged target wt- branch) and the args-array subprocess
19
+ * shape are shared, not duplicated.
20
+ *
21
+ * Lifecycle states:
22
+ * 1. dirty — uncommitted changes; refuse (never discard).
23
+ * 2. on-wt — on the per-worktree `wt-<suffix>` branch;
24
+ * fast-forward it to origin/main (ff-only).
25
+ * 3. on-main — on `main`/`master` (or detached at a merged
26
+ * point); adopt the `wt-<suffix>` branch
27
+ * (Q4: main is never a working branch).
28
+ * 4. feature-mid-sprint — on a feature branch not yet merged; absorb
29
+ * origin/main into it via `git merge --no-edit`
30
+ * (no rebase — cube workflow rule (a)) when
31
+ * origin/main advanced; else no-op.
32
+ * 5. feature-merged — on a feature branch fully merged into
33
+ * origin/main; return to `wt-<suffix>` and
34
+ * ANNOUNCE the prunable feature branch (prune
35
+ * only with `--prune`, Q3).
36
+ *
37
+ * Anti-features (intentional, unchanged):
38
+ * - No auto-stash / auto-commit / auto-discard on dirty tree.
39
+ * - No force-push, no rebase, no --force-with-lease.
40
+ * - No remote-branch deletion (Coordinator owns merge actions).
41
+ * - Local feature-branch deletion only on explicit `--prune` (Q3).
42
+ */
43
+
44
+ import { spawnSync } from 'node:child_process';
45
+ import { basename } from 'node:path';
46
+ import chalk from 'chalk';
47
+ import {
48
+ adoptWorktree,
49
+ syncWorktree,
50
+ cleanupMerged,
51
+ isMerged,
52
+ perWorktreeBranchName,
53
+ type RunSync,
54
+ } from './worktree-lifecycle.js';
55
+
56
+ // ------------------------------------------------------------------
57
+ // Types
58
+ // ------------------------------------------------------------------
59
+
60
+ export interface SpawnSyncResult {
61
+ status: number | null;
62
+ stdout: string;
63
+ stderr: string;
64
+ }
65
+
66
+ export interface SyncDeps {
67
+ /** Synchronous spawn — used for `git ...` invocations. */
68
+ runSync?: RunSync;
69
+ /** Inject for tests; defaults to `process.cwd()`. */
70
+ cwd?: () => string;
71
+ /** stderr sink — overridable for tests. */
72
+ stderr?: (line: string) => void;
73
+ /** stdout sink — overridable for tests. */
74
+ stdout?: (line: string) => void;
75
+ }
76
+
77
+ export interface SyncOptions {
78
+ /** Q3: prune a merged feature branch instead of only announcing it. */
79
+ prune: boolean;
80
+ }
81
+
82
+ const defaultDeps: Required<SyncDeps> = {
83
+ runSync: (cmd, args, cwd) => {
84
+ const r = spawnSync(cmd, args, { cwd, encoding: 'utf-8' });
85
+ return {
86
+ status: r.status,
87
+ stdout: r.stdout ?? '',
88
+ stderr: r.stderr ?? '',
89
+ };
90
+ },
91
+ cwd: () => process.cwd(),
92
+ stderr: (line) => process.stderr.write(line),
93
+ stdout: (line) => process.stdout.write(line),
94
+ };
95
+
96
+ const DEFAULT_BRANCH = 'origin/main';
97
+
98
+ // ------------------------------------------------------------------
99
+ // wt- branch resolution
100
+ // ------------------------------------------------------------------
101
+
102
+ /**
103
+ * Resolve the per-worktree `wt-` branch for the current checkout,
104
+ * DETERMINISTICALLY per worktree.
105
+ *
106
+ * - current branch is already `wt-*` → that is the branch.
107
+ * - otherwise → derive `wt-<suffix>` from THIS worktree's directory
108
+ * basename minus the main worktree's (repo) basename prefix — the
109
+ * same `perWorktreeBranchName` derivation the spawn path uses, so
110
+ * the name matches what `borg assimilate --worktree` created.
111
+ *
112
+ * This must NOT list `git branch --list wt-*`: in linked worktrees the
113
+ * local branch namespace is SHARED across all siblings (gh#33 CR-v2
114
+ * blocker 32bc45da), so every drone's `wt-` branch is visible and a
115
+ * "single match" heuristic is never satisfied in a real multi-drone
116
+ * cube. The directory-derivation is unique per worktree and never
117
+ * ambiguous. The first `worktree <path>` line of
118
+ * `git worktree list --porcelain` is the main worktree (the repo dir);
119
+ * its basename is the prefix to strip. For an independent clone the
120
+ * main worktree IS this worktree, so the prefix == basename and the
121
+ * result is `wt-<basename>` (no strip) — consistent with PR-A's
122
+ * in-place adoption.
123
+ */
124
+ export function resolveWtBranch(
125
+ runSync: RunSync,
126
+ cwd: string,
127
+ currentBranch: string
128
+ ): string {
129
+ if (currentBranch.startsWith('wt-')) return currentBranch;
130
+ const top = runSync('git', ['rev-parse', '--show-toplevel'], cwd);
131
+ const thisDir = top.status === 0 ? top.stdout.trim() : cwd;
132
+ const wtList = runSync('git', ['worktree', 'list', '--porcelain'], cwd);
133
+ let mainDir = thisDir;
134
+ if (wtList.status === 0) {
135
+ const firstWorktreeLine = wtList.stdout
136
+ .split('\n')
137
+ .find((l) => l.startsWith('worktree '));
138
+ if (firstWorktreeLine) mainDir = firstWorktreeLine.slice('worktree '.length).trim();
139
+ }
140
+ return perWorktreeBranchName(basename(thisDir), basename(mainDir));
141
+ }
142
+
143
+ // ------------------------------------------------------------------
144
+ // State detection
145
+ // ------------------------------------------------------------------
146
+
147
+ export type SyncState =
148
+ | { kind: 'dirty'; files: string[] }
149
+ | { kind: 'on-wt'; branch: string; wtBranch: string }
150
+ | { kind: 'on-main'; branch: string; wtBranch: string }
151
+ | { kind: 'feature-mid-sprint'; branch: string; wtBranch: string; commits: number }
152
+ | { kind: 'feature-merged'; branch: string; wtBranch: string }
153
+ | { kind: 'error'; reason: string };
154
+
155
+ /**
156
+ * Detect the worktree's lifecycle state. Read-only except for the
157
+ * `git fetch origin --prune` needed to measure against the latest tip.
158
+ */
159
+ export function detectState(deps: Required<SyncDeps>): SyncState {
160
+ const { runSync, cwd } = deps;
161
+ const cwdValue = cwd();
162
+
163
+ // (1) git repo?
164
+ if (runSync('git', ['rev-parse', '--show-toplevel'], cwdValue).status !== 0) {
165
+ return { kind: 'error', reason: `not in a git repository (cwd: ${cwdValue})` };
166
+ }
167
+
168
+ // (2) dirty FIRST — never act on uncommitted changes.
169
+ const status = runSync('git', ['status', '--porcelain'], cwdValue);
170
+ if (status.status !== 0) {
171
+ return { kind: 'error', reason: `git status failed: ${status.stderr.trim()}` };
172
+ }
173
+ const dirty = status.stdout.split('\n').map((l) => l.trim()).filter((l) => l.length > 0);
174
+ if (dirty.length > 0) return { kind: 'dirty', files: dirty };
175
+
176
+ // (3) current branch.
177
+ const branchProbe = runSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], cwdValue);
178
+ if (branchProbe.status !== 0) {
179
+ return { kind: 'error', reason: `cannot resolve current branch: ${branchProbe.stderr.trim()}` };
180
+ }
181
+ const branch = branchProbe.stdout.trim();
182
+ if (branch === 'HEAD') {
183
+ return { kind: 'error', reason: 'detached HEAD; run `borg assimilate` to adopt a wt- branch first' };
184
+ }
185
+
186
+ // (4) fetch — fatal on failure (can't reason about lifecycle offline).
187
+ const fetch = runSync('git', ['fetch', 'origin', '--prune'], cwdValue);
188
+ if (fetch.status !== 0) {
189
+ return { kind: 'error', reason: `git fetch origin failed: ${fetch.stderr.trim()}` };
190
+ }
191
+
192
+ const wtBranch = resolveWtBranch(runSync, cwdValue, branch);
193
+
194
+ // (5) on the per-worktree wt- branch.
195
+ if (branch.startsWith('wt-')) {
196
+ return { kind: 'on-wt', branch, wtBranch: branch };
197
+ }
198
+
199
+ // (6) on main/master — Q4: never a working branch; adopt wt-.
200
+ if (branch === 'main' || branch === 'master') {
201
+ return { kind: 'on-main', branch, wtBranch };
202
+ }
203
+
204
+ // (7) feature branch — merged vs mid-sprint.
205
+ // ASSUMPTION (gh#33 CR NIT 0e19637a): merged-detection keys off commit
206
+ // ancestry (`isMerged` = HEAD is an ancestor of origin/main), which
207
+ // holds for merge-commit / fast-forward integration. A squash- or
208
+ // rebase-merged PR leaves the feature tip a NON-ancestor → it would
209
+ // classify here as mid-sprint, not merged. That degrades safely
210
+ // (merge-back of origin/main, no discard), and this cube integrates
211
+ // via merge commits, so it is not triggered. If squash-merge is ever
212
+ // adopted, switch this to PR-merged-state detection.
213
+ if (isMerged(runSync, cwdValue, 'HEAD', DEFAULT_BRANCH)) {
214
+ return { kind: 'feature-merged', branch, wtBranch };
215
+ }
216
+ // not merged → mid-sprint. Count how far origin/main advanced past the
217
+ // merge-base so the message can report it (0 → no-op).
218
+ const mergeBase = runSync('git', ['merge-base', 'HEAD', DEFAULT_BRANCH], cwdValue);
219
+ const base = mergeBase.status === 0 ? mergeBase.stdout.trim() : '';
220
+ const count = runSync('git', ['rev-list', '--count', `${base}..${DEFAULT_BRANCH}`], cwdValue);
221
+ const commits = parseInt(count.stdout.trim(), 10) || 0;
222
+ return { kind: 'feature-mid-sprint', branch, wtBranch, commits };
223
+ }
224
+
225
+ // ------------------------------------------------------------------
226
+ // Orchestrator
227
+ // ------------------------------------------------------------------
228
+
229
+ export async function runSync(deps: SyncDeps = {}, opts: SyncOptions = { prune: false }): Promise<number> {
230
+ const merged = { ...defaultDeps, ...deps };
231
+ const { runSync: run, cwd, stderr, stdout } = merged;
232
+ const state = detectState(merged);
233
+
234
+ if (state.kind === 'error') {
235
+ stderr(chalk.red(`◼ borg sync: ${state.reason}\n`));
236
+ return 1;
237
+ }
238
+
239
+ if (state.kind === 'dirty') {
240
+ stderr(chalk.yellow(`◼ Working tree has uncommitted changes.\n`));
241
+ for (const line of state.files.slice(0, 5)) stderr(chalk.gray(` ${line}\n`));
242
+ if (state.files.length > 5) stderr(chalk.gray(` ... and ${state.files.length - 5} more\n`));
243
+ stderr(chalk.yellow(`◼ Commit, stash, or restore before running \`borg sync\`. Nothing was changed.\n`));
244
+ return 1;
245
+ }
246
+
247
+ // on-wt: fast-forward the per-worktree branch to origin/main (ff-only,
248
+ // clean-gated, never merge/rebase). Delegates to syncWorktree.
249
+ if (state.kind === 'on-wt') {
250
+ const res = syncWorktree(run, cwd(), state.wtBranch, DEFAULT_BRANCH);
251
+ if (res.action === 'fast-forwarded') {
252
+ stdout(chalk.blue(`◼ On \`${state.wtBranch}\`; fast-forwarded to ${DEFAULT_BRANCH}.\n`));
253
+ return 0;
254
+ }
255
+ if (res.action === 'already-current') {
256
+ stdout(chalk.blue(`◼ On \`${state.wtBranch}\`; up to date with ${DEFAULT_BRANCH}.\n`));
257
+ return 0;
258
+ }
259
+ // skipped-diverged: the wt- branch has local commits not on origin/main.
260
+ stderr(chalk.yellow(`◼ ${res.message ?? 'sync skipped'}.\n`));
261
+ return 1;
262
+ }
263
+
264
+ // on-main: adopt the wt- branch (Q4 — move off main). adoptWorktree
265
+ // applies the dirty / unmerged-HEAD / unmerged-target guards.
266
+ if (state.kind === 'on-main') {
267
+ return adoptAndReport(state.wtBranch, run, cwd, stdout, stderr);
268
+ }
269
+
270
+ // feature-mid-sprint: absorb origin/main into the feature branch (no
271
+ // rebase). Leaves the drone on the feature branch to keep working.
272
+ if (state.kind === 'feature-mid-sprint') {
273
+ if (state.commits === 0) {
274
+ stdout(chalk.blue(`◼ On \`${state.branch}\` (feature branch); up to date with ${DEFAULT_BRANCH}.\n`));
275
+ stdout(chalk.gray(`◼ Continue your sprint, or post REVIEW-READY when complete.\n`));
276
+ return 0;
277
+ }
278
+ const merge = run('git', ['merge', '--no-edit', DEFAULT_BRANCH], cwd());
279
+ if (merge.status !== 0) {
280
+ stderr(chalk.red(`◼ borg sync: git merge ${DEFAULT_BRANCH} failed (likely conflict). Resolve manually:\n${merge.stderr.trim()}\n`));
281
+ return 1;
282
+ }
283
+ stdout(chalk.blue(`◼ On \`${state.branch}\`; merged ${state.commits} commit${state.commits === 1 ? '' : 's'} from ${DEFAULT_BRANCH} (no rebase).\n`));
284
+ stdout(chalk.gray(`◼ Re-run tests; continue your sprint.\n`));
285
+ return 0;
286
+ }
287
+
288
+ // feature-merged: the PR merged. Return to the wt- branch (adopt) and
289
+ // announce the prunable feature branch (prune only with --prune, Q3).
290
+ if (state.kind === 'feature-merged') {
291
+ const feature = state.branch;
292
+ const code = adoptAndReport(state.wtBranch, run, cwd, stdout, stderr, {
293
+ adoptedPrefix: `◼ \`${feature}\` is merged into ${DEFAULT_BRANCH};`,
294
+ });
295
+ if (code !== 0) return code; // adoption blocked (dirty/unmerged target) — don't prune
296
+ // Now on the wt- branch — safe to prune/announce the merged feature.
297
+ const cleanup = cleanupMerged(run, cwd(), feature, DEFAULT_BRANCH, { prune: opts.prune });
298
+ if (cleanup.action === 'pruned') {
299
+ stdout(chalk.blue(`◼ Pruned merged branch \`${feature}\`.\n`));
300
+ } else if (cleanup.action === 'announced') {
301
+ stdout(chalk.gray(`◼ ${cleanup.message}\n`));
302
+ }
303
+ return 0;
304
+ }
305
+
306
+ // Exhaustiveness.
307
+ const _exhaustive: never = state;
308
+ stderr(chalk.red(`◼ borg sync: unhandled state\n`));
309
+ return 1;
310
+ }
311
+
312
+ /**
313
+ * Shared adopt-the-wt-branch path for the on-main + feature-merged
314
+ * states. Surfaces the never-discard outcomes; returns the process exit
315
+ * code (0 adopted, 1 blocked/ambiguous).
316
+ */
317
+ function adoptAndReport(
318
+ wtBranch: string,
319
+ run: RunSync,
320
+ cwd: () => string,
321
+ stdout: (l: string) => void,
322
+ stderr: (l: string) => void,
323
+ opts: { adoptedPrefix?: string } = {}
324
+ ): number {
325
+ const res = adoptWorktree(run, cwd(), wtBranch, DEFAULT_BRANCH);
326
+ if (res.action === 'adopted') {
327
+ if (opts.adoptedPrefix) {
328
+ stdout(chalk.blue(`${opts.adoptedPrefix} switched to \`${wtBranch}\` at ${DEFAULT_BRANCH}.\n`));
329
+ } else {
330
+ stdout(chalk.blue(`◼ On \`${wtBranch}\` at ${DEFAULT_BRANCH}.\n`));
331
+ }
332
+ return 0;
333
+ }
334
+ // skipped-dirty handled upstream (dirty state), but defensively surface.
335
+ stderr(chalk.yellow(`◼ borg sync: ${res.message ?? 'not adopted'}. Nothing was changed.\n`));
336
+ return 1;
337
+ }
338
+
339
+ // ------------------------------------------------------------------
340
+ // CLI surface
341
+ // ------------------------------------------------------------------
342
+
343
+ export type ParseResult = { ok: true; options: SyncOptions } | { ok: false; error: string };
344
+
345
+ /**
346
+ * Parse args after `borg sync`. Supports `--prune` (Q3: delete a merged
347
+ * feature branch after returning to the wt- branch). Rejects anything
348
+ * else to keep room for future flags.
349
+ */
350
+ export function parseSyncArgs(rawArgs: string[]): ParseResult {
351
+ let prune = false;
352
+ for (const arg of rawArgs) {
353
+ if (arg === '--prune') {
354
+ prune = true;
355
+ } else {
356
+ return {
357
+ ok: false,
358
+ error: `unexpected argument: ${arg}. Usage: borg sync [--prune]`,
359
+ };
360
+ }
361
+ }
362
+ return { ok: true, options: { prune } };
363
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Terminal-title setter for borg drone sessions.
3
+ *
4
+ * Multiple Claude Code sessions across sibling worktrees are visually
5
+ * indistinguishable in Cmd-Tab / tab bars / Mission Control on macOS,
6
+ * and likewise on most Linux terminal emulators. Setting the terminal
7
+ * title via the OSC 0 / OSC 2 escape gives each window a free per-
8
+ * session identity.
9
+ *
10
+ * Format (Queen-specified):
11
+ * - Assimilated drone session: `borg · <label> · <cubeName>`
12
+ * - Unassimilated session: `borg · <repo-basename>`
13
+ *
14
+ * Why OSC 0 (`\x1b]0;…\x07`): sets both window title AND icon name on
15
+ * most terminals. OSC 2 sets only window title; OSC 1 sets only icon
16
+ * name. OSC 0 is the maximally-portable choice — works in iTerm2,
17
+ * macOS Terminal, kitty, alacritty, ghostty, GNOME Terminal, xterm.
18
+ *
19
+ * Lifetime: the escape is emitted once, before spawning Claude Code.
20
+ * Claude Code itself does not set its own window title (verified
21
+ * 2026-05-11), so the borg-set title persists for the whole session.
22
+ *
23
+ * Limitations (acceptable for v1; flagged for future):
24
+ * - Title doesn't update mid-session on `borg_assimilate`. The
25
+ * borgmcp client process can't write the escape post-spawn
26
+ * because stdio is owned by Claude Code at that point (and stdio
27
+ * to Claude is JSON-RPC — terminal escapes would be parsed as
28
+ * invalid messages).
29
+ * - Falls back to repo-basename for the unassimilated case; loses
30
+ * drone identity until the cube is joined. Typical pattern is
31
+ * "drone has been around long enough that cubes.json is already
32
+ * populated," so the assimilated path is the common case.
33
+ */
34
+
35
+ /**
36
+ * Pure: compose the title string for a session. Exported so tests can
37
+ * exercise every branch without TTY / process / fs dependencies.
38
+ *
39
+ * @param activeDrone — `{label, cubeName}` if this project is
40
+ * assimilated to a cube, null otherwise.
41
+ * @param repoBasename — fallback identity for the unassimilated case
42
+ * (typically `basename(process.cwd())`).
43
+ */
44
+ export function composeTerminalTitle(
45
+ activeDrone: { label: string; cubeName: string } | null,
46
+ repoBasename: string
47
+ ): string {
48
+ if (activeDrone) {
49
+ return `borg · ${activeDrone.label} · ${activeDrone.cubeName}`;
50
+ }
51
+ return `borg · ${repoBasename}`;
52
+ }
53
+
54
+ /**
55
+ * Side-effecting: emit the OSC 0 escape to stdout, but only if stdout
56
+ * is a TTY. When stdout is piped (CI, redirection, scripted
57
+ * invocation), emitting the raw escape would pollute the captured
58
+ * output without doing anything useful — so we no-op.
59
+ *
60
+ * Returns the title string that WOULD have been emitted, regardless of
61
+ * TTY state, so callers can log it independently for diagnostics.
62
+ */
63
+ export function setTerminalTitle(
64
+ activeDrone: { label: string; cubeName: string } | null,
65
+ repoBasename: string,
66
+ stdout: NodeJS.WriteStream = process.stdout
67
+ ): string {
68
+ const title = composeTerminalTitle(activeDrone, repoBasename);
69
+ if (stdout.isTTY) {
70
+ // OSC 0: ESC ] 0 ; <title> BEL
71
+ // (BEL terminator is honored by all OSC-supporting terminals;
72
+ // ST = `ESC \` is an alternative but BEL has wider compat.)
73
+ stdout.write(`\x1b]0;${title}\x07`);
74
+ }
75
+ return title;
76
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Local-server credential backend.
3
+ *
4
+ * config.ts's local-server credential group sits on top of this. Storage is the
5
+ * 0600 file store (Queen rescope) — the OS keychain (@napi-rs/keyring) is GONE.
6
+ * The raw secret rests only in the 0600 file, parity with the server's own TLS
7
+ * private keys; there is no keychain and no obfuscation-grade fallback.
8
+ */
9
+
10
+ import { atomicWrite0600, readStoreFile } from './seat-store.js';
11
+
12
+ // The OS-keychain backend was retired with the Queen rescope; the only backend
13
+ // name is the 0600 file store.
14
+ export type TokenBackendName = 'file';
15
+
16
+ /**
17
+ * Account-agnostic key/value store over the 0600 credential file.
18
+ */
19
+ export interface TokenBackend {
20
+ readonly name: TokenBackendName;
21
+ get(account: string): Promise<string | null>;
22
+ set(account: string, value: string): Promise<void>;
23
+ delete(account: string): Promise<void>;
24
+ }
25
+
26
+ // ─── 0600 file backend (Queen rescope: replaces the OS keychain) ─────────────
27
+
28
+ /**
29
+ * Build a TokenBackend over a single 0600 store file, all accounts held in one
30
+ * `{version, accounts}` map. get/set/delete read-modify-write the file via the
31
+ * seat-store's atomic 0600 writer — the RAW secret rests only in the 0600 file
32
+ * (parity with the server's TLS keys), never a keychain.
33
+ *
34
+ * These ops are NON-flocking by design: the config layer holds the single store
35
+ * lock (withStoreLock over CREDENTIALS_LOCK) continuously across each
36
+ * read-compare-write, so nesting a second lock here would deadlock the O_EXCL
37
+ * lockfile. Pure reads (get) are safe lock-free because atomicWrite0600's rename
38
+ * guarantees a reader only ever sees a complete file.
39
+ */
40
+ export function makeFileBackend(filePath: string): TokenBackend {
41
+ const load = async (): Promise<Record<string, string>> => {
42
+ const raw = await readStoreFile(filePath);
43
+ // CR4 fail-closed: ONLY a missing file initializes empty. A present-but-
44
+ // malformed / wrong-version / schema-invalid credential store MUST NOT read as
45
+ // empty — a subsequent set/delete would OVERWRITE it and erase every stored
46
+ // account (parent enrollment credentials + pending records). Throw WITHOUT
47
+ // writing so the corrupt bytes are preserved for recovery.
48
+ if (raw === null) return {};
49
+ let parsed: unknown;
50
+ try {
51
+ parsed = JSON.parse(raw);
52
+ } catch {
53
+ throw new Error('Borg credential store is malformed; refusing to overwrite it');
54
+ }
55
+ if (
56
+ !parsed ||
57
+ typeof parsed !== 'object' ||
58
+ Array.isArray(parsed) ||
59
+ (parsed as { version?: unknown }).version !== 1 ||
60
+ !(parsed as { accounts?: unknown }).accounts ||
61
+ typeof (parsed as { accounts?: unknown }).accounts !== 'object' ||
62
+ Array.isArray((parsed as { accounts?: unknown }).accounts)
63
+ ) {
64
+ throw new Error(
65
+ 'Borg credential store is malformed or has an unsupported version; refusing to overwrite it',
66
+ );
67
+ }
68
+ // CR#2: FULL value validation — every account value MUST be a string. A
69
+ // non-string value (a corrupted/tampered entry) fails CLOSED without writing,
70
+ // so a following set/delete never overwrites the store off a bad read.
71
+ const accounts = (parsed as { accounts: Record<string, unknown> }).accounts;
72
+ for (const value of Object.values(accounts)) {
73
+ if (typeof value !== 'string') {
74
+ throw new Error(
75
+ 'Borg credential store is malformed (a non-string account value); refusing to overwrite it',
76
+ );
77
+ }
78
+ }
79
+ return { ...(accounts as Record<string, string>) };
80
+ };
81
+ const save = (accounts: Record<string, string>): Promise<void> =>
82
+ atomicWrite0600(filePath, JSON.stringify({ version: 1, accounts }, null, 2) + '\n');
83
+ return {
84
+ name: 'file',
85
+ async get(account) {
86
+ const accounts = await load();
87
+ return Object.prototype.hasOwnProperty.call(accounts, account) ? accounts[account] : null;
88
+ },
89
+ async set(account, value) {
90
+ const accounts = await load();
91
+ accounts[account] = value;
92
+ await save(accounts);
93
+ },
94
+ async delete(account) {
95
+ const accounts = await load();
96
+ if (Object.prototype.hasOwnProperty.call(accounts, account)) {
97
+ delete accounts[account];
98
+ await save(accounts);
99
+ }
100
+ },
101
+ };
102
+ }