@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.
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/dist/cli/commands/export.js +55 -0
- package/dist/cli/commands/list.js +39 -0
- package/dist/cli/config.js +65 -0
- package/dist/cli/index.js +50 -0
- package/dist/cli/server-runtime.js +66 -0
- package/dist/cli/startup-message.js +7 -0
- package/dist/server/app.js +62 -0
- package/dist/server/db/index.js +17 -0
- package/dist/server/db/migrations.js +34 -0
- package/dist/server/event-bus.js +14 -0
- package/dist/server/repositories/comment.repo.js +52 -0
- package/dist/server/repositories/review.repo.js +41 -0
- package/dist/server/routes/comments.js +114 -0
- package/dist/server/routes/diff.js +39 -0
- package/dist/server/routes/events.js +16 -0
- package/dist/server/routes/export.js +27 -0
- package/dist/server/routes/git.js +100 -0
- package/dist/server/routes/health.js +5 -0
- package/dist/server/routes/reviews.js +90 -0
- package/dist/server/services/comment.service.js +57 -0
- package/dist/server/services/diff.service.js +144 -0
- package/dist/server/services/export.service.js +71 -0
- package/dist/server/services/file-watcher.service.js +124 -0
- package/dist/server/services/git.service.js +219 -0
- package/dist/server/services/review.service.js +71 -0
- package/dist/shared/types.js +4 -0
- package/dist/web/assets/ErrorNotFound-C0A8n_dh.js +1 -0
- package/dist/web/assets/IndexPage-Ca7Xb-lc.js +3 -0
- package/dist/web/assets/IndexPage-Dn15ye8s.css +1 -0
- package/dist/web/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuYjalmUiAw-BCOjigVR.woff +0 -0
- package/dist/web/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuZtalmUiAw-Ocp2tNig.woff +0 -0
- package/dist/web/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuaabVmUiAw-6jEn9xEl.woff +0 -0
- package/dist/web/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWub2bVmUiAw-CCs8I9zJ.woff +0 -0
- package/dist/web/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbFmUiAw-BLaboPi0.woff +0 -0
- package/dist/web/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAw-DHuK6FBl.woff +0 -0
- package/dist/web/assets/MainLayout-CC5JJvA9.css +1 -0
- package/dist/web/assets/MainLayout-DHuQjMOV.js +1 -0
- package/dist/web/assets/QPage-DLIDrL1Z.js +1 -0
- package/dist/web/assets/ReviewsPage-f8fJEuU9.js +1 -0
- package/dist/web/assets/bash-CLZ0U2zV.js +1 -0
- package/dist/web/assets/client-TNhDHmkt.js +1 -0
- package/dist/web/assets/core-Ruj7F7dx.js +12 -0
- package/dist/web/assets/create-C46qMo0y.js +1 -0
- package/dist/web/assets/css-CdtkT07m.js +1 -0
- package/dist/web/assets/css-LbU1hoFO.js +1 -0
- package/dist/web/assets/engine-oniguruma-Bon2Icja.js +1 -0
- package/dist/web/assets/fira-code-cyrillic-400-normal-UC0NFL4U.woff2 +0 -0
- package/dist/web/assets/fira-code-cyrillic-400-normal-dc1q3VD7.woff +0 -0
- package/dist/web/assets/fira-code-cyrillic-500-normal-Bu7VCL72.woff +0 -0
- package/dist/web/assets/fira-code-cyrillic-500-normal-CX35h3Mg.woff2 +0 -0
- package/dist/web/assets/fira-code-cyrillic-600-normal-BD56RyRT.woff +0 -0
- package/dist/web/assets/fira-code-cyrillic-600-normal-CPRdbepc.woff2 +0 -0
- package/dist/web/assets/fira-code-cyrillic-ext-400-normal-Buh61xzB.woff +0 -0
- package/dist/web/assets/fira-code-cyrillic-ext-400-normal-txZ9Fk_1.woff2 +0 -0
- package/dist/web/assets/fira-code-cyrillic-ext-500-normal-CYRspHgm.woff2 +0 -0
- package/dist/web/assets/fira-code-cyrillic-ext-500-normal-CwtLmSFD.woff +0 -0
- package/dist/web/assets/fira-code-cyrillic-ext-600-normal-B9wE0zmr.woff2 +0 -0
- package/dist/web/assets/fira-code-cyrillic-ext-600-normal-DldcyWhZ.woff +0 -0
- package/dist/web/assets/fira-code-greek-400-normal-B2Gh_Y8s.woff2 +0 -0
- package/dist/web/assets/fira-code-greek-400-normal-DobhaxvF.woff +0 -0
- package/dist/web/assets/fira-code-greek-500-normal-CVO0IRNt.woff2 +0 -0
- package/dist/web/assets/fira-code-greek-500-normal-DEXIWiey.woff +0 -0
- package/dist/web/assets/fira-code-greek-600-normal-D_h3X2X5.woff +0 -0
- package/dist/web/assets/fira-code-greek-600-normal-Dxp52_By.woff2 +0 -0
- package/dist/web/assets/fira-code-greek-ext-400-normal-DR7mBgIM.woff2 +0 -0
- package/dist/web/assets/fira-code-greek-ext-400-normal-DSPUheWg.woff +0 -0
- package/dist/web/assets/fira-code-greek-ext-500-normal-BO3iq16n.woff2 +0 -0
- package/dist/web/assets/fira-code-greek-ext-500-normal-CMiNLXER.woff +0 -0
- package/dist/web/assets/fira-code-greek-ext-600-normal-C-cQDoOk.woff +0 -0
- package/dist/web/assets/fira-code-greek-ext-600-normal-DtBpFRLc.woff2 +0 -0
- package/dist/web/assets/fira-code-latin-400-normal-C-QZfXAs.woff +0 -0
- package/dist/web/assets/fira-code-latin-400-normal-DGosTW8U.woff2 +0 -0
- package/dist/web/assets/fira-code-latin-500-normal-B6gioGu8.woff2 +0 -0
- package/dist/web/assets/fira-code-latin-500-normal-aMLC2AMm.woff +0 -0
- package/dist/web/assets/fira-code-latin-600-normal-BmSSHshu.woff +0 -0
- package/dist/web/assets/fira-code-latin-600-normal-CvYZpPHg.woff2 +0 -0
- package/dist/web/assets/fira-code-latin-ext-400-normal-DG73JjOf.woff +0 -0
- package/dist/web/assets/fira-code-latin-ext-400-normal-KSMg0QLl.woff2 +0 -0
- package/dist/web/assets/fira-code-latin-ext-500-normal-CaANqXjI.woff +0 -0
- package/dist/web/assets/fira-code-latin-ext-500-normal-Crd9e_oI.woff2 +0 -0
- package/dist/web/assets/fira-code-latin-ext-600-normal-0PFQL1eg.woff +0 -0
- package/dist/web/assets/fira-code-latin-ext-600-normal-Cb7jxoAi.woff2 +0 -0
- package/dist/web/assets/fira-code-symbols2-400-normal-_QEZCEwe.woff +0 -0
- package/dist/web/assets/fira-code-symbols2-500-normal-DwsgbIjQ.woff +0 -0
- package/dist/web/assets/fira-code-symbols2-600-normal-Dvq6IQE5.woff +0 -0
- package/dist/web/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
- package/dist/web/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ-D-x-0Q06.woff2 +0 -0
- package/dist/web/assets/format-SW_H2PBI.js +1 -0
- package/dist/web/assets/github-dark-C-LZuMrd.js +1 -0
- package/dist/web/assets/github-light-EUqPIrTm.js +1 -0
- package/dist/web/assets/go-rLFTqkRN.js +1 -0
- package/dist/web/assets/html-BcACZWSP.js +1 -0
- package/dist/web/assets/html-DDNsd9lV.js +1 -0
- package/dist/web/assets/i18n-RyrLuvXU.js +1 -0
- package/dist/web/assets/index-CRwZKCt6.js +2 -0
- package/dist/web/assets/index-Fo431P2N.css +1 -0
- package/dist/web/assets/init-app-DSQzny_G.js +1 -0
- package/dist/web/assets/java-BXcb2rRD.js +1 -0
- package/dist/web/assets/javascript-BzbQS41l.js +1 -0
- package/dist/web/assets/javascript-Yelw-5ZN.js +1 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-600-normal-8K4wrrwR.woff +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-600-normal-EVf6-Yzo.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-600-normal-H7WoG9Et.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-600-normal-mc2nkWzM.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-600-normal-BfsvjouI.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-600-normal-BfB_LPfz.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-600-normal-DObL3zCW.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-600-normal-OWROknRo.woff +0 -0
- package/dist/web/assets/json-B88Oqtu5.js +1 -0
- package/dist/web/assets/json-CaljABEy.js +1 -0
- package/dist/web/assets/jsx-Bz0zcwM4.js +1 -0
- package/dist/web/assets/kotlin-DhhofPvG.js +1 -0
- package/dist/web/assets/markdown-BYOwaDjH.js +1 -0
- package/dist/web/assets/pinia-BDCzxZ18.js +1 -0
- package/dist/web/assets/pinia-BJouyyXU.js +1 -0
- package/dist/web/assets/python-gzcpVVnB.js +1 -0
- package/dist/web/assets/repository-store-DqnwJIJW.js +1 -0
- package/dist/web/assets/runtime-core.esm-bundler-oS9U9Vp7.js +1 -0
- package/dist/web/assets/rust-Cfkwpbl8.js +1 -0
- package/dist/web/assets/scss-DHtrkMkT.js +1 -0
- package/dist/web/assets/syne-greek-400-normal-B4XGRaFa.woff2 +0 -0
- package/dist/web/assets/syne-greek-400-normal-C1nzy5ms.woff +0 -0
- package/dist/web/assets/syne-greek-500-normal-DUQv5E33.woff2 +0 -0
- package/dist/web/assets/syne-greek-500-normal-L1Ji5iNA.woff +0 -0
- package/dist/web/assets/syne-greek-600-normal-BJs4kvyH.woff +0 -0
- package/dist/web/assets/syne-greek-600-normal-BpFII5Ja.woff2 +0 -0
- package/dist/web/assets/syne-greek-700-normal-C01yjWE_.woff +0 -0
- package/dist/web/assets/syne-greek-700-normal-RbvlO3f8.woff2 +0 -0
- package/dist/web/assets/syne-greek-800-normal-DtzFnRR0.woff +0 -0
- package/dist/web/assets/syne-greek-800-normal-n2LZS_yH.woff2 +0 -0
- package/dist/web/assets/syne-latin-400-normal-CULFKXub.woff +0 -0
- package/dist/web/assets/syne-latin-400-normal-CcHADMBd.woff2 +0 -0
- package/dist/web/assets/syne-latin-500-normal-DR6mUQpY.woff +0 -0
- package/dist/web/assets/syne-latin-500-normal-emxikWQ7.woff2 +0 -0
- package/dist/web/assets/syne-latin-600-normal-BiwQbQXw.woff2 +0 -0
- package/dist/web/assets/syne-latin-600-normal-dRu9QuIh.woff +0 -0
- package/dist/web/assets/syne-latin-700-normal-AF3Rs61n.woff2 +0 -0
- package/dist/web/assets/syne-latin-700-normal-Cd-_iCn5.woff +0 -0
- package/dist/web/assets/syne-latin-800-normal-BuJcbPcX.woff +0 -0
- package/dist/web/assets/syne-latin-800-normal-EehdNYzx.woff2 +0 -0
- package/dist/web/assets/syne-latin-ext-400-normal-B5s6XpS5.woff2 +0 -0
- package/dist/web/assets/syne-latin-ext-400-normal-DCnt6qEA.woff +0 -0
- package/dist/web/assets/syne-latin-ext-500-normal-C5b4wHnS.woff +0 -0
- package/dist/web/assets/syne-latin-ext-500-normal-DHUXVE3f.woff2 +0 -0
- package/dist/web/assets/syne-latin-ext-600-normal-DEzBMW8B.woff +0 -0
- package/dist/web/assets/syne-latin-ext-600-normal-ZnizrDKU.woff2 +0 -0
- package/dist/web/assets/syne-latin-ext-700-normal-C-qCH-vM.woff +0 -0
- package/dist/web/assets/syne-latin-ext-700-normal-DuyXglMe.woff2 +0 -0
- package/dist/web/assets/syne-latin-ext-800-normal-B2iBRfEf.woff +0 -0
- package/dist/web/assets/syne-latin-ext-800-normal-CAQAsWuS.woff2 +0 -0
- package/dist/web/assets/theme-DUV6mF_4.js +1 -0
- package/dist/web/assets/tsx-udAQXfEw.js +1 -0
- package/dist/web/assets/typescript-Utq2Cl8c.js +1 -0
- package/dist/web/assets/typescript-j_1H8WHN.js +1 -0
- package/dist/web/assets/use-app-theme-DrR0AvTg.js +1 -0
- package/dist/web/assets/vue-CSBlZ1UP.js +1 -0
- package/dist/web/assets/vue-i18n.runtime-nPdXRlNg.js +3 -0
- package/dist/web/assets/wasm-BnjxR4X6.js +1 -0
- package/dist/web/assets/yaml-rwi0_p6S.js +1 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/icons/favicon-128x128.png +0 -0
- package/dist/web/icons/favicon-16x16.png +0 -0
- package/dist/web/icons/favicon-32x32.png +0 -0
- package/dist/web/icons/favicon-96x96.png +0 -0
- package/dist/web/index.html +5 -0
- 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
|
+
}
|