@rsktash/beads-ui 0.1.50 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (336) hide show
  1. package/README.md +85 -144
  2. package/bin/bd-web +94 -0
  3. package/dist/assets/index-BBVIMXaM.js +73 -0
  4. package/dist/assets/index-SUXI6Mzt.css +1 -0
  5. package/dist/bd-favicon-16.svg +6 -0
  6. package/dist/bd-favicon-32.svg +6 -0
  7. package/dist/bd-logo-512.svg +6 -0
  8. package/dist/favicon.ico +0 -0
  9. package/dist/icon-180.png +0 -0
  10. package/dist/icon-192.png +0 -0
  11. package/dist/icon-512.png +0 -0
  12. package/dist/index.html +8 -5
  13. package/package.json +39 -29
  14. package/server/auth.js +68 -87
  15. package/server/db.js +91 -134
  16. package/server/dsn.js +130 -0
  17. package/server/index.js +151 -87
  18. package/server/queries.js +247 -0
  19. package/server/routes/auth.js +27 -0
  20. package/server/routes/issues.js +120 -0
  21. package/server/routes/stream.js +111 -0
  22. package/server/types.js +83 -0
  23. package/app/protocol.js +0 -216
  24. package/bin/bd-grep +0 -121
  25. package/bin/bd-ui +0 -19
  26. package/dist/assets/abap-DsBKuouk.js +0 -1
  27. package/dist/assets/actionscript-3-D_z4Izcz.js +0 -1
  28. package/dist/assets/ada-727ZlQH0.js +0 -1
  29. package/dist/assets/andromeeda-C3khCPGq.js +0 -1
  30. package/dist/assets/angular-html-LfdN0zeE.js +0 -1
  31. package/dist/assets/angular-ts-CKsD7JZE.js +0 -1
  32. package/dist/assets/apache-Dn00JSTd.js +0 -1
  33. package/dist/assets/apex-COJ4H7py.js +0 -1
  34. package/dist/assets/apl-BBq3IX1j.js +0 -1
  35. package/dist/assets/applescript-Bu5BbsvL.js +0 -1
  36. package/dist/assets/ara-7O62HKoU.js +0 -1
  37. package/dist/assets/asciidoc-BPT9niGB.js +0 -1
  38. package/dist/assets/asm-Dhn9LcZ4.js +0 -1
  39. package/dist/assets/astro-CqkE3fuf.js +0 -1
  40. package/dist/assets/aurora-x-D-2ljcwZ.js +0 -1
  41. package/dist/assets/awk-eg146-Ew.js +0 -1
  42. package/dist/assets/ayu-dark-Cv9koXgw.js +0 -1
  43. package/dist/assets/ballerina-Du268qiB.js +0 -1
  44. package/dist/assets/bat-fje9CFhw.js +0 -1
  45. package/dist/assets/beancount-BwXTMy5W.js +0 -1
  46. package/dist/assets/berry-3xVqZejG.js +0 -1
  47. package/dist/assets/bibtex-xW4inM5L.js +0 -1
  48. package/dist/assets/bicep-DHo0CJ0O.js +0 -1
  49. package/dist/assets/blade-a8OxSdnT.js +0 -1
  50. package/dist/assets/bsl-Dgyn0ogV.js +0 -1
  51. package/dist/assets/c-C3t2pwGQ.js +0 -1
  52. package/dist/assets/cadence-DNquZEk8.js +0 -1
  53. package/dist/assets/cairo--RitsXJZ.js +0 -1
  54. package/dist/assets/catppuccin-frappe-CD_QflpE.js +0 -1
  55. package/dist/assets/catppuccin-latte-DRW-0cLl.js +0 -1
  56. package/dist/assets/catppuccin-macchiato-C-_shW-Y.js +0 -1
  57. package/dist/assets/catppuccin-mocha-LGGdnPYs.js +0 -1
  58. package/dist/assets/clarity-BHOwM8T6.js +0 -1
  59. package/dist/assets/clojure-DxSadP1t.js +0 -1
  60. package/dist/assets/cmake-DbXoA79R.js +0 -1
  61. package/dist/assets/cobol-PTqiYgYu.js +0 -1
  62. package/dist/assets/codeowners-Bp6g37R7.js +0 -1
  63. package/dist/assets/codeql-sacFqUAJ.js +0 -1
  64. package/dist/assets/coffee-dyiR41kL.js +0 -1
  65. package/dist/assets/common-lisp-C7gG9l05.js +0 -1
  66. package/dist/assets/coq-Dsg_Bt_b.js +0 -1
  67. package/dist/assets/cpp-BksuvNSY.js +0 -1
  68. package/dist/assets/crystal-DtDmRg-F.js +0 -1
  69. package/dist/assets/csharp-D9R-vmeu.js +0 -1
  70. package/dist/assets/css-BPhBrDlE.js +0 -1
  71. package/dist/assets/csv-B0qRVHPH.js +0 -1
  72. package/dist/assets/cue-DtFQj3wx.js +0 -1
  73. package/dist/assets/cypher-m2LEI-9-.js +0 -1
  74. package/dist/assets/d-BoXegm-a.js +0 -1
  75. package/dist/assets/dark-plus-C3mMm8J8.js +0 -1
  76. package/dist/assets/dart-B9wLZaAG.js +0 -1
  77. package/dist/assets/dax-ClGRhx96.js +0 -1
  78. package/dist/assets/desktop-DEIpsLCJ.js +0 -1
  79. package/dist/assets/diff-BgYniUM_.js +0 -1
  80. package/dist/assets/docker-COcR7UxN.js +0 -1
  81. package/dist/assets/dotenv-BjQB5zDj.js +0 -1
  82. package/dist/assets/dracula-BzJJZx-M.js +0 -1
  83. package/dist/assets/dracula-soft-BXkSAIEj.js +0 -1
  84. package/dist/assets/dream-maker-C-nORZOA.js +0 -1
  85. package/dist/assets/edge-D5gP-w-T.js +0 -1
  86. package/dist/assets/elixir-CLiX3zqd.js +0 -1
  87. package/dist/assets/elm-CmHSxxaM.js +0 -1
  88. package/dist/assets/emacs-lisp-BX77sIaO.js +0 -1
  89. package/dist/assets/erb-BYTLMnw6.js +0 -1
  90. package/dist/assets/erlang-B-DoSBHF.js +0 -1
  91. package/dist/assets/everforest-dark-BgDCqdQA.js +0 -1
  92. package/dist/assets/everforest-light-C8M2exoo.js +0 -1
  93. package/dist/assets/fennel-bCA53EVm.js +0 -1
  94. package/dist/assets/fish-w-ucz2PV.js +0 -1
  95. package/dist/assets/fluent-Dayu4EKP.js +0 -1
  96. package/dist/assets/fortran-fixed-form-TqA4NnZg.js +0 -1
  97. package/dist/assets/fortran-free-form-DKXYxT9g.js +0 -1
  98. package/dist/assets/fsharp-XplgxFYe.js +0 -1
  99. package/dist/assets/gdresource-BHYsBjWJ.js +0 -1
  100. package/dist/assets/gdscript-DfxzS6Rs.js +0 -1
  101. package/dist/assets/gdshader-SKMF96pI.js +0 -1
  102. package/dist/assets/genie-ajMbGru0.js +0 -1
  103. package/dist/assets/gherkin--30QC5Em.js +0 -1
  104. package/dist/assets/git-commit-i4q6IMui.js +0 -1
  105. package/dist/assets/git-rebase-B-v9cOL2.js +0 -1
  106. package/dist/assets/github-dark-DHJKELXO.js +0 -1
  107. package/dist/assets/github-dark-default-Cuk6v7N8.js +0 -1
  108. package/dist/assets/github-dark-dimmed-DH5Ifo-i.js +0 -1
  109. package/dist/assets/github-dark-high-contrast-E3gJ1_iC.js +0 -1
  110. package/dist/assets/github-light-DAi9KRSo.js +0 -1
  111. package/dist/assets/github-light-default-D7oLnXFd.js +0 -1
  112. package/dist/assets/github-light-high-contrast-BfjtVDDH.js +0 -1
  113. package/dist/assets/gleam-B430Bg39.js +0 -1
  114. package/dist/assets/glimmer-js-D-cwc0-E.js +0 -1
  115. package/dist/assets/glimmer-ts-pgjy16dm.js +0 -1
  116. package/dist/assets/glsl-DBO2IWDn.js +0 -1
  117. package/dist/assets/gnuplot-CM8KxXT1.js +0 -1
  118. package/dist/assets/go-B1SYOhNW.js +0 -1
  119. package/dist/assets/graphql-cDcHW_If.js +0 -1
  120. package/dist/assets/groovy-DkBy-JyN.js +0 -1
  121. package/dist/assets/hack-D1yCygmZ.js +0 -1
  122. package/dist/assets/haml-B2EZWmdv.js +0 -1
  123. package/dist/assets/handlebars-BQGss363.js +0 -1
  124. package/dist/assets/haskell-BILxekzW.js +0 -1
  125. package/dist/assets/haxe-C5wWYbrZ.js +0 -1
  126. package/dist/assets/hcl-HzYwdGDm.js +0 -1
  127. package/dist/assets/hjson-T-Tgc4AT.js +0 -1
  128. package/dist/assets/hlsl-ifBTmRxC.js +0 -1
  129. package/dist/assets/houston-DnULxvSX.js +0 -1
  130. package/dist/assets/html-C2L_23MC.js +0 -1
  131. package/dist/assets/html-derivative-CSfWNPLT.js +0 -1
  132. package/dist/assets/http-FRrOvY1W.js +0 -1
  133. package/dist/assets/hxml-TIA70rKU.js +0 -1
  134. package/dist/assets/hy-BMj5Y0dO.js +0 -1
  135. package/dist/assets/imba-bv_oIlVt.js +0 -1
  136. package/dist/assets/index-BuQ1xTdM.css +0 -1
  137. package/dist/assets/index-DclShbpd.js +0 -125
  138. package/dist/assets/ini-BjABl1g7.js +0 -1
  139. package/dist/assets/java-xI-RfyKK.js +0 -1
  140. package/dist/assets/javascript-ySlJ1b_l.js +0 -1
  141. package/dist/assets/jinja-DGy0s7-h.js +0 -1
  142. package/dist/assets/jison-BqZprYcd.js +0 -1
  143. package/dist/assets/json-BQoSv7ci.js +0 -1
  144. package/dist/assets/json5-w8dY5SsB.js +0 -1
  145. package/dist/assets/jsonc-TU54ms6u.js +0 -1
  146. package/dist/assets/jsonl-DREVFZK8.js +0 -1
  147. package/dist/assets/jsonnet-BfivnA6A.js +0 -1
  148. package/dist/assets/jssm-P4WzXJd0.js +0 -1
  149. package/dist/assets/jsx-BAng5TT0.js +0 -1
  150. package/dist/assets/julia-BBuGR-5E.js +0 -1
  151. package/dist/assets/kanagawa-dragon-CkXjmgJE.js +0 -1
  152. package/dist/assets/kanagawa-lotus-CfQXZHmo.js +0 -1
  153. package/dist/assets/kanagawa-wave-DWedfzmr.js +0 -1
  154. package/dist/assets/kotlin-B5lbUyaz.js +0 -1
  155. package/dist/assets/kusto-mebxcVVE.js +0 -1
  156. package/dist/assets/laserwave-DUszq2jm.js +0 -1
  157. package/dist/assets/latex-C-cWTeAZ.js +0 -1
  158. package/dist/assets/lean-XBlWyCtg.js +0 -1
  159. package/dist/assets/less-BfCpw3nA.js +0 -1
  160. package/dist/assets/light-plus-B7mTdjB0.js +0 -1
  161. package/dist/assets/liquid-D3W5UaiH.js +0 -1
  162. package/dist/assets/log-Cc5clBb7.js +0 -1
  163. package/dist/assets/logo-IuBKFhSY.js +0 -1
  164. package/dist/assets/lua-CvWAzNxB.js +0 -1
  165. package/dist/assets/luau-Du5NY7AG.js +0 -1
  166. package/dist/assets/make-Bvotw-X0.js +0 -1
  167. package/dist/assets/markdown-UIAJJxZW.js +0 -1
  168. package/dist/assets/marko-z0MBrx5-.js +0 -1
  169. package/dist/assets/material-theme-D5KoaKCx.js +0 -1
  170. package/dist/assets/material-theme-darker-BfHTSMKl.js +0 -1
  171. package/dist/assets/material-theme-lighter-B0m2ddpp.js +0 -1
  172. package/dist/assets/material-theme-ocean-CyktbL80.js +0 -1
  173. package/dist/assets/material-theme-palenight-Csfq5Kiy.js +0 -1
  174. package/dist/assets/matlab-D9-PGadD.js +0 -1
  175. package/dist/assets/mdc-DB_EDNY_.js +0 -1
  176. package/dist/assets/mdx-sdHcTMYB.js +0 -1
  177. package/dist/assets/mermaid-Ci6OQyBP.js +0 -1
  178. package/dist/assets/min-dark-CafNBF8u.js +0 -1
  179. package/dist/assets/min-light-CTRr51gU.js +0 -1
  180. package/dist/assets/mipsasm-BC5c_5Pe.js +0 -1
  181. package/dist/assets/mojo-Tz6hzZYG.js +0 -1
  182. package/dist/assets/monokai-D4h5O-jR.js +0 -1
  183. package/dist/assets/move-DB_GagMm.js +0 -1
  184. package/dist/assets/narrat-DLbgOhZU.js +0 -1
  185. package/dist/assets/nextflow-B0XVJmRM.js +0 -1
  186. package/dist/assets/nginx-D_VnBJ67.js +0 -1
  187. package/dist/assets/night-owl-C39BiMTA.js +0 -1
  188. package/dist/assets/nim-ZlGxZxc3.js +0 -1
  189. package/dist/assets/nix-shcSOmrb.js +0 -1
  190. package/dist/assets/nord-Ddv68eIx.js +0 -1
  191. package/dist/assets/nushell-D4Tzg5kh.js +0 -1
  192. package/dist/assets/objective-c-Deuh7S70.js +0 -1
  193. package/dist/assets/objective-cpp-BUEGK8hf.js +0 -1
  194. package/dist/assets/ocaml-BNioltXt.js +0 -1
  195. package/dist/assets/one-dark-pro-GBQ2dnAY.js +0 -1
  196. package/dist/assets/one-light-PoHY5YXO.js +0 -1
  197. package/dist/assets/pascal-JqZropPD.js +0 -1
  198. package/dist/assets/perl-CHQXSrWU.js +0 -1
  199. package/dist/assets/php-B5ebYQev.js +0 -1
  200. package/dist/assets/plastic-3e1v2bzS.js +0 -1
  201. package/dist/assets/plsql-LKU2TuZ1.js +0 -1
  202. package/dist/assets/po-BFLt1xDp.js +0 -1
  203. package/dist/assets/poimandres-CS3Unz2-.js +0 -1
  204. package/dist/assets/polar-DKykz6zU.js +0 -1
  205. package/dist/assets/postcss-B3ZDOciz.js +0 -1
  206. package/dist/assets/powerquery-CSHBycmS.js +0 -1
  207. package/dist/assets/powershell-BIEUsx6d.js +0 -1
  208. package/dist/assets/prisma-B48N-Iqd.js +0 -1
  209. package/dist/assets/prolog-BY-TUvya.js +0 -1
  210. package/dist/assets/proto-zocC4JxJ.js +0 -1
  211. package/dist/assets/pug-CM9l7STV.js +0 -1
  212. package/dist/assets/puppet-Cza_XSSt.js +0 -1
  213. package/dist/assets/purescript-Bg-kzb6g.js +0 -1
  214. package/dist/assets/python-DhUJRlN_.js +0 -1
  215. package/dist/assets/qml-D8XfuvdV.js +0 -1
  216. package/dist/assets/qmldir-C8lEn-DE.js +0 -1
  217. package/dist/assets/qss-DhMKtDLN.js +0 -1
  218. package/dist/assets/r-CwjWoCRV.js +0 -1
  219. package/dist/assets/racket-CzouJOBO.js +0 -1
  220. package/dist/assets/raku-B1bQXN8T.js +0 -1
  221. package/dist/assets/razor-CNLDkMZG.js +0 -1
  222. package/dist/assets/red-bN70gL4F.js +0 -1
  223. package/dist/assets/reg-5LuOXUq_.js +0 -1
  224. package/dist/assets/regexp-DWJ3fJO_.js +0 -1
  225. package/dist/assets/rel-DJlmqQ1C.js +0 -1
  226. package/dist/assets/riscv-QhoSD0DR.js +0 -1
  227. package/dist/assets/rose-pine-CmCqftbK.js +0 -1
  228. package/dist/assets/rose-pine-dawn-Ds-gbosJ.js +0 -1
  229. package/dist/assets/rose-pine-moon-CjDtw9vr.js +0 -1
  230. package/dist/assets/rst-4NLicBqY.js +0 -1
  231. package/dist/assets/ruby-DeZ3UC14.js +0 -1
  232. package/dist/assets/rust-Be6lgOlo.js +0 -1
  233. package/dist/assets/sas-BmTFh92c.js +0 -1
  234. package/dist/assets/sass-BJ4Li9vH.js +0 -1
  235. package/dist/assets/scala-DQVVAn-B.js +0 -1
  236. package/dist/assets/scheme-BJGe-b2p.js +0 -1
  237. package/dist/assets/scss-C31hgJw-.js +0 -1
  238. package/dist/assets/sdbl-BLhTXw86.js +0 -1
  239. package/dist/assets/shaderlab-B7qAK45m.js +0 -1
  240. package/dist/assets/shellscript-atvbtKCR.js +0 -1
  241. package/dist/assets/shellsession-C_rIy8kc.js +0 -1
  242. package/dist/assets/slack-dark-BthQWCQV.js +0 -1
  243. package/dist/assets/slack-ochin-DqwNpetd.js +0 -1
  244. package/dist/assets/smalltalk-DkLiglaE.js +0 -1
  245. package/dist/assets/snazzy-light-Bw305WKR.js +0 -1
  246. package/dist/assets/solarized-dark-DXbdFlpD.js +0 -1
  247. package/dist/assets/solarized-light-L9t79GZl.js +0 -1
  248. package/dist/assets/solidity-C1w2a3ep.js +0 -1
  249. package/dist/assets/soy-C-lX7w71.js +0 -1
  250. package/dist/assets/sparql-bYkjHRlG.js +0 -1
  251. package/dist/assets/splunk-Cf8iN4DR.js +0 -1
  252. package/dist/assets/sql-COK4E0Yg.js +0 -1
  253. package/dist/assets/ssh-config-BknIz3MU.js +0 -1
  254. package/dist/assets/stata-DorPZHa4.js +0 -1
  255. package/dist/assets/stylus-BeQkCIfX.js +0 -1
  256. package/dist/assets/svelte-MSaWC3Je.js +0 -1
  257. package/dist/assets/swift-BSxZ-RaX.js +0 -1
  258. package/dist/assets/synthwave-84-CbfX1IO0.js +0 -1
  259. package/dist/assets/system-verilog-C7L56vO4.js +0 -1
  260. package/dist/assets/systemd-CUnW07Te.js +0 -1
  261. package/dist/assets/talonscript-C1XDQQGZ.js +0 -1
  262. package/dist/assets/tasl-CQjiPCtT.js +0 -1
  263. package/dist/assets/tcl-DQ1-QYvQ.js +0 -1
  264. package/dist/assets/templ-dwX3ZSMB.js +0 -1
  265. package/dist/assets/terraform-BbSNqyBO.js +0 -1
  266. package/dist/assets/tex-rYs2v40G.js +0 -1
  267. package/dist/assets/tokyo-night-DBQeEorK.js +0 -1
  268. package/dist/assets/toml-CB2ApiWb.js +0 -1
  269. package/dist/assets/ts-tags-CipyTH0X.js +0 -1
  270. package/dist/assets/tsv-B_m7g4N7.js +0 -1
  271. package/dist/assets/tsx-B6W0miNI.js +0 -1
  272. package/dist/assets/turtle-BMR_PYu6.js +0 -1
  273. package/dist/assets/twig-NC5TFiHP.js +0 -1
  274. package/dist/assets/typescript-Dj6nwHGl.js +0 -1
  275. package/dist/assets/typespec-BpWG_bgh.js +0 -1
  276. package/dist/assets/typst-BVUVsWT6.js +0 -1
  277. package/dist/assets/v-CAQ2eGtk.js +0 -1
  278. package/dist/assets/vala-BFOHcciG.js +0 -1
  279. package/dist/assets/vb-CdO5JTpU.js +0 -1
  280. package/dist/assets/verilog-CJaU5se_.js +0 -1
  281. package/dist/assets/vesper-BEBZ7ncR.js +0 -1
  282. package/dist/assets/vhdl-DYoNaHQp.js +0 -1
  283. package/dist/assets/viml-m4uW47V2.js +0 -1
  284. package/dist/assets/vitesse-black-Bkuqu6BP.js +0 -1
  285. package/dist/assets/vitesse-dark-D0r3Knsf.js +0 -1
  286. package/dist/assets/vitesse-light-CVO1_9PV.js +0 -1
  287. package/dist/assets/vue-BuYVFjOK.js +0 -1
  288. package/dist/assets/vue-html-xdeiXROB.js +0 -1
  289. package/dist/assets/vyper-nyqBNV6O.js +0 -1
  290. package/dist/assets/wasm-C6j12Q_x.js +0 -1
  291. package/dist/assets/wasm-CG6Dc4jp.js +0 -1
  292. package/dist/assets/wenyan-7A4Fjokl.js +0 -1
  293. package/dist/assets/wgsl-CB0Krxn9.js +0 -1
  294. package/dist/assets/wikitext-DCE3LsBG.js +0 -1
  295. package/dist/assets/wolfram-C3FkfJm5.js +0 -1
  296. package/dist/assets/xml-e3z08dGr.js +0 -1
  297. package/dist/assets/xsl-Dd0NUgwM.js +0 -1
  298. package/dist/assets/yaml-CVw76BM1.js +0 -1
  299. package/dist/assets/zenscript-HnGAYVZD.js +0 -1
  300. package/dist/assets/zig-BVz_zdnA.js +0 -1
  301. package/server/app.js +0 -165
  302. package/server/app.test.js +0 -30
  303. package/server/bd.js +0 -227
  304. package/server/bd.test.js +0 -194
  305. package/server/cli/cli.test.js +0 -207
  306. package/server/cli/commands.integration.test.js +0 -148
  307. package/server/cli/commands.js +0 -285
  308. package/server/cli/commands.unit.test.js +0 -408
  309. package/server/cli/daemon.js +0 -340
  310. package/server/cli/daemon.test.js +0 -31
  311. package/server/cli/index.js +0 -135
  312. package/server/cli/open.js +0 -178
  313. package/server/cli/open.test.js +0 -26
  314. package/server/cli/usage.js +0 -49
  315. package/server/config.js +0 -36
  316. package/server/db.test.js +0 -169
  317. package/server/dolt-pool.js +0 -313
  318. package/server/dolt-queries.js +0 -764
  319. package/server/list-adapters.js +0 -421
  320. package/server/list-adapters.test.js +0 -208
  321. package/server/logging.js +0 -23
  322. package/server/registry-watcher.js +0 -200
  323. package/server/subscriptions.js +0 -299
  324. package/server/subscriptions.test.js +0 -128
  325. package/server/validators.js +0 -124
  326. package/server/watcher.js +0 -139
  327. package/server/watcher.test.js +0 -120
  328. package/server/ws.comments.test.js +0 -262
  329. package/server/ws.delete.test.js +0 -119
  330. package/server/ws.js +0 -1329
  331. package/server/ws.labels.test.js +0 -95
  332. package/server/ws.list-refresh.coalesce.test.js +0 -95
  333. package/server/ws.list-subscriptions.test.js +0 -403
  334. package/server/ws.mutation-window.test.js +0 -147
  335. package/server/ws.mutations.test.js +0 -389
  336. package/server/ws.test.js +0 -52
