@tequila99/githuman 0.2.3 → 0.4.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 (246) hide show
  1. package/README.md +26 -0
  2. package/dist/cli/commands/export.js +2 -1
  3. package/dist/cli/server-runtime.js +13 -1
  4. package/dist/cli/startup-message.js +3 -2
  5. package/dist/server/adapters/agents/event-mapper.js +140 -0
  6. package/dist/server/app.js +21 -12
  7. package/dist/server/config/agents.js +141 -0
  8. package/dist/server/errors/agents.js +12 -0
  9. package/dist/server/errors/comments.js +2 -0
  10. package/dist/server/errors/export.js +2 -0
  11. package/dist/server/errors/http.js +4 -0
  12. package/dist/server/errors/reviews.js +3 -0
  13. package/dist/server/hooks/agents/local-origin.js +33 -0
  14. package/dist/server/repositories/review.repo.js +1 -3
  15. package/dist/server/routes/agent.js +135 -0
  16. package/dist/server/routes/comments.js +17 -67
  17. package/dist/server/routes/diff.js +40 -7
  18. package/dist/server/routes/events.js +6 -1
  19. package/dist/server/routes/export.js +6 -15
  20. package/dist/server/routes/git.js +12 -21
  21. package/dist/server/routes/reviews.js +11 -43
  22. package/dist/server/services/agent/agent-session.js +497 -0
  23. package/dist/server/services/agent/context-builder.js +137 -0
  24. package/dist/server/services/agent/file-index.js +58 -0
  25. package/dist/server/services/agent/session-registry.js +100 -0
  26. package/dist/server/services/comment.service.js +1 -2
  27. package/dist/server/services/diff.service.js +121 -2
  28. package/dist/server/services/export.service.js +1 -2
  29. package/dist/server/services/git.service.js +110 -10
  30. package/dist/server/services/review.service.js +3 -3
  31. package/dist/server/utils/executable.js +26 -0
  32. package/dist/server/utils/file-name.js +6 -0
  33. package/dist/server/utils/sse.js +11 -0
  34. package/dist/server/utils/text.js +29 -0
  35. package/dist/shared/agents/chat-names.js +17 -0
  36. package/dist/shared/agents/constants.js +6 -0
  37. package/dist/shared/agents/context-format.js +17 -0
  38. package/dist/shared/agents/file-search.js +48 -0
  39. package/dist/shared/agents/schemas.js +51 -0
  40. package/dist/shared/agents/types.js +11 -0
  41. package/dist/shared/app/types.js +1 -0
  42. package/dist/shared/comments/types.js +1 -0
  43. package/dist/shared/diff/types.js +4 -0
  44. package/dist/shared/{types.js → events/types.js} +0 -3
  45. package/dist/shared/git/types.js +1 -0
  46. package/dist/shared/http/types.js +1 -0
  47. package/dist/shared/network/loopback.js +4 -0
  48. package/dist/shared/reviews/types.js +1 -0
  49. package/dist/shared/utils/error-message.js +3 -0
  50. package/dist/shared/utils/guards.js +4 -0
  51. package/dist/web/assets/AppPage-BNPBe_fi.js +1 -0
  52. package/dist/web/assets/CreateReviewFabButton-3V9ZYSHd.js +1 -0
  53. package/dist/web/assets/CreateReviewFabButton-Cufd8hVJ.css +1 -0
  54. package/dist/web/assets/{ErrorNotFound-BuL12Ld8.js → ErrorNotFound-BAP3n0aC.js} +1 -1
  55. package/dist/web/assets/IndexPage-CztuM7ha.css +1 -0
  56. package/dist/web/assets/IndexPage-xqMEFqAN.js +1 -0
  57. package/dist/web/assets/MainLayout-CMbAu8_d.js +7 -0
  58. package/dist/web/assets/MainLayout-ChxXEt_Q.css +1 -0
  59. package/dist/web/assets/QBtn-BDbJU6_B.js +1 -0
  60. package/dist/web/assets/QMenu-QYPSkHBE.js +1 -0
  61. package/dist/web/assets/QSelect-D4U0Of4k.js +1 -0
  62. package/dist/web/assets/ReviewDetailPage-CP6YqXPT.css +1 -0
  63. package/dist/web/assets/ReviewDetailPage-DSCeLI9D.js +1 -0
  64. package/dist/web/assets/ReviewsPage-CmHdSBsU.js +1 -0
  65. package/dist/web/assets/StatusSwitcher-Ca5k9Tjx.css +1 -0
  66. package/dist/web/assets/StatusSwitcher-CenEW-Lu.js +4 -0
  67. package/dist/web/assets/abnfDiagram-O67JEVCF-C4PFTrGZ.js +1 -0
  68. package/dist/web/assets/agent-icon-BUd-vCRS.css +1 -0
  69. package/dist/web/assets/agent-icon-C1oimBWa.js +17 -0
  70. package/dist/web/assets/agent-store-D1WRRYIE.js +1 -0
  71. package/dist/web/assets/app-info-store-xZiQfdgO.js +1 -0
  72. package/dist/web/assets/arc-CiNErV2i.js +1 -0
  73. package/dist/web/assets/architecture-7GRP2DOG-DORh0iBM.js +1 -0
  74. package/dist/web/assets/architectureDiagram-NJMV4G6O-DnAqHYSW.js +36 -0
  75. package/dist/web/assets/array-BifhSqXX.js +1 -0
  76. package/dist/web/assets/blockDiagram-BEXU5L5S-CCN9Ja5L.js +140 -0
  77. package/dist/web/assets/c4Diagram-YGBWAQC7-CPzIw79A.js +38 -0
  78. package/dist/web/assets/channel-WV0H_O-w.js +1 -0
  79. package/dist/web/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  80. package/dist/web/assets/chunk-3FUC2YCW-BbeiQKv_.js +2 -0
  81. package/dist/web/assets/chunk-5DYCD2WN-CuzXaWoe.js +1 -0
  82. package/dist/web/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  83. package/dist/web/assets/chunk-742MDFTN-DJUCmQfr.js +1 -0
  84. package/dist/web/assets/chunk-7INBJB4K-BBd4ghzW.js +62 -0
  85. package/dist/web/assets/chunk-7M6MHVWA-B0L68u_O.js +213 -0
  86. package/dist/web/assets/chunk-7PRAP22T-BvEmVPr6.js +1 -0
  87. package/dist/web/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  88. package/dist/web/assets/chunk-FOHPRMQF-CoA4358r.js +161 -0
  89. package/dist/web/assets/chunk-GTNCS2PH-B_vHEKRO.js +88 -0
  90. package/dist/web/assets/chunk-GWA4HPMP-DWaW5-VL.js +1 -0
  91. package/dist/web/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
  92. package/dist/web/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  93. package/dist/web/assets/chunk-MBY4JIJT-Dfo1FM_M.js +72 -0
  94. package/dist/web/assets/chunk-NETBCI7D-DzjBtXVV.js +1 -0
  95. package/dist/web/assets/chunk-O7XYJQB3-DY5ZFCJH.js +126 -0
  96. package/dist/web/assets/chunk-UA2S7LBM-DX_QjVLS.js +1 -0
  97. package/dist/web/assets/chunk-WEXAMYUT-Cckc2fqg.js +1 -0
  98. package/dist/web/assets/chunk-XXDRQBXY-DjF0kpJr.js +1 -0
  99. package/dist/web/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  100. package/dist/web/assets/chunk-Z7XXMR3K-Do7oG6KK.js +10 -0
  101. package/dist/web/assets/chunk-ZIGJFQKS-DTcqsqHq.js +2 -0
  102. package/dist/web/assets/classDiagram-v2-NBCMYWYE-Z6ZhPv4N.js +217 -0
  103. package/dist/web/assets/{core-Ruj7F7dx.js → core-tP7fpdit.js} +1 -1
  104. package/dist/web/assets/cose-bilkent-JH36ORCC-B5DV641c.js +1 -0
  105. package/dist/web/assets/create-BkZH2Q0U.js +1 -0
  106. package/dist/web/assets/cynefin-OW5HDTMX-BYrEwheK.js +1 -0
  107. package/dist/web/assets/cynefinDiagram-VND7K2PF-BXbLnUDP.js +62 -0
  108. package/dist/web/assets/cytoscape.esm-B-1iW2ft.js +321 -0
  109. package/dist/web/assets/dagre-6A5THRUB-DJHHAqZU.js +4 -0
  110. package/dist/web/assets/debounce-CDIE7CV6.js +1 -0
  111. package/dist/web/assets/defaultLocale-BFoDCU3G.js +1 -0
  112. package/dist/web/assets/diagram-22UHCM2B-DA9deaTX.js +200 -0
  113. package/dist/web/assets/diagram-3UASUU5V-C6bo1gN6.js +24 -0
  114. package/dist/web/assets/diagram-ATOU4E4O-TTQR35E_.js +3 -0
  115. package/dist/web/assets/diagram-CDSNMT55-ChFXwow0.js +30 -0
  116. package/dist/web/assets/diagram-MLGK6HIB-D3F8BzhV.js +24 -0
  117. package/dist/web/assets/diagram-MPIPVDR6-NJ6F45a1.js +41 -0
  118. package/dist/web/assets/dist-Besl4xjp.js +1 -0
  119. package/dist/web/assets/ebnfDiagram-ZINNZB2B-u2e1UzYe.js +1 -0
  120. package/dist/web/assets/elk-276RUBZZ-tCLm9cCU.js +27 -0
  121. package/dist/web/assets/engine-oniguruma-DaaPGLR3.js +1 -0
  122. package/dist/web/assets/erDiagram-OPXOYQCR-ChNVjKDe.js +99 -0
  123. package/dist/web/assets/event-rR76ioXz.js +1 -0
  124. package/dist/web/assets/eventmodeling-NTZA5JFV-KBTHxbEB.js +1 -0
  125. package/dist/web/assets/export-file-Di_2jvjP.js +1 -0
  126. package/dist/web/assets/fira-code-symbols2-400-normal-9X26I2LK.woff2 +0 -0
  127. package/dist/web/assets/fira-code-symbols2-500-normal-B5s_njw3.woff2 +0 -0
  128. package/dist/web/assets/fira-code-symbols2-600-normal-Bu3RnO39.woff2 +0 -0
  129. package/dist/web/assets/flowDiagram-KWPJA3E3-DM-yXypD.js +1 -0
  130. package/dist/web/assets/ganttDiagram-FUAMR5RP-8mcAm2ao.js +292 -0
  131. package/dist/web/assets/gitGraph-4MIJSDKK-DtM1RZrd.js +1 -0
  132. package/dist/web/assets/gitGraphDiagram-X574FWY7-C7KXkLLU.js +106 -0
  133. package/dist/web/assets/golos-text-cyrillic-400-normal-BwL4n7Pb.woff +0 -0
  134. package/dist/web/assets/golos-text-cyrillic-400-normal-C7us6pn1.woff2 +0 -0
  135. package/dist/web/assets/golos-text-cyrillic-500-normal-BSLQUuP1.woff2 +0 -0
  136. package/dist/web/assets/golos-text-cyrillic-500-normal-hXinzVVQ.woff +0 -0
  137. package/dist/web/assets/golos-text-cyrillic-ext-400-normal-Bo67NYBq.woff2 +0 -0
  138. package/dist/web/assets/golos-text-cyrillic-ext-400-normal-D592xEk3.woff +0 -0
  139. package/dist/web/assets/golos-text-cyrillic-ext-500-normal-CLsu_eDI.woff2 +0 -0
  140. package/dist/web/assets/golos-text-cyrillic-ext-500-normal-InHNvB7v.woff +0 -0
  141. package/dist/web/assets/golos-text-latin-400-normal-Coi1FYaD.woff2 +0 -0
  142. package/dist/web/assets/golos-text-latin-400-normal-DOuJOmdK.woff +0 -0
  143. package/dist/web/assets/golos-text-latin-500-normal-BQo4s7Kn.woff +0 -0
  144. package/dist/web/assets/golos-text-latin-500-normal-BznAvurO.woff2 +0 -0
  145. package/dist/web/assets/golos-text-latin-ext-400-normal-BXgyX_iE.woff +0 -0
  146. package/dist/web/assets/golos-text-latin-ext-400-normal-BjKcJWKY.woff2 +0 -0
  147. package/dist/web/assets/golos-text-latin-ext-500-normal-9vZPKadr.woff2 +0 -0
  148. package/dist/web/assets/golos-text-latin-ext-500-normal-BmtMVntR.woff +0 -0
  149. package/dist/web/assets/graphlib-DS17s2tU.js +1 -0
  150. package/dist/web/assets/i18n-BmgFD4Gx.js +1 -0
  151. package/dist/web/assets/{index-JAYe_SFI.css → index-CgeU6Wuu.css} +1 -1
  152. package/dist/web/assets/index-CuqF4_ks.js +2 -0
  153. package/dist/web/assets/info-A6RAGUB7-C5tVl8cQ.js +1 -0
  154. package/dist/web/assets/infoDiagram-VRGFBTTK-DO4D3mI7.js +2 -0
  155. package/dist/web/assets/init-C-OQMol4.js +1 -0
  156. package/dist/web/assets/init-app-C0kkg1Ie.js +1 -0
  157. package/dist/web/assets/is-BB7rJzdU.js +1 -0
  158. package/dist/web/assets/ishikawaDiagram-OU5B5YK6-DA2H3Eih.js +70 -0
  159. package/dist/web/assets/jetbrains-mono-cyrillic-ext-400-normal-Bh0R7Dhr.woff +0 -0
  160. package/dist/web/assets/jetbrains-mono-cyrillic-ext-400-normal-C_uLvvQ5.woff2 +0 -0
  161. package/dist/web/assets/jetbrains-mono-cyrillic-ext-500-normal-C9ShMxGR.woff2 +0 -0
  162. package/dist/web/assets/jetbrains-mono-cyrillic-ext-500-normal-CEVyPk4Y.woff +0 -0
  163. package/dist/web/assets/jetbrains-mono-cyrillic-ext-600-normal-DVvC-yKp.woff +0 -0
  164. package/dist/web/assets/jetbrains-mono-cyrillic-ext-600-normal-Dj6eZy5f.woff2 +0 -0
  165. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-ByoDsISC.woff2 +0 -0
  166. package/dist/web/assets/jetbrains-mono-vietnamese-500-normal-BvH7FW2L.woff2 +0 -0
  167. package/dist/web/assets/jetbrains-mono-vietnamese-600-normal-Dtizs43-.woff2 +0 -0
  168. package/dist/web/assets/journeyDiagram-ZHPQQLJL-CdPvR3Sr.js +139 -0
  169. package/dist/web/assets/kanban-definition-PNTS6WVX-BRIiazVf.js +89 -0
  170. package/dist/web/assets/katex-ZlcWpGUi.js +257 -0
  171. package/dist/web/assets/line-CmXhYhmh.js +1 -0
  172. package/dist/web/assets/linear-Cyx-ptw-.js +1 -0
  173. package/dist/web/assets/mermaid-parser.core-CZ05RARR.js +7 -0
  174. package/dist/web/assets/mermaid.core-ZosqiKX-.js +44 -0
  175. package/dist/web/assets/mindmap-definition-NLK3R4M7-a7nG9rPn.js +96 -0
  176. package/dist/web/assets/ordinal-BDEzSJ7C.js +1 -0
  177. package/dist/web/assets/packet-AYTQ26CC-yIJsAali.js +1 -0
  178. package/dist/web/assets/path-fybaL0A-.js +1 -0
  179. package/dist/web/assets/pegDiagram-GJSIUBJH-CBdB4i41.js +1 -0
  180. package/dist/web/assets/pie-WAS4IAKB-ybMEHYPX.js +1 -0
  181. package/dist/web/assets/pieDiagram-5QR66LMP-BfiG4Lho.js +39 -0
  182. package/dist/web/assets/pinia-DmTi2T44.js +1 -0
  183. package/dist/web/assets/pinia-r4XAEwbB.js +1 -0
  184. package/dist/web/assets/quadrantDiagram-O4NWA36T-7A6x_hoF.js +7 -0
  185. package/dist/web/assets/radar-RG4KPBEZ-BbeFIczu.js +1 -0
  186. package/dist/web/assets/railroad-74A4TZTK-CAyDA7Zz.js +1 -0
  187. package/dist/web/assets/railroad-abnf-HS5TGJTU-BzS6thC5.js +1 -0
  188. package/dist/web/assets/railroad-ebnf-LZEXJU2U-MS09sAPW.js +1 -0
  189. package/dist/web/assets/railroad-peg-WCYAUIDC-DEQwqbcE.js +1 -0
  190. package/dist/web/assets/railroadDiagram-XR7U4H2S-BXiOJ_AA.js +1 -0
  191. package/dist/web/assets/requirementDiagram-PLB6GJNP-bQNGcpuV.js +84 -0
  192. package/dist/web/assets/rough.esm-Dy-Kn_BL.js +1 -0
  193. package/dist/web/assets/runtime-core.esm-bundler-CUpq88z7.js +1 -0
  194. package/dist/web/assets/safe-storage-CqQMHcn7.js +1 -0
  195. package/dist/web/assets/sankeyDiagram-IPEJSGJF-CgfGV4TY.js +40 -0
  196. package/dist/web/assets/sequenceDiagram-PO4LG4MO-BdCuKkDZ.js +169 -0
  197. package/dist/web/assets/single-flight-r8B7stt1.js +1 -0
  198. package/dist/web/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
  199. package/dist/web/assets/src-AvcVSqt3.js +1 -0
  200. package/dist/web/assets/stateDiagram-v2-GCMORJYK-aj1Op7xi.js +291 -0
  201. package/dist/web/assets/swimlanes-2SLR337P-DQ_YYhZu.js +1 -0
  202. package/dist/web/assets/swimlanesDiagram-TC7HE7FX-9frEtrG_.js +8 -0
  203. package/dist/web/assets/theme-BSjPUW3f.js +1 -0
  204. package/dist/web/assets/timeline-definition-EJHVYXUP-Bk3Mvy3w.js +120 -0
  205. package/dist/web/assets/treeView-Q6P3EWNA-BzM7oZcT.js +1 -0
  206. package/dist/web/assets/treemap-WGGIJYW6-DzgzhZei.js +1 -0
  207. package/dist/web/assets/use-app-theme-DEiGbVuj.js +1 -0
  208. package/dist/web/assets/use-panel-width-ujFPWf-l.js +1 -0
  209. package/dist/web/assets/use-virtual-scroll-BVHTKnoC.js +1 -0
  210. package/dist/web/assets/usecaseDiagram-POWQR4AR-kriFnh7L.js +328 -0
  211. package/dist/web/assets/vennDiagram-UO4OBE2U-CDdCMQs7.js +34 -0
  212. package/dist/web/assets/{vue-i18n.runtime-CxBM6QPV.js → vue-i18n.runtime-D0pkhc7g.js} +2 -2
  213. package/dist/web/assets/wardley-WFR3VGLG-BUWTYQyf.js +1 -0
  214. package/dist/web/assets/wardleyDiagram-VNRHLVJA-CW7lMcTj.js +78 -0
  215. package/dist/web/assets/xychartDiagram-PMCCYNJV-DoIu8J_3.js +7 -0
  216. package/dist/web/index.html +8 -6
  217. package/package.json +12 -3
  218. package/dist/web/assets/AppPage-B9tWPrzn.js +0 -1
  219. package/dist/web/assets/CreateReviewFabButton-BqWdgfEf.css +0 -1
  220. package/dist/web/assets/CreateReviewFabButton-DcXpOlBP.js +0 -1
  221. package/dist/web/assets/IndexPage-CsgdLXZh.css +0 -1
  222. package/dist/web/assets/IndexPage-y9DcUqW9.js +0 -1
  223. package/dist/web/assets/MainLayout-BRPoh8y0.css +0 -1
  224. package/dist/web/assets/MainLayout-fQytypS5.js +0 -1
  225. package/dist/web/assets/Platform-DDGYwfzl.js +0 -1
  226. package/dist/web/assets/QBtn-DGiC5uLo.js +0 -1
  227. package/dist/web/assets/QTooltip-CPN6UvuX.js +0 -1
  228. package/dist/web/assets/ReviewDetailPage-CL6A5zjH.css +0 -1
  229. package/dist/web/assets/ReviewDetailPage-aMMuYtax.js +0 -1
  230. package/dist/web/assets/ReviewsPage-B1CTHU13.js +0 -1
  231. package/dist/web/assets/StatusSwitcher-Bj-XCwSr.js +0 -3
  232. package/dist/web/assets/StatusSwitcher-BydQq1LI.css +0 -1
  233. package/dist/web/assets/app-info-store-Dw8pPU-w.js +0 -1
  234. package/dist/web/assets/client-De34x_P_.js +0 -1
  235. package/dist/web/assets/engine-oniguruma-Bon2Icja.js +0 -1
  236. package/dist/web/assets/event-D0kaqRwq.js +0 -1
  237. package/dist/web/assets/format-Bb6ahGqi.js +0 -1
  238. package/dist/web/assets/i18n-Dw0eMaZf.js +0 -1
  239. package/dist/web/assets/index-i1Dlwasy.js +0 -2
  240. package/dist/web/assets/init-app-CPTVCTFc.js +0 -1
  241. package/dist/web/assets/pinia-ClnoFqXt.js +0 -1
  242. package/dist/web/assets/pinia-jMX0pzvu.js +0 -1
  243. package/dist/web/assets/runtime-core.esm-bundler-Db5Q9Gsb.js +0 -1
  244. package/dist/web/assets/theme-CgDsrcvS.js +0 -1
  245. package/dist/web/assets/use-app-theme-D0Pl-VfI.js +0 -1
  246. package/dist/web/assets/use-server-events-pJWjsMIC.js +0 -1
