@tequila99/githuman 0.1.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 (187) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -0
  3. package/dist/cli/commands/export.js +55 -0
  4. package/dist/cli/commands/list.js +39 -0
  5. package/dist/cli/config.js +65 -0
  6. package/dist/cli/index.js +50 -0
  7. package/dist/cli/server-runtime.js +66 -0
  8. package/dist/cli/startup-message.js +7 -0
  9. package/dist/server/app.js +62 -0
  10. package/dist/server/db/index.js +17 -0
  11. package/dist/server/db/migrations.js +34 -0
  12. package/dist/server/event-bus.js +14 -0
  13. package/dist/server/repositories/comment.repo.js +52 -0
  14. package/dist/server/repositories/review.repo.js +41 -0
  15. package/dist/server/routes/comments.js +114 -0
  16. package/dist/server/routes/diff.js +39 -0
  17. package/dist/server/routes/events.js +16 -0
  18. package/dist/server/routes/export.js +27 -0
  19. package/dist/server/routes/git.js +100 -0
  20. package/dist/server/routes/health.js +5 -0
  21. package/dist/server/routes/reviews.js +90 -0
  22. package/dist/server/services/comment.service.js +57 -0
  23. package/dist/server/services/diff.service.js +144 -0
  24. package/dist/server/services/export.service.js +71 -0
  25. package/dist/server/services/file-watcher.service.js +124 -0
  26. package/dist/server/services/git.service.js +219 -0
  27. package/dist/server/services/review.service.js +71 -0
  28. package/dist/shared/types.js +4 -0
  29. package/dist/web/assets/ErrorNotFound-C0A8n_dh.js +1 -0
  30. package/dist/web/assets/IndexPage-Ca7Xb-lc.js +3 -0
  31. package/dist/web/assets/IndexPage-Dn15ye8s.css +1 -0
  32. package/dist/web/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuYjalmUiAw-BCOjigVR.woff +0 -0
  33. package/dist/web/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuZtalmUiAw-Ocp2tNig.woff +0 -0
  34. package/dist/web/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuaabVmUiAw-6jEn9xEl.woff +0 -0
  35. package/dist/web/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWub2bVmUiAw-CCs8I9zJ.woff +0 -0
  36. package/dist/web/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbFmUiAw-BLaboPi0.woff +0 -0
  37. package/dist/web/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAw-DHuK6FBl.woff +0 -0
  38. package/dist/web/assets/MainLayout-CC5JJvA9.css +1 -0
  39. package/dist/web/assets/MainLayout-DHuQjMOV.js +1 -0
  40. package/dist/web/assets/QPage-DLIDrL1Z.js +1 -0
  41. package/dist/web/assets/ReviewsPage-f8fJEuU9.js +1 -0
  42. package/dist/web/assets/bash-CLZ0U2zV.js +1 -0
  43. package/dist/web/assets/client-TNhDHmkt.js +1 -0
  44. package/dist/web/assets/core-Ruj7F7dx.js +12 -0
  45. package/dist/web/assets/create-C46qMo0y.js +1 -0
  46. package/dist/web/assets/css-CdtkT07m.js +1 -0
  47. package/dist/web/assets/css-LbU1hoFO.js +1 -0
  48. package/dist/web/assets/engine-oniguruma-Bon2Icja.js +1 -0
  49. package/dist/web/assets/fira-code-cyrillic-400-normal-UC0NFL4U.woff2 +0 -0
  50. package/dist/web/assets/fira-code-cyrillic-400-normal-dc1q3VD7.woff +0 -0
  51. package/dist/web/assets/fira-code-cyrillic-500-normal-Bu7VCL72.woff +0 -0
  52. package/dist/web/assets/fira-code-cyrillic-500-normal-CX35h3Mg.woff2 +0 -0
  53. package/dist/web/assets/fira-code-cyrillic-600-normal-BD56RyRT.woff +0 -0
  54. package/dist/web/assets/fira-code-cyrillic-600-normal-CPRdbepc.woff2 +0 -0
  55. package/dist/web/assets/fira-code-cyrillic-ext-400-normal-Buh61xzB.woff +0 -0
  56. package/dist/web/assets/fira-code-cyrillic-ext-400-normal-txZ9Fk_1.woff2 +0 -0
  57. package/dist/web/assets/fira-code-cyrillic-ext-500-normal-CYRspHgm.woff2 +0 -0
  58. package/dist/web/assets/fira-code-cyrillic-ext-500-normal-CwtLmSFD.woff +0 -0
  59. package/dist/web/assets/fira-code-cyrillic-ext-600-normal-B9wE0zmr.woff2 +0 -0
  60. package/dist/web/assets/fira-code-cyrillic-ext-600-normal-DldcyWhZ.woff +0 -0
  61. package/dist/web/assets/fira-code-greek-400-normal-B2Gh_Y8s.woff2 +0 -0
  62. package/dist/web/assets/fira-code-greek-400-normal-DobhaxvF.woff +0 -0
  63. package/dist/web/assets/fira-code-greek-500-normal-CVO0IRNt.woff2 +0 -0
  64. package/dist/web/assets/fira-code-greek-500-normal-DEXIWiey.woff +0 -0
  65. package/dist/web/assets/fira-code-greek-600-normal-D_h3X2X5.woff +0 -0
  66. package/dist/web/assets/fira-code-greek-600-normal-Dxp52_By.woff2 +0 -0
  67. package/dist/web/assets/fira-code-greek-ext-400-normal-DR7mBgIM.woff2 +0 -0
  68. package/dist/web/assets/fira-code-greek-ext-400-normal-DSPUheWg.woff +0 -0
  69. package/dist/web/assets/fira-code-greek-ext-500-normal-BO3iq16n.woff2 +0 -0
  70. package/dist/web/assets/fira-code-greek-ext-500-normal-CMiNLXER.woff +0 -0
  71. package/dist/web/assets/fira-code-greek-ext-600-normal-C-cQDoOk.woff +0 -0
  72. package/dist/web/assets/fira-code-greek-ext-600-normal-DtBpFRLc.woff2 +0 -0
  73. package/dist/web/assets/fira-code-latin-400-normal-C-QZfXAs.woff +0 -0
  74. package/dist/web/assets/fira-code-latin-400-normal-DGosTW8U.woff2 +0 -0
  75. package/dist/web/assets/fira-code-latin-500-normal-B6gioGu8.woff2 +0 -0
  76. package/dist/web/assets/fira-code-latin-500-normal-aMLC2AMm.woff +0 -0
  77. package/dist/web/assets/fira-code-latin-600-normal-BmSSHshu.woff +0 -0
  78. package/dist/web/assets/fira-code-latin-600-normal-CvYZpPHg.woff2 +0 -0
  79. package/dist/web/assets/fira-code-latin-ext-400-normal-DG73JjOf.woff +0 -0
  80. package/dist/web/assets/fira-code-latin-ext-400-normal-KSMg0QLl.woff2 +0 -0
  81. package/dist/web/assets/fira-code-latin-ext-500-normal-CaANqXjI.woff +0 -0
  82. package/dist/web/assets/fira-code-latin-ext-500-normal-Crd9e_oI.woff2 +0 -0
  83. package/dist/web/assets/fira-code-latin-ext-600-normal-0PFQL1eg.woff +0 -0
  84. package/dist/web/assets/fira-code-latin-ext-600-normal-Cb7jxoAi.woff2 +0 -0
  85. package/dist/web/assets/fira-code-symbols2-400-normal-_QEZCEwe.woff +0 -0
  86. package/dist/web/assets/fira-code-symbols2-500-normal-DwsgbIjQ.woff +0 -0
  87. package/dist/web/assets/fira-code-symbols2-600-normal-Dvq6IQE5.woff +0 -0
  88. package/dist/web/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
  89. package/dist/web/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ-D-x-0Q06.woff2 +0 -0
  90. package/dist/web/assets/format-SW_H2PBI.js +1 -0
  91. package/dist/web/assets/github-dark-C-LZuMrd.js +1 -0
  92. package/dist/web/assets/github-light-EUqPIrTm.js +1 -0
  93. package/dist/web/assets/go-rLFTqkRN.js +1 -0
  94. package/dist/web/assets/html-BcACZWSP.js +1 -0
  95. package/dist/web/assets/html-DDNsd9lV.js +1 -0
  96. package/dist/web/assets/i18n-RyrLuvXU.js +1 -0
  97. package/dist/web/assets/index-CRwZKCt6.js +2 -0
  98. package/dist/web/assets/index-Fo431P2N.css +1 -0
  99. package/dist/web/assets/init-app-DSQzny_G.js +1 -0
  100. package/dist/web/assets/java-BXcb2rRD.js +1 -0
  101. package/dist/web/assets/javascript-BzbQS41l.js +1 -0
  102. package/dist/web/assets/javascript-Yelw-5ZN.js +1 -0
  103. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  104. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  105. package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
  106. package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
  107. package/dist/web/assets/jetbrains-mono-cyrillic-600-normal-8K4wrrwR.woff +0 -0
  108. package/dist/web/assets/jetbrains-mono-cyrillic-600-normal-EVf6-Yzo.woff2 +0 -0
  109. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  110. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  111. package/dist/web/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
  112. package/dist/web/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
  113. package/dist/web/assets/jetbrains-mono-greek-600-normal-H7WoG9Et.woff2 +0 -0
  114. package/dist/web/assets/jetbrains-mono-greek-600-normal-mc2nkWzM.woff +0 -0
  115. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  116. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  117. package/dist/web/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  118. package/dist/web/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  119. package/dist/web/assets/jetbrains-mono-latin-600-normal-BfsvjouI.woff +0 -0
  120. package/dist/web/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
  121. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  122. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  123. package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
  124. package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
  125. package/dist/web/assets/jetbrains-mono-latin-ext-600-normal-BfB_LPfz.woff2 +0 -0
  126. package/dist/web/assets/jetbrains-mono-latin-ext-600-normal-DObL3zCW.woff +0 -0
  127. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  128. package/dist/web/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
  129. package/dist/web/assets/jetbrains-mono-vietnamese-600-normal-OWROknRo.woff +0 -0
  130. package/dist/web/assets/json-B88Oqtu5.js +1 -0
  131. package/dist/web/assets/json-CaljABEy.js +1 -0
  132. package/dist/web/assets/jsx-Bz0zcwM4.js +1 -0
  133. package/dist/web/assets/kotlin-DhhofPvG.js +1 -0
  134. package/dist/web/assets/markdown-BYOwaDjH.js +1 -0
  135. package/dist/web/assets/pinia-BDCzxZ18.js +1 -0
  136. package/dist/web/assets/pinia-BJouyyXU.js +1 -0
  137. package/dist/web/assets/python-gzcpVVnB.js +1 -0
  138. package/dist/web/assets/repository-store-DqnwJIJW.js +1 -0
  139. package/dist/web/assets/runtime-core.esm-bundler-oS9U9Vp7.js +1 -0
  140. package/dist/web/assets/rust-Cfkwpbl8.js +1 -0
  141. package/dist/web/assets/scss-DHtrkMkT.js +1 -0
  142. package/dist/web/assets/syne-greek-400-normal-B4XGRaFa.woff2 +0 -0
  143. package/dist/web/assets/syne-greek-400-normal-C1nzy5ms.woff +0 -0
  144. package/dist/web/assets/syne-greek-500-normal-DUQv5E33.woff2 +0 -0
  145. package/dist/web/assets/syne-greek-500-normal-L1Ji5iNA.woff +0 -0
  146. package/dist/web/assets/syne-greek-600-normal-BJs4kvyH.woff +0 -0
  147. package/dist/web/assets/syne-greek-600-normal-BpFII5Ja.woff2 +0 -0
  148. package/dist/web/assets/syne-greek-700-normal-C01yjWE_.woff +0 -0
  149. package/dist/web/assets/syne-greek-700-normal-RbvlO3f8.woff2 +0 -0
  150. package/dist/web/assets/syne-greek-800-normal-DtzFnRR0.woff +0 -0
  151. package/dist/web/assets/syne-greek-800-normal-n2LZS_yH.woff2 +0 -0
  152. package/dist/web/assets/syne-latin-400-normal-CULFKXub.woff +0 -0
  153. package/dist/web/assets/syne-latin-400-normal-CcHADMBd.woff2 +0 -0
  154. package/dist/web/assets/syne-latin-500-normal-DR6mUQpY.woff +0 -0
  155. package/dist/web/assets/syne-latin-500-normal-emxikWQ7.woff2 +0 -0
  156. package/dist/web/assets/syne-latin-600-normal-BiwQbQXw.woff2 +0 -0
  157. package/dist/web/assets/syne-latin-600-normal-dRu9QuIh.woff +0 -0
  158. package/dist/web/assets/syne-latin-700-normal-AF3Rs61n.woff2 +0 -0
  159. package/dist/web/assets/syne-latin-700-normal-Cd-_iCn5.woff +0 -0
  160. package/dist/web/assets/syne-latin-800-normal-BuJcbPcX.woff +0 -0
  161. package/dist/web/assets/syne-latin-800-normal-EehdNYzx.woff2 +0 -0
  162. package/dist/web/assets/syne-latin-ext-400-normal-B5s6XpS5.woff2 +0 -0
  163. package/dist/web/assets/syne-latin-ext-400-normal-DCnt6qEA.woff +0 -0
  164. package/dist/web/assets/syne-latin-ext-500-normal-C5b4wHnS.woff +0 -0
  165. package/dist/web/assets/syne-latin-ext-500-normal-DHUXVE3f.woff2 +0 -0
  166. package/dist/web/assets/syne-latin-ext-600-normal-DEzBMW8B.woff +0 -0
  167. package/dist/web/assets/syne-latin-ext-600-normal-ZnizrDKU.woff2 +0 -0
  168. package/dist/web/assets/syne-latin-ext-700-normal-C-qCH-vM.woff +0 -0
  169. package/dist/web/assets/syne-latin-ext-700-normal-DuyXglMe.woff2 +0 -0
  170. package/dist/web/assets/syne-latin-ext-800-normal-B2iBRfEf.woff +0 -0
  171. package/dist/web/assets/syne-latin-ext-800-normal-CAQAsWuS.woff2 +0 -0
  172. package/dist/web/assets/theme-DUV6mF_4.js +1 -0
  173. package/dist/web/assets/tsx-udAQXfEw.js +1 -0
  174. package/dist/web/assets/typescript-Utq2Cl8c.js +1 -0
  175. package/dist/web/assets/typescript-j_1H8WHN.js +1 -0
  176. package/dist/web/assets/use-app-theme-DrR0AvTg.js +1 -0
  177. package/dist/web/assets/vue-CSBlZ1UP.js +1 -0
  178. package/dist/web/assets/vue-i18n.runtime-nPdXRlNg.js +3 -0
  179. package/dist/web/assets/wasm-BnjxR4X6.js +1 -0
  180. package/dist/web/assets/yaml-rwi0_p6S.js +1 -0
  181. package/dist/web/favicon.ico +0 -0
  182. package/dist/web/icons/favicon-128x128.png +0 -0
  183. package/dist/web/icons/favicon-16x16.png +0 -0
  184. package/dist/web/icons/favicon-32x32.png +0 -0
  185. package/dist/web/icons/favicon-96x96.png +0 -0
  186. package/dist/web/index.html +5 -0
  187. package/package.json +89 -0
