@peaske7/readit 0.1.4 → 0.1.6

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 (322) hide show
  1. package/.agents/skills/remotion-best-practices/SKILL.md +61 -0
  2. package/.agents/skills/remotion-best-practices/rules/3d.md +86 -0
  3. package/.agents/skills/remotion-best-practices/rules/animations.md +27 -0
  4. package/.agents/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +178 -0
  5. package/.agents/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +100 -0
  6. package/.agents/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +108 -0
  7. package/.agents/skills/remotion-best-practices/rules/assets.md +78 -0
  8. package/.agents/skills/remotion-best-practices/rules/audio-visualization.md +198 -0
  9. package/.agents/skills/remotion-best-practices/rules/audio.md +169 -0
  10. package/.agents/skills/remotion-best-practices/rules/calculate-metadata.md +134 -0
  11. package/.agents/skills/remotion-best-practices/rules/can-decode.md +75 -0
  12. package/.agents/skills/remotion-best-practices/rules/charts.md +120 -0
  13. package/.agents/skills/remotion-best-practices/rules/compositions.md +154 -0
  14. package/.agents/skills/remotion-best-practices/rules/display-captions.md +184 -0
  15. package/.agents/skills/remotion-best-practices/rules/extract-frames.md +229 -0
  16. package/.agents/skills/remotion-best-practices/rules/ffmpeg.md +38 -0
  17. package/.agents/skills/remotion-best-practices/rules/fonts.md +152 -0
  18. package/.agents/skills/remotion-best-practices/rules/get-audio-duration.md +58 -0
  19. package/.agents/skills/remotion-best-practices/rules/get-video-dimensions.md +68 -0
  20. package/.agents/skills/remotion-best-practices/rules/get-video-duration.md +60 -0
  21. package/.agents/skills/remotion-best-practices/rules/gifs.md +141 -0
  22. package/.agents/skills/remotion-best-practices/rules/images.md +134 -0
  23. package/.agents/skills/remotion-best-practices/rules/import-srt-captions.md +69 -0
  24. package/.agents/skills/remotion-best-practices/rules/light-leaks.md +73 -0
  25. package/.agents/skills/remotion-best-practices/rules/lottie.md +70 -0
  26. package/.agents/skills/remotion-best-practices/rules/maps.md +412 -0
  27. package/.agents/skills/remotion-best-practices/rules/measuring-dom-nodes.md +34 -0
  28. package/.agents/skills/remotion-best-practices/rules/measuring-text.md +140 -0
  29. package/.agents/skills/remotion-best-practices/rules/parameters.md +109 -0
  30. package/.agents/skills/remotion-best-practices/rules/sequencing.md +118 -0
  31. package/.agents/skills/remotion-best-practices/rules/sfx.md +26 -0
  32. package/.agents/skills/remotion-best-practices/rules/subtitles.md +36 -0
  33. package/.agents/skills/remotion-best-practices/rules/tailwind.md +11 -0
  34. package/.agents/skills/remotion-best-practices/rules/text-animations.md +20 -0
  35. package/.agents/skills/remotion-best-practices/rules/timing.md +179 -0
  36. package/.agents/skills/remotion-best-practices/rules/transcribe-captions.md +70 -0
  37. package/.agents/skills/remotion-best-practices/rules/transitions.md +197 -0
  38. package/.agents/skills/remotion-best-practices/rules/transparent-videos.md +106 -0
  39. package/.agents/skills/remotion-best-practices/rules/trimming.md +51 -0
  40. package/.agents/skills/remotion-best-practices/rules/videos.md +171 -0
  41. package/.agents/skills/remotion-best-practices/rules/voiceover.md +99 -0
  42. package/.agents/skills/simple/SKILL.md +52 -0
  43. package/.agents/skills/vercel-react-best-practices/AGENTS.md +3254 -0
  44. package/.agents/skills/vercel-react-best-practices/README.md +123 -0
  45. package/.agents/skills/vercel-react-best-practices/SKILL.md +141 -0
  46. package/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  47. package/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
  48. package/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
  49. package/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
  50. package/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
  51. package/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
  52. package/.agents/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
  53. package/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
  54. package/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
  55. package/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
  56. package/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
  57. package/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  58. package/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
  59. package/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
  60. package/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
  61. package/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
  62. package/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
  63. package/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
  64. package/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
  65. package/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
  66. package/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
  67. package/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
  68. package/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
  69. package/.agents/skills/vercel-react-best-practices/rules/js-flatmap-filter.md +60 -0
  70. package/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
  71. package/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
  72. package/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
  73. package/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
  74. package/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
  75. package/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
  76. package/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
  77. package/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  78. package/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
  79. package/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
  80. package/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  81. package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  82. package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
  83. package/.agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md +85 -0
  84. package/.agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md +68 -0
  85. package/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
  86. package/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
  87. package/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
  88. package/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
  89. package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
  90. package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
  91. package/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
  92. package/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  93. package/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
  94. package/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
  95. package/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
  96. package/.agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md +82 -0
  97. package/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
  98. package/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
  99. package/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
  100. package/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
  101. package/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
  102. package/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
  103. package/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
  104. package/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
  105. package/.agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md +142 -0
  106. package/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
  107. package/.agents/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
  108. package/.claude/CLAUDE.md +142 -0
  109. package/.claude/commands/review.md +120 -0
  110. package/.claude/commands/sync-docs.md +71 -0
  111. package/.claude/roadmap.md +98 -0
  112. package/.claude/rules/style-guide.md +830 -0
  113. package/.claude/settings.json +18 -0
  114. package/.claude/user-stories.md +248 -0
  115. package/AGENTS.md +64 -0
  116. package/README.md +7 -7
  117. package/biome.json +69 -0
  118. package/bun.lock +1124 -0
  119. package/docs/design.md +563 -0
  120. package/docs/plans/2026-03-13-client-mode-design.md +86 -0
  121. package/docs/plans/2026-03-13-client-mode-plan.md +605 -0
  122. package/docs/plans/2026-03-13-keyboard-shortcuts-design.md +129 -0
  123. package/docs/plans/2026-03-13-keyboard-shortcuts-plan.md +1471 -0
  124. package/docs/plans/2026-03-13-multi-document-design.md +183 -0
  125. package/docs/plans/2026-03-13-performance-benchmarks-design.md +121 -0
  126. package/e2e/comments.spec.ts +125 -0
  127. package/e2e/document-load.spec.ts +54 -0
  128. package/e2e/export.spec.ts +58 -0
  129. package/e2e/fixtures/sample.html +13 -0
  130. package/e2e/fixtures/sample.md +7 -0
  131. package/e2e/persistence-file.spec.ts +342 -0
  132. package/e2e/utils/cli.ts +84 -0
  133. package/e2e/utils/selection.ts +135 -0
  134. package/{dist/index.html → index.html} +8 -2
  135. package/lefthook.yml +8 -0
  136. package/package.json +17 -39
  137. package/playwright.config.ts +22 -0
  138. package/skills-lock.json +20 -0
  139. package/src/App.tsx +396 -0
  140. package/src/cli/index.ts +467 -0
  141. package/src/components/ActionsMenu.tsx +110 -0
  142. package/src/components/DocumentViewer/CodeBlock.tsx +83 -0
  143. package/src/components/DocumentViewer/DocumentViewer.tsx +257 -0
  144. package/src/components/DocumentViewer/IframeContainer.tsx +251 -0
  145. package/src/components/DocumentViewer/MermaidDiagram.tsx +137 -0
  146. package/src/components/DocumentViewer/index.ts +1 -0
  147. package/src/components/FloatingTOC.tsx +59 -0
  148. package/src/components/Header.tsx +63 -0
  149. package/src/components/InlineEditor.tsx +72 -0
  150. package/src/components/MarginNote.tsx +198 -0
  151. package/src/components/MarginNotes.tsx +50 -0
  152. package/src/components/RawModal.tsx +141 -0
  153. package/src/components/ReanchorConfirm.tsx +33 -0
  154. package/src/components/SettingsModal.tsx +221 -0
  155. package/src/components/ShortcutCapture.tsx +45 -0
  156. package/src/components/ShortcutList.tsx +157 -0
  157. package/src/components/TabBar.tsx +60 -0
  158. package/src/components/TableOfContents.tsx +108 -0
  159. package/src/components/comments/CommentBadge.tsx +43 -0
  160. package/src/components/comments/CommentInput.tsx +119 -0
  161. package/src/components/comments/CommentListItem.tsx +82 -0
  162. package/src/components/comments/CommentManager.tsx +106 -0
  163. package/src/components/comments/CommentMinimap.tsx +62 -0
  164. package/src/components/comments/CommentNav.tsx +104 -0
  165. package/src/components/ui/ActionBar.tsx +16 -0
  166. package/src/components/ui/ActionLink.tsx +32 -0
  167. package/src/components/ui/Button.tsx +55 -0
  168. package/src/components/ui/Dialog.tsx +156 -0
  169. package/src/components/ui/DropdownMenu.tsx +114 -0
  170. package/src/components/ui/SeparatorDot.tsx +9 -0
  171. package/src/components/ui/Text.tsx +54 -0
  172. package/src/contexts/CommentContext.tsx +222 -0
  173. package/src/contexts/LayoutContext.tsx +76 -0
  174. package/src/hooks/useClickOutside.ts +35 -0
  175. package/src/hooks/useClipboard.ts +79 -0
  176. package/src/hooks/useCommentNavigation.ts +130 -0
  177. package/src/hooks/useComments.ts +323 -0
  178. package/src/hooks/useDocument.ts +141 -0
  179. package/src/hooks/useFontPreference.ts +76 -0
  180. package/src/hooks/useHeadings.test.ts +159 -0
  181. package/src/hooks/useHeadings.ts +129 -0
  182. package/src/hooks/useKeybindings.ts +120 -0
  183. package/src/hooks/useKeyboardShortcuts.ts +63 -0
  184. package/src/hooks/useLayoutMode.ts +44 -0
  185. package/src/hooks/useReanchorMode.ts +33 -0
  186. package/src/hooks/useScrollMetrics.ts +56 -0
  187. package/src/hooks/useScrollSpy.ts +81 -0
  188. package/src/hooks/useTextSelection.ts +123 -0
  189. package/src/hooks/useThemePreference.ts +66 -0
  190. package/src/index.css +823 -0
  191. package/src/lib/__fixtures__/bench-data.ts +167 -0
  192. package/src/lib/anchor.bench.ts +112 -0
  193. package/src/lib/anchor.test.ts +531 -0
  194. package/src/lib/anchor.ts +465 -0
  195. package/src/lib/comment-storage.bench.ts +63 -0
  196. package/src/lib/comment-storage.test.ts +624 -0
  197. package/src/lib/comment-storage.ts +263 -0
  198. package/src/lib/context.bench.ts +41 -0
  199. package/src/lib/context.test.ts +224 -0
  200. package/src/lib/context.ts +193 -0
  201. package/src/lib/export.bench.ts +35 -0
  202. package/src/lib/export.ts +43 -0
  203. package/src/lib/highlight/colors.ts +37 -0
  204. package/src/lib/highlight/core.test.ts +98 -0
  205. package/src/lib/highlight/core.ts +54 -0
  206. package/src/lib/highlight/dom.ts +342 -0
  207. package/src/lib/highlight/highlighter.ts +427 -0
  208. package/src/lib/highlight/index.ts +23 -0
  209. package/src/lib/highlight/script-builder.ts +485 -0
  210. package/src/lib/highlight/types.ts +57 -0
  211. package/src/lib/html-processor.test.tsx +170 -0
  212. package/src/lib/html-processor.tsx +95 -0
  213. package/src/lib/layout-constants.ts +12 -0
  214. package/src/lib/margin-layout.bench.ts +28 -0
  215. package/src/lib/margin-layout.ts +100 -0
  216. package/src/lib/scroll.test.ts +118 -0
  217. package/src/lib/scroll.ts +47 -0
  218. package/src/lib/shortcut-registry.test.ts +173 -0
  219. package/src/lib/shortcut-registry.ts +209 -0
  220. package/src/lib/utils.test.ts +110 -0
  221. package/src/lib/utils.ts +61 -0
  222. package/src/main.tsx +10 -0
  223. package/src/server/index.ts +883 -0
  224. package/src/store/index.test.ts +220 -0
  225. package/src/store/index.ts +234 -0
  226. package/src/test-setup.ts +1 -0
  227. package/src/types/index.ts +115 -0
  228. package/test.md +74 -0
  229. package/tsconfig.cli.json +12 -0
  230. package/tsconfig.json +20 -0
  231. package/vite.config.ts +19 -0
  232. package/vitest.config.ts +15 -0
  233. package/dist/assets/_basePickBy-hOr-yGsE.js +0 -1
  234. package/dist/assets/_baseUniq-b7bzdUSn.js +0 -1
  235. package/dist/assets/arc-D65wG9gm.js +0 -1
  236. package/dist/assets/architecture-PBZL5I3N-DBa6CAv_.js +0 -1
  237. package/dist/assets/architectureDiagram-2XIMDMQ5-Djwpsh98.js +0 -36
  238. package/dist/assets/array-DOVTz2Mq.js +0 -1
  239. package/dist/assets/blockDiagram-WCTKOSBZ-BdW5TTxj.js +0 -132
  240. package/dist/assets/c4Diagram-IC4MRINW-DTmkHEXu.js +0 -10
  241. package/dist/assets/channel-B3MUFipN.js +0 -1
  242. package/dist/assets/chunk-4BX2VUAB-DEqzsvDc.js +0 -1
  243. package/dist/assets/chunk-55IACEB6-BzVuSUV8.js +0 -1
  244. package/dist/assets/chunk-7E7YKBS2-CZ8IcA4c.js +0 -1
  245. package/dist/assets/chunk-7R4GIKGN-CWVVC8HX.js +0 -79
  246. package/dist/assets/chunk-C72U2L5F-B1Tso5TH.js +0 -1
  247. package/dist/assets/chunk-EGIJ26TM-Cx_7CFik.js +0 -1
  248. package/dist/assets/chunk-FMBD7UC4-Cfk_iGhv.js +0 -15
  249. package/dist/assets/chunk-GEFDOKGD-C_5hRbJt.js +0 -2
  250. package/dist/assets/chunk-GLR3WWYH-CkY7IyBj.js +0 -2
  251. package/dist/assets/chunk-HHEYEP7N-B0I4X5cr.js +0 -1
  252. package/dist/assets/chunk-JSJVCQXG-CAjwlVLg.js +0 -1
  253. package/dist/assets/chunk-KX2RTZJC-DWqnZZ02.js +0 -1
  254. package/dist/assets/chunk-KYZI473N-gjRVhJgJ.js +0 -53
  255. package/dist/assets/chunk-L3YUKLVL-D7C9GuxL.js +0 -1
  256. package/dist/assets/chunk-MX3YWQON-i-77iuVj.js +0 -1
  257. package/dist/assets/chunk-NQ4KR5QH-B22Pvemm.js +0 -220
  258. package/dist/assets/chunk-O4XLMI2P-ZQd5L6ZD.js +0 -7
  259. package/dist/assets/chunk-OZEHJAEY-BaPKTELw.js +0 -1
  260. package/dist/assets/chunk-PQ6SQG4A-DqE1eupT.js +0 -1
  261. package/dist/assets/chunk-PU5JKC2W-BTqWqedh.js +0 -70
  262. package/dist/assets/chunk-QZHKN3VN-Nm9TvMss.js +0 -1
  263. package/dist/assets/chunk-R5LLSJPH-DkiNs1dN.js +0 -1
  264. package/dist/assets/chunk-WL4C6EOR-CioD2fv2.js +0 -189
  265. package/dist/assets/chunk-XIRO2GV7-B4GGQONY.js +0 -1
  266. package/dist/assets/chunk-XPW4576I-C0IbbQos.js +0 -32
  267. package/dist/assets/chunk-XZSTWKYB-DMOqFWmT.js +0 -94
  268. package/dist/assets/chunk-YBOYWFTD-CoeQgeVY.js +0 -1
  269. package/dist/assets/classDiagram-VBA2DB6C-DV9ltQ7h.js +0 -1
  270. package/dist/assets/classDiagram-v2-RAHNMMFH-C6nD9wmM.js +0 -1
  271. package/dist/assets/clone-DuY6BQEm.js +0 -1
  272. package/dist/assets/cose-bilkent-S5V4N54A-B6FexK6p.js +0 -1
  273. package/dist/assets/cytoscape.esm-DoTFyJaN.js +0 -321
  274. package/dist/assets/dagre-CCcocoCU.js +0 -1
  275. package/dist/assets/dagre-KLK3FWXG-DIELowj9.js +0 -4
  276. package/dist/assets/defaultLocale-Ck2Xxk-C.js +0 -1
  277. package/dist/assets/diagram-E7M64L7V-D1mm0PoO.js +0 -24
  278. package/dist/assets/diagram-IFDJBPK2-7DVjly8y.js +0 -43
  279. package/dist/assets/diagram-P4PSJMXO-jO7pfyMb.js +0 -24
  280. package/dist/assets/dist-BywRdrPx.js +0 -1
  281. package/dist/assets/erDiagram-INFDFZHY-DSRxlRFy.js +0 -70
  282. package/dist/assets/flowDiagram-PKNHOUZH-CgKzzNdR.js +0 -162
  283. package/dist/assets/ganttDiagram-A5KZAMGK-CtsE7Y4E.js +0 -292
  284. package/dist/assets/gitGraph-HDMCJU4V-BU9uhwtz.js +0 -1
  285. package/dist/assets/gitGraphDiagram-K3NZZRJ6-DOU8RGdw.js +0 -65
  286. package/dist/assets/graphlib-WkJoBgka.js +0 -1
  287. package/dist/assets/index-CKVArt9D.js +0 -562
  288. package/dist/assets/index-DzRKJazf.css +0 -2
  289. package/dist/assets/info-3K5VOQVL-CPpvM-SG.js +0 -1
  290. package/dist/assets/infoDiagram-LFFYTUFH-VKLs5DsF.js +0 -2
  291. package/dist/assets/init-Bft5Ffpj.js +0 -1
  292. package/dist/assets/isArrayLikeObject-icl0H0jo.js +0 -1
  293. package/dist/assets/isEmpty-Du8sNmkE.js +0 -1
  294. package/dist/assets/ishikawaDiagram-PHBUUO56-CsWvEjux.js +0 -70
  295. package/dist/assets/journeyDiagram-4ABVD52K-BzJGTdIT.js +0 -139
  296. package/dist/assets/kanban-definition-K7BYSVSG-B_9ClJ1A.js +0 -89
  297. package/dist/assets/katex-BJrMXEjr.js +0 -261
  298. package/dist/assets/line-CC_tDGId.js +0 -1
  299. package/dist/assets/linear-Cts_d04Y.js +0 -1
  300. package/dist/assets/math-CNhlSIO3.js +0 -1
  301. package/dist/assets/mermaid-parser.core-Vb9KKv1R.js +0 -4
  302. package/dist/assets/mermaid.core-C_7xsp3d.js +0 -11
  303. package/dist/assets/mindmap-definition-YRQLILUH-BWmfy5wB.js +0 -68
  304. package/dist/assets/ordinal-DIg8h6NI.js +0 -1
  305. package/dist/assets/packet-RMMSAZCW-Q-WG6o3b.js +0 -1
  306. package/dist/assets/path-DfRbCp9y.js +0 -1
  307. package/dist/assets/pie-UPGHQEXC-Cwi2tLlt.js +0 -1
  308. package/dist/assets/pieDiagram-SKSYHLDU-Dyf3X_in.js +0 -30
  309. package/dist/assets/quadrantDiagram-337W2JSQ-B5_5m61Q.js +0 -7
  310. package/dist/assets/radar-KQ55EAFF-Dtw2VzxY.js +0 -1
  311. package/dist/assets/requirementDiagram-Z7DCOOCP-BSERBnlW.js +0 -73
  312. package/dist/assets/rough.esm-KjoEK0it.js +0 -1
  313. package/dist/assets/sankeyDiagram-WA2Y5GQK-CMcEY8Cz.js +0 -10
  314. package/dist/assets/sequenceDiagram-2WXFIKYE-D28qcXwC.js +0 -145
  315. package/dist/assets/src-C8kkzlHX.js +0 -1
  316. package/dist/assets/stateDiagram-RAJIS63D-7oVrCmRl.js +0 -1
  317. package/dist/assets/stateDiagram-v2-FVOUBMTO-DtFptQAd.js +0 -1
  318. package/dist/assets/timeline-definition-YZTLITO2-rbCfBEvG.js +0 -61
  319. package/dist/assets/treemap-KZPCXAKY-BlRvF0um.js +0 -1
  320. package/dist/assets/vennDiagram-LZ73GAT5-DBit3zWa.js +0 -34
  321. package/dist/assets/xychartDiagram-JWTSCODW-BVYXv51y.js +0 -7
  322. package/dist/index.js +0 -1040
