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,212 @@
1
+ // Automatic snapshots of the projects directory: a plain recursive copy into backups/<timestamp>/, so restoring
2
+ // is just copying files back. Taken on server start and then hourly, each only if something changed since the
3
+ // newest snapshot. Pruning keeps the newest KEEP_RECENT snapshots plus the first snapshot of each (UTC) day for
4
+ // KEEP_DAYS days, and can only ever remove directories directly inside the backups dir whose names are
5
+ // snapshot timestamps (see assertPrunable).
6
+ // Image assets (projects/<slug>/assets/<hash>.<ext>) are hard-linked into a snapshot rather than copied: they're
7
+ // named by their content and never rewritten in place (safefs writes a new file and renames it), so every
8
+ // snapshot can share the one copy on disk. Where linking fails (another file system), they're copied.
9
+ import crypto from 'node:crypto';
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { ASSET_NAME } from '../shared/assets.js';
13
+ export const KEEP_RECENT = 50;
14
+ export const KEEP_DAYS = 30;
15
+ export const INTERVAL_MS = 60 * 60 * 1000;
16
+ /** `2026-10-09T08-45-12Z` (ISO 8601 UTC, ':' -> '-' so it's a valid file name everywhere), maybe `-2`, `-3`… */
17
+ export const SNAPSHOT_NAME = /^(\d{4}-\d{2}-\d{2})T\d{2}-\d{2}-\d{2}Z(?:-\d{1,3})?$/;
18
+ export function snapshotName(d) {
19
+ return d.toISOString().replace(/\.\d{3}Z$/, 'Z').replace(/:/g, '-');
20
+ }
21
+ /** Our own atomic-write temp files: never worth backing up. */
22
+ const isTemp = (name) => name.startsWith('.') && name.endsWith('.tmp');
23
+ /** An image asset: `<slug>/assets/<content hash>.<ext>` relative to the projects dir (see server/assets.ts). */
24
+ const isAsset = (rel) => {
25
+ const parts = rel.split('/');
26
+ return parts.length === 3 && parts[1] === 'assets' && ASSET_NAME.test(parts[2]);
27
+ };
28
+ /** A hash of every file's relative path and contents (symlinks are skipped, as cpSync would copy them as links). */
29
+ export function fingerprint(dir) {
30
+ const h = crypto.createHash('sha256');
31
+ let files = 0;
32
+ const walk = (d, rel) => {
33
+ let entries;
34
+ try {
35
+ entries = fs.readdirSync(d, { withFileTypes: true });
36
+ }
37
+ catch {
38
+ return;
39
+ }
40
+ entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
41
+ for (const e of entries) {
42
+ if (isTemp(e.name))
43
+ continue;
44
+ const p = path.join(d, e.name);
45
+ const r = rel ? `${rel}/${e.name}` : e.name;
46
+ if (e.isDirectory())
47
+ walk(p, r);
48
+ else if (e.isFile()) {
49
+ files++;
50
+ // an asset's name is its content hash, so its name and size stand for its bytes (no need to read MBs hourly)
51
+ if (isAsset(r))
52
+ h.update(`${r}\0${fs.statSync(p).size}\0`);
53
+ else
54
+ h.update(`${r}\0`).update(fs.readFileSync(p)).update('\0');
55
+ }
56
+ }
57
+ };
58
+ walk(dir, '');
59
+ return files ? h.digest('hex') : null;
60
+ }
61
+ /** Snapshot directory names in `backupsDir`, oldest first (the names sort chronologically). */
62
+ export function listSnapshots(backupsDir) {
63
+ let entries;
64
+ try {
65
+ entries = fs.readdirSync(backupsDir, { withFileTypes: true });
66
+ }
67
+ catch {
68
+ return [];
69
+ }
70
+ return entries
71
+ .filter((e) => e.isDirectory() && SNAPSHOT_NAME.test(e.name))
72
+ .map((e) => e.name)
73
+ .sort(compareNames);
74
+ }
75
+ function compareNames(a, b) {
76
+ const [ta, na] = splitName(a);
77
+ const [tb, nb] = splitName(b);
78
+ return ta < tb ? -1 : ta > tb ? 1 : na - nb;
79
+ }
80
+ const splitName = (n) => {
81
+ const m = /^(.*Z)(?:-(\d+))?$/.exec(n);
82
+ return [m[1], m[2] ? Number(m[2]) : 1];
83
+ };
84
+ /**
85
+ * Copy `projectsDir` to `backupsDir/<timestamp>/` unless it's empty or identical to the newest snapshot.
86
+ * The copy is made under a dot-name and renamed into place, so a snapshot directory is always complete.
87
+ * Returns the new snapshot's name, or null if none was needed.
88
+ */
89
+ export function takeSnapshot(projectsDir, backupsDir, now = new Date()) {
90
+ const fp = fingerprint(projectsDir);
91
+ if (!fp)
92
+ return null; // nothing to back up
93
+ const existing = listSnapshots(backupsDir);
94
+ const newest = existing[existing.length - 1];
95
+ if (newest && fingerprint(path.join(backupsDir, newest)) === fp)
96
+ return null;
97
+ fs.mkdirSync(backupsDir, { recursive: true });
98
+ const base = snapshotName(now);
99
+ let name = base;
100
+ for (let n = 2; fs.existsSync(path.join(backupsDir, name)); n++)
101
+ name = `${base}-${n}`;
102
+ const partial = path.join(backupsDir, `.partial-${name}-${process.pid}`);
103
+ fs.cpSync(projectsDir, partial, {
104
+ recursive: true,
105
+ preserveTimestamps: true,
106
+ errorOnExist: true,
107
+ force: false,
108
+ filter: (src, dst) => !isTemp(path.basename(src)) && !(isAsset(path.relative(projectsDir, src).split(path.sep).join('/')) && linked(src, dst)),
109
+ });
110
+ const dest = path.join(backupsDir, name);
111
+ if (fs.existsSync(dest))
112
+ throw new Error(`${dest} appeared while snapshotting; leaving the copy at ${partial}`);
113
+ fs.renameSync(partial, dest); // fs-guard: a fresh dot-dir onto a name we just checked is free
114
+ return name;
115
+ }
116
+ /** Hard-link an asset into the snapshot being made; false (so it's copied instead) if that isn't possible. */
117
+ function linked(src, dst) {
118
+ try {
119
+ if (!fs.lstatSync(src).isFile())
120
+ return false;
121
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
122
+ fs.linkSync(src, dst); // fails rather than replace anything (the snapshot dir is brand new anyway)
123
+ return true;
124
+ }
125
+ catch {
126
+ return false;
127
+ }
128
+ }
129
+ /** Which snapshots to keep: the newest KEEP_RECENT, plus the first of each UTC day in the last KEEP_DAYS days. */
130
+ export function snapshotsToKeep(names, now = new Date()) {
131
+ const sorted = [...names].sort(compareNames);
132
+ const keep = new Set(sorted.slice(-KEEP_RECENT));
133
+ const cutoff = new Date(now.getTime() - KEEP_DAYS * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
134
+ const seenDays = new Set();
135
+ for (const n of sorted) {
136
+ const day = SNAPSHOT_NAME.exec(n)[1];
137
+ if (seenDays.has(day))
138
+ continue;
139
+ seenDays.add(day);
140
+ if (day >= cutoff)
141
+ keep.add(n);
142
+ }
143
+ return keep;
144
+ }
145
+ /**
146
+ * Throws unless `backupsDir/name` is something pruning may remove: `name` is a bare snapshot timestamp, the
147
+ * target is a real directory (not a symlink) directly inside `backupsDir`, and `backupsDir` is a directory
148
+ * called `backups` that is neither inside nor containing the projects directory.
149
+ */
150
+ export function assertPrunable(backupsDir, name, projectsDir) {
151
+ if (typeof name !== 'string' || !SNAPSHOT_NAME.test(name) || name !== path.basename(name) || name.includes('/') || name.includes('\\')) {
152
+ throw new Error(`refusing to prune ${JSON.stringify(name)}: not a snapshot name`);
153
+ }
154
+ const root = path.resolve(backupsDir);
155
+ if (path.basename(root) !== 'backups')
156
+ throw new Error(`refusing to prune in ${root}: not a backups/ directory`);
157
+ const projects = path.resolve(projectsDir);
158
+ const inside = (a, b) => a === b || a.startsWith(b + path.sep);
159
+ if (inside(root, projects) || inside(projects, root))
160
+ throw new Error(`refusing to prune in ${root}: overlaps ${projects}`);
161
+ const target = path.join(root, name);
162
+ if (path.dirname(target) !== root)
163
+ throw new Error(`refusing to prune ${target}: not directly inside ${root}`);
164
+ const st = fs.lstatSync(target);
165
+ if (!st.isDirectory() || st.isSymbolicLink())
166
+ throw new Error(`refusing to prune ${target}: not a directory`);
167
+ // the real path too, in case backupsDir itself is reached through a symlink into the projects
168
+ const realRoot = fs.realpathSync(root);
169
+ if (path.dirname(fs.realpathSync(target)) !== realRoot)
170
+ throw new Error(`refusing to prune ${target}: resolves outside ${realRoot}`);
171
+ if (fs.existsSync(projects) && inside(realRoot, fs.realpathSync(projects)))
172
+ throw new Error(`refusing to prune in ${realRoot}: inside the projects`);
173
+ return target;
174
+ }
175
+ /** Remove snapshots that `snapshotsToKeep` doesn't keep. Never removes the newest. Returns the removed names. */
176
+ export function pruneSnapshots(backupsDir, projectsDir, now = new Date()) {
177
+ const names = listSnapshots(backupsDir);
178
+ const keep = snapshotsToKeep(names, now);
179
+ const newest = names[names.length - 1];
180
+ const removed = [];
181
+ for (const n of names) {
182
+ if (keep.has(n) || n === newest)
183
+ continue;
184
+ const target = assertPrunable(backupsDir, n, projectsDir);
185
+ fs.rmSync(target, { recursive: true }); // fs-guard: backup pruning (assertPrunable checked the path)
186
+ removed.push(n);
187
+ }
188
+ return removed;
189
+ }
190
+ /** Snapshot (if changed) and prune. Never throws: a failed snapshot must not take the server down. */
191
+ export function snapshotNow(projectsDir, backupsDir, now = new Date()) {
192
+ try {
193
+ const name = takeSnapshot(projectsDir, backupsDir, now);
194
+ if (name)
195
+ console.log(`[backups] snapshot ${path.join(backupsDir, name)}`);
196
+ const removed = pruneSnapshots(backupsDir, projectsDir, now);
197
+ if (removed.length)
198
+ console.log(`[backups] pruned ${removed.length} old snapshot(s)`);
199
+ return name;
200
+ }
201
+ catch (e) {
202
+ console.error('[backups] snapshot failed:', e);
203
+ return null;
204
+ }
205
+ }
206
+ /** On start, and then hourly while running. */
207
+ export function startSnapshots(projectsDir, backupsDir) {
208
+ if (!backupsDir)
209
+ return;
210
+ snapshotNow(projectsDir, backupsDir);
211
+ setInterval(() => snapshotNow(projectsDir, backupsDir), INTERVAL_MS).unref();
212
+ }
@@ -0,0 +1,113 @@
1
+ // Branch tagging: which git branch stands for each candidate PR (commit). Local Review has no naming scheme: the
2
+ // agent that writes the code creates plain branches (`git branch <name> <sha>`) and records the name in
3
+ // prs/<repo>/<sha>.yaml (`branch:`, or `github.branch` once the PR exists). Here we read the repo's branches
4
+ // (read-only, one `git for-each-ref` per range) and reconcile the recorded name with what git says.
5
+ import { git, isSha } from './git.js';
6
+ const SEP = '\x1f';
7
+ export function emptyRefIndex() {
8
+ return { refs: [], byRef: new Map(), bySha: new Map(), remotes: [], symrefs: new Map() };
9
+ }
10
+ export function parseRefs(out) {
11
+ const idx = emptyRefIndex();
12
+ const remotes = new Set();
13
+ for (const line of out.split('\n')) {
14
+ if (!line)
15
+ continue;
16
+ const [sha, ref, symref] = line.split(SEP);
17
+ if (!ref)
18
+ continue;
19
+ const remote = ref.startsWith('refs/remotes/');
20
+ const name = ref.replace(/^refs\/(heads|remotes)\//, '');
21
+ if (remote)
22
+ remotes.add(name.split('/')[0]);
23
+ if (symref) {
24
+ // origin/HEAD: not a branch of its own, just remember where it points
25
+ idx.symrefs.set(name, symref.replace(/^refs\/(heads|remotes)\//, ''));
26
+ continue;
27
+ }
28
+ if (!isSha(sha))
29
+ continue; // a ref to something that isn't a 40-hex object name
30
+ const e = { ref, name, sha, remote };
31
+ idx.refs.push(e);
32
+ idx.byRef.set(ref, e);
33
+ const list = idx.bySha.get(sha) ?? [];
34
+ list.push(e);
35
+ idx.bySha.set(sha, list);
36
+ }
37
+ idx.remotes = [...remotes].sort();
38
+ return idx;
39
+ }
40
+ /**
41
+ * All local and remote-tracking branches of a repo, in one read-only call. (`--points-at` takes a single
42
+ * object, so for a whole range one listing of every branch is the batched form.) Never throws.
43
+ */
44
+ export async function loadRefs(repo) {
45
+ try {
46
+ const out = await git(repo, ['for-each-ref', `--format=%(objectname)${SEP}%(refname)${SEP}%(symref)`, '--end-of-options', 'refs/heads', 'refs/remotes']);
47
+ return parseRefs(out);
48
+ }
49
+ catch {
50
+ return emptyRefIndex();
51
+ }
52
+ }
53
+ /** Find a recorded branch name: local `refs/heads/<n>` first, then `refs/remotes/<n>`, then `<remote>/<n>`. */
54
+ export function lookupBranch(idx, name) {
55
+ const local = idx.byRef.get(`refs/heads/${name}`);
56
+ if (local)
57
+ return local;
58
+ const asRemote = idx.byRef.get(`refs/remotes/${name}`);
59
+ if (asRemote)
60
+ return asRemote;
61
+ for (const r of idx.remotes) {
62
+ const e = idx.byRef.get(`refs/remotes/${r}/${name}`);
63
+ if (e)
64
+ return e;
65
+ }
66
+ return null;
67
+ }
68
+ /** `origin/main` -> `main` (any remote that has refs here), `refs/heads/main` -> `main`; else unchanged. */
69
+ export function stripRemote(idx, ref) {
70
+ let r = ref.replace(/^refs\/heads\//, '').replace(/^refs\/remotes\//, '');
71
+ r = idx.symrefs.get(r) ?? r; // origin/HEAD -> origin/main
72
+ for (const remote of idx.remotes)
73
+ if (r.startsWith(remote + '/'))
74
+ return r.slice(remote.length + 1);
75
+ return r;
76
+ }
77
+ /** Is this ref the range's own tip branch (e.g. me/my-feature, or origin/me/my-feature)? */
78
+ function isTip(idx, e, tip) {
79
+ if (!tip)
80
+ return false;
81
+ const t = tip.replace(/^refs\/heads\//, '');
82
+ if (!e.remote)
83
+ return e.name === t;
84
+ return e.name === t || stripRemote(idx, e.name) === t;
85
+ }
86
+ /** Branches pointing at `sha`, best first: local non-tip, remote non-tip, then the tip branch (local, remote). */
87
+ export function discoveredFor(idx, sha, tip) {
88
+ const list = idx.bySha.get(sha) ?? [];
89
+ const rank = (e) => (isTip(idx, e, tip) ? 2 : 0) + (e.remote ? 1 : 0);
90
+ return [...list].sort((a, b) => rank(a) - rank(b) || a.name.localeCompare(b.name)).map((e) => e.name);
91
+ }
92
+ /**
93
+ * Reconcile what prs/<sha>.yaml records with git. Recorded name: `github.branch`, else `branch`. Then:
94
+ * ok (exists, points here), moved (exists, points elsewhere: `points_at`), missing (not in git). With nothing
95
+ * recorded: the best discovered branch (a local non-tip one first), status `discovered`; else null. A landed PR's
96
+ * recorded branch is `merged`, whatever git says (never a warning).
97
+ */
98
+ export function reconcileBranch(idx, sha, override, tip) {
99
+ const recorded = override?.github?.branch ?? override?.branch;
100
+ // a landed PR's branch is history: it may be deleted or reused by now, and that's no problem
101
+ if (override?.landed)
102
+ return recorded ? { name: recorded, status: 'merged', discovered: [] } : null;
103
+ const discovered = discoveredFor(idx, sha, tip);
104
+ if (recorded) {
105
+ const e = lookupBranch(idx, recorded);
106
+ if (!e)
107
+ return { name: recorded, status: 'missing', discovered };
108
+ if (e.sha === sha)
109
+ return { name: recorded, status: 'ok', discovered };
110
+ return { name: recorded, status: 'moved', points_at: e.sha, discovered };
111
+ }
112
+ return discovered.length ? { name: discovered[0], status: 'discovered', discovered } : null;
113
+ }
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env node
2
+ // `local-review [command] …`: the command-line entry point (the package's bin; `npm run lr -- …` from a checkout).
3
+ // With no command it serves the UI. `plan` and `guide` are read-only: they never write a file and never run gh (or
4
+ // anything but read-only git, via server/git.ts). `init` only ever creates a new project.yaml, never replaces one.
5
+ // `asset add` only ever creates a new, content-named image file; `asset url` writes only the asset's own <name>.yaml.
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { AGENT_GUIDE, DEFAULT_PORT, EXAMPLE_PROJECT, PROJECTS_DIR, ROOT, SAFE_NAME, parsePort } from './config.js';
9
+ import { loadProject } from './projects.js';
10
+ import { buildPlan, planMarkdown, planYaml } from './plan.js';
11
+ import { ExistsError, atomicWrite } from './safefs.js';
12
+ import { AssetError, addAsset, setPublishedUrl } from './assets.js';
13
+ import { MAX_ASSET_BYTES, typeOfName } from '../shared/assets.js';
14
+ const USAGE = `Usage: local-review [serve] [--port <n>]
15
+ local-review plan <project> [--repo <name>] [--stack <n>] [--include-follow-on] [--format md|json|yaml]
16
+ local-review init <project>
17
+ local-review asset add <project> <file> [--alt <text>]
18
+ local-review asset url <project> <name> <url>
19
+ local-review guide
20
+ local-review --version | --help
21
+
22
+ serve (the default) Serve the review UI and API on http://localhost:<port> (127.0.0.1 only).
23
+ --port <n> the port (default ${DEFAULT_PORT}, or $PORT)
24
+ plan For each PR in scope, in order: commit, status (draft / ready / published), branch (with its
25
+ status), base, title and the exact full body (project intro, stack note, ---, PR description), plus
26
+ any recorded github: info. Warns about stacks with draft PRs, GitHub bases that differ from the
27
+ plan's, and published PRs that landed upstream without being frozen. Landed (merged) PRs are
28
+ never planned. Read-only.
29
+ --repo <name> one repo of the project
30
+ --stack <n> one declared stack (1-based, as in project.yaml); needs --repo if several repos have it.
31
+ Naming a follow-on stack plans it (with a note).
32
+ --include-follow-on
33
+ also plan follow-on stacks (follow_on: true, parked work); left out by default
34
+ --format <f> md (default), json or yaml
35
+ init Create a project to review: <projects>/<project>/project.yaml, a commented example to edit.
36
+ Never replaces an existing one.
37
+ asset add
38
+ Copy an image (PNG, JPEG, GIF, WebP or SVG, up to 5 MB) into the project's assets folder, named by its
39
+ content, and print the markdown that shows it, e.g. ![alt](assets/3f2a9c1e0b7d4a65.png), for a PR
40
+ description, stack note or project intro. Adding the same image again is a no-op.
41
+ --alt <text> the alt text (default: the file name)
42
+ asset url
43
+ Record where an asset is published (a URL GitHub's readers can reach), so \`plan\` puts that URL in
44
+ the bodies instead of the local assets/<name> reference.
45
+ guide Print the guide for agents using Local Review to get their work reviewed (AGENT-GUIDE.md).
46
+
47
+ Data: $LOCAL_REVIEW_HOME/projects (default ~/.local-review/projects). Agents: run \`local-review guide\`.`;
48
+ /** Bad usage: exit 2, with the usage. Anything else thrown exits 1. */
49
+ class UsageError extends Error {
50
+ }
51
+ const VALUE_FLAGS = ['repo', 'stack', 'format', 'port', 'alt'];
52
+ function parseArgs(argv) {
53
+ const pos = [];
54
+ const flags = {};
55
+ for (let i = 0; i < argv.length; i++) {
56
+ const a = argv[i];
57
+ if (a === '-h' || a === '--help')
58
+ flags.help = '1';
59
+ else if (a === '-v' || a === '--version')
60
+ flags.version = '1';
61
+ else if (a === '--include-follow-on')
62
+ flags['include-follow-on'] = '1';
63
+ else if (a.startsWith('--')) {
64
+ const eq = a.indexOf('=');
65
+ const [k, v] = eq > 0 ? [a.slice(2, eq), a.slice(eq + 1)] : [a.slice(2), argv[++i]];
66
+ if (!VALUE_FLAGS.includes(k))
67
+ throw new UsageError(`unknown option --${k}`);
68
+ if (v === undefined)
69
+ throw new UsageError(`--${k} needs a value`);
70
+ flags[k] = v;
71
+ }
72
+ else if (a.startsWith('-') && a.length > 1)
73
+ throw new UsageError(`unknown option ${a}`);
74
+ else
75
+ pos.push(a);
76
+ }
77
+ return { pos, flags };
78
+ }
79
+ /** Each command takes its own options and at most `max` arguments. */
80
+ function check(cmd, flags, allowed, args, max) {
81
+ const bad = Object.keys(flags).find((k) => !allowed.includes(k));
82
+ if (bad)
83
+ throw new UsageError(`${cmd} doesn't take --${bad}`);
84
+ if (args.length > max)
85
+ throw new UsageError(`unexpected argument '${args[max]}'`);
86
+ }
87
+ async function plan(slug, flags) {
88
+ if (!slug)
89
+ throw new UsageError('plan needs a project');
90
+ if (!SAFE_NAME.test(slug))
91
+ throw new UsageError(`bad project name '${slug}'`);
92
+ const format = flags.format ?? 'md';
93
+ if (!['md', 'json', 'yaml'].includes(format))
94
+ throw new UsageError(`--format must be md, json or yaml`);
95
+ let stack;
96
+ if (flags.stack !== undefined) {
97
+ if (!/^[1-9][0-9]{0,3}$/.test(flags.stack))
98
+ throw new UsageError(`--stack must be a stack number (1, 2, …)`);
99
+ stack = Number(flags.stack);
100
+ }
101
+ const project = await loadProject(slug);
102
+ if (!project)
103
+ throw new Error(`no project '${slug}' (looked for ${path.join(PROJECTS_DIR, slug, 'project.yaml')}; \`local-review init ${slug}\` creates one)`);
104
+ const p = await buildPlan(project, { repo: flags.repo, stack, includeFollowOn: !!flags['include-follow-on'] });
105
+ process.stdout.write(format === 'json' ? JSON.stringify(p, null, 2) + '\n' : format === 'yaml' ? planYaml(p) : planMarkdown(p));
106
+ }
107
+ function init(slug) {
108
+ if (!slug)
109
+ throw new UsageError('init needs a project name, e.g. `local-review init my-feature`');
110
+ if (!SAFE_NAME.test(slug))
111
+ throw new UsageError(`bad project name '${slug}': use letters, digits, '.', '_' and '-', not starting with '.'`);
112
+ const file = path.join(PROJECTS_DIR, slug, 'project.yaml');
113
+ const text = fs.readFileSync(EXAMPLE_PROJECT, 'utf8').replace(/^title: .*$/m, `title: ${slug}`);
114
+ try {
115
+ atomicWrite(file, text, { createOnly: true });
116
+ }
117
+ catch (e) {
118
+ if (e instanceof ExistsError)
119
+ throw new Error(`${file} already exists; not replacing it`);
120
+ throw e;
121
+ }
122
+ const port = (process.env.PORT && parsePort(process.env.PORT)) || DEFAULT_PORT;
123
+ process.stdout.write(`Created ${file}\n\n` +
124
+ `Next: edit it (each repo's path and branch), then run \`local-review\` and open http://localhost:${port}/${slug}\n` +
125
+ `Edits to project.yaml are picked up live.\n`);
126
+ }
127
+ function projectDirFor(cmd, slug) {
128
+ if (!slug)
129
+ throw new UsageError(`${cmd} needs a project`);
130
+ if (!SAFE_NAME.test(slug))
131
+ throw new UsageError(`bad project name '${slug}'`);
132
+ const dir = path.join(PROJECTS_DIR, slug);
133
+ if (!fs.existsSync(path.join(dir, 'project.yaml')))
134
+ throw new Error(`no project '${slug}' (looked for ${path.join(dir, 'project.yaml')}; \`local-review init ${slug}\` creates one)`);
135
+ return dir;
136
+ }
137
+ function asset(args, flags) {
138
+ const [sub, slug, a, b] = args;
139
+ if (sub === 'add') {
140
+ if (args.length > 3)
141
+ throw new UsageError(`unexpected argument '${args[3]}'`);
142
+ const dir = projectDirFor('asset add', slug);
143
+ if (!a)
144
+ throw new UsageError('asset add needs an image file');
145
+ let st;
146
+ try {
147
+ st = fs.statSync(a);
148
+ }
149
+ catch {
150
+ throw new Error(`can't read ${a}`);
151
+ }
152
+ if (!st.isFile())
153
+ throw new Error(`${a} isn't a file`);
154
+ if (st.size > MAX_ASSET_BYTES)
155
+ throw new Error(`${a} is ${(st.size / 1024 / 1024).toFixed(1)} MB; the limit is ${MAX_ASSET_BYTES / 1024 / 1024} MB`);
156
+ const alt = flags.alt ?? path.basename(a).replace(/\.[^.]*$/, '');
157
+ try {
158
+ const r = addAsset(dir, fs.readFileSync(a), typeOfName(a), alt);
159
+ process.stderr.write(`${r.created ? 'Added' : 'Already there:'} ${r.file}\n`);
160
+ process.stdout.write(r.markdown + '\n');
161
+ }
162
+ catch (e) {
163
+ if (e instanceof AssetError)
164
+ throw new Error(`${a}: ${e.message}`);
165
+ throw e;
166
+ }
167
+ return;
168
+ }
169
+ if (sub === 'url') {
170
+ if (flags.alt !== undefined)
171
+ throw new UsageError("asset url doesn't take --alt");
172
+ if (args.length > 4)
173
+ throw new UsageError(`unexpected argument '${args[4]}'`);
174
+ const dir = projectDirFor('asset url', slug);
175
+ if (!a || !b)
176
+ throw new UsageError('asset url needs an asset name and a URL, e.g. `local-review asset url my-feature 3f2a9c1e0b7d4a65.png https://…`');
177
+ const name = a.replace(/^(?:\.\/)?assets\//, ''); // accept the reference as written in markdown
178
+ try {
179
+ process.stderr.write(`Recorded in ${setPublishedUrl(dir, name, b)}\n`);
180
+ }
181
+ catch (e) {
182
+ if (e instanceof AssetError)
183
+ throw e.status === 400 ? new UsageError(e.message) : new Error(e.message);
184
+ throw e;
185
+ }
186
+ return;
187
+ }
188
+ throw new UsageError(sub ? `unknown asset command '${sub}' (add or url)` : 'asset needs a command: add or url');
189
+ }
190
+ async function serve(flags) {
191
+ const given = flags.port ?? process.env.PORT;
192
+ // `npm run dev`: the API sits next to Vite's UI port (see vite.config.ts)
193
+ const port = given === undefined || given === '' ? DEFAULT_PORT + (process.env.LOCAL_REVIEW_DEV ? 1 : 0) : parsePort(given);
194
+ if (port === null)
195
+ throw new UsageError(`${flags.port !== undefined ? '--port' : 'PORT'} must be a port number (0-65535), not '${given}'`);
196
+ const { startServer } = await import('./index.js');
197
+ try {
198
+ await startServer(port);
199
+ }
200
+ catch (e) {
201
+ const code = e.code;
202
+ if (code === 'EADDRINUSE')
203
+ throw new Error(`port ${port} is already in use; pick another with --port <n>`);
204
+ if (code === 'EACCES')
205
+ throw new Error(`not allowed to listen on port ${port}; pick another with --port <n>`);
206
+ throw e;
207
+ }
208
+ }
209
+ /** Resolves with the exit code, or undefined while the server runs. */
210
+ async function main(argv) {
211
+ const { pos, flags } = parseArgs(argv);
212
+ if (flags.help) {
213
+ process.stdout.write(USAGE + '\n');
214
+ return 0;
215
+ }
216
+ if (flags.version) {
217
+ process.stdout.write(JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version + '\n');
218
+ return 0;
219
+ }
220
+ const [cmd = 'serve', ...args] = pos;
221
+ switch (cmd) {
222
+ case 'serve':
223
+ check(cmd, flags, ['port'], args, 0);
224
+ await serve(flags);
225
+ return undefined;
226
+ case 'plan':
227
+ check(cmd, flags, ['repo', 'stack', 'format', 'include-follow-on'], args, 1);
228
+ await plan(args[0], flags);
229
+ return 0;
230
+ case 'init':
231
+ check(cmd, flags, [], args, 1);
232
+ init(args[0]);
233
+ return 0;
234
+ case 'asset':
235
+ check(cmd, flags, ['alt'], args, 4);
236
+ asset(args, flags);
237
+ return 0;
238
+ case 'guide':
239
+ check(cmd, flags, [], args, 0);
240
+ process.stdout.write(fs.readFileSync(AGENT_GUIDE, 'utf8'));
241
+ return 0;
242
+ default:
243
+ throw new UsageError(`unknown command '${cmd}'`);
244
+ }
245
+ }
246
+ // exitCode, not exit(): exiting could cut off output still being written to a pipe
247
+ main(process.argv.slice(2)).then((code) => {
248
+ if (code !== undefined)
249
+ process.exitCode = code;
250
+ }, (e) => {
251
+ process.stderr.write(`local-review: ${e.message}\n`);
252
+ if (e instanceof UsageError)
253
+ process.stderr.write(`\n${USAGE}\n`);
254
+ process.exitCode = e instanceof UsageError ? 2 : 1;
255
+ });