@@ -0,0 +1,41 @@
1
+ function rowToReview(row) {
2
+ return {
3
+ id: row.id,
4
+ repositoryPath: row.repository_path,
5
+ baseRef: row.base_ref,
6
+ sourceType: row.source_type,
7
+ sourceRef: row.source_ref,
8
+ snapshotData: row.snapshot_data,
9
+ status: row.status,
10
+ createdAt: row.created_at,
11
+ updatedAt: row.updated_at
12
+ };
13
+ }
14
+ export function createReview(db, review) {
15
+ db.prepare(`INSERT INTO reviews (id, repository_path, base_ref, source_type, source_ref, snapshot_data, status, created_at, updated_at)
16
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(review.id, review.repositoryPath, review.baseRef, review.sourceType, review.sourceRef, review.snapshotData, review.status, review.createdAt, review.updatedAt);
17
+ return review;
18
+ }
19
+ export function findReviewById(db, id) {
20
+ const row = db.prepare('SELECT * FROM reviews WHERE id = ?').get(id);
21
+ return row ? rowToReview(row) : null;
22
+ }
23
+ export function listReviews(db) {
24
+ const rows = db
25
+ .prepare('SELECT * FROM reviews ORDER BY created_at DESC')
26
+ .all();
27
+ return rows.map(rowToReview);
28
+ }
29
+ export function deleteReview(db, id) {
30
+ db.prepare('DELETE FROM reviews WHERE id = ?').run(id);
31
+ }
32
+ export function updateReviewStatus(db, id, status) {
33
+ const updatedAt = new Date().toISOString();
34
+ const result = db
35
+ .prepare('UPDATE reviews SET status = ?, updated_at = ? WHERE id = ?')
36
+ .run(status, updatedAt, id);
37
+ if (result.changes === 0) {
38
+ return null;
39
+ }
40
+ return findReviewById(db, id);
41
+ }
@@ -0,0 +1,114 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ import { getReview } from "../services/review.service.js";
3
+ import { createComment, getComments, editComment, removeComment, resolveComment, unresolveComment, ValidationError } from "../services/comment.service.js";
4
+ const CreateCommentBody = Type.Object({
5
+ filePath: Type.String({ minLength: 1 }),
6
+ lineNumber: Type.Optional(Type.Union([Type.Integer(), Type.Null()])),
7
+ lineType: Type.Optional(Type.Union([
8
+ Type.Literal('added'),
9
+ Type.Literal('removed'),
10
+ Type.Literal('context'),
11
+ Type.Null()
12
+ ])),
13
+ content: Type.String(),
14
+ suggestion: Type.Optional(Type.Union([Type.String(), Type.Null()]))
15
+ });
16
+ const UpdateCommentBody = Type.Object({
17
+ content: Type.String()
18
+ });
19
+ const ReviewIdParams = Type.Object({ id: Type.String() });
20
+ const CommentIdParams = Type.Object({ id: Type.String() });
21
+ export async function commentRoutes(app, opts) {
22
+ const { db, eventBus } = opts;
23
+ const typedApp = app.withTypeProvider();
24
+ typedApp.post('/api/reviews/:id/comments', { schema: { params: ReviewIdParams, body: CreateCommentBody } }, async (request, reply) => {
25
+ if (!getReview(db, request.params.id)) {
26
+ reply.code(404);
27
+ return {
28
+ error: 'Not Found',
29
+ message: `Review ${request.params.id} not found`,
30
+ statusCode: 404
31
+ };
32
+ }
33
+ try {
34
+ const comment = createComment(db, request.params.id, request.body, eventBus);
35
+ reply.code(201);
36
+ return comment;
37
+ }
38
+ catch (error) {
39
+ if (error instanceof ValidationError) {
40
+ reply.code(400);
41
+ return {
42
+ error: 'Bad Request',
43
+ message: error.message,
44
+ statusCode: 400
45
+ };
46
+ }
47
+ throw error;
48
+ }
49
+ });
50
+ typedApp.get('/api/reviews/:id/comments', { schema: { params: ReviewIdParams } }, async (request, reply) => {
51
+ if (!getReview(db, request.params.id)) {
52
+ reply.code(404);
53
+ return {
54
+ error: 'Not Found',
55
+ message: `Review ${request.params.id} not found`,
56
+ statusCode: 404
57
+ };
58
+ }
59
+ return getComments(db, request.params.id);
60
+ });
61
+ typedApp.patch('/api/comments/:id', { schema: { params: CommentIdParams, body: UpdateCommentBody } }, async (request, reply) => {
62
+ try {
63
+ const updated = editComment(db, request.params.id, request.body.content, eventBus);
64
+ if (!updated) {
65
+ reply.code(404);
66
+ return {
67
+ error: 'Not Found',
68
+ message: `Comment ${request.params.id} not found`,
69
+ statusCode: 404
70
+ };
71
+ }
72
+ return updated;
73
+ }
74
+ catch (error) {
75
+ if (error instanceof ValidationError) {
76
+ reply.code(400);
77
+ return {
78
+ error: 'Bad Request',
79
+ message: error.message,
80
+ statusCode: 400
81
+ };
82
+ }
83
+ throw error;
84
+ }
85
+ });
86
+ typedApp.patch('/api/comments/:id/resolve', { schema: { params: CommentIdParams } }, async (request, reply) => {
87
+ const updated = resolveComment(db, request.params.id, eventBus);
88
+ if (!updated) {
89
+ reply.code(404);
90
+ return {
91
+ error: 'Not Found',
92
+ message: `Comment ${request.params.id} not found`,
93
+ statusCode: 404
94
+ };
95
+ }
96
+ return updated;
97
+ });
98
+ typedApp.patch('/api/comments/:id/unresolve', { schema: { params: CommentIdParams } }, async (request, reply) => {
99
+ const updated = unresolveComment(db, request.params.id, eventBus);
100
+ if (!updated) {
101
+ reply.code(404);
102
+ return {
103
+ error: 'Not Found',
104
+ message: `Comment ${request.params.id} not found`,
105
+ statusCode: 404
106
+ };
107
+ }
108
+ return updated;
109
+ });
110
+ typedApp.delete('/api/comments/:id', { schema: { params: CommentIdParams } }, async (request, reply) => {
111
+ removeComment(db, request.params.id, eventBus);
112
+ reply.code(204);
113
+ });
114
+ }
@@ -0,0 +1,39 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ import { getStagedDiff, getUnstagedDiff, getBranchDiff, getCommitsDiff } from "../services/diff.service.js";
3
+ const BranchQuery = Type.Object({
4
+ base: Type.String({ minLength: 1 })
5
+ });
6
+ const CommitsQuery = Type.Object({
7
+ from: Type.String({ minLength: 1 }),
8
+ to: Type.String({ minLength: 1 })
9
+ });
10
+ export async function diffRoutes(app, opts) {
11
+ const { repositoryPath } = opts;
12
+ const typedApp = app.withTypeProvider();
13
+ typedApp.get('/api/diff/staged', async () => {
14
+ return getStagedDiff(repositoryPath);
15
+ });
16
+ typedApp.get('/api/diff/unstaged', async () => {
17
+ return getUnstagedDiff(repositoryPath);
18
+ });
19
+ typedApp.get('/api/diff/branch', { schema: { querystring: BranchQuery } }, async (request, reply) => {
20
+ try {
21
+ return await getBranchDiff(repositoryPath, request.query.base);
22
+ }
23
+ catch (err) {
24
+ reply.code(400);
25
+ const message = err instanceof Error ? err.message : 'Bad ref';
26
+ return { error: 'Bad Request', message, statusCode: 400 };
27
+ }
28
+ });
29
+ typedApp.get('/api/diff/commits', { schema: { querystring: CommitsQuery } }, async (request, reply) => {
30
+ try {
31
+ return await getCommitsDiff(repositoryPath, request.query.from, request.query.to);
32
+ }
33
+ catch (err) {
34
+ reply.code(400);
35
+ const message = err instanceof Error ? err.message : 'Bad ref';
36
+ return { error: 'Bad Request', message, statusCode: 400 };
37
+ }
38
+ });
39
+ }
@@ -0,0 +1,16 @@
1
+ export async function eventRoutes(app, opts) {
2
+ const { eventBus } = opts;
3
+ app.get('/api/events', { sse: true }, async (request, reply) => {
4
+ reply.sse.keepAlive();
5
+ await reply.sse.send({ event: 'connected', data: {} });
6
+ const unsubscribe = eventBus.subscribe(event => {
7
+ void reply.sse.send({
8
+ event: event.type,
9
+ data: { reviewId: event.reviewId }
10
+ });
11
+ });
12
+ reply.sse.onClose(() => {
13
+ unsubscribe();
14
+ });
15
+ });
16
+ }
@@ -0,0 +1,27 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ import { exportAsJson, exportAsMarkdown, ExportNotFoundError } from "../services/export.service.js";
3
+ const ExportParams = Type.Object({ id: Type.String() });
4
+ const ExportQuery = Type.Object({
5
+ format: Type.Union([Type.Literal('json'), Type.Literal('markdown')])
6
+ });
7
+ export async function exportRoutes(app, opts) {
8
+ const { db } = opts;
9
+ const typedApp = app.withTypeProvider();
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 = exportAsMarkdown(db, request.params.id);
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;
25
+ }
26
+ });
27
+ }
@@ -0,0 +1,100 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ import { getRepositoryInfo, stagePaths, unstagePaths, discardPaths, getFilesAtRef, getFileAtRef } from "../services/git.service.js";
3
+ const TreeParams = Type.Object({
4
+ ref: Type.String({ minLength: 1 })
5
+ });
6
+ const FileContentQuery = Type.Object({
7
+ ref: Type.String({ minLength: 1 })
8
+ });
9
+ /** Body for `/stage` and `/unstage`: an omitted/empty `paths` means "all". */
10
+ const OptionalPathsBody = Type.Object({
11
+ paths: Type.Optional(Type.Array(Type.String()))
12
+ });
13
+ /** Body for `/discard`: `paths` is required — no "discard everything". */
14
+ const DiscardBody = Type.Object({
15
+ paths: Type.Array(Type.String(), { minItems: 1 })
16
+ });
17
+ export async function gitRoutes(app, opts) {
18
+ const { repositoryPath } = opts;
19
+ const typedApp = app.withTypeProvider();
20
+ app.get('/api/git/info', async () => {
21
+ return getRepositoryInfo(repositoryPath);
22
+ });
23
+ typedApp.post('/api/git/stage', { schema: { body: OptionalPathsBody } }, async (request, reply) => {
24
+ try {
25
+ await stagePaths(repositoryPath, request.body.paths ?? []);
26
+ return { ok: true };
27
+ }
28
+ catch (err) {
29
+ reply.code(400);
30
+ const message = err instanceof Error ? err.message : 'Failed to stage';
31
+ return { error: 'Bad Request', message, statusCode: 400 };
32
+ }
33
+ });
34
+ typedApp.post('/api/git/unstage', { schema: { body: OptionalPathsBody } }, async (request, reply) => {
35
+ try {
36
+ await unstagePaths(repositoryPath, request.body.paths ?? []);
37
+ return { ok: true };
38
+ }
39
+ catch (err) {
40
+ reply.code(400);
41
+ const message = err instanceof Error ? err.message : 'Failed to unstage';
42
+ return { error: 'Bad Request', message, statusCode: 400 };
43
+ }
44
+ });
45
+ typedApp.post('/api/git/discard', { schema: { body: DiscardBody } }, async (request, reply) => {
46
+ try {
47
+ await discardPaths(repositoryPath, request.body.paths);
48
+ return { ok: true };
49
+ }
50
+ catch (err) {
51
+ reply.code(400);
52
+ const message = err instanceof Error ? err.message : 'Failed to discard changes';
53
+ return { error: 'Bad Request', message, statusCode: 400 };
54
+ }
55
+ });
56
+ typedApp.get('/api/git/tree/:ref', { schema: { params: TreeParams } }, async (request, reply) => {
57
+ const { ref } = request.params;
58
+ try {
59
+ const files = await getFilesAtRef(repositoryPath, ref);
60
+ return { ref, files };
61
+ }
62
+ catch (err) {
63
+ reply.code(400);
64
+ const message = err instanceof Error ? err.message : 'Failed to list files';
65
+ return { error: 'Bad Request', message, statusCode: 400 };
66
+ }
67
+ });
68
+ typedApp.get('/api/git/file/*', { schema: { querystring: FileContentQuery } }, async (request, reply) => {
69
+ const filePath = request.params['*'];
70
+ const { ref } = request.query;
71
+ if (!filePath) {
72
+ reply.code(400);
73
+ return {
74
+ error: 'Bad Request',
75
+ message: 'File path is required',
76
+ statusCode: 400
77
+ };
78
+ }
79
+ let content;
80
+ let isBinary;
81
+ try {
82
+ ;
83
+ ({ content, isBinary } = await getFileAtRef(repositoryPath, ref, filePath));
84
+ }
85
+ catch (err) {
86
+ reply.code(400);
87
+ const message = err instanceof Error ? err.message : 'Failed to read file';
88
+ return { error: 'Bad Request', message, statusCode: 400 };
89
+ }
90
+ const lines = isBinary ? [] : content.split('\n');
91
+ return {
92
+ path: filePath,
93
+ ref,
94
+ content,
95
+ lines,
96
+ lineCount: lines.length,
97
+ isBinary
98
+ };
99
+ });
100
+ }
@@ -0,0 +1,5 @@
1
+ export async function healthRoutes(app) {
2
+ app.get('/health', async () => {
3
+ return { status: 'ok' };
4
+ });
5
+ }
@@ -0,0 +1,90 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ import { createReview, getReview, getReviews, setReviewStatus, removeReview, ValidationError } from "../services/review.service.js";
3
+ const CreateReviewBody = Type.Object({
4
+ sourceType: Type.Optional(Type.Union([
5
+ Type.Literal('staged'),
6
+ Type.Literal('unstaged'),
7
+ Type.Literal('branch'),
8
+ Type.Literal('commits')
9
+ ])),
10
+ sourceRef: Type.Optional(Type.String()),
11
+ baseRef: Type.Optional(Type.String())
12
+ });
13
+ const UpdateReviewBody = Type.Object({
14
+ status: Type.Optional(Type.Union([
15
+ Type.Literal('in_progress'),
16
+ Type.Literal('approved'),
17
+ Type.Literal('changes_requested')
18
+ ]))
19
+ });
20
+ const ReviewIdParams = Type.Object({ id: Type.String() });
21
+ export async function reviewRoutes(app, opts) {
22
+ const { repositoryPath, db, eventBus } = opts;
23
+ const typedApp = app.withTypeProvider();
24
+ typedApp.post('/api/reviews', { schema: { body: CreateReviewBody } }, async (request, reply) => {
25
+ try {
26
+ const review = await createReview(db, repositoryPath, request.body, eventBus);
27
+ reply.code(201);
28
+ return review;
29
+ }
30
+ catch (error) {
31
+ if (error instanceof ValidationError) {
32
+ reply.code(400);
33
+ return {
34
+ error: 'Bad Request',
35
+ message: error.message,
36
+ statusCode: 400
37
+ };
38
+ }
39
+ throw error;
40
+ }
41
+ });
42
+ typedApp.get('/api/reviews', async () => {
43
+ return getReviews(db);
44
+ });
45
+ typedApp.get('/api/reviews/:id', { schema: { params: ReviewIdParams } }, async (request, reply) => {
46
+ const review = getReview(db, request.params.id);
47
+ if (!review) {
48
+ reply.code(404);
49
+ return {
50
+ error: 'Not Found',
51
+ message: `Review ${request.params.id} not found`,
52
+ statusCode: 404
53
+ };
54
+ }
55
+ return review;
56
+ });
57
+ typedApp.patch('/api/reviews/:id', { schema: { params: ReviewIdParams, body: UpdateReviewBody } }, async (request, reply) => {
58
+ if (!request.body.status) {
59
+ reply.code(400);
60
+ return {
61
+ error: 'Bad Request',
62
+ message: '"status" is required',
63
+ statusCode: 400
64
+ };
65
+ }
66
+ const updated = setReviewStatus(db, request.params.id, request.body.status, eventBus);
67
+ if (!updated) {
68
+ reply.code(404);
69
+ return {
70
+ error: 'Not Found',
71
+ message: `Review ${request.params.id} not found`,
72
+ statusCode: 404
73
+ };
74
+ }
75
+ return updated;
76
+ });
77
+ typedApp.delete('/api/reviews/:id', { schema: { params: ReviewIdParams } }, async (request, reply) => {
78
+ if (!getReview(db, request.params.id)) {
79
+ reply.code(404);
80
+ return {
81
+ error: 'Not Found',
82
+ message: `Review ${request.params.id} not found`,
83
+ statusCode: 404
84
+ };
85
+ }
86
+ removeReview(db, request.params.id, eventBus);
87
+ reply.code(204);
88
+ return null;
89
+ });
90
+ }
@@ -0,0 +1,57 @@
1
+ import { createComment as insertComment, findCommentById, listCommentsByReview, updateComment as updateCommentRepo, deleteComment as deleteCommentRepo, setCommentResolved } from "../repositories/comment.repo.js";
2
+ export class ValidationError extends Error {
3
+ }
4
+ export function createComment(db, reviewId, input, eventBus) {
5
+ if (!input.content) {
6
+ throw new ValidationError('"content" is required');
7
+ }
8
+ const now = new Date().toISOString();
9
+ const comment = {
10
+ id: crypto.randomUUID(),
11
+ reviewId,
12
+ filePath: input.filePath,
13
+ lineNumber: input.lineNumber ?? null,
14
+ lineType: input.lineType ?? null,
15
+ content: input.content,
16
+ createdAt: now,
17
+ updatedAt: now,
18
+ suggestion: input.suggestion ?? null
19
+ };
20
+ const created = insertComment(db, comment);
21
+ eventBus?.publish({ type: 'comment:created', reviewId });
22
+ return created;
23
+ }
24
+ export function getComments(db, reviewId) {
25
+ return listCommentsByReview(db, reviewId);
26
+ }
27
+ export function editComment(db, id, content, eventBus) {
28
+ if (!content) {
29
+ throw new ValidationError('"content" is required');
30
+ }
31
+ const updated = updateCommentRepo(db, id, content);
32
+ if (updated) {
33
+ eventBus?.publish({ type: 'comment:updated', reviewId: updated.reviewId });
34
+ }
35
+ return updated;
36
+ }
37
+ export function resolveComment(db, id, eventBus) {
38
+ const updated = setCommentResolved(db, id, true);
39
+ if (updated) {
40
+ eventBus?.publish({ type: 'comment:updated', reviewId: updated.reviewId });
41
+ }
42
+ return updated;
43
+ }
44
+ export function unresolveComment(db, id, eventBus) {
45
+ const updated = setCommentResolved(db, id, false);
46
+ if (updated) {
47
+ eventBus?.publish({ type: 'comment:updated', reviewId: updated.reviewId });
48
+ }
49
+ return updated;
50
+ }
51
+ export function removeComment(db, id, eventBus) {
52
+ const comment = findCommentById(db, id);
53
+ deleteCommentRepo(db, id);
54
+ if (comment) {
55
+ eventBus?.publish({ type: 'comment:deleted', reviewId: comment.reviewId });
56
+ }
57
+ }
@@ -0,0 +1,144 @@
1
+ import { diffLines } from 'diff';
2
+ import { assertSafeRef, getFileAtRef, listChangedPaths, listUntrackedPaths } from "./git.service.js";
3
+ function splitLines(text) {
4
+ if (text === '')
5
+ return [];
6
+ const lines = text.split('\n');
7
+ if (lines[lines.length - 1] === '')
8
+ lines.pop();
9
+ return lines;
10
+ }
11
+ const CONTEXT_LINES = 3;
12
+ function windowHunks(lines, contextLines = CONTEXT_LINES) {
13
+ if (lines.length === 0)
14
+ return [];
15
+ const keep = Array.from({ length: lines.length }).fill(false);
16
+ for (let i = 0; i < lines.length; i++) {
17
+ if (lines[i].type === 'context')
18
+ continue;
19
+ for (let j = Math.max(0, i - contextLines); j <= Math.min(lines.length - 1, i + contextLines); j++) {
20
+ keep[j] = true;
21
+ }
22
+ }
23
+ const hunks = [];
24
+ let i = 0;
25
+ while (i < lines.length) {
26
+ if (!keep[i]) {
27
+ i += 1;
28
+ continue;
29
+ }
30
+ let j = i;
31
+ while (j < lines.length && keep[j])
32
+ j += 1;
33
+ const segment = lines.slice(i, j);
34
+ const oldNumbers = segment
35
+ .map(l => l.oldLineNumber)
36
+ .filter((n) => n !== null);
37
+ const newNumbers = segment
38
+ .map(l => l.newLineNumber)
39
+ .filter((n) => n !== null);
40
+ hunks.push({
41
+ oldStart: oldNumbers.length > 0 ? oldNumbers[0] : 0,
42
+ oldLines: oldNumbers.length,
43
+ newStart: newNumbers.length > 0 ? newNumbers[0] : 0,
44
+ newLines: newNumbers.length,
45
+ lines: segment
46
+ });
47
+ i = j;
48
+ }
49
+ return hunks;
50
+ }
51
+ export function computeFileDiff(oldContent, newContent, meta) {
52
+ const base = {
53
+ oldPath: meta.oldPath,
54
+ newPath: meta.newPath,
55
+ status: meta.status,
56
+ isBinary: meta.isBinary
57
+ };
58
+ if (meta.isBinary) {
59
+ return { ...base, additions: 0, deletions: 0, hunks: [] };
60
+ }
61
+ if (oldContent === newContent) {
62
+ return { ...base, additions: 0, deletions: 0, hunks: [] };
63
+ }
64
+ const parts = diffLines(oldContent, newContent);
65
+ let oldLineNumber = 1;
66
+ let newLineNumber = 1;
67
+ let additions = 0;
68
+ let deletions = 0;
69
+ const lines = [];
70
+ for (const part of parts) {
71
+ for (const content of splitLines(part.value)) {
72
+ if (part.added) {
73
+ lines.push({
74
+ type: 'added',
75
+ content,
76
+ oldLineNumber: null,
77
+ newLineNumber
78
+ });
79
+ newLineNumber += 1;
80
+ additions += 1;
81
+ }
82
+ else if (part.removed) {
83
+ lines.push({
84
+ type: 'removed',
85
+ content,
86
+ oldLineNumber,
87
+ newLineNumber: null
88
+ });
89
+ oldLineNumber += 1;
90
+ deletions += 1;
91
+ }
92
+ else {
93
+ lines.push({ type: 'context', content, oldLineNumber, newLineNumber });
94
+ oldLineNumber += 1;
95
+ newLineNumber += 1;
96
+ }
97
+ }
98
+ }
99
+ const hunks = windowHunks(lines);
100
+ return { ...base, additions, deletions, hunks };
101
+ }
102
+ async function buildDiffFiles(repoPath, diffArgs, oldRef, newRef) {
103
+ const changes = await listChangedPaths(repoPath, diffArgs);
104
+ return Promise.all(changes.map(async (change) => {
105
+ const [oldFile, newFile] = await Promise.all([
106
+ getFileAtRef(repoPath, oldRef, change.oldPath),
107
+ getFileAtRef(repoPath, newRef, change.newPath)
108
+ ]);
109
+ return computeFileDiff(oldFile.content, newFile.content, {
110
+ oldPath: change.oldPath,
111
+ newPath: change.newPath,
112
+ status: change.status,
113
+ isBinary: oldFile.isBinary || newFile.isBinary
114
+ });
115
+ }));
116
+ }
117
+ export async function getStagedDiff(repoPath) {
118
+ return buildDiffFiles(repoPath, ['--cached'], 'HEAD', 'INDEX');
119
+ }
120
+ export async function getUnstagedDiff(repoPath) {
121
+ const [trackedFiles, untrackedPaths] = await Promise.all([
122
+ buildDiffFiles(repoPath, [], 'INDEX', 'WORKTREE'),
123
+ listUntrackedPaths(repoPath)
124
+ ]);
125
+ const untrackedFiles = await Promise.all(untrackedPaths.map(async (path) => {
126
+ const worktreeFile = await getFileAtRef(repoPath, 'WORKTREE', path);
127
+ return computeFileDiff('', worktreeFile.content, {
128
+ oldPath: path,
129
+ newPath: path,
130
+ status: 'added',
131
+ isBinary: worktreeFile.isBinary
132
+ });
133
+ }));
134
+ return [...trackedFiles, ...untrackedFiles];
135
+ }
136
+ export async function getBranchDiff(repoPath, base) {
137
+ assertSafeRef(base);
138
+ return buildDiffFiles(repoPath, [base, 'HEAD'], base, 'HEAD');
139
+ }
140
+ export async function getCommitsDiff(repoPath, from, to) {
141
+ assertSafeRef(from);
142
+ assertSafeRef(to);
143
+ return buildDiffFiles(repoPath, [from, to], from, to);
144
+ }