local-review 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (364) hide show
  1. package/AGENT-GUIDE.md +468 -0
  2. package/CHANGELOG.md +53 -0
  3. package/LICENSE +21 -0
  4. package/README.md +126 -2
  5. package/dist/server/anchors.js +207 -0
  6. package/dist/server/assets.js +147 -0
  7. package/dist/server/backups.js +212 -0
  8. package/dist/server/branches.js +113 -0
  9. package/dist/server/cli.js +255 -0
  10. package/dist/server/commits.js +393 -0
  11. package/dist/server/config.js +59 -0
  12. package/dist/server/context.js +68 -0
  13. package/dist/server/events.js +87 -0
  14. package/dist/server/git.js +103 -0
  15. package/dist/server/index.js +566 -0
  16. package/dist/server/landed.js +71 -0
  17. package/dist/server/plan.js +260 -0
  18. package/dist/server/port.js +3 -0
  19. package/dist/server/projectEdit.js +117 -0
  20. package/dist/server/projects.js +319 -0
  21. package/dist/server/prs.js +314 -0
  22. package/dist/server/reviewer.js +33 -0
  23. package/dist/server/reviews.js +586 -0
  24. package/dist/server/safefs.js +92 -0
  25. package/dist/server/security.js +38 -0
  26. package/dist/server/stackviz.js +446 -0
  27. package/dist/shared/assets.js +54 -0
  28. package/dist/shared/paths.js +13 -0
  29. package/dist/shared/prText.js +22 -0
  30. package/dist/shared/readiness.js +37 -0
  31. package/dist/shared/repoLabel.js +13 -0
  32. package/dist/shared/totals.js +75 -0
  33. package/dist/web/assets/abap-CLvhMVsD.js +1 -0
  34. package/dist/web/assets/actionscript-3-Cs6JJXT5.js +1 -0
  35. package/dist/web/assets/ada-X3Y2mlq2.js +1 -0
  36. package/dist/web/assets/ahk-DwADkxyd.js +1 -0
  37. package/dist/web/assets/ahk2-_6rzW9BS.js +1 -0
  38. package/dist/web/assets/andromeeda-vGVdxbeo.js +1 -0
  39. package/dist/web/assets/angular-html-DJ4eInGE.js +1 -0
  40. package/dist/web/assets/angular-ts-DBhgg2a7.js +1 -0
  41. package/dist/web/assets/apache-U0d_L8uA.js +1 -0
  42. package/dist/web/assets/apex-KFGFyKwG.js +1 -0
  43. package/dist/web/assets/apl-TCQAu201.js +1 -0
  44. package/dist/web/assets/apple-touch-icon-BNrSfFwW.png +0 -0
  45. package/dist/web/assets/applescript-CCn79oCD.js +1 -0
  46. package/dist/web/assets/ara-4CJ0cIlV.js +1 -0
  47. package/dist/web/assets/asciidoc-SCjQUq34.js +1 -0
  48. package/dist/web/assets/asm-Cmm7eHzH.js +1 -0
  49. package/dist/web/assets/astro-DVtFuwC2.js +1 -0
  50. package/dist/web/assets/aurora-x-CDeNXAV0.js +1 -0
  51. package/dist/web/assets/awk-BWXHIvNe.js +1 -0
  52. package/dist/web/assets/ayu-dark-DluEY0Gj.js +1 -0
  53. package/dist/web/assets/ayu-light-C3h-C4tm.js +1 -0
  54. package/dist/web/assets/ayu-mirage-Bqwy1Gya.js +1 -0
  55. package/dist/web/assets/ballerina-B7ZEbQpA.js +1 -0
  56. package/dist/web/assets/bat-qdxAdBWQ.js +1 -0
  57. package/dist/web/assets/beancount-C2IN4Vk8.js +1 -0
  58. package/dist/web/assets/berry-DKpUyyne.js +1 -0
  59. package/dist/web/assets/bibtex-Ci_nEsc7.js +1 -0
  60. package/dist/web/assets/bicep-CUHmPFLl.js +1 -0
  61. package/dist/web/assets/bird2-CYIUgYqB.js +1 -0
  62. package/dist/web/assets/blade-CMp-sZYg.js +1 -0
  63. package/dist/web/assets/bsl-_VjlB9mv.js +1 -0
  64. package/dist/web/assets/c-DgqspIM2.js +1 -0
  65. package/dist/web/assets/c3-D0apG41V.js +1 -0
  66. package/dist/web/assets/cadence-CQ2zXKGN.js +1 -0
  67. package/dist/web/assets/cairo-DLTphjLi.js +1 -0
  68. package/dist/web/assets/catppuccin-frappe-C9Q5zVZY.js +1 -0
  69. package/dist/web/assets/catppuccin-latte-CtsyrtPq.js +1 -0
  70. package/dist/web/assets/catppuccin-macchiato-R8nAf1vR.js +1 -0
  71. package/dist/web/assets/catppuccin-mocha-DjuaAJKi.js +1 -0
  72. package/dist/web/assets/chapel-NTLt7i8E.js +1 -0
  73. package/dist/web/assets/clarity-CtfBBspg.js +1 -0
  74. package/dist/web/assets/clojure-DqKBuwfJ.js +1 -0
  75. package/dist/web/assets/cmake-Bj61d0ZC.js +1 -0
  76. package/dist/web/assets/cobol-CJiWQrT4.js +1 -0
  77. package/dist/web/assets/codeowners-C8r90Shi.js +1 -0
  78. package/dist/web/assets/codeql-oeQT6MSM.js +1 -0
  79. package/dist/web/assets/coffee-Bv_hE6wf.js +1 -0
  80. package/dist/web/assets/common-lisp-Cv5bFMCO.js +1 -0
  81. package/dist/web/assets/coq-CGaSfhwR.js +1 -0
  82. package/dist/web/assets/cpp-CuUsyAvc.js +1 -0
  83. package/dist/web/assets/crystal-BeLopiD_.js +1 -0
  84. package/dist/web/assets/csharp-dmMxLVLW.js +1 -0
  85. package/dist/web/assets/css-OngwBV3K.js +1 -0
  86. package/dist/web/assets/csv-Dx-8-gkx.js +1 -0
  87. package/dist/web/assets/cue-CE9AQfxI.js +1 -0
  88. package/dist/web/assets/cypher-ClKdZ_lG.js +1 -0
  89. package/dist/web/assets/d-qD-0Kul2.js +1 -0
  90. package/dist/web/assets/dark-plus-Cs2F2srj.js +1 -0
  91. package/dist/web/assets/dart-CHZY9Xhz.js +1 -0
  92. package/dist/web/assets/dax-BkyTk9wS.js +1 -0
  93. package/dist/web/assets/desktop-Dlh5hvp9.js +1 -0
  94. package/dist/web/assets/diff-woXpYk--.js +1 -0
  95. package/dist/web/assets/docker-IyjqRm3v.js +1 -0
  96. package/dist/web/assets/dotenv-CuH5my32.js +1 -0
  97. package/dist/web/assets/dracula-BHWKrbxM.js +1 -0
  98. package/dist/web/assets/dracula-soft-5eyTD99u.js +1 -0
  99. package/dist/web/assets/dream-maker-DW3nJb8Q.js +1 -0
  100. package/dist/web/assets/edge-BFLildoE.js +1 -0
  101. package/dist/web/assets/elixir-1Fs6FjVs.js +1 -0
  102. package/dist/web/assets/elm-piMM67vF.js +1 -0
  103. package/dist/web/assets/emacs-lisp-DR2vtWaj.js +1 -0
  104. package/dist/web/assets/erb-JJ0sF8GL.js +1 -0
  105. package/dist/web/assets/erlang-Cphh6RMH.js +1 -0
  106. package/dist/web/assets/everforest-dark-sB-x3p7T.js +1 -0
  107. package/dist/web/assets/everforest-light-Df2xbC6M.js +1 -0
  108. package/dist/web/assets/favicon-Bld6z4jR.svg +13 -0
  109. package/dist/web/assets/favicon-CDTyigy2.ico +0 -0
  110. package/dist/web/assets/fennel-DQxkIbk2.js +1 -0
  111. package/dist/web/assets/fish-BJitypiv.js +1 -0
  112. package/dist/web/assets/fluent-C03EYrpw.js +1 -0
  113. package/dist/web/assets/fortran-fixed-form-DEKoE2YW.js +1 -0
  114. package/dist/web/assets/fortran-free-form-CYNrtFtB.js +1 -0
  115. package/dist/web/assets/fsharp-D13ZGOAj.js +1 -0
  116. package/dist/web/assets/gdresource-xMC7jr7x.js +1 -0
  117. package/dist/web/assets/gdscript-DYvRD3hg.js +1 -0
  118. package/dist/web/assets/gdshader-CBce3t8t.js +1 -0
  119. package/dist/web/assets/genie-CV2tkWYe.js +1 -0
  120. package/dist/web/assets/gherkin-DExj1W_8.js +1 -0
  121. package/dist/web/assets/git-commit-BSykSTBG.js +1 -0
  122. package/dist/web/assets/git-rebase-CtqXywWe.js +1 -0
  123. package/dist/web/assets/github-dark-C-LZuMrd.js +1 -0
  124. package/dist/web/assets/github-dark-default-DXG-b-1a.js +1 -0
  125. package/dist/web/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
  126. package/dist/web/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
  127. package/dist/web/assets/github-light-EUqPIrTm.js +1 -0
  128. package/dist/web/assets/github-light-default-BXViO-2h.js +1 -0
  129. package/dist/web/assets/github-light-high-contrast-B68TUdTA.js +1 -0
  130. package/dist/web/assets/gleam-CSRkHgEL.js +1 -0
  131. package/dist/web/assets/glimmer-js-BPewWxGC.js +1 -0
  132. package/dist/web/assets/glimmer-ts-DyUQWB47.js +1 -0
  133. package/dist/web/assets/glsl-D6tJoMaT.js +1 -0
  134. package/dist/web/assets/gn-CAXYAgV-.js +1 -0
  135. package/dist/web/assets/gnuplot-7GGW24-e.js +1 -0
  136. package/dist/web/assets/go-rLFTqkRN.js +1 -0
  137. package/dist/web/assets/graphql-E6u2hkWE.js +1 -0
  138. package/dist/web/assets/groovy-CacY0gHj.js +1 -0
  139. package/dist/web/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
  140. package/dist/web/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
  141. package/dist/web/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
  142. package/dist/web/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
  143. package/dist/web/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
  144. package/dist/web/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
  145. package/dist/web/assets/hack-Djq191-P.js +1 -0
  146. package/dist/web/assets/haml-C0LqmZG3.js +1 -0
  147. package/dist/web/assets/handlebars-DwdxF6uN.js +1 -0
  148. package/dist/web/assets/haskell-D8IpX4py.js +1 -0
  149. package/dist/web/assets/haxe-C8oCnwJJ.js +1 -0
  150. package/dist/web/assets/hcl-Dh228itO.js +1 -0
  151. package/dist/web/assets/hjson-CxZEssPk.js +1 -0
  152. package/dist/web/assets/hlsl-Cvrh5tZx.js +1 -0
  153. package/dist/web/assets/horizon-CE9ld1lL.js +1 -0
  154. package/dist/web/assets/horizon-bright-DSNQnXHK.js +1 -0
  155. package/dist/web/assets/houston-CsvMBhTu.js +1 -0
  156. package/dist/web/assets/html-B5Myt0fy.js +1 -0
  157. package/dist/web/assets/html-derivative-DTVrLAw3.js +1 -0
  158. package/dist/web/assets/http-CFoh3cQf.js +1 -0
  159. package/dist/web/assets/hurl-B_38naAo.js +1 -0
  160. package/dist/web/assets/hxml-Dcm88srW.js +1 -0
  161. package/dist/web/assets/hy-CZbG8q4J.js +1 -0
  162. package/dist/web/assets/imba-DsUTQ-LC.js +1 -0
  163. package/dist/web/assets/index-CfxIojO0.js +451 -0
  164. package/dist/web/assets/index-QywYYqTw.css +1 -0
  165. package/dist/web/assets/ini-B5eOa1yu.js +1 -0
  166. package/dist/web/assets/java-odnexk6r.js +1 -0
  167. package/dist/web/assets/javascript-DvFCJKws.js +1 -0
  168. package/dist/web/assets/jinja-UeLt2DYu.js +1 -0
  169. package/dist/web/assets/jison-HNaDYeqE.js +1 -0
  170. package/dist/web/assets/json-D57CnrjR.js +1 -0
  171. package/dist/web/assets/json5-BR5RXkoi.js +1 -0
  172. package/dist/web/assets/jsonc-CYpm1nAK.js +1 -0
  173. package/dist/web/assets/jsonl-CmCQp5Yx.js +1 -0
  174. package/dist/web/assets/jsonnet-CJTPZ8u_.js +1 -0
  175. package/dist/web/assets/jssm-DXw9l8Rf.js +1 -0
  176. package/dist/web/assets/jsx-o_5ZAydi.js +1 -0
  177. package/dist/web/assets/julia-20UebbrD.js +1 -0
  178. package/dist/web/assets/just-DqnPCg-Q.js +1 -0
  179. package/dist/web/assets/kanagawa-dragon-Bozd7klA.js +1 -0
  180. package/dist/web/assets/kanagawa-lotus-BmuOG9ZW.js +1 -0
  181. package/dist/web/assets/kanagawa-wave-BML5mF2J.js +1 -0
  182. package/dist/web/assets/kdl-CsD5j6eV.js +1 -0
  183. package/dist/web/assets/kotlin-DhhofPvG.js +1 -0
  184. package/dist/web/assets/kusto-C7mF5XQf.js +1 -0
  185. package/dist/web/assets/laserwave-C_8bwKvT.js +1 -0
  186. package/dist/web/assets/latex-CZhIB-LV.js +1 -0
  187. package/dist/web/assets/lean-CewbzKMR.js +1 -0
  188. package/dist/web/assets/less-DVTAwKKz.js +1 -0
  189. package/dist/web/assets/light-plus-DVQuIRkW.js +1 -0
  190. package/dist/web/assets/liquid-BzouPsN0.js +1 -0
  191. package/dist/web/assets/llvm-DqHOMS-s.js +1 -0
  192. package/dist/web/assets/log-BNLmms1o.js +1 -0
  193. package/dist/web/assets/logo-Cluzi2Zq.js +1 -0
  194. package/dist/web/assets/lua-BXclv8Fm.js +1 -0
  195. package/dist/web/assets/luau-VGWVLoqX.js +1 -0
  196. package/dist/web/assets/make-Dixweg8N.js +1 -0
  197. package/dist/web/assets/markdown-BYOwaDjH.js +1 -0
  198. package/dist/web/assets/marko-M78_INlG.js +1 -0
  199. package/dist/web/assets/material-theme-Bm3Qr25_.js +1 -0
  200. package/dist/web/assets/material-theme-darker-2IIEA8gg.js +1 -0
  201. package/dist/web/assets/material-theme-lighter-uhdI0v04.js +1 -0
  202. package/dist/web/assets/material-theme-ocean-CHQ94UKr.js +1 -0
  203. package/dist/web/assets/material-theme-palenight-B5W6OYN7.js +1 -0
  204. package/dist/web/assets/matlab-D7qyCx1q.js +1 -0
  205. package/dist/web/assets/mdc-CBluOiY7.js +1 -0
  206. package/dist/web/assets/mdx-DQZ5AkYe.js +1 -0
  207. package/dist/web/assets/mermaid-BxpfTIzt.js +1 -0
  208. package/dist/web/assets/min-dark-BSWPekZh.js +1 -0
  209. package/dist/web/assets/min-light-DDpmG2fV.js +1 -0
  210. package/dist/web/assets/mipsasm-BMqwQI7S.js +1 -0
  211. package/dist/web/assets/mojo-fg40_ntQ.js +1 -0
  212. package/dist/web/assets/monokai-BscAjiQZ.js +1 -0
  213. package/dist/web/assets/moonbit-D9W784E_.js +1 -0
  214. package/dist/web/assets/move-BYnGhZoM.js +1 -0
  215. package/dist/web/assets/narrat-_X_XdTYD.js +1 -0
  216. package/dist/web/assets/nextflow-CSgcYZcx.js +1 -0
  217. package/dist/web/assets/nextflow-groovy-D7IRt6Pp.js +1 -0
  218. package/dist/web/assets/nginx-CZFFQFbT.js +1 -0
  219. package/dist/web/assets/night-owl-DhmEMT88.js +1 -0
  220. package/dist/web/assets/night-owl-light-eJ-hLW7d.js +1 -0
  221. package/dist/web/assets/nim-17kKsGqk.js +1 -0
  222. package/dist/web/assets/nix-IvuFDN5E.js +1 -0
  223. package/dist/web/assets/nord-Cb4Vim4T.js +1 -0
  224. package/dist/web/assets/nsis-CjNLYCLg.js +1 -0
  225. package/dist/web/assets/nushell--0fT3PI1.js +1 -0
  226. package/dist/web/assets/objective-c-D1A_Heim.js +1 -0
  227. package/dist/web/assets/objective-cpp-BsSzOQcm.js +1 -0
  228. package/dist/web/assets/ocaml-O90oeIOV.js +1 -0
  229. package/dist/web/assets/odin-B1RWQWA5.js +1 -0
  230. package/dist/web/assets/one-dark-pro-C6YyR8OD.js +1 -0
  231. package/dist/web/assets/one-light-D7Lr4KcI.js +1 -0
  232. package/dist/web/assets/openscad-BUDT5pXO.js +1 -0
  233. package/dist/web/assets/org-BZABHRQH.js +1 -0
  234. package/dist/web/assets/pascal-4ZHwLPI5.js +1 -0
  235. package/dist/web/assets/perl-CMrAQ9Je.js +1 -0
  236. package/dist/web/assets/php-DwA3kETk.js +1 -0
  237. package/dist/web/assets/pierre-dark-CpLgRqie.js +1 -0
  238. package/dist/web/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
  239. package/dist/web/assets/pierre-dark-soft-kZQmAZld.js +1 -0
  240. package/dist/web/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
  241. package/dist/web/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
  242. package/dist/web/assets/pierre-light-CoaEpmwp.js +1 -0
  243. package/dist/web/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
  244. package/dist/web/assets/pierre-light-soft-lWLdNTOI.js +1 -0
  245. package/dist/web/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
  246. package/dist/web/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
  247. package/dist/web/assets/pkl-ot-7Btpt.js +1 -0
  248. package/dist/web/assets/plastic-DQwYfKfQ.js +1 -0
  249. package/dist/web/assets/plsql-DGHpHOYJ.js +1 -0
  250. package/dist/web/assets/po-BiJDBrnU.js +1 -0
  251. package/dist/web/assets/poimandres-DRFjx7u4.js +1 -0
  252. package/dist/web/assets/polar-C7UOKdEL.js +1 -0
  253. package/dist/web/assets/postcss-BXeXVLqQ.js +1 -0
  254. package/dist/web/assets/powerquery-DNMTfnFr.js +1 -0
  255. package/dist/web/assets/powershell-QtSnmXcu.js +1 -0
  256. package/dist/web/assets/prisma-DMgNhnYS.js +1 -0
  257. package/dist/web/assets/prolog-iXnhIJG7.js +1 -0
  258. package/dist/web/assets/proto-DB4EqR-F.js +1 -0
  259. package/dist/web/assets/pug-D4fM85kA.js +1 -0
  260. package/dist/web/assets/puppet-CDv2pdJW.js +1 -0
  261. package/dist/web/assets/purescript-9MfHhQsQ.js +1 -0
  262. package/dist/web/assets/python-gzcpVVnB.js +1 -0
  263. package/dist/web/assets/qml-C3893RHi.js +1 -0
  264. package/dist/web/assets/qmldir-DCQb3MpD.js +1 -0
  265. package/dist/web/assets/qss-Fe1Jh2GI.js +1 -0
  266. package/dist/web/assets/r-DwJhLW77.js +1 -0
  267. package/dist/web/assets/racket-DcIDlBhZ.js +1 -0
  268. package/dist/web/assets/raku-B3gFvitq.js +1 -0
  269. package/dist/web/assets/razor-_KkCNZ8V.js +1 -0
  270. package/dist/web/assets/rbs-C0MpOrj4.js +1 -0
  271. package/dist/web/assets/red-CYlAq2b0.js +1 -0
  272. package/dist/web/assets/reg-CRGYupPL.js +1 -0
  273. package/dist/web/assets/regexp-BZ9NHg3b.js +1 -0
  274. package/dist/web/assets/rel-BtDbiS_P.js +1 -0
  275. package/dist/web/assets/riscv-Ckw8ddFX.js +1 -0
  276. package/dist/web/assets/ron-VUp2lXgN.js +1 -0
  277. package/dist/web/assets/rose-pine-aMsaMD1D.js +1 -0
  278. package/dist/web/assets/rose-pine-dawn-8PwUyIlP.js +1 -0
  279. package/dist/web/assets/rose-pine-moon-DO-qXg-I.js +1 -0
  280. package/dist/web/assets/rosmsg-CAekHB0j.js +1 -0
  281. package/dist/web/assets/rst-gdK-1c4t.js +1 -0
  282. package/dist/web/assets/ruby-DsmqYtlH.js +1 -0
  283. package/dist/web/assets/rust-DpiIf5JO.js +1 -0
  284. package/dist/web/assets/sas-BNGv1ScF.js +1 -0
  285. package/dist/web/assets/sass-DXrisJhu.js +1 -0
  286. package/dist/web/assets/scala-BiFt_7mu.js +1 -0
  287. package/dist/web/assets/scheme-DQCgrYNe.js +1 -0
  288. package/dist/web/assets/scss-DjYUXT73.js +1 -0
  289. package/dist/web/assets/sdbl-bTVj8UrX.js +1 -0
  290. package/dist/web/assets/shaderlab-TOUzSsQk.js +1 -0
  291. package/dist/web/assets/shellscript-DBtV-eli.js +1 -0
  292. package/dist/web/assets/shellsession-BKEV8Fnx.js +1 -0
  293. package/dist/web/assets/slack-dark-DnToyrRv.js +1 -0
  294. package/dist/web/assets/slack-ochin-B2OO5cIa.js +1 -0
  295. package/dist/web/assets/smalltalk-BHpQb5nM.js +1 -0
  296. package/dist/web/assets/smithy-Dgaz7aur.js +1 -0
  297. package/dist/web/assets/snazzy-light-4G7pJPwS.js +1 -0
  298. package/dist/web/assets/solarized-dark-b97czJOk.js +1 -0
  299. package/dist/web/assets/solarized-light-DFPxpIYQ.js +1 -0
  300. package/dist/web/assets/solidity-Bj7E1mRn.js +1 -0
  301. package/dist/web/assets/soy-DUapMxos.js +1 -0
  302. package/dist/web/assets/sparql-D_iOobhT.js +1 -0
  303. package/dist/web/assets/splunk-BC2Px7Mm.js +1 -0
  304. package/dist/web/assets/sql-B-UyqKkd.js +1 -0
  305. package/dist/web/assets/ssh-config-BgfXC-Er.js +1 -0
  306. package/dist/web/assets/stata-Xq3iZiMK.js +1 -0
  307. package/dist/web/assets/stylus-B6D30XZt.js +1 -0
  308. package/dist/web/assets/surrealql-CzXacK8i.js +1 -0
  309. package/dist/web/assets/svelte-CZaBEmPP.js +1 -0
  310. package/dist/web/assets/swift-CyEgAFGc.js +1 -0
  311. package/dist/web/assets/synthwave-84-nFMaYfgc.js +1 -0
  312. package/dist/web/assets/system-verilog-Dh5ihWbf.js +1 -0
  313. package/dist/web/assets/systemd-BxMlprV5.js +1 -0
  314. package/dist/web/assets/talonscript-CohzipZa.js +1 -0
  315. package/dist/web/assets/tasl-DMoTqEGO.js +1 -0
  316. package/dist/web/assets/tcl-CZd0xW_V.js +1 -0
  317. package/dist/web/assets/templ-CxkGuUpr.js +1 -0
  318. package/dist/web/assets/terraform-DswuEJGm.js +1 -0
  319. package/dist/web/assets/tex-DP74JH6g.js +1 -0
  320. package/dist/web/assets/tokyo-night-oM2G3aXe.js +1 -0
  321. package/dist/web/assets/toml-CcmNWLt0.js +1 -0
  322. package/dist/web/assets/ts-tags-BAcrCE2Y.js +1 -0
  323. package/dist/web/assets/tsv-sltzmVWM.js +1 -0
  324. package/dist/web/assets/tsx-Dw11YP6z.js +1 -0
  325. package/dist/web/assets/turtle-ByJddavk.js +1 -0
  326. package/dist/web/assets/twig-BMVjBxD6.js +1 -0
  327. package/dist/web/assets/typescript-DoBLlVdC.js +1 -0
  328. package/dist/web/assets/typespec-BGhR2SqG.js +1 -0
  329. package/dist/web/assets/typst-w2u_vl4_.js +1 -0
  330. package/dist/web/assets/v-DBj4K9d_.js +1 -0
  331. package/dist/web/assets/vala-zf12oZj6.js +1 -0
  332. package/dist/web/assets/vb-DOwzaHZ0.js +1 -0
  333. package/dist/web/assets/verilog-B2gwz-sJ.js +1 -0
  334. package/dist/web/assets/vesper-DdrHHSXu.js +1 -0
  335. package/dist/web/assets/vhdl-BroJfC0k.js +1 -0
  336. package/dist/web/assets/viml-DvXPmvsu.js +1 -0
  337. package/dist/web/assets/vitesse-black-fwtXNY1n.js +1 -0
  338. package/dist/web/assets/vitesse-dark-BZCL-v6S.js +1 -0
  339. package/dist/web/assets/vitesse-light-VbXTXTou.js +1 -0
  340. package/dist/web/assets/vue-BmuHTv5p.js +1 -0
  341. package/dist/web/assets/vue-html-Dt0zSF4I.js +1 -0
  342. package/dist/web/assets/vue-vine-wvJ1BTex.js +1 -0
  343. package/dist/web/assets/vyper-CgoNMtux.js +1 -0
  344. package/dist/web/assets/wasm-BnjxR4X6.js +1 -0
  345. package/dist/web/assets/wasm-ByWQv1Qj.js +1 -0
  346. package/dist/web/assets/wenyan-C8pVoKbM.js +1 -0
  347. package/dist/web/assets/wgsl-BsKzXJz4.js +1 -0
  348. package/dist/web/assets/wikitext-ClFFjSW2.js +1 -0
  349. package/dist/web/assets/wit-DdvCle-K.js +1 -0
  350. package/dist/web/assets/wolfram-DLL8P-h_.js +1 -0
  351. package/dist/web/assets/xml-CXftIBCQ.js +1 -0
  352. package/dist/web/assets/xsl-BoHSOMz1.js +1 -0
  353. package/dist/web/assets/yaml-dGreldtD.js +1 -0
  354. package/dist/web/assets/zenscript-BnlCZFoB.js +1 -0
  355. package/dist/web/assets/zig-CMLA9XwU.js +1 -0
  356. package/dist/web/index.html +16 -0
  357. package/examples/project.example.yaml +40 -0
  358. package/package.json +89 -3
  359. package/server/fonts/Inter-OFL.txt +93 -0
  360. package/server/fonts/Inter_400Regular.ttf +0 -0
  361. package/server/fonts/Inter_600SemiBold.ttf +0 -0
  362. package/server/fonts/Inter_700Bold.ttf +0 -0
  363. package/server/fonts/JetBrainsMono-OFL.txt +93 -0
  364. package/server/fonts/JetBrainsMono_400Regular.ttf +0 -0