package/server/db.js CHANGED
@@ -1,154 +1,111 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
1
+ // Database adapter for the web server. Pool-per-schema design so a single
2
+ // bd-web instance can serve every project on a shared postgres without
3
+ // SET-search-path races between concurrent requests.
4
+ //
5
+ // Workflow:
6
+ // const root = await openRoot(dsn); // for `/api/projects` etc.
7
+ // const project = await root.forProject('yuklar'); // for project-scoped queries
4
8
 
5
- /**
6
- * Resolve the SQLite DB path used by beads according to precedence:
7
- * 1) explicit --db flag (provided via options.explicit_db)
8
- * 2) BEADS_DB environment variable
9
- * 3) nearest ".beads/*.db" by walking up from cwd (excluding
10
- * "~/.beads/default.db", which is reserved for fallback)
11
- * 4) "~/.beads/default.db" fallback
12
- *
13
- * Returns a normalized absolute path and a `source` indicator. Existence is
14
- * returned via the `exists` boolean.
15
- *
16
- * @param {{ cwd?: string, env?: Record<string, string | undefined>, explicit_db?: string }} [options]
17
- * @returns {{ path: string, source: 'flag'|'env'|'nearest'|'home-default', exists: boolean }}
18
- */
19
- export function resolveDbPath(options = {}) {
20
- const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
21
- const env = options.env || process.env;
22
- const home_default = path.join(os.homedir(), '.beads', 'default.db');
9
+ import Database from 'better-sqlite3';
10
+ import pg from 'pg';
11
+ import { URL } from 'node:url';
12
+ import { detectDriver, sqlitePath } from './dsn.js';
23
13
 
24
- // 1) explicit flag
25
- if (options.explicit_db && options.explicit_db.length > 0) {
26
- const p = absFrom(options.explicit_db, cwd);
27
- return { path: p, source: 'flag', exists: fileExists(p) };
28
- }
14
+ const { Pool } = pg;
29
15
 
30
- // 2) BEADS_DB env
31
- if (env.BEADS_DB && String(env.BEADS_DB).length > 0) {
32
- const p = absFrom(String(env.BEADS_DB), cwd);
33
- return { path: p, source: 'env', exists: fileExists(p) };
34
- }
16
+ export async function openRoot(dsn) {
17
+ const driver = detectDriver(dsn);
18
+ if (driver === 'sqlite') return new SqliteAdapter(dsn);
19
+ if (driver === 'postgres') return await PgRoot.create(dsn);
20
+ throw new Error(`unsupported driver: ${driver}`);
21
+ }
35
22
 
36
- // 3) nearest .beads/*.db walking up
37
- const nearest = findNearestBeadsDb(cwd);
38
- if (nearest && path.normalize(nearest) !== path.normalize(home_default)) {
39
- return { path: nearest, source: 'nearest', exists: fileExists(nearest) };
23
+ // SqliteAdapter has only one schema by definition. forProject() returns the
24
+ // same adapter regardless of name — projects don't apply.
25
+ class SqliteAdapter {
26
+ constructor(dsn) {
27
+ this.driver = 'sqlite';
28
+ this.db = new Database(sqlitePath(dsn), { readonly: false, fileMustExist: true });
29
+ this.db.pragma('foreign_keys = ON');
40
30
  }
41
-
42
- // 4) ~/.beads/default.db
43
- return {
44
- path: home_default,
45
- source: 'home-default',
46
- exists: fileExists(home_default)
47
- };
31
+ all(sql, params = []) { return this.db.prepare(sql).all(...params); }
32
+ one(sql, params = []) { return this.db.prepare(sql).get(...params); }
33
+ exec(sql, params = []) { return this.db.prepare(sql).run(...params); }
34
+ async close() { this.db.close(); }
35
+ async forProject(_prefix) { return this; } // single-project file
36
+ async listProjectNames() { return []; } // no schemas to enumerate
48
37
  }
49
38
 
50
- /**
51
- * Resolve the workspace database location used by the UI/server.
52
- *
53
- * For non-SQLite backends (for example Dolt), this returns the nearest
54
- * workspace `.beads` directory when metadata exists. This avoids collapsing
55
- * all such workspaces onto the `~/.beads/default.db` fallback.
56
- *
57
- * @param {{ cwd?: string, env?: Record<string, string | undefined>, explicit_db?: string }} [options]
58
- * @returns {{ path: string, source: 'flag'|'env'|'nearest'|'metadata'|'home-default', exists: boolean }}
59
- */
60
- export function resolveWorkspaceDatabase(options = {}) {
61
- const sqlite_db = resolveDbPath(options);
62
- if (sqlite_db.source !== 'home-default') {
63
- return sqlite_db;
39
+ // PgRoot owns one Pool with no search_path (queries against
40
+ // information_schema.schemata) and a Map<prefix, PgProject> of schema-bound
41
+ // pools created lazily.
42
+ class PgRoot {
43
+ static async create(dsn) {
44
+ const a = new PgRoot();
45
+ a.driver = 'postgres';
46
+ a.dsn = dsn;
47
+ // Strip any leftover search_path from the boot DSN we manage it
48
+ // per-project below.
49
+ const u = new URL(dsn);
50
+ u.searchParams.delete('search_path');
51
+ a.rootPool = new Pool({ connectionString: u.toString() });
52
+ a.projects = new Map();
53
+ return a;
64
54
  }
65
55
 
66
- const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
67
- const metadata_path = findNearestBeadsMetadata(cwd);
68
- if (metadata_path) {
69
- return {
70
- path: path.dirname(metadata_path),
71
- source: 'metadata',
72
- exists: true
73
- };
56
+ // root pool queries used by listProjectNames + auth/config endpoints.
57
+ async all(sql, params = []) { return (await this.rootPool.query(rebind(sql), params)).rows; }
58
+ async one(sql, params = []) { return (await this.rootPool.query(rebind(sql), params)).rows[0]; }
59
+ async exec(sql, params = []) { return await this.rootPool.query(rebind(sql), params); }
60
+ async close() {
61
+ await this.rootPool.end();
62
+ for (const p of this.projects.values()) await p.pool.end();
74
63
  }
75
64
 
76
- return sqlite_db;
77
- }
78
-
79
- /**
80
- * Find nearest `.beads/metadata.json` by walking up from start.
81
- *
82
- * @param {string} start
83
- * @returns {string | null}
84
- */
85
- export function findNearestBeadsMetadata(start) {
86
- let dir = path.resolve(start);
87
- for (let i = 0; i < 100; i++) {
88
- const metadata_path = path.join(dir, '.beads', 'metadata.json');
89
- if (fileExists(metadata_path)) {
90
- return metadata_path;
91
- }
92
- const parent = path.dirname(dir);
93
- if (parent === dir) {
94
- break;
65
+ async forProject(prefix) {
66
+ if (!prefix || !/^[a-z0-9_-]+$/.test(prefix)) {
67
+ throw new Error(`invalid project prefix: ${prefix}`);
95
68
  }
96
- dir = parent;
69
+ if (this.projects.has(prefix)) return this.projects.get(prefix);
70
+
71
+ const u = new URL(this.dsn);
72
+ u.searchParams.delete('search_path');
73
+ const existing = u.searchParams.get('options') || '';
74
+ const suffix = `-c search_path=${prefix}`;
75
+ u.searchParams.set('options', existing ? `${existing} ${suffix}` : suffix);
76
+ const pool = new Pool({ connectionString: u.toString() });
77
+ const proj = new PgProject(pool);
78
+ this.projects.set(prefix, proj);
79
+ return proj;
97
80
  }
98
- return null;
99
- }
100
81
 
101
- /**
102
- * Find nearest .beads/*.db by walking up from start.
103
- * First alphabetical .db.
104
- *
105
- * @param {string} start
106
- * @returns {string | null}
107
- */
108
- export function findNearestBeadsDb(start) {
109
- let dir = path.resolve(start);
110
- // Cap iterations to avoid infinite loop in degenerate cases
111
- for (let i = 0; i < 100; i++) {
112
- const beads_dir = path.join(dir, '.beads');
113
- try {
114
- const entries = fs.readdirSync(beads_dir, { withFileTypes: true });
115
- const dbs = entries
116
- .filter((e) => e.isFile() && e.name.endsWith('.db'))
117
- .map((e) => e.name)
118
- .sort();
119
- if (dbs.length > 0) {
120
- return path.join(beads_dir, dbs[0]);
121
- }
122
- } catch {
123
- // ignore and walk up
124
- }
125
- const parent = path.dirname(dir);
126
- if (parent === dir) {
127
- break;
128
- }
129
- dir = parent;
82
+ async listProjectNames() {
83
+ const rows = await this.all(`
84
+ SELECT s.schema_name FROM information_schema.schemata s
85
+ WHERE s.schema_name NOT IN ('pg_catalog','information_schema','pg_toast','public')
86
+ AND s.schema_name NOT LIKE 'pg_%'
87
+ AND EXISTS (
88
+ SELECT 1 FROM information_schema.tables t
89
+ WHERE t.table_schema = s.schema_name AND t.table_name = 'config'
90
+ )
91
+ ORDER BY s.schema_name`);
92
+ return rows.map((r) => r.schema_name);
130
93
  }
131
- return null;
132
94
  }
133
95
 
134
- /**
135
- * Resolve possibly relative `p` against `cwd` to an absolute filesystem path.
136
- *
137
- * @param {string} p
138
- * @param {string} cwd
139
- */
140
- function absFrom(p, cwd) {
141
- return path.isAbsolute(p) ? path.normalize(p) : path.join(cwd, p);
96
+ // PgProject is the schema-bound view a route handler queries.
97
+ class PgProject {
98
+ constructor(pool) {
99
+ this.driver = 'postgres';
100
+ this.pool = pool;
101
+ }
102
+ async all(sql, params = []) { return (await this.pool.query(rebind(sql), params)).rows; }
103
+ async one(sql, params = []) { return (await this.pool.query(rebind(sql), params)).rows[0]; }
104
+ async exec(sql, params = []) { return await this.pool.query(rebind(sql), params); }
105
+ async forProject(_p) { return this; }
142
106
  }
143
107
 
144
- /**
145
- * @param {string} p
146
- */
147
- function fileExists(p) {
148
- try {
149
- fs.accessSync(p, fs.constants.F_OK);
150
- return true;
151
- } catch {
152
- return false;
153
- }
108
+ function rebind(sql) {
109
+ let n = 0;
110
+ return sql.replace(/\?/g, () => `$${++n}`);
154
111
  }
package/server/dsn.js ADDED
@@ -0,0 +1,130 @@
1
+ // DSN resolution for the web server. Mirrors internal/config/config.go in the
2
+ // Go CLI: read .bd/config (or $BD_DB), inject password from $BD_DB_PASSWORD or
3
+ // a .env file. Sqlite paths and postgres URIs are both supported.
4
+
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+
8
+ export const DIR_NAME = '.bd';
9
+ export const CONFIG_NAME = 'config';
10
+ export const ENV_FILE_NAME = '.env';
11
+ export const ENV_DSN = 'BD_DB';
12
+ export const ENV_DIR = 'BD_DIR';
13
+ export const ENV_DB_PASSWORD = 'BD_DB_PASSWORD';
14
+
15
+ export function resolve({ cwd = process.cwd(), env = process.env } = {}) {
16
+ let dsn = '';
17
+ let beadDir = '';
18
+
19
+ if (env[ENV_DSN]) dsn = env[ENV_DSN];
20
+
21
+ beadDir = findBeadDir(cwd, env);
22
+ if (!dsn && beadDir) dsn = readDSNFromFile(beadDir);
23
+ if (!dsn) {
24
+ if (!beadDir) {
25
+ throw new Error('no .bd directory found — run `bd init` first');
26
+ }
27
+ dsn = path.join(beadDir, 'bd.db');
28
+ }
29
+
30
+ const pw = lookupDBPassword(beadDir, env);
31
+ dsn = injectPassword(dsn, pw);
32
+ return { dsn, beadDir };
33
+ }
34
+
35
+ function findBeadDir(cwd, env) {
36
+ if (env[ENV_DIR]) return env[ENV_DIR];
37
+ let cur = path.resolve(cwd);
38
+ for (;;) {
39
+ const candidate = path.join(cur, DIR_NAME);
40
+ try {
41
+ if (fs.statSync(candidate).isDirectory()) return candidate;
42
+ } catch {}
43
+ const parent = path.dirname(cur);
44
+ if (parent === cur) break;
45
+ cur = parent;
46
+ }
47
+ return '';
48
+ }
49
+
50
+ function readDSNFromFile(dir) {
51
+ try {
52
+ const text = fs.readFileSync(path.join(dir, CONFIG_NAME), 'utf8');
53
+ for (const line of text.split('\n')) {
54
+ const t = line.trim();
55
+ if (!t || t.startsWith('#')) continue;
56
+ const eq = t.indexOf('=');
57
+ if (eq < 0) continue;
58
+ const k = t.slice(0, eq).trim();
59
+ const v = t.slice(eq + 1).trim();
60
+ if (k === 'db') return v;
61
+ }
62
+ } catch {}
63
+ return '';
64
+ }
65
+
66
+ function lookupDBPassword(beadDir, env) {
67
+ if (env[ENV_DB_PASSWORD]) return env[ENV_DB_PASSWORD];
68
+ const candidates = [path.join(process.cwd(), ENV_FILE_NAME)];
69
+ if (beadDir) candidates.push(path.join(beadDir, ENV_FILE_NAME));
70
+ for (const p of candidates) {
71
+ const v = readEnvKey(p, ENV_DB_PASSWORD);
72
+ if (v) return v;
73
+ }
74
+ return '';
75
+ }
76
+
77
+ function readEnvKey(file, key) {
78
+ try {
79
+ const text = fs.readFileSync(file, 'utf8');
80
+ const prefix = `${key}=`;
81
+ const exportPrefix = `export ${prefix}`;
82
+ for (const raw of text.split('\n')) {
83
+ let line = raw.trim();
84
+ if (!line || line.startsWith('#')) continue;
85
+ if (line.startsWith(exportPrefix)) line = line.slice('export '.length);
86
+ if (!line.startsWith(prefix)) continue;
87
+ let v = line.slice(prefix.length).trim();
88
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
89
+ v = v.slice(1, -1);
90
+ }
91
+ return v;
92
+ }
93
+ } catch {}
94
+ return '';
95
+ }
96
+
97
+ export function injectPassword(dsn, pw) {
98
+ if (!pw) return dsn;
99
+ if (dsn.startsWith('postgres://') || dsn.startsWith('postgresql://')) {
100
+ return injectURIPassword(dsn, pw);
101
+ }
102
+ return dsn;
103
+ }
104
+
105
+ function injectURIPassword(dsn, pw) {
106
+ const idx = dsn.indexOf('://');
107
+ if (idx < 0) return dsn;
108
+ const scheme = dsn.slice(0, idx);
109
+ const rest = dsn.slice(idx + 3);
110
+ const at = rest.indexOf('@');
111
+ if (at <= 0) return dsn;
112
+ const cred = rest.slice(0, at);
113
+ const hostPath = rest.slice(at);
114
+ if (cred.includes(':')) return dsn;
115
+ return `${scheme}://${cred}:${encodeURIComponent(pw)}${hostPath}`;
116
+ }
117
+
118
+ export function detectDriver(dsn) {
119
+ if (dsn.startsWith('postgres://') || dsn.startsWith('postgresql://')) return 'postgres';
120
+ if (dsn.startsWith('sqlite://')) return 'sqlite';
121
+ if (dsn.startsWith('sqlite:')) return 'sqlite';
122
+ if (dsn.endsWith('.db') || dsn.endsWith('.sqlite') || dsn.endsWith('.sqlite3')) return 'sqlite';
123
+ throw new Error(`cannot determine driver from DSN ${dsn}`);
124
+ }
125
+
126
+ export function sqlitePath(dsn) {
127
+ if (dsn.startsWith('sqlite://')) return dsn.slice('sqlite://'.length);
128
+ if (dsn.startsWith('sqlite:')) return dsn.slice('sqlite:'.length);
129
+ return dsn;
130
+ }
package/server/index.js CHANGED
@@ -1,97 +1,161 @@
1
- import { createServer } from 'node:http';
2
- import { createApp } from './app.js';
3
- import { printServerUrl } from './cli/daemon.js';
4
- import { getConfig } from './config.js';
5
- import { resolveWorkspaceDatabase } from './db.js';
6
- import { startDoltServer, stopDoltServer } from './dolt-pool.js';
7
- import { debug, enableAllDebug } from './logging.js';
8
- import { registerWorkspace, watchRegistry } from './registry-watcher.js';
9
- import { watchDb } from './watcher.js';
10
- import { attachWsServer } from './ws.js';
11
-
12
- if (process.argv.includes('--debug') || process.argv.includes('-d')) {
13
- enableAllDebug();
14
- }
1
+ // Entry point for `bd-web`. Multi-project capable: a single instance routes
2
+ // project-scoped requests to per-schema connection pools (see db.js). URLs
3
+ // look like /api/p/<prefix>/issues, /api/p/<prefix>/issues/:id, etc.
4
+ // `/api/me` and `/api/projects` are project-agnostic.
15
5
 
16
- // Parse --host and --port from argv and set env vars before getConfig()
17
- for (let i = 0; i < process.argv.length; i++) {
18
- if (process.argv[i] === '--host' && process.argv[i + 1]) {
19
- process.env.HOST = process.argv[++i];
20
- }
21
- if (process.argv[i] === '--port' && process.argv[i + 1]) {
22
- process.env.PORT = process.argv[++i];
23
- }
24
- }
6
+ import { serve } from '@hono/node-server';
7
+ import { serveStatic } from '@hono/node-server/serve-static';
8
+ import { Hono } from 'hono';
9
+ import { existsSync } from 'node:fs';
10
+ import { dirname, resolve as resolvePath } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ import { openRoot } from './db.js';
14
+ import { resolve as resolveDSN } from './dsn.js';
15
+ import { getConfigValue } from './queries.js';
16
+ import { authMiddleware, fingerprint, loadAuth } from './auth.js';
17
+ import { authRouter } from './routes/auth.js';
18
+ import { issuesRouter } from './routes/issues.js';
19
+ import { streamHandler } from './routes/stream.js';
20
+
21
+ const HOST = process.env.HOST || '127.0.0.1';
22
+ const PORT = Number(process.env.PORT || 3333);
23
+
24
+ const PKG_DIR = resolvePath(dirname(fileURLToPath(import.meta.url)), '..');
25
+ const DIST_DIR = resolvePath(PKG_DIR, 'dist');
26
+
27
+ async function main() {
28
+ const { dsn, beadDir } = resolveDSN();
29
+ const root = await openRoot(dsn);
30
+ const auth = loadAuth();
25
31
 
26
- const config = getConfig();
27
- const app = createApp(config);
28
- const server = createServer(app);
29
- const log = debug('server');
30
-
31
- // Register the initial workspace (from cwd) so it appears in the workspace picker
32
- // even without the beads daemon running
33
- const workspace_database = resolveWorkspaceDatabase({ cwd: config.root_dir });
34
- if (workspace_database.source !== 'home-default' && workspace_database.exists) {
35
- registerWorkspace({
36
- path: config.root_dir,
37
- database: workspace_database.path
32
+ const app = new Hono();
33
+
34
+ app.use('*', async (c, next) => {
35
+ const t = Date.now();
36
+ await next();
37
+ if (process.env.DEBUG) {
38
+ console.log(`${c.req.method} ${c.req.path} ${c.res.status} ${Date.now() - t}ms`);
39
+ }
38
40
  });
39
- }
40
41
 
41
- // Start Dolt SQL server BEFORE accepting connections.
42
- // The SQL server holds an exclusive lock on the embedded DB, so bd CLI
43
- // can't run as fallback while it's up. We must wait for the pool.
44
- const doltPool = await startDoltServer(config.root_dir);
45
- if (doltPool) {
46
- log('Dolt SQL server started — fast query mode enabled (sub-ms queries)');
47
- } else {
48
- log('Dolt SQL server not available — using bd CLI fallback (~600ms/query)');
49
- }
42
+ app.get('/api/healthz', (c) => c.json({ ok: true }));
43
+ app.use('/api/*', authMiddleware(auth));
44
+ app.route('/api/auth', authRouter(auth));
50
45
 
51
- // Watch the active beads DB and schedule subscription refresh for active lists
52
- const db_watcher = watchDb(config.root_dir, () => {
53
- // Schedule subscription list refresh run for active subscriptions
54
- log('db change detected → schedule refresh');
55
- scheduleListRefresh();
56
- // v2: all updates flow via subscription push envelopes only
57
- });
46
+ // /api/me returns auth + the active set of projects.
47
+ app.get('/api/me', async (c) => {
48
+ const user = c.get('user') || { username: 'anon', role: 'Anonymous' };
49
+ let projects = [];
50
+ try {
51
+ const names = await root.listProjectNames();
52
+ projects = await Promise.all(names.map(async (prefix) => {
53
+ try {
54
+ const p = await root.forProject(prefix);
55
+ const idMode = (await getConfigValue(p, 'issue_id_mode')) || 'hash';
56
+ return { prefix, id_mode: idMode };
57
+ } catch {
58
+ return { prefix, id_mode: 'hash' };
59
+ }
60
+ }));
61
+ } catch (err) {
62
+ // sqlite or unreachable — return empty list, the client falls back to
63
+ // the project-less single-DB UI.
64
+ if (process.env.DEBUG) console.error('listProjectNames failed:', err.message);
65
+ }
66
+ return c.json({
67
+ user,
68
+ driver: root.driver,
69
+ projects,
70
+ auth_enabled: auth.enabled,
71
+ auth_fingerprint: fingerprint(auth),
72
+ bead_dir: beadDir,
73
+ version: process.env.npm_package_version || '0.0.0',
74
+ file_attachment_base_url: (process.env.FILE_ATTACHMENT_BASE_URL || '').replace(/\/$/, ''),
75
+ });
76
+ });
58
77
 
59
- const { scheduleListRefresh } = attachWsServer(server, {
60
- path: '/ws',
61
- heartbeat_ms: 30000,
62
- // Coalesce DB change bursts into one refresh run
63
- refresh_debounce_ms: 75,
64
- root_dir: config.root_dir,
65
- watcher: db_watcher
66
- });
78
+ // /api/projects same as /api/me's projects field, kept as a separate
79
+ // endpoint for the (now path-prefixed) project picker page.
80
+ app.get('/api/projects', async (c) => {
81
+ const names = await root.listProjectNames();
82
+ return c.json({ projects: names.map((prefix) => ({ prefix })) });
83
+ });
67
84
 
68
- // Watch the global registry for workspace changes (e.g., when user starts
69
- // bd daemon in a different project). This enables automatic workspace switching.
70
- watchRegistry(
71
- (entries) => {
72
- log('registry changed: %d entries', entries.length);
73
- // Find if there's a newer workspace that matches our initial root
74
- // For now, we just log the change - users can switch via set-workspace
75
- // Future: could auto-switch if a workspace was started in a parent/child dir
76
- },
77
- { debounce_ms: 500 }
78
- );
79
-
80
- // Graceful shutdown: stop Dolt server
81
- process.on('SIGTERM', async () => {
82
- await stopDoltServer();
83
- process.exit(0);
84
- });
85
- process.on('SIGINT', async () => {
86
- await stopDoltServer();
87
- process.exit(0);
88
- });
85
+ // Project-scoped middleware: validates the prefix and attaches the pool.
86
+ const projectScope = async (c, next) => {
87
+ const prefix = c.req.param('prefix');
88
+ if (!prefix || !/^[a-z0-9_-]+$/.test(prefix)) {
89
+ return c.json({ error: 'invalid project prefix' }, 400);
90
+ }
91
+ let p;
92
+ try {
93
+ p = await root.forProject(prefix);
94
+ // sanity-check: schema has a config table
95
+ await p.one('SELECT 1 FROM config LIMIT 1');
96
+ } catch (err) {
97
+ return c.json({ error: `project not found: ${prefix}` }, 404);
98
+ }
99
+ c.set('db', p);
100
+ c.set('project', prefix);
101
+ await next();
102
+ };
89
103
 
90
- server.listen(config.port, config.host, () => {
91
- printServerUrl();
92
- });
104
+ // Per-project endpoints. Sub-routers read the db from context.
105
+ app.use('/api/p/:prefix/*', projectScope);
106
+
107
+ app.get('/api/p/:prefix/me', async (c) => {
108
+ const db = c.get('db');
109
+ const prefix = c.get('project');
110
+ const idMode = (await getConfigValue(db, 'issue_id_mode')) || 'hash';
111
+ return c.json({ project: { prefix, id_mode: idMode } });
112
+ });
113
+
114
+ app.route('/api/p/:prefix/issues', issuesRouter());
115
+ app.get('/api/p/:prefix/stream', streamHandler);
116
+
117
+ app.notFound((c) => c.json({ error: 'not found' }, 404));
118
+ app.onError((err, c) => {
119
+ console.error('error:', err);
120
+ return c.json({ error: err.message || 'internal error' }, 500);
121
+ });
122
+
123
+ if (existsSync(DIST_DIR)) {
124
+ app.use('/*', serveStatic({ root: DIST_DIR }));
125
+ app.get('/*', serveStatic({ path: resolvePath(DIST_DIR, 'index.html') }));
126
+ } else {
127
+ app.get('/', (c) => c.json({
128
+ hint: `client build not found at ${DIST_DIR}; run \`npm run build\` or use vite dev (5173).`,
129
+ }));
130
+ }
131
+
132
+ serve({ fetch: app.fetch, hostname: HOST, port: PORT }, (info) => {
133
+ console.log(`bd-web ${PORT}/${info.port} listening on http://${HOST}:${info.port}`);
134
+ console.log(` driver: ${root.driver}`);
135
+ console.log(` auth: ${auth.enabled ? 'enabled' : 'disabled (BD_WEB_AUTH_FILE unset)'}`);
136
+ if (process.env.BD_WEB_OPEN === '1') {
137
+ tryOpen(`http://${HOST === '0.0.0.0' ? '127.0.0.1' : HOST}:${info.port}`);
138
+ }
139
+ });
140
+
141
+ const stop = async () => {
142
+ try { await root.close(); } catch {}
143
+ process.exit(0);
144
+ };
145
+ process.on('SIGINT', stop);
146
+ process.on('SIGTERM', stop);
147
+ }
148
+
149
+ function tryOpen(url) {
150
+ const cmd = process.platform === 'darwin' ? 'open'
151
+ : process.platform === 'win32' ? 'start'
152
+ : 'xdg-open';
153
+ import('node:child_process').then(({ spawn }) => {
154
+ spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
155
+ }).catch(() => {});
156
+ }
93
157
 
94
- server.on('error', (err) => {
95
- log('server error %o', err);
96
- process.exitCode = 1;
158
+ main().catch((err) => {
159
+ console.error('fatal:', err.message);
160
+ process.exit(1);
97
161
  });