@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ivan Milovanov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # GitHuman
2
+
3
+ Локальный CLI + веб-интерфейс для просмотра изменений в git-репозитории —
4
+ staged/unstaged diff, сравнение веток и коммитов, браузер файлов с
5
+ подсветкой синтаксиса — до коммита. Данные никуда не уходят: всё работает
6
+ локально, поверх обычного `git`.
7
+
8
+ Идея и часть архитектуры унаследованы от оригинального
9
+ [mcollina/githuman](https://github.com/mcollina/githuman) (MIT) — эта версия
10
+ переосмысливает стек (Fastify + `node:sqlite` на бэкенде, Vue 3 + Quasar на
11
+ фронтенде вместо React) и интерфейс.
12
+
13
+ ## Что уже работает
14
+
15
+ - Просмотр diff: staged, unstaged, между ветками, между произвольными
16
+ коммитами.
17
+ - Браузер файлов репозитория (не только изменённых) с подсветкой
18
+ синтаксиса (shiki).
19
+ - Live-обновления через SSE: интерфейс сам обновляется при изменении
20
+ файлов в репозитории — удобно смотреть, как редактирует код AI-агент.
21
+ - Ручной/авто выбор тёмной-светлой темы.
22
+ - CLI: `serve` (поднять сервер), `list`/`export` (быстрый доступ к
23
+ сохранённым ревью без веб-интерфейса).
24
+
25
+ ## Известное ограничение (WIP)
26
+
27
+ Бэкенд полностью поддерживает модель "ревью" — создание ревью
28
+ (staged/branch/commits), инлайн-комментарии, статусы
29
+ (`in_progress`/`approved`/`changes_requested`), экспорт в Markdown/JSON — всё
30
+ это покрыто тестами (`src/server/routes/reviews.ts`, `comments.ts`,
31
+ `export.ts`). **Соответствующий экран во фронтенде пока не реализован** —
32
+ `ReviewsPage.vue` сейчас заглушка. Работает только просмотр diff/файлов;
33
+ создание ревью и комментирование через UI недоступно. Это следующий
34
+ логический кусок работы, не забытая функциональность — трекается в
35
+ `.claude/docs/adr/0014-quasar-frontend-migration.md` (не публикуется, файл
36
+ только для локальной разработки).
37
+
38
+ ## Установка и запуск
39
+
40
+ Требуется Node.js ≥ 24 (используется нативный `node:sqlite`).
41
+
42
+ ### Быстрый старт (npx)
43
+
44
+ Без клонирования и локальной установки — пакет опубликован на npm как
45
+ [`@tequila99/githuman`](https://www.npmjs.com/package/@tequila99/githuman):
46
+
47
+ ```bash
48
+ npx @tequila99/githuman serve
49
+ ```
50
+
51
+ Запускать из корня того `git`-репозитория, изменения в котором нужно
52
+ просматривать (определяется как `git rev-parse --show-toplevel` от текущей
53
+ директории).
54
+
55
+ ### Из исходников
56
+
57
+ Требуется pnpm.
58
+
59
+ ```bash
60
+ git clone <url-этого-репозитория>
61
+ cd githuman
62
+ pnpm install
63
+ pnpm run build
64
+ node dist/cli/index.js serve
65
+ ```
66
+
67
+ По умолчанию сервер слушает `http://localhost:3847` и открывает браузер
68
+ автоматически (отключить: `--no-open`). Репозиторий, который анализируется —
69
+ это `git`-репозиторий, из корня которого запущена команда (определяется как
70
+ `git rev-parse --show-toplevel` от текущей директории).
71
+
72
+ ```bash
73
+ node dist/cli/index.js serve --port 4000 --host 0.0.0.0 # LAN-доступ, см. предупреждение ниже
74
+ node dist/cli/index.js list --status in_progress # список ревью без веб-интерфейса
75
+ node dist/cli/index.js export last --format markdown -o review.md
76
+ ```
77
+
78
+ > **Осторожно с `--host 0.0.0.0`**: сервер по умолчанию не требует
79
+ > аутентификации. Открывая его в локальной сети, любой в этой сети сможет
80
+ > просматривать содержимое репозитория.
81
+
82
+ ### Данные и совместимость с оригинальным mcollina/githuman
83
+
84
+ Ревью хранятся в `.githuman/ght-reviews.db` (SQLite) в корне анализируемого
85
+ репозитория. Префикс `ght-` — сознательный выбор: оригинальный
86
+ [mcollina/githuman](https://github.com/mcollina/githuman) хранит свои данные
87
+ в том же `.githuman/reviews.db` с несовместимой схемой — без префикса оба
88
+ инструмента затирали бы файлы друг друга при использовании в одном
89
+ репозитории.
90
+
91
+ Префикс настраивается флагом `--db-prefix` (доступен у `serve`, `list`,
92
+ `export` — указывайте один и тот же для всех трёх) или переменной окружения
93
+ `GITHUMAN_DB_PREFIX`; флаг имеет приоритет над переменной. Пустое значение
94
+ (`--db-prefix ""`) — осознанный режим совместимости: работать с
95
+ `.githuman/reviews.db` оригинального инструмента напрямую (миграция схем на
96
+ вашей ответственности).
97
+
98
+ ```bash
99
+ node dist/cli/index.js serve --db-prefix myteam- # .githuman/myteam-reviews.db
100
+ node dist/cli/index.js list --db-prefix "" # .githuman/reviews.db (оригинальный githuman)
101
+ ```
102
+
103
+ ## Разработка
104
+
105
+ Два процесса нужны одновременно — backend (Fastify, автоперезапуск при
106
+ правках) и frontend (Quasar dev-сервер с HMR):
107
+
108
+ ```bash
109
+ # терминал 1 — backend
110
+ pnpm run dev:server
111
+
112
+ # терминал 2 — frontend, из этого же репозитория
113
+ pnpm run dev
114
+ ```
115
+
116
+ Если backend должен анализировать _другой_ репозиторий, а не сам
117
+ githuman — запустите `dev:server` из cwd того репозитория, указав
118
+ абсолютный путь к скрипту:
119
+
120
+ ```bash
121
+ cd /path/to/other/repo
122
+ node --watch-path=/path/to/githuman/src \
123
+ /path/to/githuman/src/cli/index.ts serve --no-open
124
+ ```
125
+
126
+ Прочие команды:
127
+
128
+ ```bash
129
+ pnpm test # server (node:test) + CLI
130
+ pnpm run typecheck # tsc (server) + vue-tsc (web)
131
+ pnpm run lint:check # oxfmt --check + oxlint (read-only)
132
+ pnpm run lint # то же самое, но с автофиксом
133
+ pnpm run build # server (tsc) + web (quasar build) → dist/
134
+ ```
135
+
136
+ Нет отдельного набора тестов для Vue-компонентов и e2e-спеков в
137
+ `tests/e2e/` пока не существует (несмотря на `playwright.config.ts` и
138
+ `test:e2e`-скрипт) — покрытие сейчас только на уровне backend/CLI (175
139
+ тестов, `node:test`).
140
+
141
+ ## Стек
142
+
143
+ - **Backend**: Fastify 5, `node:sqlite` (нативный, синхронный), TypeBox
144
+ (валидация + OpenAPI), SSE для live-обновлений.
145
+ - **Frontend**: Vue 3 + Quasar (Composition API, `<script setup>`), Pinia,
146
+ vue-i18n, shiki.
147
+ - **Тесты**: `node:test` (backend/CLI).
148
+ - **Пакетный менеджер**: pnpm.
149
+
150
+ ## Лицензия
151
+
152
+ MIT, см. [LICENSE](LICENSE).
@@ -0,0 +1,55 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { exportAsJson, exportAsMarkdown, ExportNotFoundError } from "../../server/services/export.service.js";
3
+ import { getReviews } from "../../server/services/review.service.js";
4
+ import { resolveDbPrefix } from "../config.js";
5
+ export class ExportCliError extends Error {
6
+ }
7
+ export function parseExportArgs(argv) {
8
+ const { values, positionals } = parseArgs({
9
+ args: argv,
10
+ options: {
11
+ format: { type: 'string', default: 'markdown' },
12
+ output: { type: 'string', short: 'o' },
13
+ 'db-prefix': { type: 'string' }
14
+ },
15
+ strict: false,
16
+ allowPositionals: true
17
+ });
18
+ const [id] = positionals;
19
+ if (!id) {
20
+ throw new ExportCliError('Usage: githuman export <id|last> [--format json|markdown] [-o <file>]');
21
+ }
22
+ if (values.format !== 'json' && values.format !== 'markdown') {
23
+ throw new ExportCliError(`Invalid --format value "${String(values.format)}": expected "json" or "markdown"`);
24
+ }
25
+ return {
26
+ id,
27
+ format: values.format,
28
+ output: typeof values.output === 'string' ? values.output : undefined,
29
+ dbPrefix: resolveDbPrefix(typeof values['db-prefix'] === 'string' ? values['db-prefix'] : undefined)
30
+ };
31
+ }
32
+ function resolveReviewId(db, id) {
33
+ if (id !== 'last') {
34
+ return id;
35
+ }
36
+ const [latest] = getReviews(db);
37
+ if (!latest) {
38
+ throw new ExportCliError('No reviews found in this repository');
39
+ }
40
+ return latest.id;
41
+ }
42
+ export function runExport(db, id, format = 'markdown') {
43
+ const reviewId = resolveReviewId(db, id);
44
+ try {
45
+ return format === 'json'
46
+ ? JSON.stringify(exportAsJson(db, reviewId), null, 2)
47
+ : exportAsMarkdown(db, reviewId);
48
+ }
49
+ catch (error) {
50
+ if (error instanceof ExportNotFoundError) {
51
+ throw new ExportCliError(error.message);
52
+ }
53
+ throw error;
54
+ }
55
+ }
@@ -0,0 +1,39 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { getReviews } from "../../server/services/review.service.js";
3
+ import { resolveDbPrefix } from "../config.js";
4
+ export function parseListArgs(argv) {
5
+ const { values } = parseArgs({
6
+ args: argv,
7
+ options: {
8
+ json: { type: 'boolean', default: false },
9
+ status: { type: 'string' },
10
+ 'db-prefix': { type: 'string' }
11
+ },
12
+ strict: false
13
+ });
14
+ return {
15
+ json: values.json === true,
16
+ status: isReviewStatus(values.status) ? values.status : undefined,
17
+ dbPrefix: resolveDbPrefix(typeof values['db-prefix'] === 'string' ? values['db-prefix'] : undefined)
18
+ };
19
+ }
20
+ function isReviewStatus(value) {
21
+ return (value === 'in_progress' ||
22
+ value === 'approved' ||
23
+ value === 'changes_requested');
24
+ }
25
+ export function runList(db, options = {}) {
26
+ let reviews = getReviews(db);
27
+ if (options.status) {
28
+ reviews = reviews.filter(review => review.status === options.status);
29
+ }
30
+ if (options.json) {
31
+ return JSON.stringify(reviews);
32
+ }
33
+ if (reviews.length === 0) {
34
+ return 'Ревью не найдены.';
35
+ }
36
+ return reviews
37
+ .map(review => `${review.id}\t${review.status}\t${review.createdAt}`)
38
+ .join('\n');
39
+ }
@@ -0,0 +1,65 @@
1
+ import { parseArgs } from 'node:util';
2
+ const DEFAULT_PORT = 3847;
3
+ const DEFAULT_HOST = 'localhost';
4
+ /**
5
+ * Default prefix for filenames written under `.githuman/` (e.g.
6
+ * `ght-reviews.db`). The original mcollina/githuman CLI reads and writes
7
+ * `.githuman/reviews.db` in the same repo root with an incompatible schema —
8
+ * without a prefix, running both tools against the same repository would
9
+ * silently corrupt or crash on the other tool's database file.
10
+ */
11
+ export const DEFAULT_DB_PREFIX = 'ght-';
12
+ /**
13
+ * `GITHUMAN_DB_PREFIX` is intentionally not one of the env vars the original
14
+ * mcollina/githuman CLI reads (`GITHUMAN_DB_PATH`, `GITHUMAN_TOKEN`), so
15
+ * setting it can't be mistaken for configuring the original tool.
16
+ */
17
+ const DB_PREFIX_ENV_VAR = 'GITHUMAN_DB_PREFIX';
18
+ /**
19
+ * Resolves the `.githuman/`-internal filename prefix: an explicit value (CLI
20
+ * flag) wins, then the env var, then the default — each checked with `!==
21
+ * undefined` rather than truthiness, so an explicitly empty string ("") is a
22
+ * valid override that means "use the original mcollina/githuman filenames
23
+ * unprefixed" (e.g. for a deliberate interop/migration attempt), not "unset".
24
+ */
25
+ export function resolveDbPrefix(explicit) {
26
+ if (explicit !== undefined) {
27
+ return explicit;
28
+ }
29
+ const fromEnv = process.env[DB_PREFIX_ENV_VAR];
30
+ if (fromEnv !== undefined) {
31
+ return fromEnv;
32
+ }
33
+ return DEFAULT_DB_PREFIX;
34
+ }
35
+ function resolvePort(explicit) {
36
+ if (explicit === undefined) {
37
+ return DEFAULT_PORT;
38
+ }
39
+ const parsed = Number(explicit);
40
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
41
+ throw new Error(`Invalid --port value "${explicit}": must be an integer between 1 and 65535`);
42
+ }
43
+ return parsed;
44
+ }
45
+ function resolveHost(explicit) {
46
+ return explicit !== undefined && explicit.length > 0 ? explicit : DEFAULT_HOST;
47
+ }
48
+ export function parseServeArgs(argv) {
49
+ const { values } = parseArgs({
50
+ args: argv,
51
+ options: {
52
+ port: { type: 'string' },
53
+ host: { type: 'string' },
54
+ open: { type: 'boolean', default: true },
55
+ 'no-open': { type: 'boolean', default: false },
56
+ 'db-prefix': { type: 'string' }
57
+ },
58
+ strict: false
59
+ });
60
+ const port = resolvePort(typeof values.port === 'string' ? values.port : undefined);
61
+ const host = resolveHost(typeof values.host === 'string' ? values.host : undefined);
62
+ const open = values['no-open'] === true ? false : values.open !== false;
63
+ const dbPrefix = resolveDbPrefix(typeof values['db-prefix'] === 'string' ? values['db-prefix'] : undefined);
64
+ return { port, host, open, dbPrefix };
65
+ }
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ import { writeFileSync } from 'node:fs';
3
+ import { parseServeArgs } from "./config.js";
4
+ import { startServer, resolveRepositoryPath, resolveReviewsDbPath } from "./server-runtime.js";
5
+ import { formatStartupMessage } from "./startup-message.js";
6
+ import { parseListArgs, runList } from "./commands/list.js";
7
+ import { parseExportArgs, runExport } from "./commands/export.js";
8
+ import { createFileDatabase } from "../server/db/index.js";
9
+ /** Opens the reviews DB for the repo at cwd, shared by `list` and `export`. */
10
+ async function openReviewsDb(dbPrefix) {
11
+ const repositoryPath = await resolveRepositoryPath(process.cwd());
12
+ return createFileDatabase(resolveReviewsDbPath(repositoryPath, dbPrefix));
13
+ }
14
+ async function main(argv) {
15
+ const [command, ...rest] = argv;
16
+ switch (command) {
17
+ case 'serve': {
18
+ const options = parseServeArgs(rest);
19
+ const { url } = await startServer(options);
20
+ console.log(formatStartupMessage(url, options.host));
21
+ return;
22
+ }
23
+ case 'list': {
24
+ const options = parseListArgs(rest);
25
+ const db = await openReviewsDb(options.dbPrefix);
26
+ console.log(runList(db, options));
27
+ return;
28
+ }
29
+ case 'export': {
30
+ const options = parseExportArgs(rest);
31
+ const db = await openReviewsDb(options.dbPrefix);
32
+ const output = runExport(db, options.id, options.format);
33
+ if (options.output) {
34
+ writeFileSync(options.output, output);
35
+ }
36
+ else {
37
+ console.log(output);
38
+ }
39
+ return;
40
+ }
41
+ default:
42
+ console.error(`Unknown command: ${command ?? '(none)'}`);
43
+ console.error('Usage: githuman serve|list|export [options]');
44
+ process.exitCode = 1;
45
+ }
46
+ }
47
+ main(process.argv.slice(2)).catch((error) => {
48
+ console.error(error instanceof Error ? error.message : error);
49
+ process.exitCode = 1;
50
+ });
@@ -0,0 +1,66 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { execFile } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import { join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { buildApp } from "../server/app.js";
7
+ import { createFileDatabase } from "../server/db/index.js";
8
+ import { resolveDbPrefix } from "./config.js";
9
+ const execFileAsync = promisify(execFile);
10
+ function resolveStaticRoot() {
11
+ // Compiled layout: dist/cli/server-runtime.js sits next to dist/web/.
12
+ // fileURLToPath (not URL.pathname) is required so this survives a percent-
13
+ // encoded install path or a Windows drive letter (`file:///C:/...`).
14
+ const candidatePath = fileURLToPath(new URL('../web', import.meta.url));
15
+ return existsSync(join(candidatePath, 'index.html'))
16
+ ? candidatePath
17
+ : undefined;
18
+ }
19
+ /** Best-effort opens `url` in the OS default browser — never throws or blocks startup. */
20
+ function openBrowser(url) {
21
+ const [command, args] = process.platform === 'darwin'
22
+ ? ['open', [url]]
23
+ : process.platform === 'win32'
24
+ ? ['cmd', ['/c', 'start', '""', url]]
25
+ : ['xdg-open', [url]];
26
+ execFile(command, args, () => {
27
+ // Ignore failures (no GUI/browser available, headless CI, etc.) — the
28
+ // startup message already printed the URL for the user to open by hand.
29
+ });
30
+ }
31
+ /** Resolves the repository root from cwd, falling back to cwd itself outside a git repo. */
32
+ export async function resolveRepositoryPath(cwd) {
33
+ try {
34
+ const { stdout } = await execFileAsync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf-8' });
35
+ return stdout.trim();
36
+ }
37
+ catch {
38
+ return cwd;
39
+ }
40
+ }
41
+ /**
42
+ * Path to the reviews SQLite file for a given repository, shared by `serve`,
43
+ * `list` and `export`. `dbPrefix` defaults (via `resolveDbPrefix`) the same
44
+ * way `parseServeArgs` does, so callers that don't care about it — like
45
+ * `serve`/`list`/`export` reading each other's file, or tests — land on the
46
+ * same path without having to thread the prefix through explicitly.
47
+ */
48
+ export function resolveReviewsDbPath(repositoryPath, dbPrefix) {
49
+ const prefix = resolveDbPrefix(dbPrefix);
50
+ return join(repositoryPath, '.githuman', `${prefix}reviews.db`);
51
+ }
52
+ export async function startServer(options) {
53
+ const repositoryPath = await resolveRepositoryPath(process.cwd());
54
+ const db = createFileDatabase(resolveReviewsDbPath(repositoryPath, options.dbPrefix));
55
+ const app = buildApp({
56
+ staticRoot: resolveStaticRoot(),
57
+ repositoryPath,
58
+ db,
59
+ watchFiles: true
60
+ });
61
+ const address = await app.listen({ port: options.port, host: options.host });
62
+ if (options.open) {
63
+ openBrowser(address);
64
+ }
65
+ return { app, url: address };
66
+ }
@@ -0,0 +1,7 @@
1
+ export function formatStartupMessage(url, host) {
2
+ const lines = [`githuman listening on ${url}`];
3
+ if (host !== 'localhost' && host !== '127.0.0.1') {
4
+ lines.push('Warning: server is reachable from your local network without authentication.');
5
+ }
6
+ return lines.join('\n');
7
+ }
@@ -0,0 +1,62 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import Fastify from 'fastify';
4
+ import fastifyStatic from '@fastify/static';
5
+ // @fastify/sse's published types declare an ESM default export that doesn't match
6
+ // its actual CJS runtime shape, so the default import resolves to the whole module
7
+ // namespace under NodeNext — cast it back to the plugin function type.
8
+ import fastifySseImport from '@fastify/sse';
9
+ import { healthRoutes } from "./routes/health.js";
10
+ import { diffRoutes } from "./routes/diff.js";
11
+ import { gitRoutes } from "./routes/git.js";
12
+ import { reviewRoutes } from "./routes/reviews.js";
13
+ import { commentRoutes } from "./routes/comments.js";
14
+ import { exportRoutes } from "./routes/export.js";
15
+ import { eventRoutes } from "./routes/events.js";
16
+ import { createTestDatabase } from "./db/index.js";
17
+ import { createEventBus } from "./event-bus.js";
18
+ import { watchRepository } from "./services/file-watcher.service.js";
19
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- documented workaround above
20
+ const fastifySse = fastifySseImport;
21
+ export function buildApp(options = {}) {
22
+ const app = Fastify({ logger: false, forceCloseConnections: true });
23
+ const repositoryPath = options.repositoryPath ?? process.cwd();
24
+ const db = options.db ?? createTestDatabase();
25
+ const eventBus = options.eventBus ?? createEventBus();
26
+ app.register(fastifySse);
27
+ app.register(healthRoutes);
28
+ app.register(diffRoutes, { repositoryPath });
29
+ app.register(gitRoutes, { repositoryPath });
30
+ app.register(reviewRoutes, { repositoryPath, db, eventBus });
31
+ app.register(commentRoutes, { db, eventBus });
32
+ app.register(exportRoutes, { db });
33
+ app.register(eventRoutes, { eventBus });
34
+ if (options.watchFiles) {
35
+ const watcher = watchRepository(repositoryPath, () => {
36
+ eventBus.publish({ type: 'files:changed' });
37
+ });
38
+ app.addHook('onClose', async () => {
39
+ watcher.close();
40
+ });
41
+ }
42
+ if (options.staticRoot) {
43
+ const staticRoot = options.staticRoot;
44
+ app.register(fastifyStatic, {
45
+ root: staticRoot,
46
+ wildcard: false
47
+ });
48
+ app.setNotFoundHandler((request, reply) => {
49
+ if (request.url.startsWith('/api/')) {
50
+ reply.code(404).send({
51
+ error: 'Not Found',
52
+ message: `Route ${request.url} not found`,
53
+ statusCode: 404
54
+ });
55
+ return;
56
+ }
57
+ const indexHtml = readFileSync(join(staticRoot, 'index.html'), 'utf-8');
58
+ reply.code(200).type('text/html').send(indexHtml);
59
+ });
60
+ }
61
+ return app;
62
+ }
@@ -0,0 +1,17 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { mkdirSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import { applyMigrations } from "./migrations.js";
5
+ export function createTestDatabase() {
6
+ const db = new DatabaseSync(':memory:');
7
+ applyMigrations(db);
8
+ return db;
9
+ }
10
+ export function createFileDatabase(path) {
11
+ mkdirSync(dirname(path), { recursive: true });
12
+ const db = new DatabaseSync(path);
13
+ // WAL allows a concurrent reader (`list`/`export`) while `serve` holds the file open.
14
+ db.exec('PRAGMA journal_mode = WAL;');
15
+ applyMigrations(db);
16
+ return db;
17
+ }
@@ -0,0 +1,34 @@
1
+ export function applyMigrations(db) {
2
+ db.exec(`
3
+ CREATE TABLE IF NOT EXISTS reviews (
4
+ id TEXT PRIMARY KEY,
5
+ repository_path TEXT NOT NULL,
6
+ base_ref TEXT,
7
+ source_type TEXT NOT NULL,
8
+ source_ref TEXT,
9
+ snapshot_data TEXT NOT NULL,
10
+ status TEXT NOT NULL,
11
+ created_at TEXT NOT NULL,
12
+ updated_at TEXT NOT NULL
13
+ );
14
+
15
+ CREATE TABLE IF NOT EXISTS comments (
16
+ id TEXT PRIMARY KEY,
17
+ review_id TEXT NOT NULL REFERENCES reviews(id),
18
+ file_path TEXT NOT NULL,
19
+ line_number INTEGER,
20
+ line_type TEXT,
21
+ content TEXT NOT NULL,
22
+ created_at TEXT NOT NULL,
23
+ updated_at TEXT NOT NULL
24
+ );
25
+ `);
26
+ const columns = db.prepare('PRAGMA table_info(comments)').all();
27
+ const columnNames = new Set(columns.map(c => c.name));
28
+ if (!columnNames.has('resolved')) {
29
+ db.exec('ALTER TABLE comments ADD COLUMN resolved INTEGER NOT NULL DEFAULT 0');
30
+ }
31
+ if (!columnNames.has('suggestion')) {
32
+ db.exec('ALTER TABLE comments ADD COLUMN suggestion TEXT');
33
+ }
34
+ }
@@ -0,0 +1,14 @@
1
+ export function createEventBus() {
2
+ const listeners = new Set();
3
+ return {
4
+ publish(event) {
5
+ for (const listener of listeners) {
6
+ listener(event);
7
+ }
8
+ },
9
+ subscribe(listener) {
10
+ listeners.add(listener);
11
+ return () => listeners.delete(listener);
12
+ }
13
+ };
14
+ }
@@ -0,0 +1,52 @@
1
+ function rowToComment(row) {
2
+ return {
3
+ id: row.id,
4
+ reviewId: row.review_id,
5
+ filePath: row.file_path,
6
+ lineNumber: row.line_number,
7
+ lineType: row.line_type,
8
+ content: row.content,
9
+ createdAt: row.created_at,
10
+ updatedAt: row.updated_at,
11
+ resolved: Boolean(row.resolved),
12
+ suggestion: row.suggestion
13
+ };
14
+ }
15
+ export function findCommentById(db, id) {
16
+ const row = db.prepare('SELECT * FROM comments WHERE id = ?').get(id);
17
+ return row ? rowToComment(row) : null;
18
+ }
19
+ export function createComment(db, comment) {
20
+ db.prepare(`INSERT INTO comments (id, review_id, file_path, line_number, line_type, content, created_at, updated_at, suggestion)
21
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(comment.id, comment.reviewId, comment.filePath, comment.lineNumber, comment.lineType, comment.content, comment.createdAt, comment.updatedAt, comment.suggestion ?? null);
22
+ return findCommentById(db, comment.id);
23
+ }
24
+ export function listCommentsByReview(db, reviewId) {
25
+ const rows = db
26
+ .prepare('SELECT * FROM comments WHERE review_id = ? ORDER BY created_at ASC')
27
+ .all(reviewId);
28
+ return rows.map(rowToComment);
29
+ }
30
+ export function updateComment(db, id, content) {
31
+ const updatedAt = new Date().toISOString();
32
+ const result = db
33
+ .prepare('UPDATE comments SET content = ?, updated_at = ? WHERE id = ?')
34
+ .run(content, updatedAt, id);
35
+ if (result.changes === 0) {
36
+ return null;
37
+ }
38
+ return findCommentById(db, id);
39
+ }
40
+ export function deleteComment(db, id) {
41
+ db.prepare('DELETE FROM comments WHERE id = ?').run(id);
42
+ }
43
+ export function setCommentResolved(db, id, resolved) {
44
+ const updatedAt = new Date().toISOString();
45
+ const result = db
46
+ .prepare('UPDATE comments SET resolved = ?, updated_at = ? WHERE id = ?')
47
+ .run(resolved ? 1 : 0, updatedAt, id);
48
+ if (result.changes === 0) {
49
+ return null;
50
+ }
51
+ return findCommentById(db, id);
52
+ }