@@ -0,0 +1,393 @@
1
+ import fs from 'node:fs';
2
+ import { EMPTY_TREE, git, gitBuffer, gitWithInput, safeRef, sha40 } from './git.js';
3
+ // ---- caches keyed by immutable data (commit shas) ----
4
+ const summaryCache = new Map(); // repo\0sha
5
+ const patchIdCache = new Map();
6
+ const filesCache = new Map();
7
+ const SEP = '\x1f';
8
+ async function resolveBase(cfg) {
9
+ const candidates = cfg.base ? [cfg.base] : ['origin/HEAD', 'main', 'master'];
10
+ for (const c of candidates) {
11
+ try {
12
+ await git(cfg.path, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${safeRef(c)}^{commit}`]);
13
+ return c;
14
+ }
15
+ catch {
16
+ /* try next */
17
+ }
18
+ }
19
+ throw new Error(cfg.base ? `base ref '${cfg.base}' not found` : `no base found (tried origin/HEAD, main, master)`);
20
+ }
21
+ export const rangeEnd = (cfg) => cfg.to ?? cfg.branch ?? '';
22
+ /** The sha the range ends at (branch head or `to`), or null if it doesn't resolve. */
23
+ export async function headOf(cfg) {
24
+ try {
25
+ return (await git(cfg.path, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${safeRef(rangeEnd(cfg))}^{commit}`])).trim();
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ async function revParse(repo, ref) {
32
+ try {
33
+ return (await git(repo, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${safeRef(ref)}^{commit}`])).trim();
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ export async function summarize(repo, sha) {
40
+ const key = repo + '\0' + sha;
41
+ const hit = summaryCache.get(key);
42
+ if (hit)
43
+ return hit;
44
+ const meta = await git(repo, ['show', '-s', `--format=%P${SEP}%s${SEP}%an${SEP}%ae${SEP}%aI${SEP}%(trailers:key=Change-Id,valueonly,separator=%x2C)`, sha40(sha)]);
45
+ const [parents, subject, author, authorEmail, date, changeIds] = meta.trim().split(SEP);
46
+ const parent = parents.split(' ').filter(Boolean)[0] ?? null;
47
+ const files = await changedFiles(repo, sha, parent);
48
+ const s = {
49
+ parent,
50
+ subject,
51
+ author,
52
+ authorEmail,
53
+ date,
54
+ changeId: changeIds?.split(',')[0]?.trim() || null,
55
+ additions: files.reduce((a, f) => a + f.additions, 0),
56
+ deletions: files.reduce((a, f) => a + f.deletions, 0),
57
+ filesChanged: files.length,
58
+ };
59
+ summaryCache.set(key, s);
60
+ return s;
61
+ }
62
+ /**
63
+ * The repo's live range. With `optional` (the repo has landed PRs), a missing `branch`/`to`, or one that no longer
64
+ * resolves (deleted after the merge), is no range at all rather than an error, and other range problems are a
65
+ * `warning`; only a missing repo is still an error.
66
+ */
67
+ export async function loadRepo(cfg, opts = {}) {
68
+ const end = rangeEnd(cfg);
69
+ const info = { name: cfg.name, path: cfg.path, branch: cfg.branch ?? null, end, base: cfg.base ?? null, from: cfg.from ?? null, head: null, mergeBase: null, commits: [] };
70
+ if (!fs.existsSync(cfg.path)) {
71
+ info.error = `repo not found: ${cfg.path}`;
72
+ return info;
73
+ }
74
+ try {
75
+ if (!end) {
76
+ if (opts.optional)
77
+ return info;
78
+ throw new Error('no `branch` or `to` given');
79
+ }
80
+ info.head = await headOf(cfg);
81
+ if (!info.head && opts.optional)
82
+ return info;
83
+ if (!info.head)
84
+ throw new Error(cfg.to ? `'to' ref '${cfg.to}' not found` : `branch '${end}' not found`);
85
+ let range;
86
+ if (cfg.from) {
87
+ const first = await revParse(cfg.path, cfg.from);
88
+ if (!first)
89
+ throw new Error(`'from' commit '${cfg.from}' not found`);
90
+ try {
91
+ await git(cfg.path, ['merge-base', '--is-ancestor', '--end-of-options', sha40(first), sha40(info.head)]);
92
+ }
93
+ catch {
94
+ throw new Error(`'from' commit '${cfg.from}' is not an ancestor of ${end}`);
95
+ }
96
+ const parent = (await summarize(cfg.path, first)).parent;
97
+ info.mergeBase = parent;
98
+ range = parent ? `${sha40(parent)}..${sha40(info.head)}` : sha40(info.head);
99
+ }
100
+ else {
101
+ const base = await resolveBase(cfg);
102
+ info.base = base;
103
+ const mb = (await git(cfg.path, ['merge-base', '--end-of-options', safeRef(base), info.head])).trim();
104
+ info.mergeBase = mb;
105
+ range = `${sha40(mb)}..${sha40(info.head)}`;
106
+ }
107
+ const shas = (await git(cfg.path, ['rev-list', '--reverse', '--end-of-options', range])).split('\n').filter(Boolean);
108
+ info.commits = await Promise.all(shas.map(async (sha) => ({ sha, short: sha.slice(0, 8), ...(await summarize(cfg.path, sha)) })));
109
+ }
110
+ catch (e) {
111
+ if (opts.optional) {
112
+ info.warning = `live range: ${e.message}`;
113
+ info.commits = [];
114
+ }
115
+ else
116
+ info.error = e.message;
117
+ }
118
+ return info;
119
+ }
120
+ /** The repo's base ref as a sha (`base`, else origin/HEAD, main, master), or null. */
121
+ export async function baseSha(cfg) {
122
+ try {
123
+ return await revParse(cfg.path, await resolveBase(cfg));
124
+ }
125
+ catch {
126
+ return null;
127
+ }
128
+ }
129
+ const isFull = (s) => typeof s === 'string' && /^[0-9a-f]{40}$/.test(s);
130
+ /** A full sha from a YAML file, if the repo has it as a commit; returned as git itself prints it. */
131
+ const present = (repo, s) => (isFull(s) ? revParse(repo, s) : Promise.resolve(null));
132
+ async function mergeBaseOf(repo, a, b) {
133
+ try {
134
+ return (await git(repo, ['merge-base', '--end-of-options', sha40(a), sha40(b)])).trim() || null;
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ }
140
+ const landedCache = new Map(); // only complete (merge-base) results: objects don't vanish
141
+ /**
142
+ * A landed PR as a range entry: `sha` is its prs file's key, and its diff is merge-base(base, head)...head (GitHub's
143
+ * view of a stacked PR once it's retargeted and rebased onto the base). If the head or base object isn't in the repo
144
+ * (a deleted branch, never fetched), it falls back to the merge commit's first-parent diff, else no diff at all.
145
+ * Subject: the prs file's stored `subject:`, else the head's (or the key commit's), else the PR title.
146
+ */
147
+ export async function landedSummary(repo, key, l, stored = {}) {
148
+ sha40(key);
149
+ const ck = [repo, key, l.head, l.base, l.commit, l.method, l.at, stored.subject, stored.title].join('\0');
150
+ const hit = landedCache.get(ck);
151
+ if (hit)
152
+ return hit;
153
+ const [head, base, merge, keyObj] = await Promise.all([present(repo, l.head), present(repo, l.base), present(repo, l.commit), present(repo, key)]);
154
+ let from = null;
155
+ let to = null;
156
+ let diff = 'none';
157
+ if (head && base) {
158
+ const mb = await mergeBaseOf(repo, base, head);
159
+ if (mb)
160
+ [from, to, diff] = [mb, head, 'merge-base'];
161
+ }
162
+ if (!to && merge) {
163
+ from = (await summarize(repo, merge)).parent;
164
+ [to, diff] = [merge, 'merge-commit'];
165
+ }
166
+ const metaSha = head ?? keyObj; // whose author, date and subject stand for the PR (not the merge commit's)
167
+ const meta = metaSha ? await summarize(repo, metaSha) : merge ? await summarize(repo, merge) : null;
168
+ const files = to ? await changedFiles(repo, to, from) : [];
169
+ const s = {
170
+ sha: key,
171
+ short: key.slice(0, 8),
172
+ parent: from,
173
+ subject: stored.subject?.trim() || (metaSha && meta?.subject) || stored.title || meta?.subject || '',
174
+ author: meta?.author ?? '',
175
+ authorEmail: meta?.authorEmail ?? '',
176
+ date: (metaSha && meta?.date) || l.at || meta?.date || '',
177
+ changeId: null,
178
+ additions: files.reduce((a, f) => a + f.additions, 0),
179
+ deletions: files.reduce((a, f) => a + f.deletions, 0),
180
+ filesChanged: files.length,
181
+ tip: to,
182
+ landed: { ...l, diff },
183
+ };
184
+ if (diff === 'merge-base') {
185
+ if (landedCache.size > 2000)
186
+ landedCache.clear();
187
+ landedCache.set(ck, s);
188
+ }
189
+ return s;
190
+ }
191
+ /** The files a range entry changes: its first-parent diff, or a landed PR's diff (none if it has none). */
192
+ export function filesOf(repo, c) {
193
+ if (c.tip === null)
194
+ return Promise.resolve([]);
195
+ return changedFiles(repo, c.tip ?? c.sha, c.parent);
196
+ }
197
+ function parseStatus(code) {
198
+ switch (code[0]) {
199
+ case 'A': return 'added';
200
+ case 'D': return 'deleted';
201
+ case 'R': return 'renamed';
202
+ case 'C': return 'copied';
203
+ case 'T': return 'type-changed';
204
+ default: return 'modified';
205
+ }
206
+ }
207
+ /** Files changed by `sha` relative to its first parent (or the empty tree), with -M rename detection. */
208
+ export async function changedFiles(repo, sha, parent) {
209
+ const key = repo + '\0' + sha + '\0' + (parent ?? '');
210
+ const hit = filesCache.get(key);
211
+ if (hit)
212
+ return hit;
213
+ const from = parent ? sha40(parent) : EMPTY_TREE;
214
+ sha40(sha);
215
+ const [rawOut, numOut] = await Promise.all([
216
+ git(repo, ['diff', '-M', '--no-ext-diff', '--no-textconv', '--raw', '-z', from, sha, '--']),
217
+ git(repo, ['diff', '-M', '--no-ext-diff', '--no-textconv', '--numstat', '-z', from, sha, '--']),
218
+ ]);
219
+ // --raw -z: ":old new oldsha newsha STATUS\0path\0[newpath\0]"
220
+ const raw = rawOut.split('\0');
221
+ const files = [];
222
+ for (let i = 0; i < raw.length;) {
223
+ const head = raw[i++];
224
+ if (!head || !head.startsWith(':'))
225
+ continue;
226
+ const statusCode = head.split(' ')[4] ?? 'M';
227
+ const status = parseStatus(statusCode);
228
+ if (status === 'renamed' || status === 'copied') {
229
+ const oldPath = raw[i++];
230
+ const newPath = raw[i++];
231
+ files.push({ path: newPath, oldPath, status, similarity: Number(statusCode.slice(1)) || undefined, additions: 0, deletions: 0, binary: false });
232
+ }
233
+ else {
234
+ const p = raw[i++];
235
+ files.push({ path: p, oldPath: status === 'added' ? null : p, status, additions: 0, deletions: 0, binary: false });
236
+ }
237
+ }
238
+ // --numstat -z: "add\tdel\tpath\0" or for renames "add\tdel\t\0old\0new\0"
239
+ const num = numOut.split('\0');
240
+ const stats = new Map();
241
+ for (let i = 0; i < num.length;) {
242
+ const rec = num[i++];
243
+ if (!rec)
244
+ continue;
245
+ const [a, d, p] = rec.split('\t');
246
+ let pathKey = p;
247
+ if (p === '' || p === undefined) {
248
+ i++; // old path
249
+ pathKey = num[i++];
250
+ }
251
+ stats.set(pathKey, { a: a === '-' ? 0 : Number(a), d: d === '-' ? 0 : Number(d), bin: a === '-' });
252
+ }
253
+ for (const f of files) {
254
+ const s = stats.get(f.path);
255
+ if (s) {
256
+ f.additions = s.a;
257
+ f.deletions = s.d;
258
+ f.binary = s.bin;
259
+ }
260
+ }
261
+ filesCache.set(key, files);
262
+ return files;
263
+ }
264
+ const MAX_BLOB = 2 * 1024 * 1024;
265
+ async function readBlob(repo, rev, p) {
266
+ const spec = `${sha40(rev)}:${p}`;
267
+ const size = Number((await git(repo, ['cat-file', '-s', spec])).trim());
268
+ if (size > MAX_BLOB)
269
+ return { text: null, tooLarge: true, binary: false };
270
+ const buf = await gitBuffer(repo, ['cat-file', 'blob', spec]);
271
+ if (buf.subarray(0, 8000).includes(0))
272
+ return { text: null, tooLarge: false, binary: true };
273
+ return { text: buf.toString('utf8'), tooLarge: false, binary: false };
274
+ }
275
+ // commit details are immutable per sha; keep the last few (file contents can be large)
276
+ const detailCache = new Map();
277
+ const DETAIL_CACHE_SIZE = 12;
278
+ export function commitDetail(repo, sha) {
279
+ return cachedDetail(repo + '\0' + sha, () => loadCommitDetail(repo, sha));
280
+ }
281
+ /** A range entry's detail: a commit's, or a landed PR's (its own diff, the head's message; see landedSummary). */
282
+ export function detailOf(repo, c) {
283
+ if (!c.landed)
284
+ return commitDetail(repo, c.sha);
285
+ return cachedDetail(`${repo}\0landed\0${c.sha}\0${c.parent}\0${c.tip}\0${c.subject}`, () => loadLandedDetail(repo, c));
286
+ }
287
+ function cachedDetail(key, load) {
288
+ let p = detailCache.get(key);
289
+ if (p) {
290
+ detailCache.delete(key); // refresh LRU position
291
+ }
292
+ else {
293
+ p = load();
294
+ p.catch(() => detailCache.delete(key));
295
+ }
296
+ detailCache.set(key, p);
297
+ while (detailCache.size > DETAIL_CACHE_SIZE)
298
+ detailCache.delete(detailCache.keys().next().value);
299
+ return p;
300
+ }
301
+ const messageBody = (message) => message.replace(/^[^\n]*\n?/, '').replace(/^\n+/, '').trimEnd();
302
+ /** A commit's message body (everything after the subject line), without loading its files. */
303
+ export async function commitBody(repo, sha) {
304
+ return messageBody(await git(repo, ['show', '-s', '--format=%B', sha40(sha)]));
305
+ }
306
+ async function loadCommitDetail(repo, sha) {
307
+ const s = await summarize(repo, sha);
308
+ const meta = await git(repo, ['show', '-s', `--format=%B${SEP}%cn${SEP}%cI`, sha40(sha)]);
309
+ const [message, committer, committerDate] = meta.split(SEP);
310
+ const body = messageBody(message);
311
+ const files = await withContents(repo, await changedFiles(repo, sha, s.parent), s.parent, sha);
312
+ return { sha, short: sha.slice(0, 8), ...s, body, committer, committerDate, files };
313
+ }
314
+ async function loadLandedDetail(repo, c) {
315
+ // the message of the head (or the key commit), if the repo has it; a merge commit's message isn't the PR's
316
+ const msgSha = (await present(repo, c.landed?.head)) ?? (await present(repo, c.sha));
317
+ let body = '';
318
+ let committer = '';
319
+ let committerDate = '';
320
+ if (msgSha) {
321
+ const [message, cn, cd] = (await git(repo, ['show', '-s', `--format=%B${SEP}%cn${SEP}%cI`, sha40(msgSha)])).split(SEP);
322
+ [body, committer, committerDate] = [messageBody(message), cn, cd.trim()];
323
+ }
324
+ const files = c.tip ? await withContents(repo, await changedFiles(repo, c.tip, c.parent), c.parent, c.tip) : [];
325
+ return { ...c, body, committer, committerDate, files };
326
+ }
327
+ /** Old and new contents for each changed file, `from` (null: the empty tree) .. `to`. */
328
+ async function withContents(repo, files, from, to) {
329
+ const s = { parent: from };
330
+ const sha = to;
331
+ return Promise.all(files.map(async (f) => {
332
+ const out = { ...f, oldContent: null, newContent: null };
333
+ if (f.binary)
334
+ return out;
335
+ try {
336
+ if (f.status !== 'added' && f.oldPath && s.parent) {
337
+ const r = await readBlob(repo, s.parent, f.oldPath);
338
+ out.oldContent = r.text;
339
+ out.tooLarge ||= r.tooLarge;
340
+ out.binary ||= r.binary;
341
+ }
342
+ if (f.status !== 'deleted') {
343
+ const r = await readBlob(repo, sha, f.path);
344
+ out.newContent = r.text;
345
+ out.tooLarge ||= r.tooLarge;
346
+ out.binary ||= r.binary;
347
+ }
348
+ }
349
+ catch {
350
+ // e.g. submodule entries: leave contents empty
351
+ out.binary = true;
352
+ }
353
+ return out;
354
+ }));
355
+ }
356
+ const ancestorCache = new Map(); // repo\0sha\0head
357
+ /** Is `sha` an ancestor of (or equal to) `head`? False if either is unknown to the repo. */
358
+ export async function isAncestor(repo, sha, head) {
359
+ const key = `${repo}\0${sha}\0${head}`;
360
+ const hit = ancestorCache.get(key);
361
+ if (hit !== undefined)
362
+ return hit;
363
+ let yes = false;
364
+ try {
365
+ await git(repo, ['merge-base', '--is-ancestor', '--end-of-options', sha40(sha), sha40(head)]);
366
+ yes = true;
367
+ }
368
+ catch {
369
+ yes = false;
370
+ }
371
+ if (ancestorCache.size > 5000)
372
+ ancestorCache.clear();
373
+ ancestorCache.set(key, yes);
374
+ return yes;
375
+ }
376
+ /** `git patch-id --stable` of a commit's diff against its first parent. */
377
+ export async function patchId(repo, sha) {
378
+ const key = repo + '\0' + sha;
379
+ if (patchIdCache.has(key))
380
+ return patchIdCache.get(key);
381
+ let id = null;
382
+ try {
383
+ const s = await summarize(repo, sha);
384
+ const diff = await git(repo, ['diff', '-M', '--no-ext-diff', '--no-textconv', s.parent ? sha40(s.parent) : EMPTY_TREE, sha40(sha), '--']);
385
+ const out = await gitWithInput(repo, ['patch-id', '--stable'], diff);
386
+ id = out.trim().split(/\s+/)[0] || null;
387
+ }
388
+ catch {
389
+ id = null;
390
+ }
391
+ patchIdCache.set(key, id);
392
+ return id;
393
+ }
@@ -0,0 +1,59 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ /**
6
+ * The package root: the directory holding Local Review's own package.json. This file runs from server/ (from
7
+ * source, via tsx: dev and tests) or from dist/server/ (the compiled, installed package), so walk up to it rather
8
+ * than assume a depth.
9
+ */
10
+ export function packageRoot(from = path.dirname(fileURLToPath(import.meta.url))) {
11
+ for (let dir = from;; dir = path.dirname(dir)) {
12
+ try {
13
+ if (JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).name === 'local-review')
14
+ return dir;
15
+ }
16
+ catch {
17
+ /* no package.json here (or not ours): keep going */
18
+ }
19
+ if (path.dirname(dir) === dir)
20
+ throw new Error(`can't find Local Review's package.json above ${from}`);
21
+ }
22
+ }
23
+ export const ROOT = packageRoot();
24
+ /** The built UI (`npm run build`), the bundled fonts and the files `local-review init` / `guide` use. */
25
+ export const WEB_DIST = path.join(ROOT, 'dist', 'web');
26
+ export const FONT_DIR = path.join(ROOT, 'server', 'fonts');
27
+ export const EXAMPLE_PROJECT = path.join(ROOT, 'examples', 'project.example.yaml');
28
+ export const AGENT_GUIDE = path.join(ROOT, 'AGENT-GUIDE.md');
29
+ export function expandHome(p) {
30
+ if (p === '~')
31
+ return os.homedir();
32
+ if (p.startsWith('~/'))
33
+ return path.join(os.homedir(), p.slice(2));
34
+ return p;
35
+ }
36
+ /**
37
+ * User data lives outside the app repo, in ~/.local-review (or $LOCAL_REVIEW_HOME):
38
+ * projects/<slug>/{project.yaml, prs/, reviews/, assets/} (assets/: images, server/assets.ts)
39
+ * backups/<ISO timestamp>/ automatic snapshots of projects/ (server/backups.ts)
40
+ */
41
+ export const HOME = path.resolve(expandHome(process.env.LOCAL_REVIEW_HOME || path.join(os.homedir(), '.local-review')));
42
+ /**
43
+ * LOCAL_REVIEW_PROJECTS_DIR points just the projects at another directory (the tests use it). It's not a data
44
+ * home, so automatic snapshots are off unless LOCAL_REVIEW_HOME is set too (then they go to $LOCAL_REVIEW_HOME/backups).
45
+ */
46
+ const projectsOverride = process.env.LOCAL_REVIEW_PROJECTS_DIR;
47
+ export const PROJECTS_DIR = projectsOverride ? path.resolve(expandHome(projectsOverride)) : path.join(HOME, 'projects');
48
+ export const BACKUPS_DIR = !projectsOverride || process.env.LOCAL_REVIEW_HOME ? path.join(HOME, 'backups') : null;
49
+ /** The default port. The one place it's set: the CLI's help, the startup line and `npm run dev` all derive from it. */
50
+ export { DEFAULT_PORT } from './port.js';
51
+ /** A port number from the command line or $PORT: an integer 0–65535 (0 picks any free port), else null. */
52
+ export function parsePort(v) {
53
+ return /^[0-9]{1,5}$/.test(v) && Number(v) <= 65535 ? Number(v) : null;
54
+ }
55
+ /** A name we use as a directory and a URL segment: [A-Za-z0-9._-], no leading dot. */
56
+ export const SAFE_NAME = /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/;
57
+ export function safeName(s, fallback) {
58
+ return s.replace(/[^A-Za-z0-9._-]/g, '-').replace(/^\.+/, '_') || fallback;
59
+ }
@@ -0,0 +1,68 @@
1
+ import { commitDetail, detailOf, isAncestor, patchId, rangeEnd, summarize } from './commits.js';
2
+ export function liveCommits(cfg, info) {
3
+ // landed PRs are in the list (so nothing takes their files) but frozen (so they take nothing)
4
+ return info.commits.map((c) => ({ sha: c.sha, subject: c.subject, changeId: c.changeId, getPatchId: () => patchId(cfg.path, c.sha), ...(c.landed ? { frozen: true } : {}) }));
5
+ }
6
+ /**
7
+ * Look up the sha of an orphaned review / PR file in the reviewed repo (read-only): is it still an ancestor of
8
+ * the range end (it only left the range), and its subject, Change-Id and patch-id for files that lack them.
9
+ */
10
+ export function inspectOrphan(cfg, info) {
11
+ return async (sha) => {
12
+ if (!/^[0-9a-f]{40}$/.test(sha))
13
+ return null;
14
+ let s;
15
+ try {
16
+ s = await summarize(cfg.path, sha);
17
+ }
18
+ catch {
19
+ return { exists: false, reachable: false }; // not in this repo (or gc'd)
20
+ }
21
+ return {
22
+ exists: true,
23
+ reachable: info.head ? await isAncestor(cfg.path, sha, info.head) : false,
24
+ subject: s.subject,
25
+ changeId: s.changeId,
26
+ getPatchId: () => patchId(cfg.path, sha),
27
+ };
28
+ };
29
+ }
30
+ /** Everything the review store needs to know about one commit of a loaded repo range. */
31
+ export function contextFor(dir, cfg, info, commit) {
32
+ return {
33
+ repo: cfg.name,
34
+ dir,
35
+ repoPath: cfg.path,
36
+ branch: rangeEnd(cfg),
37
+ sha: commit.sha,
38
+ subject: commit.subject,
39
+ changeId: commit.changeId,
40
+ getPatchId: () => patchId(cfg.path, commit.sha),
41
+ liveCommits: liveCommits(cfg, info),
42
+ inspectOrphan: inspectOrphan(cfg, info),
43
+ // sha is either a range commit or an old sha taken from a review file name (40 hex), never user input
44
+ getFiles: async (sha) => {
45
+ if (!/^[0-9a-f]{40}$/.test(sha))
46
+ return null;
47
+ try {
48
+ // a landed PR's own diff (merge-base(base, head)...head), not its key commit's
49
+ return (await (sha === commit.sha ? detailOf(cfg.path, commit) : commitDetail(cfg.path, sha))).files;
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ },
55
+ };
56
+ }
57
+ export function prContextFor(dir, cfg, info, commit) {
58
+ return {
59
+ repo: cfg.name,
60
+ dir,
61
+ sha: commit.sha,
62
+ subject: commit.subject,
63
+ changeId: commit.changeId,
64
+ getPatchId: () => patchId(cfg.path, commit.sha),
65
+ liveCommits: liveCommits(cfg, info),
66
+ inspectOrphan: inspectOrphan(cfg, info),
67
+ };
68
+ }
@@ -0,0 +1,87 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { watch } from 'chokidar';
4
+ import { BACKUPS_DIR, PROJECTS_DIR } from './config.js';
5
+ import { startSnapshots } from './backups.js';
6
+ import { headOf } from './commits.js';
7
+ import { listProjectSlugs, loadProjectConfig } from './projects.js';
8
+ const listeners = new Set();
9
+ export function subscribe(l) {
10
+ listeners.add(l);
11
+ return () => listeners.delete(l);
12
+ }
13
+ export function emit(e) {
14
+ for (const l of listeners) {
15
+ try {
16
+ l(e);
17
+ }
18
+ catch {
19
+ /* ignore broken listener */
20
+ }
21
+ }
22
+ }
23
+ // debounce per key so a burst of fs events becomes one push
24
+ const timers = new Map();
25
+ function emitSoon(key, e, ms = 150) {
26
+ clearTimeout(timers.get(key));
27
+ timers.set(key, setTimeout(() => { timers.delete(key); emit(e); }, ms));
28
+ }
29
+ /** Map a changed path under projects/ to the event the browser needs. */
30
+ export function eventFor(rel) {
31
+ const [project, kind, repo, name] = rel;
32
+ if (!project)
33
+ return null;
34
+ if (rel.length === 1)
35
+ return { type: 'projects', reason: 'dir' };
36
+ if (rel.length === 2)
37
+ return kind === 'project.yaml' ? { type: 'project', project, reason: 'config' } : null;
38
+ if (rel.length !== 4 || (kind !== 'reviews' && kind !== 'prs'))
39
+ return null;
40
+ const m = /^([0-9a-f]{40})\.yaml$/.exec(name);
41
+ if (m)
42
+ return { type: kind === 'reviews' ? 'review' : 'pr', project, repo, sha: m[1] };
43
+ // renamed/backup files can still affect rebase re-matching
44
+ if (name.endsWith('.yaml') || name.endsWith('.bak'))
45
+ return { type: 'project', project, reason: kind, repo };
46
+ return null;
47
+ }
48
+ /** Watch projects/ and the branch heads, and take automatic snapshots (backups.ts). Called once on server start. */
49
+ export function startWatchers() {
50
+ fs.mkdirSync(PROJECTS_DIR, { recursive: true });
51
+ startSnapshots(PROJECTS_DIR, BACKUPS_DIR);
52
+ const onPath = (p) => {
53
+ const rel = path.relative(PROJECTS_DIR, p).split(path.sep);
54
+ if (rel[0] === '' || rel[0] === '..')
55
+ return;
56
+ const e = eventFor(rel);
57
+ if (!e)
58
+ return;
59
+ const key = e.type === 'review' || e.type === 'pr' ? `${e.type}:${e.project}:${e.repo}:${e.sha}` : e.type === 'project' ? `project:${e.project}` : 'projects';
60
+ emitSoon(key, e);
61
+ if (e.type === 'project' && e.reason === 'config')
62
+ emitSoon('projects', { type: 'projects', reason: 'config' });
63
+ };
64
+ watch(PROJECTS_DIR, { ignoreInitial: true, depth: 3, awaitWriteFinish: false, ignored: (p) => path.basename(p).endsWith('.tmp') })
65
+ .on('add', onPath)
66
+ .on('change', onPath)
67
+ .on('unlink', onPath)
68
+ .on('addDir', onPath)
69
+ .on('unlinkDir', onPath)
70
+ .on('error', (err) => console.error('[watch projects]', err));
71
+ // poll range ends (branch heads) so new / amended / rebased commits show up
72
+ const heads = new Map();
73
+ const poll = async () => {
74
+ for (const slug of listProjectSlugs()) {
75
+ const cfg = loadProjectConfig(slug);
76
+ for (const r of cfg?.repos ?? []) {
77
+ const key = `${slug}\0${r.name}\0${r.path}\0${r.to ?? r.branch}`;
78
+ const h = await headOf(r);
79
+ if (heads.has(key) && heads.get(key) !== h)
80
+ emit({ type: 'project', project: slug, reason: 'head', repo: r.name });
81
+ heads.set(key, h);
82
+ }
83
+ }
84
+ };
85
+ poll().catch(() => { });
86
+ setInterval(() => poll().catch(() => { }), 3000).unref();
87
+ }