collabmd 0.1.44 → 0.1.46

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 (291) hide show
  1. package/README.md +39 -19
  2. package/bin/collabmd.js +27 -1
  3. package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-CYM5nxUi.js +1 -0
  4. package/dist/client/assets/browser-utils--H_DkyEF.js +1 -0
  5. package/dist/client/assets/chunk-ERSWUTIN-hHdvnhaY.js +41 -0
  6. package/dist/client/assets/{chunk-CZQZEIFA-DT3N9fo7.js → chunk-YGJVY4SA-CjSe7Bx7.js} +2 -2
  7. package/dist/client/assets/comment-thread-store-CdcwrObc.js +1 -0
  8. package/dist/client/assets/comment-threads-9J6Em-_y.js +4 -0
  9. package/dist/client/assets/comment-ui-shared-BC4pt4w8.js +7 -0
  10. package/dist/client/assets/dist-B5HTH2BO.js +1 -0
  11. package/dist/client/assets/{dist-DhLvC8DS.js → dist-B5Jf1SLZ.js} +2 -2
  12. package/dist/client/assets/{dist-CnL7Y2sM.js → dist-BDt3rz7l.js} +1 -1
  13. package/dist/client/assets/dist-BEwK4csK.js +1 -0
  14. package/dist/client/assets/dist-BKW4PALF.js +1 -0
  15. package/dist/client/assets/dist-BWcHHOx-.js +1 -0
  16. package/dist/client/assets/{dist-CYZmbokb.js → dist-BalScOwZ.js} +9 -9
  17. package/dist/client/assets/dist-Bf8f4HmM.js +6 -0
  18. package/dist/client/assets/dist-BjHJDtyU.js +1 -0
  19. package/dist/client/assets/dist-Bm6pf9Bl.js +1 -0
  20. package/dist/client/assets/dist-BoVBdSjk.js +1 -0
  21. package/dist/client/assets/dist-CC5IrQMG.js +1 -0
  22. package/dist/client/assets/{dist-C11hHqv_.js → dist-CCP_AKPG.js} +12 -12
  23. package/dist/client/assets/dist-CZgOmxK6.js +1 -0
  24. package/dist/client/assets/{dist-D2AQ-76g.js → dist-Cqhv5LAk.js} +1 -1
  25. package/dist/client/assets/{dist-MvcV4190.js → dist-D-Z6FLem.js} +1 -1
  26. package/dist/client/assets/dist-DLMiIsNC.js +11 -0
  27. package/dist/client/assets/{dist-3bgCp7iY.js → dist-DZKu9Cs8.js} +1 -1
  28. package/dist/client/assets/dist-DaYdhcVs.js +1 -0
  29. package/dist/client/assets/dist-FWJsgGYI.js +1 -0
  30. package/dist/client/assets/dist-Kh2COra_.js +1 -0
  31. package/dist/client/assets/{dist-JYEDRrPT.js → dist-PDZ3bSqm.js} +1 -1
  32. package/dist/client/assets/{dist-Drp8uu5J.js → dist-TjMh8n1u.js} +1 -1
  33. package/dist/client/assets/{dist-DGb27Dl6.js → dist-dtAdgL39.js} +1 -1
  34. package/dist/client/assets/dist-jv4WdPnK.js +9 -0
  35. package/dist/client/assets/{dist-DK5Gs2wa.js → dist-rGBmqxC7.js} +1 -1
  36. package/dist/client/assets/{dist-DCzuX1AF.js → dist-shrl6K_0.js} +1 -1
  37. package/dist/client/assets/dist-tOaegDYS2.js +1 -0
  38. package/dist/client/assets/dist-ule2n6ak.js +1 -0
  39. package/dist/client/assets/dist-xElaxbia.js +1 -0
  40. package/dist/client/assets/drawio-editor-FzY1cVw8.js +9 -0
  41. package/dist/client/assets/drawioEditor-BjiKJqlc.js +2 -0
  42. package/dist/client/assets/editor-session-Cm9ekeNo.js +54 -0
  43. package/dist/client/assets/embedded-editor-base-CwTkk911.css +1 -0
  44. package/dist/client/assets/{en-GADW4C6J-DQ6zyCEM.js → en-7NFCYGKX-BrgYZAEe.js} +1 -1
  45. package/dist/client/assets/{preview-render-profile-BZzGSg6K.js → es-DXGV3RHd.js} +13 -13
  46. package/dist/client/assets/excalidraw-editor-B46zsTFI.js +9 -0
  47. package/dist/client/assets/excalidraw-editor-Bt7gMSoh.css +1 -0
  48. package/dist/client/assets/excalidraw-scene-BOben2gr.js +1 -0
  49. package/dist/client/assets/excalidrawEditor-CT2pHq7e.js +2 -0
  50. package/dist/client/assets/export-page-A7NHawJY.js +39 -0
  51. package/dist/client/assets/exportDocument-B2L8fe34.css +1 -0
  52. package/dist/client/assets/exportDocument-BaqcOAAW.js +2 -0
  53. package/dist/client/assets/file-icon-svg-CQAjlWjK.js +1 -0
  54. package/dist/client/assets/file-kind-BbqgGzpc.js +1 -0
  55. package/dist/client/assets/image-QXRYHUNP-SWEV8H0J.js +1 -0
  56. package/dist/client/assets/index-ClbjMBVt.js +2 -0
  57. package/dist/client/assets/index-Pum8ET-5.css +1 -0
  58. package/dist/client/assets/main-B3uPyqic.js +893 -0
  59. package/dist/client/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
  60. package/dist/client/assets/pdf-BL9Pr2D5.js +54 -0
  61. package/dist/client/assets/pdf.worker-CLesOks4.mjs +64857 -0
  62. package/dist/client/assets/pdf_viewer-FVkkThnz.js +551 -0
  63. package/dist/client/assets/preview-diagram-utils-txBCoW8d.js +1 -0
  64. package/dist/client/assets/{preview-render-compiler-BSaQO_p3.js → preview-render-compiler-BD-XhkS2.js} +2 -2
  65. package/dist/client/assets/preview-render-compiler-BKedaMxx.js +1 -0
  66. package/dist/client/assets/preview-render-profile-CE-5rVTC.js +1 -0
  67. package/dist/client/assets/{preview-render-worker-B42LWvrD.js → preview-render-worker-CiPqc9n6.js} +20 -20
  68. package/dist/client/assets/prod-Dy20X7eT.js +1 -0
  69. package/dist/client/assets/prod-hnwh7wRM.js +177 -0
  70. package/dist/client/assets/quick-switcher-controller-fqOYR7qR.js +12 -0
  71. package/dist/client/assets/runtime-config-BLrEKVO1.js +1 -0
  72. package/dist/client/assets/vault-api-client-_LpV1aIn.js +6 -0
  73. package/dist/client/assets/{yjs-provider-reset-guard-Cox4nxwu.js → yjs-provider-reset-guard-DE0vINDP.js} +1 -1
  74. package/dist/client/drawio-editor.html +3 -2
  75. package/dist/client/excalidraw-editor.html +3 -2
  76. package/dist/client/export-document.html +4 -3
  77. package/dist/client/index.html +109 -27
  78. package/docker-compose.yml +42 -0
  79. package/package.json +26 -15
  80. package/scripts/local-structurizr-compose.mjs +101 -0
  81. package/src/client/app/drawio-editor-entry.js +0 -3
  82. package/src/client/app/drawio-editor.html +1 -0
  83. package/src/client/app/excalidraw-editor-entry.js +0 -3
  84. package/src/client/app/excalidraw-editor.html +1 -0
  85. package/src/client/app/export-document-entry.js +0 -3
  86. package/src/client/app/export-document.html +1 -0
  87. package/src/client/app/index.html +106 -24
  88. package/src/client/app/main-entry.js +0 -5
  89. package/src/client/application/app-shell/chat-feature.js +180 -124
  90. package/src/client/application/app-shell/comments-feature.js +20 -11
  91. package/src/client/application/app-shell/git-feature.js +16 -9
  92. package/src/client/application/app-shell/presence-feature.js +18 -11
  93. package/src/client/application/app-shell/ui-feature-shell.js +78 -17
  94. package/src/client/application/app-shell/ui-feature-sidebar.js +74 -1
  95. package/src/client/application/app-shell/ui-feature-tab-activity.js +2 -2
  96. package/src/client/application/app-shell/ui-feature-toolbar.js +2 -52
  97. package/src/client/application/app-shell/workspace-feature.js +27 -84
  98. package/src/client/application/app-shell-elements.js +9 -1
  99. package/src/client/application/diagram-chrome.js +161 -166
  100. package/src/client/application/diagram-preview-export.js +7 -34
  101. package/src/client/application/diagram-preview-hydrator.js +1 -19
  102. package/src/client/application/markdown-frontmatter.js +0 -2
  103. package/src/client/application/mermaid-preview-hydrator.js +1 -14
  104. package/src/client/application/pdf-preview-controller.js +992 -0
  105. package/src/client/application/plantuml-preview-hydrator.js +0 -10
  106. package/src/client/application/preview-diagram-utils.js +31 -2
  107. package/src/client/application/preview-render-compiler.js +2 -1
  108. package/src/client/application/preview-renderer.js +2 -2
  109. package/src/client/application/quick-switcher-loader.js +3 -1
  110. package/src/client/application/structurizr-preview-controller.js +395 -0
  111. package/src/client/application/workspace-coordinator.js +25 -3
  112. package/src/client/application/workspace-preview-controller.js +91 -5
  113. package/src/client/bootstrap/collabmd-app-shell.js +155 -43
  114. package/src/client/browser-utils.js +44 -0
  115. package/src/client/domain/excalidraw-collaboration.js +0 -2
  116. package/src/client/domain/room.js +38 -0
  117. package/src/client/domain/runtime-paths.js +26 -5
  118. package/src/client/domain/structurizr-language.js +99 -0
  119. package/src/client/domain/vault-paths.js +48 -0
  120. package/src/client/excalidraw-editor.js +833 -51
  121. package/src/client/export/export-pipeline.js +6 -34
  122. package/src/client/infrastructure/auth-client.js +4 -10
  123. package/src/client/infrastructure/browser-notification-port.js +33 -20
  124. package/src/client/infrastructure/browser-preferences-port.js +46 -9
  125. package/src/client/infrastructure/comment-thread-store.js +34 -13
  126. package/src/client/infrastructure/editor-session.js +10 -0
  127. package/src/client/infrastructure/editor-view-adapter.js +133 -5
  128. package/src/client/infrastructure/excalidraw-room-client.js +103 -14
  129. package/src/client/infrastructure/git-api-client.js +5 -0
  130. package/src/client/infrastructure/lobby-presence.js +1 -0
  131. package/src/client/infrastructure/vault-api-client.js +51 -0
  132. package/src/client/infrastructure/workspace-sync-client.js +13 -1
  133. package/src/client/presentation/comment-overview-controller.js +18 -53
  134. package/src/client/presentation/comment-ui/comment-ui-card.js +1 -12
  135. package/src/client/presentation/comment-ui/comment-ui-layout.js +242 -219
  136. package/src/client/presentation/comment-ui/comment-ui-render.js +32 -45
  137. package/src/client/presentation/comment-ui/comment-ui-shared.js +68 -13
  138. package/src/client/presentation/comment-ui-controller.js +5 -1
  139. package/src/client/presentation/drawio-embed-controller.js +9 -102
  140. package/src/client/presentation/drawio-viewer.js +118 -0
  141. package/src/client/presentation/excalidraw-embed-controller.js +74 -8
  142. package/src/client/presentation/file-action-controller.js +134 -0
  143. package/src/client/presentation/file-explorer-controller.js +33 -3
  144. package/src/client/presentation/file-explorer-view.js +59 -34
  145. package/src/client/presentation/file-icon-svg.js +40 -0
  146. package/src/client/presentation/file-tree-state.js +12 -4
  147. package/src/client/presentation/git-diff-view-controller.js +731 -3
  148. package/src/client/presentation/outline-controller.js +86 -0
  149. package/src/client/presentation/quick-switcher-controller.js +257 -74
  150. package/src/client/presentation/quick-switcher-text-search.js +3 -3
  151. package/src/client/presentation/toast-controller.js +3 -0
  152. package/src/client/styles/components/feedback.css +6 -0
  153. package/src/client/styles/components/navigation.css +11 -1
  154. package/src/client/styles/features/collaboration-chat.css +49 -53
  155. package/src/client/styles/features/collaboration-presence.css +9 -7
  156. package/src/client/styles/features/comment-card.css +25 -11
  157. package/src/client/styles/features/comment-overlays.css +0 -4
  158. package/src/client/styles/features/comment-overview.css +44 -21
  159. package/src/client/styles/features/comments-drawer.css +2 -100
  160. package/src/client/styles/features/diagram-preview.css +81 -18
  161. package/src/client/styles/features/diff-special.css +309 -0
  162. package/src/client/styles/features/embedded-preview.css +8 -33
  163. package/src/client/styles/features/quick-switcher.css +8 -0
  164. package/src/client/styles/foundation/themes.css +6 -0
  165. package/src/client/styles/foundation/tokens.css +1 -0
  166. package/src/client/styles/layout/editor-page.css +40 -0
  167. package/src/client/styles/layout/responsive.css +31 -1
  168. package/src/client/styles/layout/shell.css +383 -7
  169. package/src/client/styles/layout/sidebar.css +47 -15
  170. package/src/client/styles/layout/view-modes.css +1 -0
  171. package/src/client/styles/overrides/codemirror.css +217 -15
  172. package/src/client/styles/style.css +1 -0
  173. package/src/client/styles/surfaces/excalidraw-editor.css +294 -0
  174. package/src/domain/comment-threads.js +63 -5
  175. package/src/domain/excalidraw-room-codec.js +6 -4
  176. package/src/domain/file-kind.js +35 -3
  177. package/src/server/application/workspace-reconciliation.js +1 -1
  178. package/src/server/config/env.js +79 -0
  179. package/src/server/create-app-server.js +14 -3
  180. package/src/server/domain/bases/base-definition.js +1 -1
  181. package/src/server/domain/bases/base-expression-runtime.js +1 -4
  182. package/src/server/domain/bases/base-index-snapshot-store.js +1 -4
  183. package/src/server/domain/bases/base-query-service.js +1 -77
  184. package/src/server/domain/bases/base-transform.js +1 -4
  185. package/src/server/domain/collaboration/collaboration-room.js +55 -21
  186. package/src/server/domain/comment-overview.js +25 -8
  187. package/src/server/domain/docx-exporter.js +24 -33
  188. package/src/server/domain/ripgrep-search-service.js +7 -1
  189. package/src/server/domain/workspace-state.js +32 -11
  190. package/src/server/infrastructure/git/diff-service.js +29 -13
  191. package/src/server/infrastructure/git/git-service.js +61 -10
  192. package/src/server/infrastructure/git/history-service.js +96 -29
  193. package/src/server/infrastructure/git/image-mime.js +14 -0
  194. package/src/server/infrastructure/git/path-utils.js +1 -1
  195. package/src/server/infrastructure/git/responses.js +0 -95
  196. package/src/server/infrastructure/git/untracked-files.js +28 -1
  197. package/src/server/infrastructure/github/github-app-client.js +22 -15
  198. package/src/server/infrastructure/http/create-git-api-query-handler.js +31 -1
  199. package/src/server/infrastructure/http/create-request-handler.js +18 -31
  200. package/src/server/infrastructure/http/create-static-handler.js +5 -2
  201. package/src/server/infrastructure/http/create-structurizr-api-handler.js +145 -0
  202. package/src/server/infrastructure/http/create-vault-api-command-handler.js +70 -8
  203. package/src/server/infrastructure/http/create-vault-api-query-handler.js +51 -3
  204. package/src/server/infrastructure/http/http-errors.js +7 -1
  205. package/src/server/infrastructure/http/http-request-helpers.js +22 -0
  206. package/src/server/infrastructure/persistence/path-utils.js +1 -1
  207. package/src/server/infrastructure/persistence/vault-file-store.js +3 -0
  208. package/src/server/infrastructure/structurizr/structurizr-embed.css +43 -0
  209. package/src/server/infrastructure/structurizr/structurizr-workspace-service.js +374 -0
  210. package/src/server/infrastructure/websocket/attach-collaboration-gateway.js +4 -22
  211. package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-CbSISC1w.js +0 -1
  212. package/dist/client/assets/bases-preview-controller-2pLHk0uk.js +0 -143
  213. package/dist/client/assets/browser-utils-CKtI_rZl.js +0 -1
  214. package/dist/client/assets/button-BCEiw4Rw.js +0 -1
  215. package/dist/client/assets/chunk-AXR3VHVW-0GTk_evH.js +0 -41
  216. package/dist/client/assets/comment-threads-D-iKphIp.js +0 -4
  217. package/dist/client/assets/diagram-action-icons-aa_KOv9h.js +0 -36
  218. package/dist/client/assets/dist-BH87JoHx.js +0 -11
  219. package/dist/client/assets/dist-BIkPRZO7.js +0 -6
  220. package/dist/client/assets/dist-BqCsFpx4.js +0 -1
  221. package/dist/client/assets/dist-C2JHCt8U.js +0 -1
  222. package/dist/client/assets/dist-CZjTtmTu.js +0 -1
  223. package/dist/client/assets/dist-Cig4Ov_X.js +0 -1
  224. package/dist/client/assets/dist-CqQEC8gR.js +0 -1
  225. package/dist/client/assets/dist-D4Rzqod1.js +0 -1
  226. package/dist/client/assets/dist-D5sn-mwk.js +0 -9
  227. package/dist/client/assets/dist-D7G-n87v.js +0 -1
  228. package/dist/client/assets/dist-DRqOrNgN.js +0 -1
  229. package/dist/client/assets/dist-DetkMS_4.js +0 -1
  230. package/dist/client/assets/dist-Dp6FVkQ1.js +0 -1
  231. package/dist/client/assets/dist-DtbjNB28.js +0 -1
  232. package/dist/client/assets/dist-DyrYcvpo2.js +0 -1
  233. package/dist/client/assets/dist-GrygD2zE.js +0 -1
  234. package/dist/client/assets/dist-HmEpWUow.js +0 -1
  235. package/dist/client/assets/dist-RTa81Km7.js +0 -1
  236. package/dist/client/assets/drawio-editor-DDvFoarP.js +0 -9
  237. package/dist/client/assets/drawio-embed-controller-BD1GTRwn.js +0 -1
  238. package/dist/client/assets/drawioEditor-Bu5grlH5.js +0 -2
  239. package/dist/client/assets/editor-session-DGlpiAsK.js +0 -16
  240. package/dist/client/assets/embedded-editor-base-C9roKYpW.css +0 -1
  241. package/dist/client/assets/excalidraw-editor-BHu6k8d4.js +0 -9
  242. package/dist/client/assets/excalidraw-editor-Cr_184EF.css +0 -1
  243. package/dist/client/assets/excalidraw-embed-controller-y2fenN_g.js +0 -1
  244. package/dist/client/assets/excalidraw-embed-reconciler-DGiagExg.js +0 -1
  245. package/dist/client/assets/excalidraw-scene-BvsQ6mLP.js +0 -176
  246. package/dist/client/assets/excalidrawEditor-4vFE--Uw.js +0 -2
  247. package/dist/client/assets/export-page-Cby8JzPO.js +0 -39
  248. package/dist/client/assets/exportDocument-C9v-bc7I.css +0 -1
  249. package/dist/client/assets/exportDocument-CRHCfsdo.js +0 -2
  250. package/dist/client/assets/file-history-view-controller-BDjEV9xJ.js +0 -112
  251. package/dist/client/assets/file-kind-BKPOIRPE.js +0 -1
  252. package/dist/client/assets/git-diff-view-controller-DgdM1aqb.js +0 -150
  253. package/dist/client/assets/git-panel-controller-BDTR2HVp.js +0 -207
  254. package/dist/client/assets/image-XGD7JQR6-XDUWHRpO.js +0 -1
  255. package/dist/client/assets/index-7xOyXDhP.css +0 -1
  256. package/dist/client/assets/index-Bz1KuQ6y.js +0 -2
  257. package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  258. package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  259. package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  260. package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  261. package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  262. package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  263. package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  264. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  265. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  266. package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  267. package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  268. package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  269. package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  270. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  271. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  272. package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  273. package/dist/client/assets/main-Ci07m9oP.js +0 -152
  274. package/dist/client/assets/preview-diagram-utils-n_bbJA9V.js +0 -1
  275. package/dist/client/assets/preview-render-compiler-csGM5ZFz.js +0 -1
  276. package/dist/client/assets/quick-switcher-controller-CoiYvllm.js +0 -12
  277. package/dist/client/assets/room-CDrmIdDt.js +0 -1
  278. package/dist/client/assets/runtime-config-CDgeuPWN.js +0 -1
  279. package/dist/client/assets/runtime-config-loader-BHGniHP5.js +0 -1
  280. package/dist/client/assets/runtime-paths-DvpihCEq.js +0 -1
  281. package/dist/client/assets/segmented-control-DJrHSz8M.js +0 -1
  282. package/dist/client/assets/vault-api-client-sLB2bo56.js +0 -1
  283. package/dist/client/assets/vault-paths-CVkuh0j-.js +0 -5
  284. package/src/client/bootstrap/lazy-controller-feature.js +0 -433
  285. package/src/client/infrastructure/backlinks-api-client.js +0 -16
  286. package/src/client/infrastructure/plantuml-api-client.js +0 -19
  287. package/src/client/infrastructure/runtime-config-loader.js +0 -56
  288. package/src/server/domain/collaboration/room-persistence-controller.js +0 -80
  289. package/src/server/infrastructure/git/command-runner.js +0 -49
  290. package/src/server/infrastructure/git/errors.js +0 -8
  291. package/src/server/infrastructure/http/create-esm-proxy-handler.js +0 -130