@@ -1,6 +1,7 @@
1
+ import { NotFoundError } from "../errors/http.js";
1
2
  import { Type } from '@sinclair/typebox';
2
3
  import { getReview } from "../services/review.service.js";
3
- import { createComment, getComments, editComment, removeComment, resolveComment, unresolveComment, ValidationError } from "../services/comment.service.js";
4
+ import { createComment, getComments, editComment, removeComment, resolveComment, unresolveComment } from "../services/comment.service.js";
4
5
  // Null goes first in each union: Fastify's Ajv coerces types, and with
5
6
  // Integer (or String) first it turns a JSON null into 0 (or "") before the
6
7
  // Null branch is ever tried — a file-level comment would be stored as line 0.
@@ -27,87 +28,36 @@ export async function commentRoutes(app, opts) {
27
28
  const typedApp = app.withTypeProvider();
28
29
  typedApp.post('/api/reviews/:id/comments', { schema: { params: ReviewIdParams, body: CreateCommentBody } }, async (request, reply) => {
29
30
  if (!getReview(db, request.params.id)) {
30
- reply.code(404);
31
- return {
32
- error: 'Not Found',
33
- message: `Review ${request.params.id} not found`,
34
- statusCode: 404
35
- };
36
- }
37
- try {
38
- const comment = createComment(db, request.params.id, request.body, eventBus);
39
- reply.code(201);
40
- return comment;
41
- }
42
- catch (error) {
43
- if (error instanceof ValidationError) {
44
- reply.code(400);
45
- return {
46
- error: 'Bad Request',
47
- message: error.message,
48
- statusCode: 400
49
- };
50
- }
51
- throw error;
31
+ throw new NotFoundError(`Review ${request.params.id} not found`);
52
32
  }
33
+ const comment = createComment(db, request.params.id, request.body, eventBus);
34
+ reply.code(201);
35
+ return comment;
53
36
  });
54
- typedApp.get('/api/reviews/:id/comments', { schema: { params: ReviewIdParams } }, async (request, reply) => {
37
+ typedApp.get('/api/reviews/:id/comments', { schema: { params: ReviewIdParams } }, async (request) => {
55
38
  if (!getReview(db, request.params.id)) {
56
- reply.code(404);
57
- return {
58
- error: 'Not Found',
59
- message: `Review ${request.params.id} not found`,
60
- statusCode: 404
61
- };
39
+ throw new NotFoundError(`Review ${request.params.id} not found`);
62
40
  }
63
41
  return getComments(db, request.params.id);
64
42
  });
65
- typedApp.patch('/api/comments/:id', { schema: { params: CommentIdParams, body: UpdateCommentBody } }, async (request, reply) => {
66
- try {
67
- const updated = editComment(db, request.params.id, request.body.content, eventBus);
68
- if (!updated) {
69
- reply.code(404);
70
- return {
71
- error: 'Not Found',
72
- message: `Comment ${request.params.id} not found`,
73
- statusCode: 404
74
- };
75
- }
76
- return updated;
77
- }
78
- catch (error) {
79
- if (error instanceof ValidationError) {
80
- reply.code(400);
81
- return {
82
- error: 'Bad Request',
83
- message: error.message,
84
- statusCode: 400
85
- };
86
- }
87
- throw error;
43
+ typedApp.patch('/api/comments/:id', { schema: { params: CommentIdParams, body: UpdateCommentBody } }, async (request) => {
44
+ const updated = editComment(db, request.params.id, request.body.content, eventBus);
45
+ if (!updated) {
46
+ throw new NotFoundError(`Comment ${request.params.id} not found`);
88
47
  }
48
+ return updated;
89
49
  });
90
- typedApp.patch('/api/comments/:id/resolve', { schema: { params: CommentIdParams } }, async (request, reply) => {
50
+ typedApp.patch('/api/comments/:id/resolve', { schema: { params: CommentIdParams } }, async (request) => {
91
51
  const updated = resolveComment(db, request.params.id, eventBus);
92
52
  if (!updated) {
93
- reply.code(404);
94
- return {
95
- error: 'Not Found',
96
- message: `Comment ${request.params.id} not found`,
97
- statusCode: 404
98
- };
53
+ throw new NotFoundError(`Comment ${request.params.id} not found`);
99
54
  }
100
55
  return updated;
101
56
  });
102
- typedApp.patch('/api/comments/:id/unresolve', { schema: { params: CommentIdParams } }, async (request, reply) => {
57
+ typedApp.patch('/api/comments/:id/unresolve', { schema: { params: CommentIdParams } }, async (request) => {
103
58
  const updated = unresolveComment(db, request.params.id, eventBus);
104
59
  if (!updated) {
105
- reply.code(404);
106
- return {
107
- error: 'Not Found',
108
- message: `Comment ${request.params.id} not found`,
109
- statusCode: 404
110
- };
60
+ throw new NotFoundError(`Comment ${request.params.id} not found`);
111
61
  }
112
62
  return updated;
113
63
  });
@@ -1,5 +1,8 @@
1
+ import { BadRequestError } from "../errors/http.js";
1
2
  import { Type } from '@sinclair/typebox';
2
- import { getStagedDiff, getUnstagedDiff, getBranchDiff, getCommitsDiff } from "../services/diff.service.js";
3
+ import { getStagedDiff, getUnstagedDiff, getBranchDiff, getCommitsDiff, getDiffSummaries, getFileDiff } from "../services/diff.service.js";
4
+ import { resolveWithinRepo } from "../services/git.service.js";
5
+ import { errorMessage } from "../../shared/utils/error-message.js";
3
6
  const BranchQuery = Type.Object({
4
7
  base: Type.String({ minLength: 1 })
5
8
  });
@@ -7,6 +10,19 @@ const CommitsQuery = Type.Object({
7
10
  from: Type.String({ minLength: 1 }),
8
11
  to: Type.String({ minLength: 1 })
9
12
  });
13
+ const SourceParams = Type.Object({
14
+ source: Type.Union([Type.Literal('staged'), Type.Literal('unstaged')])
15
+ });
16
+ const FileQuery = Type.Object({
17
+ path: Type.String({ minLength: 1 }),
18
+ oldPath: Type.Optional(Type.String({ minLength: 1 })),
19
+ status: Type.Union([
20
+ Type.Literal('added'),
21
+ Type.Literal('modified'),
22
+ Type.Literal('deleted'),
23
+ Type.Literal('renamed')
24
+ ])
25
+ });
10
26
  export async function diffRoutes(app, opts) {
11
27
  const { repositoryPath } = opts;
12
28
  const typedApp = app.withTypeProvider();
@@ -16,24 +32,41 @@ export async function diffRoutes(app, opts) {
16
32
  typedApp.get('/api/diff/unstaged', async () => {
17
33
  return getUnstagedDiff(repositoryPath);
18
34
  });
19
- typedApp.get('/api/diff/branch', { schema: { querystring: BranchQuery } }, async (request, reply) => {
35
+ // Files without hunks, then one file with hunks: the list stays small (ADR 0033).
36
+ typedApp.get('/api/diff/:source/files', { schema: { params: SourceParams } }, async (request) => getDiffSummaries(repositoryPath, request.params.source));
37
+ typedApp.get('/api/diff/:source/file', { schema: { params: SourceParams, querystring: FileQuery } }, async (request) => {
38
+ const { path, oldPath, status } = request.query;
39
+ // Only a path outside the repository is a bad request. Other errors stay server errors.
40
+ try {
41
+ resolveWithinRepo(repositoryPath, path);
42
+ if (oldPath !== undefined)
43
+ resolveWithinRepo(repositoryPath, oldPath);
44
+ }
45
+ catch (err) {
46
+ throw new BadRequestError(errorMessage(err), { cause: err });
47
+ }
48
+ return getFileDiff(repositoryPath, request.params.source, {
49
+ oldPath: oldPath ?? path,
50
+ newPath: path,
51
+ status
52
+ });
53
+ });
54
+ typedApp.get('/api/diff/branch', { schema: { querystring: BranchQuery } }, async (request) => {
20
55
  try {
21
56
  return await getBranchDiff(repositoryPath, request.query.base);
22
57
  }
23
58
  catch (err) {
24
- reply.code(400);
25
59
  const message = err instanceof Error ? err.message : 'Bad ref';
26
- return { error: 'Bad Request', message, statusCode: 400 };
60
+ throw new BadRequestError(message, { cause: err });
27
61
  }
28
62
  });
29
- typedApp.get('/api/diff/commits', { schema: { querystring: CommitsQuery } }, async (request, reply) => {
63
+ typedApp.get('/api/diff/commits', { schema: { querystring: CommitsQuery } }, async (request) => {
30
64
  try {
31
65
  return await getCommitsDiff(repositoryPath, request.query.from, request.query.to);
32
66
  }
33
67
  catch (err) {
34
- reply.code(400);
35
68
  const message = err instanceof Error ? err.message : 'Bad ref';
36
- return { error: 'Bad Request', message, statusCode: 400 };
69
+ throw new BadRequestError(message, { cause: err });
37
70
  }
38
71
  });
39
72
  }
@@ -1,11 +1,16 @@
1
+ import { safeSend } from "../utils/sse.js";
1
2
  export async function eventRoutes(app, opts) {
2
3
  const { eventBus, instanceId } = opts;
3
4
  const hello = { instanceId };
4
5
  app.get('/api/events', { sse: true }, async (request, reply) => {
5
6
  reply.sse.keepAlive();
6
7
  await reply.sse.send({ event: 'connected', data: hello });
8
+ // The client can leave during the await; a later `onClose` would never run.
9
+ if (!reply.sse.isConnected) {
10
+ return;
11
+ }
7
12
  const unsubscribe = eventBus.subscribe(event => {
8
- void reply.sse.send({
13
+ safeSend(reply.sse, {
9
14
  event: event.type,
10
15
  data: { reviewId: event.reviewId }
11
16
  });
@@ -1,5 +1,5 @@
1
1
  import { Type } from '@sinclair/typebox';
2
- import { exportAsJson, exportAsMarkdown, ExportNotFoundError } from "../services/export.service.js";
2
+ import { exportAsJson, exportAsMarkdown } from "../services/export.service.js";
3
3
  const ExportParams = Type.Object({ id: Type.String() });
4
4
  const ExportQuery = Type.Object({
5
5
  format: Type.Union([Type.Literal('json'), Type.Literal('markdown')])
@@ -8,20 +8,11 @@ export async function exportRoutes(app, opts) {
8
8
  const { db, repositoryPath } = opts;
9
9
  const typedApp = app.withTypeProvider();
10
10
  typedApp.get('/api/reviews/:id/export', { schema: { params: ExportParams, querystring: ExportQuery } }, async (request, reply) => {
11
- try {
12
- if (request.query.format === 'json') {
13
- return exportAsJson(db, request.params.id);
14
- }
15
- const markdown = await exportAsMarkdown(db, request.params.id, repositoryPath);
16
- reply.type('text/markdown');
17
- return markdown;
18
- }
19
- catch (error) {
20
- if (error instanceof ExportNotFoundError) {
21
- reply.code(404);
22
- return { error: 'Not Found', message: error.message, statusCode: 404 };
23
- }
24
- throw error;
11
+ if (request.query.format === 'json') {
12
+ return exportAsJson(db, request.params.id);
25
13
  }
14
+ const markdown = await exportAsMarkdown(db, request.params.id, repositoryPath);
15
+ reply.type('text/markdown');
16
+ return markdown;
26
17
  });
27
18
  }
@@ -1,3 +1,4 @@
1
+ import { BadRequestError } from "../errors/http.js";
1
2
  import { Type } from '@sinclair/typebox';
2
3
  import { getRepositoryInfo, stagePaths, unstagePaths, discardPaths, getFilesAtRef, getFileAtRef } from "../services/git.service.js";
3
4
  const TreeParams = Type.Object({
@@ -20,61 +21,52 @@ export async function gitRoutes(app, opts) {
20
21
  app.get('/api/git/info', async () => {
21
22
  return getRepositoryInfo(repositoryPath);
22
23
  });
23
- typedApp.post('/api/git/stage', { schema: { body: OptionalPathsBody } }, async (request, reply) => {
24
+ typedApp.post('/api/git/stage', { schema: { body: OptionalPathsBody } }, async (request) => {
24
25
  try {
25
26
  await stagePaths(repositoryPath, request.body.paths ?? []);
26
27
  return { ok: true };
27
28
  }
28
29
  catch (err) {
29
- reply.code(400);
30
30
  const message = err instanceof Error ? err.message : 'Failed to stage';
31
- return { error: 'Bad Request', message, statusCode: 400 };
31
+ throw new BadRequestError(message, { cause: err });
32
32
  }
33
33
  });
34
- typedApp.post('/api/git/unstage', { schema: { body: OptionalPathsBody } }, async (request, reply) => {
34
+ typedApp.post('/api/git/unstage', { schema: { body: OptionalPathsBody } }, async (request) => {
35
35
  try {
36
36
  await unstagePaths(repositoryPath, request.body.paths ?? []);
37
37
  return { ok: true };
38
38
  }
39
39
  catch (err) {
40
- reply.code(400);
41
40
  const message = err instanceof Error ? err.message : 'Failed to unstage';
42
- return { error: 'Bad Request', message, statusCode: 400 };
41
+ throw new BadRequestError(message, { cause: err });
43
42
  }
44
43
  });
45
- typedApp.post('/api/git/discard', { schema: { body: DiscardBody } }, async (request, reply) => {
44
+ typedApp.post('/api/git/discard', { schema: { body: DiscardBody } }, async (request) => {
46
45
  try {
47
46
  await discardPaths(repositoryPath, request.body.paths);
48
47
  return { ok: true };
49
48
  }
50
49
  catch (err) {
51
- reply.code(400);
52
50
  const message = err instanceof Error ? err.message : 'Failed to discard changes';
53
- return { error: 'Bad Request', message, statusCode: 400 };
51
+ throw new BadRequestError(message, { cause: err });
54
52
  }
55
53
  });
56
- typedApp.get('/api/git/tree/:ref', { schema: { params: TreeParams } }, async (request, reply) => {
54
+ typedApp.get('/api/git/tree/:ref', { schema: { params: TreeParams } }, async (request) => {
57
55
  const { ref } = request.params;
58
56
  try {
59
57
  const files = await getFilesAtRef(repositoryPath, ref);
60
58
  return { ref, files };
61
59
  }
62
60
  catch (err) {
63
- reply.code(400);
64
61
  const message = err instanceof Error ? err.message : 'Failed to list files';
65
- return { error: 'Bad Request', message, statusCode: 400 };
62
+ throw new BadRequestError(message, { cause: err });
66
63
  }
67
64
  });
68
- typedApp.get('/api/git/file/*', { schema: { querystring: FileContentQuery } }, async (request, reply) => {
65
+ typedApp.get('/api/git/file/*', { schema: { querystring: FileContentQuery } }, async (request) => {
69
66
  const filePath = request.params['*'];
70
67
  const { ref } = request.query;
71
68
  if (!filePath) {
72
- reply.code(400);
73
- return {
74
- error: 'Bad Request',
75
- message: 'File path is required',
76
- statusCode: 400
77
- };
69
+ throw new BadRequestError('File path is required');
78
70
  }
79
71
  let content;
80
72
  let isBinary;
@@ -83,9 +75,8 @@ export async function gitRoutes(app, opts) {
83
75
  ({ content, isBinary } = await getFileAtRef(repositoryPath, ref, filePath));
84
76
  }
85
77
  catch (err) {
86
- reply.code(400);
87
78
  const message = err instanceof Error ? err.message : 'Failed to read file';
88
- return { error: 'Bad Request', message, statusCode: 400 };
79
+ throw new BadRequestError(message, { cause: err });
89
80
  }
90
81
  const lines = isBinary ? [] : content.split('\n');
91
82
  return {
@@ -1,5 +1,6 @@
1
+ import { NotFoundError, BadRequestError } from "../errors/http.js";
1
2
  import { Type } from '@sinclair/typebox';
2
- import { createReview, getReview, getReviews, setReviewStatus, removeReview, ValidationError } from "../services/review.service.js";
3
+ import { createReview, getReview, getReviews, setReviewStatus, removeReview } from "../services/review.service.js";
3
4
  import { getRepositoryInfo } from "../services/git.service.js";
4
5
  const CreateReviewBody = Type.Object({
5
6
  sourceType: Type.Optional(Type.Union([
@@ -33,22 +34,9 @@ export async function reviewRoutes(app, opts) {
33
34
  const { repositoryPath, db, eventBus } = opts;
34
35
  const typedApp = app.withTypeProvider();
35
36
  typedApp.post('/api/reviews', { schema: { body: CreateReviewBody } }, async (request, reply) => {
36
- try {
37
- const review = await createReview(db, repositoryPath, request.body, eventBus);
38
- reply.code(201);
39
- return review;
40
- }
41
- catch (error) {
42
- if (error instanceof ValidationError) {
43
- reply.code(400);
44
- return {
45
- error: 'Bad Request',
46
- message: error.message,
47
- statusCode: 400
48
- };
49
- }
50
- throw error;
51
- }
37
+ const review = await createReview(db, repositoryPath, request.body, eventBus);
38
+ reply.code(201);
39
+ return review;
52
40
  });
53
41
  typedApp.get('/api/reviews', { schema: { querystring: ReviewsQuery } }, async (request) => {
54
42
  const { branch, search, createdFrom, createdTo, files } = request.query;
@@ -63,46 +51,26 @@ export async function reviewRoutes(app, opts) {
63
51
  : undefined
64
52
  });
65
53
  });
66
- typedApp.get('/api/reviews/:id', { schema: { params: ReviewIdParams } }, async (request, reply) => {
54
+ typedApp.get('/api/reviews/:id', { schema: { params: ReviewIdParams } }, async (request) => {
67
55
  const review = getReview(db, request.params.id);
68
56
  if (!review) {
69
- reply.code(404);
70
- return {
71
- error: 'Not Found',
72
- message: `Review ${request.params.id} not found`,
73
- statusCode: 404
74
- };
57
+ throw new NotFoundError(`Review ${request.params.id} not found`);
75
58
  }
76
59
  return review;
77
60
  });
78
- typedApp.patch('/api/reviews/:id', { schema: { params: ReviewIdParams, body: UpdateReviewBody } }, async (request, reply) => {
61
+ typedApp.patch('/api/reviews/:id', { schema: { params: ReviewIdParams, body: UpdateReviewBody } }, async (request) => {
79
62
  if (!request.body.status) {
80
- reply.code(400);
81
- return {
82
- error: 'Bad Request',
83
- message: '"status" is required',
84
- statusCode: 400
85
- };
63
+ throw new BadRequestError('"status" is required');
86
64
  }
87
65
  const updated = setReviewStatus(db, request.params.id, request.body.status, eventBus);
88
66
  if (!updated) {
89
- reply.code(404);
90
- return {
91
- error: 'Not Found',
92
- message: `Review ${request.params.id} not found`,
93
- statusCode: 404
94
- };
67
+ throw new NotFoundError(`Review ${request.params.id} not found`);
95
68
  }
96
69
  return updated;
97
70
  });
98
71
  typedApp.delete('/api/reviews/:id', { schema: { params: ReviewIdParams } }, async (request, reply) => {
99
72
  if (!getReview(db, request.params.id)) {
100
- reply.code(404);
101
- return {
102
- error: 'Not Found',
103
- message: `Review ${request.params.id} not found`,
104
- statusCode: 404
105
- };
73
+ throw new NotFoundError(`Review ${request.params.id} not found`);
106
74
  }
107
75
  removeReview(db, request.params.id, eventBus);
108
76
  reply.code(204);