@@ -0,0 +1,883 @@
1
+ import { type FSWatcher, watch } from "node:fs";
2
+ import * as fs from "node:fs/promises";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { basename, dirname, join } from "node:path";
6
+ import { findAnchorWithFallback } from "../lib/anchor.js";
7
+ import {
8
+ computeHash,
9
+ createComment,
10
+ getCommentPath,
11
+ getLineHint,
12
+ parseCommentFile,
13
+ serializeComments,
14
+ truncateSelection,
15
+ } from "../lib/comment-storage.js";
16
+ import { getFileType } from "../lib/utils.js";
17
+ import {
18
+ AnchorConfidences,
19
+ type Comment,
20
+ type DocumentSettings,
21
+ type DocumentType,
22
+ FontFamilies,
23
+ type FontFamily,
24
+ } from "../types/index.js";
25
+
26
+ // ─── Helpers ─────────────────────────────────────────────────────────
27
+
28
+ function isErrnoException(err: unknown): err is NodeJS.ErrnoException {
29
+ return err instanceof Error && "code" in err;
30
+ }
31
+
32
+ export interface FileEntry {
33
+ content: string;
34
+ type: DocumentType;
35
+ filePath: string;
36
+ }
37
+
38
+ export interface ServerOptions {
39
+ files: FileEntry[];
40
+ port: number;
41
+ host: string;
42
+ clean?: boolean;
43
+ }
44
+
45
+ export interface ServerResult {
46
+ port: number;
47
+ url: string;
48
+ server: { stop(): void };
49
+ }
50
+
51
+ async function readCommentsFromFile(
52
+ filePath: string,
53
+ sourceContent: string,
54
+ ): Promise<Comment[]> {
55
+ const commentPath = getCommentPath(filePath);
56
+
57
+ try {
58
+ const content = await fs.readFile(commentPath, "utf-8");
59
+ const file = parseCommentFile(content);
60
+
61
+ return file.comments.map((comment) => {
62
+ const textForMatching = comment.anchorPrefix || comment.selectedText;
63
+ const anchor = findAnchorWithFallback({
64
+ source: sourceContent,
65
+ selectedText: textForMatching,
66
+ lineHint: comment.lineHint || "L1",
67
+ });
68
+
69
+ if (anchor) {
70
+ return {
71
+ ...comment,
72
+ startOffset: anchor.start,
73
+ endOffset: anchor.end,
74
+ lineHint: `L${anchor.line}`,
75
+ anchorConfidence: anchor.confidence,
76
+ };
77
+ }
78
+
79
+ return {
80
+ ...comment,
81
+ anchorConfidence: AnchorConfidences.UNRESOLVED,
82
+ };
83
+ });
84
+ } catch (err) {
85
+ if (isErrnoException(err) && err.code === "ENOENT") {
86
+ return [];
87
+ }
88
+ throw err;
89
+ }
90
+ }
91
+
92
+ async function writeCommentsToFile(
93
+ filePath: string,
94
+ sourceContent: string,
95
+ comments: Comment[],
96
+ ): Promise<void> {
97
+ const commentPath = getCommentPath(filePath);
98
+ const commentDir = dirname(commentPath);
99
+
100
+ await fs.mkdir(commentDir, { recursive: true });
101
+
102
+ const file = {
103
+ source: filePath,
104
+ hash: computeHash(sourceContent),
105
+ version: 1,
106
+ comments,
107
+ };
108
+
109
+ const content = serializeComments(file);
110
+ const tempPath = `${commentPath}.tmp`;
111
+ await fs.writeFile(tempPath, content, "utf-8");
112
+ await fs.rename(tempPath, commentPath);
113
+ }
114
+
115
+ async function deleteCommentFile(filePath: string): Promise<void> {
116
+ const commentPath = getCommentPath(filePath);
117
+ try {
118
+ await fs.unlink(commentPath);
119
+ } catch (err) {
120
+ if (!isErrnoException(err) || err.code !== "ENOENT") {
121
+ throw err;
122
+ }
123
+ }
124
+ }
125
+
126
+ function getSettingsPath(sourcePath: string): string {
127
+ const absolute = path.resolve(sourcePath);
128
+ const normalized = absolute.replace(/^\//, "").replace(/^[A-Z]:[\\/]/, "");
129
+ return path.join(
130
+ os.homedir(),
131
+ ".readit",
132
+ "settings",
133
+ `${normalized}.settings.json`,
134
+ );
135
+ }
136
+
137
+ const DEFAULT_SETTINGS: DocumentSettings = {
138
+ version: 1,
139
+ fontFamily: FontFamilies.SERIF,
140
+ };
141
+
142
+ async function readSettingsFromFile(
143
+ filePath: string,
144
+ ): Promise<DocumentSettings> {
145
+ const settingsPath = getSettingsPath(filePath);
146
+ try {
147
+ const content = await fs.readFile(settingsPath, "utf-8");
148
+ return JSON.parse(content) as DocumentSettings;
149
+ } catch (err) {
150
+ if (isErrnoException(err) && err.code === "ENOENT") {
151
+ return DEFAULT_SETTINGS;
152
+ }
153
+ throw err;
154
+ }
155
+ }
156
+
157
+ async function writeSettingsToFile(
158
+ filePath: string,
159
+ settings: DocumentSettings,
160
+ ): Promise<void> {
161
+ const settingsPath = getSettingsPath(filePath);
162
+ const settingsDir = dirname(settingsPath);
163
+
164
+ await fs.mkdir(settingsDir, { recursive: true });
165
+
166
+ const tempPath = `${settingsPath}.tmp`;
167
+ await fs.writeFile(tempPath, JSON.stringify(settings, null, 2), "utf-8");
168
+ await fs.rename(tempPath, settingsPath);
169
+ }
170
+
171
+ function isValidFontFamily(value: unknown): value is FontFamily {
172
+ return value === FontFamilies.SERIF || value === FontFamilies.SANS_SERIF;
173
+ }
174
+
175
+ // ─── PID file helpers ───────────────────────────────────────────────
176
+
177
+ export const SERVER_INFO_PATH = path.join(
178
+ os.homedir(),
179
+ ".readit",
180
+ "server.json",
181
+ );
182
+
183
+ async function writeServerInfo(port: number): Promise<void> {
184
+ await fs.mkdir(path.dirname(SERVER_INFO_PATH), { recursive: true });
185
+ await fs.writeFile(
186
+ SERVER_INFO_PATH,
187
+ JSON.stringify({ port, pid: process.pid }),
188
+ "utf-8",
189
+ );
190
+ }
191
+
192
+ export async function removeServerInfo(): Promise<void> {
193
+ try {
194
+ await fs.unlink(SERVER_INFO_PATH);
195
+ } catch (err) {
196
+ if (!isErrnoException(err) || err.code !== "ENOENT") {
197
+ console.error("Failed to remove server info:", err);
198
+ }
199
+ }
200
+ }
201
+
202
+ // ─── Response helpers ───────────────────────────────────────────────
203
+
204
+ function json(data: unknown, status = 200): Response {
205
+ return Response.json(data, { status });
206
+ }
207
+
208
+ function errorResponse(message: string, status: number): Response {
209
+ return Response.json({ error: message }, { status });
210
+ }
211
+
212
+ // ─── Route context ──────────────────────────────────────────────────
213
+
214
+ interface RouteContext {
215
+ filePath: string;
216
+ getCurrentContent: () => string;
217
+ }
218
+
219
+ // ─── Route handlers ─────────────────────────────────────────────────
220
+
221
+ async function getComments(ctx: RouteContext): Promise<Response> {
222
+ try {
223
+ const comments = await readCommentsFromFile(
224
+ ctx.filePath,
225
+ ctx.getCurrentContent(),
226
+ );
227
+ return json({ comments });
228
+ } catch (err) {
229
+ console.error("Failed to read comments:", err);
230
+ return errorResponse("Failed to read comments", 500);
231
+ }
232
+ }
233
+
234
+ async function addComment(ctx: RouteContext, req: Request): Promise<Response> {
235
+ try {
236
+ const {
237
+ selectedText,
238
+ comment: commentText,
239
+ startOffset,
240
+ endOffset,
241
+ } = await req.json();
242
+
243
+ if (
244
+ !selectedText ||
245
+ typeof commentText !== "string" ||
246
+ startOffset === undefined ||
247
+ endOffset === undefined
248
+ ) {
249
+ return errorResponse("Missing required fields", 400);
250
+ }
251
+
252
+ const currentContent = ctx.getCurrentContent();
253
+ const newComment = createComment(
254
+ selectedText,
255
+ commentText,
256
+ startOffset,
257
+ endOffset,
258
+ currentContent,
259
+ );
260
+
261
+ const existingComments = await readCommentsFromFile(
262
+ ctx.filePath,
263
+ currentContent,
264
+ );
265
+ const allComments = [...existingComments, newComment];
266
+
267
+ await writeCommentsToFile(ctx.filePath, currentContent, allComments);
268
+
269
+ return json({ comment: newComment }, 201);
270
+ } catch (err) {
271
+ console.error("Failed to add comment:", err);
272
+ return errorResponse("Failed to add comment", 500);
273
+ }
274
+ }
275
+
276
+ async function updateComment(
277
+ ctx: RouteContext,
278
+ req: Request,
279
+ id: string,
280
+ ): Promise<Response> {
281
+ try {
282
+ const { comment: commentText } = await req.json();
283
+
284
+ if (typeof commentText !== "string") {
285
+ return errorResponse("Missing comment text", 400);
286
+ }
287
+
288
+ const currentContent = ctx.getCurrentContent();
289
+ const existingComments = await readCommentsFromFile(
290
+ ctx.filePath,
291
+ currentContent,
292
+ );
293
+ const commentIndex = existingComments.findIndex((c) => c.id === id);
294
+
295
+ if (commentIndex === -1) {
296
+ return errorResponse("Comment not found", 404);
297
+ }
298
+
299
+ const updatedComments = existingComments.map((c, i) =>
300
+ i === commentIndex ? { ...c, comment: commentText.trim() } : c,
301
+ );
302
+
303
+ await writeCommentsToFile(ctx.filePath, currentContent, updatedComments);
304
+
305
+ return json({ comment: updatedComments[commentIndex] });
306
+ } catch (err) {
307
+ console.error("Failed to update comment:", err);
308
+ return errorResponse("Failed to update comment", 500);
309
+ }
310
+ }
311
+
312
+ async function deleteComment(ctx: RouteContext, id: string): Promise<Response> {
313
+ try {
314
+ const currentContent = ctx.getCurrentContent();
315
+ const existingComments = await readCommentsFromFile(
316
+ ctx.filePath,
317
+ currentContent,
318
+ );
319
+ const filteredComments = existingComments.filter((c) => c.id !== id);
320
+
321
+ if (filteredComments.length === existingComments.length) {
322
+ return errorResponse("Comment not found", 404);
323
+ }
324
+
325
+ if (filteredComments.length === 0) {
326
+ await deleteCommentFile(ctx.filePath);
327
+ } else {
328
+ await writeCommentsToFile(ctx.filePath, currentContent, filteredComments);
329
+ }
330
+
331
+ return json({ success: true });
332
+ } catch (err) {
333
+ console.error("Failed to delete comment:", err);
334
+ return errorResponse("Failed to delete comment", 500);
335
+ }
336
+ }
337
+
338
+ async function clearComments(ctx: RouteContext): Promise<Response> {
339
+ try {
340
+ await deleteCommentFile(ctx.filePath);
341
+ return json({ success: true });
342
+ } catch (err) {
343
+ console.error("Failed to clear comments:", err);
344
+ return errorResponse("Failed to clear comments", 500);
345
+ }
346
+ }
347
+
348
+ async function getRawComments(ctx: RouteContext): Promise<Response> {
349
+ const commentPath = getCommentPath(ctx.filePath);
350
+ try {
351
+ const content = await fs.readFile(commentPath, "utf-8");
352
+ return json({ content, path: commentPath });
353
+ } catch (err) {
354
+ if (isErrnoException(err) && err.code === "ENOENT") {
355
+ return json({ content: null, path: commentPath });
356
+ }
357
+ console.error("Failed to read raw comments:", err);
358
+ return errorResponse("Failed to read raw comments", 500);
359
+ }
360
+ }
361
+
362
+ async function reanchorComment(
363
+ ctx: RouteContext,
364
+ req: Request,
365
+ id: string,
366
+ ): Promise<Response> {
367
+ try {
368
+ const { selectedText, startOffset, endOffset } = await req.json();
369
+
370
+ if (!selectedText || startOffset === undefined || endOffset === undefined) {
371
+ return errorResponse("Missing required fields", 400);
372
+ }
373
+
374
+ const currentContent = ctx.getCurrentContent();
375
+ const existingComments = await readCommentsFromFile(
376
+ ctx.filePath,
377
+ currentContent,
378
+ );
379
+ const commentIndex = existingComments.findIndex((c) => c.id === id);
380
+
381
+ if (commentIndex === -1) {
382
+ return errorResponse("Comment not found", 404);
383
+ }
384
+
385
+ const lineHint = getLineHint(currentContent, startOffset, endOffset);
386
+ const truncatedText = truncateSelection(selectedText);
387
+
388
+ const updatedComment: Comment = {
389
+ ...existingComments[commentIndex],
390
+ selectedText: truncatedText,
391
+ startOffset,
392
+ endOffset,
393
+ lineHint,
394
+ anchorConfidence: AnchorConfidences.EXACT,
395
+ anchorPrefix:
396
+ selectedText.length > 1000 ? selectedText.slice(0, 200) : undefined,
397
+ };
398
+
399
+ const updatedComments = existingComments.map((c, i) =>
400
+ i === commentIndex ? updatedComment : c,
401
+ );
402
+
403
+ await writeCommentsToFile(ctx.filePath, currentContent, updatedComments);
404
+
405
+ return json({ comment: updatedComment });
406
+ } catch (err) {
407
+ console.error("Failed to re-anchor comment:", err);
408
+ return errorResponse("Failed to re-anchor comment", 500);
409
+ }
410
+ }
411
+
412
+ async function getSettings(ctx: RouteContext): Promise<Response> {
413
+ try {
414
+ const settings = await readSettingsFromFile(ctx.filePath);
415
+ return json(settings);
416
+ } catch (err) {
417
+ console.error("Failed to read settings:", err);
418
+ return errorResponse("Failed to read settings", 500);
419
+ }
420
+ }
421
+
422
+ async function updateSettings(
423
+ ctx: RouteContext,
424
+ req: Request,
425
+ ): Promise<Response> {
426
+ try {
427
+ const body = await req.json();
428
+ const { fontFamily, keybindings } = body;
429
+
430
+ if (fontFamily !== undefined && !isValidFontFamily(fontFamily)) {
431
+ return errorResponse("Invalid font family", 400);
432
+ }
433
+
434
+ // Read current settings and merge
435
+ const current = await readSettingsFromFile(ctx.filePath);
436
+ const settings: DocumentSettings = {
437
+ ...current,
438
+ ...(fontFamily !== undefined && { fontFamily }),
439
+ ...(keybindings !== undefined && { keybindings }),
440
+ };
441
+
442
+ await writeSettingsToFile(ctx.filePath, settings);
443
+ return json(settings);
444
+ } catch (err) {
445
+ console.error("Failed to save settings:", err);
446
+ return errorResponse("Failed to save settings", 500);
447
+ }
448
+ }
449
+
450
+ // ─── SSE helpers ────────────────────────────────────────────────────
451
+
452
+ function createDocumentStream(
453
+ sseClients: Set<ReadableStreamDefaultController>,
454
+ ): Response {
455
+ const stream = new ReadableStream({
456
+ start(controller) {
457
+ controller.enqueue("data: connected\n\n");
458
+ sseClients.add(controller);
459
+ },
460
+ cancel(controller) {
461
+ sseClients.delete(controller);
462
+ },
463
+ });
464
+
465
+ return new Response(stream, {
466
+ headers: {
467
+ "Content-Type": "text/event-stream",
468
+ "Cache-Control": "no-cache",
469
+ Connection: "keep-alive",
470
+ },
471
+ });
472
+ }
473
+
474
+ function createHeartbeat(isDev: boolean): Response {
475
+ let interval: ReturnType<typeof setInterval>;
476
+
477
+ const stream = new ReadableStream({
478
+ start(controller) {
479
+ controller.enqueue("data: connected\n\n");
480
+ interval = setInterval(() => {
481
+ try {
482
+ controller.enqueue("data: ping\n\n");
483
+ } catch {
484
+ clearInterval(interval);
485
+ }
486
+ }, 5000);
487
+ },
488
+ cancel() {
489
+ clearInterval(interval);
490
+ if (isDev) return;
491
+ setTimeout(() => {
492
+ console.log("\nBrowser disconnected, shutting down...");
493
+ process.exit(0);
494
+ }, 100);
495
+ },
496
+ });
497
+
498
+ return new Response(stream, {
499
+ headers: {
500
+ "Content-Type": "text/event-stream",
501
+ "Cache-Control": "no-cache",
502
+ Connection: "keep-alive",
503
+ },
504
+ });
505
+ }
506
+
507
+ // ─── Static file serving ────────────────────────────────────────────
508
+
509
+ async function serveStaticFile(
510
+ distPath: string,
511
+ pathname: string,
512
+ ): Promise<Response> {
513
+ const filePath = join(distPath, pathname);
514
+ const file = Bun.file(filePath);
515
+
516
+ if (await file.exists()) {
517
+ return new Response(file);
518
+ }
519
+
520
+ // SPA fallback: serve index.html for non-API routes
521
+ const indexFile = Bun.file(join(distPath, "index.html"));
522
+ if (await indexFile.exists()) {
523
+ return new Response(indexFile);
524
+ }
525
+
526
+ return new Response("Not Found", { status: 404 });
527
+ }
528
+
529
+ // ─── Extract route param ────────────────────────────────────────────
530
+
531
+ function extractCommentId(pathname: string): string | undefined {
532
+ const match = pathname.match(/^\/api\/comments\/([^/]+)/);
533
+ return match?.[1];
534
+ }
535
+
536
+ // ─── Multi-file state ───────────────────────────────────────────────
537
+
538
+ interface FileState {
539
+ content: string;
540
+ type: DocumentType;
541
+ debounceTimer: ReturnType<typeof setTimeout> | null;
542
+ }
543
+
544
+ // ─── Server creation ────────────────────────────────────────────────
545
+
546
+ interface ServerWithWatchers {
547
+ server: ReturnType<typeof Bun.serve>;
548
+ watchers: FSWatcher[];
549
+ }
550
+
551
+ function createServer(options: ServerOptions): ServerWithWatchers {
552
+ // Map of absolute path → mutable file state
553
+ const fileMap = new Map<string, FileState>();
554
+ // Ordered list of file paths (insertion order for tab display)
555
+ const fileOrder: string[] = [];
556
+
557
+ for (const entry of options.files) {
558
+ fileMap.set(entry.filePath, {
559
+ content: entry.content,
560
+ type: entry.type,
561
+ debounceTimer: null,
562
+ });
563
+ fileOrder.push(entry.filePath);
564
+ }
565
+
566
+ const defaultPath = fileOrder[0];
567
+ const sseClients = new Set<ReadableStreamDefaultController>();
568
+
569
+ // Resolve the target file from ?path= query param, falling back to first file
570
+ function resolveContext(url: URL): RouteContext | null {
571
+ const requestedPath = url.searchParams.get("path") ?? defaultPath;
572
+ const state = fileMap.get(requestedPath);
573
+ if (!state) return null;
574
+ return {
575
+ filePath: requestedPath,
576
+ getCurrentContent: () => state.content,
577
+ };
578
+ }
579
+
580
+ function requireContext(url: URL): RouteContext | Response {
581
+ const ctx = resolveContext(url);
582
+ if (!ctx) {
583
+ return errorResponse("File not found", 404);
584
+ }
585
+ return ctx;
586
+ }
587
+
588
+ const isDev = process.env.NODE_ENV === "development";
589
+ const distPath = import.meta.dir;
590
+
591
+ function watchFile(targetPath: string): FSWatcher | null {
592
+ try {
593
+ const watcher = watch(targetPath, async (eventType) => {
594
+ if (eventType !== "change") return;
595
+
596
+ const state = fileMap.get(targetPath);
597
+ if (!state) return;
598
+
599
+ if (state.debounceTimer) clearTimeout(state.debounceTimer);
600
+ state.debounceTimer = setTimeout(async () => {
601
+ try {
602
+ const newContent = await fs.readFile(targetPath, "utf-8");
603
+ if (newContent !== state.content) {
604
+ state.content = newContent;
605
+ console.log(`File changed: ${basename(targetPath)}`);
606
+
607
+ const message = `data: ${JSON.stringify({ type: "update", path: targetPath })}\n\n`;
608
+ for (const controller of sseClients) {
609
+ try {
610
+ controller.enqueue(message);
611
+ } catch {
612
+ sseClients.delete(controller);
613
+ }
614
+ }
615
+ }
616
+ } catch (err) {
617
+ console.error(`Failed to read updated file ${targetPath}:`, err);
618
+ }
619
+ }, 100);
620
+ });
621
+ return watcher;
622
+ } catch (err) {
623
+ console.warn(`File watching not available for ${targetPath}:`, err);
624
+ return null;
625
+ }
626
+ }
627
+
628
+ const server = Bun.serve({
629
+ port: options.port,
630
+ hostname: options.host,
631
+
632
+ async fetch(req) {
633
+ const url = new URL(req.url);
634
+ const { pathname } = url;
635
+ const method = req.method;
636
+
637
+ // ── API routes ──────────────────────────────────────────
638
+
639
+ // Document list (multi-file)
640
+ if (pathname === "/api/documents" && method === "GET") {
641
+ const files = fileOrder.map((fp) => {
642
+ const state = fileMap.get(fp)!;
643
+ return {
644
+ path: fp,
645
+ fileName: basename(fp),
646
+ type: state.type,
647
+ };
648
+ });
649
+ return json({ files, clean: options.clean || false });
650
+ }
651
+
652
+ // Hot-add or refresh a file
653
+ if (pathname === "/api/files" && method === "POST") {
654
+ try {
655
+ const { path: requestedPath } = await req.json();
656
+
657
+ if (!requestedPath || typeof requestedPath !== "string") {
658
+ return errorResponse("Missing 'path' field", 400);
659
+ }
660
+
661
+ const filePath = path.resolve(requestedPath);
662
+ const fileType = getFileType(filePath);
663
+
664
+ if (!fileType) {
665
+ return errorResponse(
666
+ `Unsupported file type: ${filePath} (expected .md, .markdown, .html, or .htm)`,
667
+ 400,
668
+ );
669
+ }
670
+
671
+ let content: string;
672
+ try {
673
+ content = await fs.readFile(filePath, "utf-8");
674
+ } catch (err) {
675
+ if (isErrnoException(err) && err.code === "ENOENT") {
676
+ return errorResponse(`File not found: ${filePath}`, 404);
677
+ }
678
+ throw err;
679
+ }
680
+
681
+ const existingState = fileMap.get(filePath);
682
+
683
+ if (existingState) {
684
+ // File already loaded — refresh content
685
+ existingState.content = content;
686
+ const message = `data: ${JSON.stringify({ type: "update", path: filePath })}\n\n`;
687
+ for (const controller of sseClients) {
688
+ try {
689
+ controller.enqueue(message);
690
+ } catch {
691
+ sseClients.delete(controller);
692
+ }
693
+ }
694
+ } else {
695
+ // New file — add to server
696
+ fileMap.set(filePath, {
697
+ content,
698
+ type: fileType,
699
+ debounceTimer: null,
700
+ });
701
+ fileOrder.push(filePath);
702
+
703
+ // Set up file watcher for the new file
704
+ const watcher = watchFile(filePath);
705
+ if (watcher) watchers.push(watcher);
706
+
707
+ const message = `data: ${JSON.stringify({
708
+ type: "file-added",
709
+ path: filePath,
710
+ fileName: basename(filePath),
711
+ fileType,
712
+ })}\n\n`;
713
+ for (const controller of sseClients) {
714
+ try {
715
+ controller.enqueue(message);
716
+ } catch {
717
+ sseClients.delete(controller);
718
+ }
719
+ }
720
+ }
721
+
722
+ return json({
723
+ path: filePath,
724
+ fileName: basename(filePath),
725
+ type: fileType,
726
+ });
727
+ } catch (err) {
728
+ console.error("Failed to add file:", err);
729
+ return errorResponse("Failed to add file", 500);
730
+ }
731
+ }
732
+
733
+ // Single document (backward compat + path-aware)
734
+ if (pathname === "/api/document" && method === "GET") {
735
+ const ctxOrRes = requireContext(url);
736
+ if (ctxOrRes instanceof Response) return ctxOrRes;
737
+ const state = fileMap.get(ctxOrRes.filePath)!;
738
+ return json({
739
+ content: state.content,
740
+ type: state.type,
741
+ filePath: ctxOrRes.filePath,
742
+ fileName: basename(ctxOrRes.filePath),
743
+ clean: options.clean || false,
744
+ });
745
+ }
746
+
747
+ if (pathname === "/api/document/stream" && method === "GET") {
748
+ return createDocumentStream(sseClients);
749
+ }
750
+
751
+ if (pathname === "/api/health" && method === "GET") {
752
+ return json({ status: "ok" });
753
+ }
754
+
755
+ if (pathname === "/api/heartbeat" && method === "GET") {
756
+ return createHeartbeat(isDev);
757
+ }
758
+
759
+ // Comments routes
760
+ if (pathname === "/api/comments" && method === "GET") {
761
+ const ctxOrRes = requireContext(url);
762
+ if (ctxOrRes instanceof Response) return ctxOrRes;
763
+ return getComments(ctxOrRes);
764
+ }
765
+
766
+ if (pathname === "/api/comments/raw" && method === "GET") {
767
+ const ctxOrRes = requireContext(url);
768
+ if (ctxOrRes instanceof Response) return ctxOrRes;
769
+ return getRawComments(ctxOrRes);
770
+ }
771
+
772
+ if (pathname === "/api/comments" && method === "POST") {
773
+ const ctxOrRes = requireContext(url);
774
+ if (ctxOrRes instanceof Response) return ctxOrRes;
775
+ return addComment(ctxOrRes, req);
776
+ }
777
+
778
+ if (pathname === "/api/comments" && method === "DELETE") {
779
+ const ctxOrRes = requireContext(url);
780
+ if (ctxOrRes instanceof Response) return ctxOrRes;
781
+ return clearComments(ctxOrRes);
782
+ }
783
+
784
+ // Parameterized comment routes
785
+ const commentId = extractCommentId(pathname);
786
+ if (commentId) {
787
+ const ctxOrRes = requireContext(url);
788
+ if (ctxOrRes instanceof Response) return ctxOrRes;
789
+
790
+ if (pathname.endsWith("/reanchor") && method === "PUT") {
791
+ return reanchorComment(ctxOrRes, req, commentId);
792
+ }
793
+ if (method === "PUT") {
794
+ return updateComment(ctxOrRes, req, commentId);
795
+ }
796
+ if (method === "DELETE") {
797
+ return deleteComment(ctxOrRes, commentId);
798
+ }
799
+ }
800
+
801
+ // Settings routes
802
+ if (pathname === "/api/settings" && method === "GET") {
803
+ const ctxOrRes = requireContext(url);
804
+ if (ctxOrRes instanceof Response) return ctxOrRes;
805
+ return getSettings(ctxOrRes);
806
+ }
807
+
808
+ if (pathname === "/api/settings" && method === "PUT") {
809
+ const ctxOrRes = requireContext(url);
810
+ if (ctxOrRes instanceof Response) return ctxOrRes;
811
+ return updateSettings(ctxOrRes, req);
812
+ }
813
+
814
+ // ── Static / SPA serving ────────────────────────────────
815
+
816
+ if (isDev && pathname === "/") {
817
+ return Response.redirect("http://localhost:5173", 302);
818
+ }
819
+
820
+ if (!isDev) {
821
+ return serveStaticFile(distPath, pathname);
822
+ }
823
+
824
+ return new Response("Not Found", { status: 404 });
825
+ },
826
+ });
827
+
828
+ // Set up per-file watchers after Bun.serve() succeeds to avoid
829
+ // leaking FSWatcher handles if the server fails to bind.
830
+ const watchers: FSWatcher[] = [];
831
+ for (const fp of fileOrder) {
832
+ const watcher = watchFile(fp);
833
+ if (watcher) watchers.push(watcher);
834
+ }
835
+
836
+ return { server, watchers };
837
+ }
838
+
839
+ // ─── Port fallback + start ──────────────────────────────────────────
840
+
841
+ export async function startServer(
842
+ options: ServerOptions,
843
+ ): Promise<ServerResult> {
844
+ const MAX_PORT = 65535;
845
+
846
+ for (let port = options.port; port <= MAX_PORT; port++) {
847
+ try {
848
+ const { server, watchers } = createServer({ ...options, port });
849
+
850
+ const displayHost =
851
+ options.host === "0.0.0.0" ? "localhost" : options.host;
852
+
853
+ const originalStop = server.stop.bind(server);
854
+ const wrappedServer = {
855
+ stop() {
856
+ for (const w of watchers) w.close();
857
+ originalStop();
858
+ },
859
+ };
860
+
861
+ const actualPort = server.port ?? port;
862
+
863
+ await writeServerInfo(actualPort);
864
+
865
+ return {
866
+ port: actualPort,
867
+ url: `http://${displayHost}:${actualPort}`,
868
+ server: wrappedServer,
869
+ };
870
+ } catch (err) {
871
+ if (
872
+ isErrnoException(err) &&
873
+ (err.code === "EADDRINUSE" || err.code === "EACCES")
874
+ ) {
875
+ console.log(`Port ${port} is busy, trying ${port + 1}...`);
876
+ continue;
877
+ }
878
+ throw err;
879
+ }
880
+ }
881
+
882
+ throw new Error(`No available port found starting from ${options.port}`);
883
+ }