@@ -1,6 +1,7 @@
1
- import { createGitRequestError } from './errors.js';
1
+ import { createRequestError as createGitRequestError } from '../http/http-errors.js';
2
2
  import { getVaultFileKind, isImageAttachmentFilePath } from '../../../domain/file-kind.js';
3
3
  import { normalizeRelativeGitPath } from './path-utils.js';
4
+ import { getImageMimeType } from './image-mime.js';
4
5
  import {
5
6
  countPatchLines,
6
7
  parseGitLogHeader,
@@ -9,11 +10,7 @@ import {
9
10
  parseUnifiedDiff,
10
11
  } from './parsers.js';
11
12
  import {
12
- createCommitDiffResponse,
13
13
  createEmptyStats,
14
- createFileHistoryResponse,
15
- createFileSnapshotResponse,
16
- createHistoryResponse,
17
14
  } from './responses.js';
18
15
 
19
16
  const EMPTY_TREE_HASH = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
@@ -127,13 +124,13 @@ export class GitHistoryService {
127
124
  const normalizedOffset = normalizeHistoryOffset(offset);
128
125
 
129
126
  if (!isGitRepo) {
130
- return createHistoryResponse({
127
+ return {
131
128
  commits: [],
132
129
  hasMore: false,
133
130
  isGitRepo: false,
134
131
  limit: normalizedLimit,
135
132
  offset: normalizedOffset,
136
- });
133
+ };
137
134
  }
138
135
 
139
136
  const cacheKey = JSON.stringify({
@@ -149,12 +146,13 @@ export class GitHistoryService {
149
146
  return this.runRequest(cacheKey, async () => {
150
147
  const hasHeadCommit = await this.hasHeadCommit();
151
148
  if (!hasHeadCommit) {
152
- return setCachedValue(this.historyCache, cacheKey, createHistoryResponse({
149
+ return setCachedValue(this.historyCache, cacheKey, {
153
150
  commits: [],
154
151
  hasMore: false,
152
+ isGitRepo: true,
155
153
  limit: normalizedLimit,
156
154
  offset: normalizedOffset,
157
- }), this.responseCacheTtlMs);
155
+ }, this.responseCacheTtlMs);
158
156
  }
159
157
 
160
158
  const output = await this.commandRunner.execGit([
@@ -175,12 +173,13 @@ export class GitHistoryService {
175
173
  .map((chunk) => this.parseHistoryChunk(chunk));
176
174
 
177
175
  const hasMore = commits.length > normalizedLimit;
178
- const response = createHistoryResponse({
176
+ const response = {
179
177
  commits: commits.slice(0, normalizedLimit),
180
178
  hasMore,
179
+ isGitRepo: true,
181
180
  limit: normalizedLimit,
182
181
  offset: normalizedOffset,
183
- });
182
+ };
184
183
 
185
184
  return setCachedValue(this.historyCache, cacheKey, response, this.responseCacheTtlMs);
186
185
  });
@@ -193,14 +192,14 @@ export class GitHistoryService {
193
192
  const normalizedPath = normalizeRelativeGitPath(path);
194
193
 
195
194
  if (!isGitRepo) {
196
- return createFileHistoryResponse({
195
+ return {
197
196
  commits: [],
198
197
  hasMore: false,
199
198
  isGitRepo: false,
200
199
  limit: normalizedLimit,
201
200
  offset: normalizedOffset,
202
201
  path: normalizedPath,
203
- });
202
+ };
204
203
  }
205
204
 
206
205
  const cacheKey = JSON.stringify({
@@ -217,13 +216,14 @@ export class GitHistoryService {
217
216
  return this.runRequest(cacheKey, async () => {
218
217
  const hasHeadCommit = await this.hasHeadCommit();
219
218
  if (!hasHeadCommit) {
220
- return setCachedValue(this.historyCache, cacheKey, createFileHistoryResponse({
219
+ return setCachedValue(this.historyCache, cacheKey, {
221
220
  commits: [],
222
221
  hasMore: false,
222
+ isGitRepo: true,
223
223
  limit: normalizedLimit,
224
224
  offset: normalizedOffset,
225
225
  path: normalizedPath,
226
- }), this.responseCacheTtlMs);
226
+ }, this.responseCacheTtlMs);
227
227
  }
228
228
 
229
229
  const logArgs = [
@@ -254,13 +254,14 @@ export class GitHistoryService {
254
254
 
255
255
  const commits = this.mergeFileHistoryChunks(nameStatusOutput, numstatOutput);
256
256
  const hasMore = commits.length > normalizedLimit;
257
- const response = createFileHistoryResponse({
257
+ const response = {
258
258
  commits: commits.slice(0, normalizedLimit),
259
259
  hasMore,
260
+ isGitRepo: true,
260
261
  limit: normalizedLimit,
261
262
  offset: normalizedOffset,
262
263
  path: normalizedPath,
263
- });
264
+ };
264
265
 
265
266
  return setCachedValue(this.historyCache, cacheKey, response, this.responseCacheTtlMs);
266
267
  });
@@ -269,13 +270,19 @@ export class GitHistoryService {
269
270
  async getCommit({ allowLargePatch = false, hash, metaOnly = false, path = null } = {}) {
270
271
  const isGitRepo = await this.commandRunner.isGitRepo();
271
272
  if (!isGitRepo) {
272
- return createCommitDiffResponse({
273
+ return {
273
274
  commit: null,
274
275
  files: [],
275
276
  isGitRepo: false,
276
277
  metaOnly,
277
278
  path: null,
278
- });
279
+ source: 'commit',
280
+ summary: {
281
+ additions: 0,
282
+ deletions: 0,
283
+ filesChanged: 0,
284
+ },
285
+ };
279
286
  }
280
287
 
281
288
  const normalizedHash = this.normalizeCommitHash(hash);
@@ -301,13 +308,16 @@ export class GitHistoryService {
301
308
  const summary = createSummaryFromFiles(meta.files);
302
309
 
303
310
  if (metaOnly) {
304
- const metaResponse = createCommitDiffResponse({
311
+ const metaResponse = {
312
+ baseRef: meta.baseRef,
305
313
  commit: meta.commit,
306
314
  files: meta.files,
315
+ isGitRepo: true,
307
316
  metaOnly: true,
308
317
  path: normalizedPath,
318
+ source: 'commit',
309
319
  summary,
310
- });
320
+ };
311
321
  return setCachedValue(this.commitCache, cacheKey, metaResponse, this.responseCacheTtlMs);
312
322
  }
313
323
 
@@ -326,7 +336,8 @@ export class GitHistoryService {
326
336
  !allowLargePatch
327
337
  && (fileSummary.additions + fileSummary.deletions) > this.maxInitialPatchLines
328
338
  ) {
329
- const guardedResponse = createCommitDiffResponse({
339
+ const guardedResponse = {
340
+ baseRef: meta.baseRef,
330
341
  commit: meta.commit,
331
342
  files: [{
332
343
  ...baseFile,
@@ -335,10 +346,12 @@ export class GitHistoryService {
335
346
  patchLineCount: fileSummary.additions + fileSummary.deletions,
336
347
  tooLarge: true,
337
348
  }],
349
+ isGitRepo: true,
338
350
  metaOnly: false,
339
351
  path: normalizedPath,
352
+ source: 'commit',
340
353
  summary,
341
- });
354
+ };
342
355
  return setCachedValue(this.commitCache, cacheKey, guardedResponse, this.responseCacheTtlMs);
343
356
  }
344
357
 
@@ -385,13 +398,16 @@ export class GitHistoryService {
385
398
  tooLarge: false,
386
399
  };
387
400
 
388
- const response = createCommitDiffResponse({
401
+ const response = {
402
+ baseRef: meta.baseRef,
389
403
  commit: meta.commit,
390
404
  files: [detailedFile],
405
+ isGitRepo: true,
391
406
  metaOnly: false,
392
407
  path: normalizedPath,
408
+ source: 'commit',
393
409
  summary,
394
- });
410
+ };
395
411
  return setCachedValue(this.commitCache, cacheKey, response, this.responseCacheTtlMs);
396
412
  });
397
413
  }
@@ -402,13 +418,13 @@ export class GitHistoryService {
402
418
  const normalizedPath = normalizeRelativeGitPath(path);
403
419
 
404
420
  if (!isGitRepo) {
405
- return createFileSnapshotResponse({
421
+ return {
406
422
  content: '',
407
423
  fileKind: getVaultFileKind(normalizedPath),
408
424
  hash: normalizedHash,
409
425
  isGitRepo: false,
410
426
  path: normalizedPath,
411
- });
427
+ };
412
428
  }
413
429
 
414
430
  if (isImageAttachmentFilePath(normalizedPath)) {
@@ -438,12 +454,53 @@ export class GitHistoryService {
438
454
  throw createGitRequestError(404, 'Commit file not found');
439
455
  }
440
456
 
441
- return setCachedValue(this.commitCache, cacheKey, createFileSnapshotResponse({
457
+ return setCachedValue(this.commitCache, cacheKey, {
442
458
  content,
443
459
  fileKind,
444
460
  hash: normalizedHash,
461
+ isGitRepo: true,
462
+ path: normalizedPath,
463
+ }, this.responseCacheTtlMs);
464
+ });
465
+ }
466
+
467
+ async getFileAttachment({ hash, path } = {}) {
468
+ const isGitRepo = await this.commandRunner.isGitRepo();
469
+ const normalizedHash = this.normalizeAttachmentRef(hash);
470
+ const normalizedPath = normalizeRelativeGitPath(path);
471
+
472
+ if (!isGitRepo) {
473
+ throw createGitRequestError(404, 'Git repository not found');
474
+ }
475
+
476
+ if (!isImageAttachmentFilePath(normalizedPath)) {
477
+ throw createGitRequestError(400, 'Unsupported image attachment path');
478
+ }
479
+
480
+ const cacheKey = JSON.stringify({
481
+ hash: normalizedHash,
482
+ path: normalizedPath,
483
+ type: 'file-attachment',
484
+ });
485
+ const cached = getCachedValue(this.commitCache, cacheKey);
486
+ if (cached) {
487
+ return cached;
488
+ }
489
+
490
+ return this.runRequest(cacheKey, async () => {
491
+ let content;
492
+ try {
493
+ content = await this.commandRunner.execGitBuffer(['cat-file', 'blob', `${normalizedHash}:${normalizedPath}`]);
494
+ } catch {
495
+ throw createGitRequestError(404, 'Commit image not found');
496
+ }
497
+
498
+ return setCachedValue(this.commitCache, cacheKey, {
499
+ content,
500
+ hash: normalizedHash,
501
+ mimeType: getImageMimeType(normalizedPath),
445
502
  path: normalizedPath,
446
- }), this.responseCacheTtlMs);
503
+ }, this.responseCacheTtlMs);
447
504
  });
448
505
  }
449
506
 
@@ -481,6 +538,14 @@ export class GitHistoryService {
481
538
  return normalizedHash;
482
539
  }
483
540
 
541
+ normalizeAttachmentRef(hash) {
542
+ const normalizedHash = String(hash ?? '').trim();
543
+ if (normalizedHash === 'HEAD') {
544
+ return normalizedHash;
545
+ }
546
+ return this.normalizeCommitHash(normalizedHash);
547
+ }
548
+
484
549
  parseHistoryChunk(chunk) {
485
550
  const [headerLine = '', ...statLines] = String(chunk ?? '')
486
551
  .split(/\r?\n/u)
@@ -575,6 +640,7 @@ export class GitHistoryService {
575
640
  authoredAt: chunk.authoredAt,
576
641
  deletions: Number(numstatEntry?.deletions || 0),
577
642
  filesChanged: 1,
643
+ fileKind: getVaultFileKind(nameStatusEntry?.path),
578
644
  hash: chunk.hash,
579
645
  isMergeCommit: chunk.isMergeCommit,
580
646
  oldPath: nameStatusEntry?.oldPath ?? null,
@@ -666,6 +732,7 @@ export class GitHistoryService {
666
732
  const numstatEntry = numstatEntries[index];
667
733
  return {
668
734
  ...entry,
735
+ fileKind: getVaultFileKind(entry.path),
669
736
  stats: numstatEntry
670
737
  ? {
671
738
  additions: numstatEntry.additions,
@@ -0,0 +1,14 @@
1
+ const IMAGE_MIME_TYPES = Object.freeze({
2
+ '.gif': 'image/gif',
3
+ '.jpeg': 'image/jpeg',
4
+ '.jpg': 'image/jpeg',
5
+ '.png': 'image/png',
6
+ '.svg': 'image/svg+xml',
7
+ '.webp': 'image/webp',
8
+ });
9
+
10
+ export function getImageMimeType(filePath) {
11
+ const normalizedPath = String(filePath ?? '').toLowerCase();
12
+ const extension = Object.keys(IMAGE_MIME_TYPES).find((candidate) => normalizedPath.endsWith(candidate));
13
+ return extension ? IMAGE_MIME_TYPES[extension] : 'application/octet-stream';
14
+ }
@@ -1,4 +1,4 @@
1
- import { createGitRequestError } from './errors.js';
1
+ import { createRequestError as createGitRequestError } from '../http/http-errors.js';
2
2
 
3
3
  export function decodeQuotedPath(pathValue) {
4
4
  const rawValue = String(pathValue ?? '').trim();
@@ -54,98 +54,3 @@ export function createEmptyStatusResponse() {
54
54
  summary: createEmptySummary(),
55
55
  };
56
56
  }
57
-
58
- export function createDiffResponse({
59
- files = [],
60
- isGitRepo = true,
61
- metaOnly = false,
62
- path = null,
63
- scope = 'working-tree',
64
- summary = {
65
- additions: 0,
66
- deletions: 0,
67
- filesChanged: 0,
68
- },
69
- } = {}) {
70
- return {
71
- files,
72
- isGitRepo,
73
- metaOnly,
74
- path,
75
- scope,
76
- summary,
77
- };
78
- }
79
-
80
- export function createHistoryResponse({
81
- commits = [],
82
- hasMore = false,
83
- isGitRepo = true,
84
- limit = 30,
85
- offset = 0,
86
- } = {}) {
87
- return {
88
- commits,
89
- hasMore,
90
- isGitRepo,
91
- limit,
92
- offset,
93
- };
94
- }
95
-
96
- export function createFileHistoryResponse({
97
- commits = [],
98
- hasMore = false,
99
- isGitRepo = true,
100
- limit = 30,
101
- offset = 0,
102
- path = null,
103
- } = {}) {
104
- return {
105
- commits,
106
- hasMore,
107
- isGitRepo,
108
- limit,
109
- offset,
110
- path,
111
- };
112
- }
113
-
114
- export function createCommitDiffResponse({
115
- commit = null,
116
- files = [],
117
- isGitRepo = true,
118
- metaOnly = false,
119
- path = null,
120
- summary = {
121
- additions: 0,
122
- deletions: 0,
123
- filesChanged: 0,
124
- },
125
- } = {}) {
126
- return {
127
- commit,
128
- files,
129
- isGitRepo,
130
- metaOnly,
131
- path,
132
- source: 'commit',
133
- summary,
134
- };
135
- }
136
-
137
- export function createFileSnapshotResponse({
138
- content = '',
139
- fileKind = null,
140
- hash = null,
141
- isGitRepo = true,
142
- path = null,
143
- } = {}) {
144
- return {
145
- content,
146
- fileKind,
147
- hash,
148
- isGitRepo,
149
- path,
150
- };
151
- }
@@ -2,11 +2,17 @@ import { createReadStream } from 'node:fs';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
 
5
+ import { getVaultFileKind, isImageAttachmentFilePath } from '../../../domain/file-kind.js';
5
6
  import { mapWithConcurrency } from '../../shared/async-utils.js';
6
7
  import { createEmptyStats } from './responses.js';
7
8
  import { splitContentLines } from './parsers.js';
9
+ import { getImageMimeType } from './image-mime.js';
8
10
 
9
11
  async function countFileLines(filePath) {
12
+ if (isImageAttachmentFilePath(filePath)) {
13
+ return 0;
14
+ }
15
+
10
16
  return new Promise((resolve, reject) => {
11
17
  const stream = createReadStream(filePath, { encoding: 'utf8' });
12
18
  let lineCount = 0;
@@ -36,6 +42,7 @@ function buildSyntheticAddedFileDiff(pathValue, content) {
36
42
  const normalizedContent = String(content ?? '').replace(/\r\n/g, '\n');
37
43
  const file = {
38
44
  code: 'U',
45
+ fileKind: getVaultFileKind(pathValue),
39
46
  hunks: [],
40
47
  isBinary: false,
41
48
  oldPath: null,
@@ -89,7 +96,27 @@ export class GitUntrackedFileService {
89
96
  async buildSyntheticDiffs(files = []) {
90
97
  const results = await mapWithConcurrency(files, 2, async (file) => {
91
98
  try {
92
- const content = await readFile(join(this.vaultDir, file.path), 'utf8');
99
+ const absolutePath = join(this.vaultDir, file.path);
100
+ if (isImageAttachmentFilePath(file.path)) {
101
+ const content = await readFile(absolutePath);
102
+ return {
103
+ ...file,
104
+ binaryMessage: 'Binary image file',
105
+ byteLength: content.byteLength,
106
+ code: file.code || 'U',
107
+ fileKind: 'image',
108
+ hunks: [],
109
+ isBinary: true,
110
+ mimeType: getImageMimeType(file.path),
111
+ oldPath: file.oldPath ?? null,
112
+ path: file.path,
113
+ stats: createEmptyStats(),
114
+ status: file.status || 'untracked',
115
+ synthetic: true,
116
+ };
117
+ }
118
+
119
+ const content = await readFile(absolutePath, 'utf8');
93
120
  return buildSyntheticAddedFileDiff(file.path, content);
94
121
  } catch {
95
122
  return null;
@@ -1,16 +1,11 @@
1
1
  import { createSign } from 'node:crypto';
2
2
 
3
+ import { createRequestError } from '../http/http-errors.js';
4
+
3
5
  function base64UrlJson(value) {
4
6
  return Buffer.from(JSON.stringify(value)).toString('base64url');
5
7
  }
6
8
 
7
- function createRequestError(statusCode, message, code) {
8
- const error = new Error(message);
9
- error.statusCode = statusCode;
10
- error.code = code;
11
- return error;
12
- }
13
-
14
9
  function normalizeBaseUrl(value) {
15
10
  return String(value ?? 'https://api.github.com').trim().replace(/\/+$/u, '') || 'https://api.github.com';
16
11
  }
@@ -80,7 +75,9 @@ export class GitHubAppClient {
80
75
 
81
76
  createAppJwt() {
82
77
  if (!this.isConfigured()) {
83
- throw createRequestError(503, 'GitHub App setup is not configured.', 'HOSTED_GITHUB_APP_NOT_CONFIGURED');
78
+ throw createRequestError(503, 'GitHub App setup is not configured.', {
79
+ code: 'HOSTED_GITHUB_APP_NOT_CONFIGURED',
80
+ });
84
81
  }
85
82
 
86
83
  return createGitHubAppJwt({
@@ -91,7 +88,9 @@ export class GitHubAppClient {
91
88
 
92
89
  createInstallationSetupUrl({ state } = {}) {
93
90
  if (!this.appSlug) {
94
- throw createRequestError(503, 'GitHub App slug is not configured.', 'HOSTED_GITHUB_APP_NOT_CONFIGURED');
91
+ throw createRequestError(503, 'GitHub App slug is not configured.', {
92
+ code: 'HOSTED_GITHUB_APP_NOT_CONFIGURED',
93
+ });
95
94
  }
96
95
 
97
96
  const setupUrl = new URL(`/apps/${encodeURIComponent(this.appSlug)}/installations/new`, this.htmlBaseUrl);
@@ -131,7 +130,7 @@ export class GitHubAppClient {
131
130
  throw createRequestError(
132
131
  response.status,
133
132
  payload?.message || 'GitHub API request failed.',
134
- 'HOSTED_GITHUB_API_FAILED',
133
+ { code: 'HOSTED_GITHUB_API_FAILED' },
135
134
  );
136
135
  }
137
136
 
@@ -141,7 +140,9 @@ export class GitHubAppClient {
141
140
  async createInstallationAccessToken(installationId, { appJwt = null } = {}) {
142
141
  const normalizedInstallationId = String(installationId ?? '').trim();
143
142
  if (!normalizedInstallationId) {
144
- throw createRequestError(400, 'GitHub installation id is required.', 'HOSTED_GITHUB_INSTALLATION_REQUIRED');
143
+ throw createRequestError(400, 'GitHub installation id is required.', {
144
+ code: 'HOSTED_GITHUB_INSTALLATION_REQUIRED',
145
+ });
145
146
  }
146
147
 
147
148
  const payload = await this.request(`/app/installations/${encodeURIComponent(normalizedInstallationId)}/access_tokens`, {
@@ -150,7 +151,9 @@ export class GitHubAppClient {
150
151
  });
151
152
  const token = String(payload?.token ?? '').trim();
152
153
  if (!token) {
153
- throw createRequestError(502, 'GitHub did not return an installation access token.', 'HOSTED_GITHUB_TOKEN_MISSING');
154
+ throw createRequestError(502, 'GitHub did not return an installation access token.', {
155
+ code: 'HOSTED_GITHUB_TOKEN_MISSING',
156
+ });
154
157
  }
155
158
  return token;
156
159
  }
@@ -158,7 +161,9 @@ export class GitHubAppClient {
158
161
  async getInstallation(installationId, { appJwt = null } = {}) {
159
162
  const normalizedInstallationId = String(installationId ?? '').trim();
160
163
  if (!normalizedInstallationId) {
161
- throw createRequestError(400, 'GitHub installation id is required.', 'HOSTED_GITHUB_INSTALLATION_REQUIRED');
164
+ throw createRequestError(400, 'GitHub installation id is required.', {
165
+ code: 'HOSTED_GITHUB_INSTALLATION_REQUIRED',
166
+ });
162
167
  }
163
168
 
164
169
  return this.request(`/app/installations/${encodeURIComponent(normalizedInstallationId)}`, {
@@ -176,7 +181,9 @@ export class GitHubAppClient {
176
181
  async resolveVaultSourceFromInstallation(installationId) {
177
182
  const normalizedInstallationId = String(installationId ?? '').trim();
178
183
  if (!normalizedInstallationId) {
179
- throw createRequestError(400, 'GitHub installation id is required.', 'HOSTED_GITHUB_INSTALLATION_REQUIRED');
184
+ throw createRequestError(400, 'GitHub installation id is required.', {
185
+ code: 'HOSTED_GITHUB_INSTALLATION_REQUIRED',
186
+ });
180
187
  }
181
188
 
182
189
  const appJwt = this.createAppJwt();
@@ -189,7 +196,7 @@ export class GitHubAppClient {
189
196
  throw createRequestError(
190
197
  409,
191
198
  'GitHub App installation must grant access to exactly one selected repository.',
192
- 'HOSTED_GITHUB_REPOSITORY_SELECTION_REQUIRED',
199
+ { code: 'HOSTED_GITHUB_REPOSITORY_SELECTION_REQUIRED' },
193
200
  );
194
201
  }
195
202
 
@@ -1,5 +1,12 @@
1
+ import { basename } from 'node:path';
2
+
1
3
  import { handleApiError } from './http-request-helpers.js';
2
- import { jsonResponse } from './http-response.js';
4
+ import {
5
+ createSafeAsciiFilename,
6
+ encodeContentDispositionFilename,
7
+ jsonResponse,
8
+ sendResponse,
9
+ } from './http-response.js';
3
10
 
4
11
  function resolveDiffScope(requestUrl) {
5
12
  const scope = String(requestUrl.searchParams.get('scope') || '').trim().toLowerCase();
@@ -106,6 +113,29 @@ export function createGitApiQueryHandler({ gitService }) {
106
113
  return true;
107
114
  }
108
115
 
116
+ if (requestUrl.pathname === '/api/git/file-attachment' && req.method === 'GET') {
117
+ try {
118
+ const attachment = await gitService.getFileAttachment({
119
+ hash: requestUrl.searchParams.get('hash'),
120
+ path: requestUrl.searchParams.get('path'),
121
+ });
122
+ const fileName = basename(String(attachment.path || 'image'));
123
+ sendResponse(req, res, {
124
+ body: attachment.content,
125
+ headers: {
126
+ 'Cache-Control': 'private, max-age=300, stale-while-revalidate=3600',
127
+ 'Content-Disposition': `inline; filename="${createSafeAsciiFilename(fileName)}"; filename*=UTF-8''${encodeContentDispositionFilename(fileName)}`,
128
+ 'Content-Type': attachment.mimeType || 'application/octet-stream',
129
+ 'X-Content-Type-Options': 'nosniff',
130
+ },
131
+ statusCode: 200,
132
+ });
133
+ } catch (error) {
134
+ handleApiError(req, res, error, '[api] Failed to read git file attachment:', 'Failed to read git file attachment');
135
+ }
136
+ return true;
137
+ }
138
+
109
139
  if (requestUrl.pathname === '/api/git/pull-backups' && req.method === 'GET') {
110
140
  try {
111
141
  jsonResponse(req, res, 200, {