@cuongtran001/kanna 0.39.2

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 (473) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +355 -0
  3. package/bin/kanna +9 -0
  4. package/dist/client/apple-touch-icon.png +0 -0
  5. package/dist/client/assets/abap-BdImnpbu.js +1 -0
  6. package/dist/client/assets/actionscript-3-CoDkCxhg.js +1 -0
  7. package/dist/client/assets/ada-bCR0ucgS.js +1 -0
  8. package/dist/client/assets/andromeeda-C4gqWexZ.js +1 -0
  9. package/dist/client/assets/angular-html-CU67Zn6k.js +1 -0
  10. package/dist/client/assets/angular-ts-BwZT4LLn.js +1 -0
  11. package/dist/client/assets/apache-Pmp26Uib.js +1 -0
  12. package/dist/client/assets/apex-D8_7TLub.js +1 -0
  13. package/dist/client/assets/apl-dKokRX4l.js +1 -0
  14. package/dist/client/assets/applescript-Co6uUVPk.js +1 -0
  15. package/dist/client/assets/ara-BRHolxvo.js +1 -0
  16. package/dist/client/assets/asciidoc-Ve4PFQV2.js +1 -0
  17. package/dist/client/assets/asm-D_Q5rh1f.js +1 -0
  18. package/dist/client/assets/astro-CbQHKStN.js +1 -0
  19. package/dist/client/assets/aurora-x-D-2ljcwZ.js +1 -0
  20. package/dist/client/assets/awk-DMzUqQB5.js +1 -0
  21. package/dist/client/assets/ayu-dark-DYE7WIF3.js +1 -0
  22. package/dist/client/assets/ayu-light-BA47KaF1.js +1 -0
  23. package/dist/client/assets/ayu-mirage-32ctXXKs.js +1 -0
  24. package/dist/client/assets/ballerina-BFfxhgS-.js +1 -0
  25. package/dist/client/assets/bat-BkioyH1T.js +1 -0
  26. package/dist/client/assets/beancount-k_qm7-4y.js +1 -0
  27. package/dist/client/assets/berry-uYugtg8r.js +1 -0
  28. package/dist/client/assets/bibtex-CHM0blh-.js +1 -0
  29. package/dist/client/assets/bicep-Bmn6On1c.js +1 -0
  30. package/dist/client/assets/bird2-DPOp833l.js +1 -0
  31. package/dist/client/assets/blade-D4QpJJKB.js +1 -0
  32. package/dist/client/assets/bricolage-grotesque-latin-ext-wght-normal-CcLUaPy7.woff2 +0 -0
  33. package/dist/client/assets/bricolage-grotesque-latin-wght-normal-DLoelf7F.woff2 +0 -0
  34. package/dist/client/assets/bricolage-grotesque-vietnamese-wght-normal-BUzh504Q.woff2 +0 -0
  35. package/dist/client/assets/bsl-BO_Y6i37.js +1 -0
  36. package/dist/client/assets/c-BIGW1oBm.js +1 -0
  37. package/dist/client/assets/c3-eo99z4R2.js +1 -0
  38. package/dist/client/assets/cadence-Bv_4Rxtq.js +1 -0
  39. package/dist/client/assets/cairo-KRGpt6FW.js +1 -0
  40. package/dist/client/assets/catppuccin-frappe-DFWUc33u.js +1 -0
  41. package/dist/client/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
  42. package/dist/client/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
  43. package/dist/client/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
  44. package/dist/client/assets/clarity-D53aC0YG.js +1 -0
  45. package/dist/client/assets/clojure-P80f7IUj.js +1 -0
  46. package/dist/client/assets/cmake-D1j8_8rp.js +1 -0
  47. package/dist/client/assets/cobol-nwyudZeR.js +1 -0
  48. package/dist/client/assets/codeowners-Bp6g37R7.js +1 -0
  49. package/dist/client/assets/codeql-DsOJ9woJ.js +1 -0
  50. package/dist/client/assets/coffee-Ch7k5sss.js +1 -0
  51. package/dist/client/assets/common-lisp-Cg-RD9OK.js +1 -0
  52. package/dist/client/assets/coq-DkFqJrB1.js +1 -0
  53. package/dist/client/assets/cpp-CofmeUqb.js +1 -0
  54. package/dist/client/assets/crystal-tKQVLTB8.js +1 -0
  55. package/dist/client/assets/csharp-COcwbKMJ.js +1 -0
  56. package/dist/client/assets/css-DPfMkruS.js +1 -0
  57. package/dist/client/assets/csv-fuZLfV_i.js +1 -0
  58. package/dist/client/assets/cue-D82EKSYY.js +1 -0
  59. package/dist/client/assets/cypher-COkxafJQ.js +1 -0
  60. package/dist/client/assets/d-85-TOEBH.js +1 -0
  61. package/dist/client/assets/dark-plus-C3mMm8J8.js +1 -0
  62. package/dist/client/assets/dart-CF10PKvl.js +1 -0
  63. package/dist/client/assets/dax-CEL-wOlO.js +1 -0
  64. package/dist/client/assets/desktop-BmXAJ9_W.js +1 -0
  65. package/dist/client/assets/diff-D97Zzqfu.js +1 -0
  66. package/dist/client/assets/docker-BcOcwvcX.js +1 -0
  67. package/dist/client/assets/dotenv-Da5cRb03.js +1 -0
  68. package/dist/client/assets/dracula-BzJJZx-M.js +1 -0
  69. package/dist/client/assets/dracula-soft-BXkSAIEj.js +1 -0
  70. package/dist/client/assets/dream-maker-BtqSS_iP.js +1 -0
  71. package/dist/client/assets/edge-BkV0erSs.js +1 -0
  72. package/dist/client/assets/elixir-CDX3lj18.js +1 -0
  73. package/dist/client/assets/elm-DbKCFpqz.js +1 -0
  74. package/dist/client/assets/emacs-lisp-C9XAeP06.js +1 -0
  75. package/dist/client/assets/erb-B12qg9BL.js +1 -0
  76. package/dist/client/assets/erlang-DsQrWhSR.js +1 -0
  77. package/dist/client/assets/everforest-dark-BgDCqdQA.js +1 -0
  78. package/dist/client/assets/everforest-light-C8M2exoo.js +1 -0
  79. package/dist/client/assets/fennel-BYunw83y.js +1 -0
  80. package/dist/client/assets/fish-BvzEVeQv.js +1 -0
  81. package/dist/client/assets/fluent-C4IJs8-o.js +1 -0
  82. package/dist/client/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
  83. package/dist/client/assets/fortran-free-form-BxgE0vQu.js +1 -0
  84. package/dist/client/assets/fsharp-CXgrBDvD.js +1 -0
  85. package/dist/client/assets/gdresource-BOOCDP_w.js +1 -0
  86. package/dist/client/assets/gdscript-C5YyOfLZ.js +1 -0
  87. package/dist/client/assets/gdshader-DkwncUOv.js +1 -0
  88. package/dist/client/assets/genie-D0YGMca9.js +1 -0
  89. package/dist/client/assets/gherkin-DyxjwDmM.js +1 -0
  90. package/dist/client/assets/git-commit-F4YmCXRG.js +1 -0
  91. package/dist/client/assets/git-rebase-r7XF79zn.js +1 -0
  92. package/dist/client/assets/github-dark-DHJKELXO.js +1 -0
  93. package/dist/client/assets/github-dark-default-Cuk6v7N8.js +1 -0
  94. package/dist/client/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
  95. package/dist/client/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
  96. package/dist/client/assets/github-light-DAi9KRSo.js +1 -0
  97. package/dist/client/assets/github-light-default-D7oLnXFd.js +1 -0
  98. package/dist/client/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
  99. package/dist/client/assets/gleam-BspZqrRM.js +1 -0
  100. package/dist/client/assets/glimmer-js-Rg0-pVw9.js +1 -0
  101. package/dist/client/assets/glimmer-ts-U6CK756n.js +1 -0
  102. package/dist/client/assets/glsl-DplSGwfg.js +1 -0
  103. package/dist/client/assets/gn-n2N0HUVH.js +1 -0
  104. package/dist/client/assets/gnuplot-DdkO51Og.js +1 -0
  105. package/dist/client/assets/go-CxLEBnE3.js +1 -0
  106. package/dist/client/assets/graphql-ChdNCCLP.js +1 -0
  107. package/dist/client/assets/groovy-gcz8RCvz.js +1 -0
  108. package/dist/client/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
  109. package/dist/client/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
  110. package/dist/client/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
  111. package/dist/client/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
  112. package/dist/client/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
  113. package/dist/client/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
  114. package/dist/client/assets/hack-CaT9iCJl.js +1 -0
  115. package/dist/client/assets/haml-B8DHNrY2.js +1 -0
  116. package/dist/client/assets/handlebars-BL8al0AC.js +1 -0
  117. package/dist/client/assets/haskell-Df6bDoY_.js +1 -0
  118. package/dist/client/assets/haxe-CzTSHFRz.js +1 -0
  119. package/dist/client/assets/hcl-BWvSN4gD.js +1 -0
  120. package/dist/client/assets/hjson-D5-asLiD.js +1 -0
  121. package/dist/client/assets/hlsl-D3lLCCz7.js +1 -0
  122. package/dist/client/assets/horizon-BUw7H-hv.js +1 -0
  123. package/dist/client/assets/horizon-bright-Cn-bp-IR.js +1 -0
  124. package/dist/client/assets/houston-DnULxvSX.js +1 -0
  125. package/dist/client/assets/html-GMplVEZG.js +1 -0
  126. package/dist/client/assets/html-derivative-BFtXZ54Q.js +1 -0
  127. package/dist/client/assets/http-jrhK8wxY.js +1 -0
  128. package/dist/client/assets/hurl-irOxFIW8.js +1 -0
  129. package/dist/client/assets/hxml-Bvhsp5Yf.js +1 -0
  130. package/dist/client/assets/hy-DFXneXwc.js +1 -0
  131. package/dist/client/assets/imba-DGztddWO.js +1 -0
  132. package/dist/client/assets/index-Do7324M0.css +32 -0
  133. package/dist/client/assets/index-ktE9DLCD.js +2620 -0
  134. package/dist/client/assets/ini-BEwlwnbL.js +1 -0
  135. package/dist/client/assets/java-CylS5w8V.js +1 -0
  136. package/dist/client/assets/javascript-wDzz0qaB.js +1 -0
  137. package/dist/client/assets/jinja-4LBKfQ-Z.js +1 -0
  138. package/dist/client/assets/jison-wvAkD_A8.js +1 -0
  139. package/dist/client/assets/json-Cp-IABpG.js +1 -0
  140. package/dist/client/assets/json5-C9tS-k6U.js +1 -0
  141. package/dist/client/assets/jsonc-Des-eS-w.js +1 -0
  142. package/dist/client/assets/jsonl-DcaNXYhu.js +1 -0
  143. package/dist/client/assets/jsonnet-DFQXde-d.js +1 -0
  144. package/dist/client/assets/jssm-C2t-YnRu.js +1 -0
  145. package/dist/client/assets/jsx-g9-lgVsj.js +1 -0
  146. package/dist/client/assets/julia-CxzCAyBv.js +1 -0
  147. package/dist/client/assets/just-Cw27pwNe.js +1 -0
  148. package/dist/client/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
  149. package/dist/client/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
  150. package/dist/client/assets/kanagawa-wave-DWedfzmr.js +1 -0
  151. package/dist/client/assets/kdl-DV7GczEv.js +1 -0
  152. package/dist/client/assets/kotlin-BdnUsdx6.js +1 -0
  153. package/dist/client/assets/kusto-DZf3V79B.js +1 -0
  154. package/dist/client/assets/laserwave-DUszq2jm.js +1 -0
  155. package/dist/client/assets/latex-CWtU0Tv5.js +1 -0
  156. package/dist/client/assets/lean-BZvkOJ9d.js +1 -0
  157. package/dist/client/assets/less-B1dDrJ26.js +1 -0
  158. package/dist/client/assets/light-plus-B7mTdjB0.js +1 -0
  159. package/dist/client/assets/liquid-DYVedYrR.js +1 -0
  160. package/dist/client/assets/llvm-DjAJT7YJ.js +1 -0
  161. package/dist/client/assets/log-2UxHyX5q.js +1 -0
  162. package/dist/client/assets/logo-BtOb2qkB.js +1 -0
  163. package/dist/client/assets/lua-BaeVxFsk.js +1 -0
  164. package/dist/client/assets/luau-C-HG3fhB.js +1 -0
  165. package/dist/client/assets/make-CHLpvVh8.js +1 -0
  166. package/dist/client/assets/markdown-Cvjx9yec.js +1 -0
  167. package/dist/client/assets/marko-CnJfTvn9.js +1 -0
  168. package/dist/client/assets/material-theme-D5KoaKCx.js +1 -0
  169. package/dist/client/assets/material-theme-darker-BfHTSMKl.js +1 -0
  170. package/dist/client/assets/material-theme-lighter-B0m2ddpp.js +1 -0
  171. package/dist/client/assets/material-theme-ocean-CyktbL80.js +1 -0
  172. package/dist/client/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
  173. package/dist/client/assets/matlab-D7o27uSR.js +1 -0
  174. package/dist/client/assets/mdc-BMNejdWA.js +1 -0
  175. package/dist/client/assets/mdx-Cmh6b_Ma.js +1 -0
  176. package/dist/client/assets/mermaid-mWjccvbQ.js +1 -0
  177. package/dist/client/assets/min-dark-CafNBF8u.js +1 -0
  178. package/dist/client/assets/min-light-CTRr51gU.js +1 -0
  179. package/dist/client/assets/mipsasm-CKIfxQSi.js +1 -0
  180. package/dist/client/assets/mojo-rZm6bMo-.js +1 -0
  181. package/dist/client/assets/monokai-D4h5O-jR.js +1 -0
  182. package/dist/client/assets/moonbit-_H4v1dQx.js +1 -0
  183. package/dist/client/assets/move-IF9eRakj.js +1 -0
  184. package/dist/client/assets/narrat-DRg8JJMk.js +1 -0
  185. package/dist/client/assets/nextflow-Zz6hmt5N.js +1 -0
  186. package/dist/client/assets/nextflow-groovy-BeH2EWoN.js +1 -0
  187. package/dist/client/assets/nginx-BpAMiNFr.js +1 -0
  188. package/dist/client/assets/night-owl-C39BiMTA.js +1 -0
  189. package/dist/client/assets/night-owl-light-CMTm3GFP.js +1 -0
  190. package/dist/client/assets/nim-CVrawwO9.js +1 -0
  191. package/dist/client/assets/nix-CwoSXNpI.js +1 -0
  192. package/dist/client/assets/nord-Ddv68eIx.js +1 -0
  193. package/dist/client/assets/nushell-Cz2AlsmD.js +1 -0
  194. package/dist/client/assets/objective-c-DXmwc3jG.js +1 -0
  195. package/dist/client/assets/objective-cpp-CLxacb5B.js +1 -0
  196. package/dist/client/assets/ocaml-C0hk2d4L.js +1 -0
  197. package/dist/client/assets/odin-BBf5iR-q.js +1 -0
  198. package/dist/client/assets/one-dark-pro-DVMEJ2y_.js +1 -0
  199. package/dist/client/assets/one-light-C3Wv6jpd.js +1 -0
  200. package/dist/client/assets/openscad-C4EeE6gA.js +1 -0
  201. package/dist/client/assets/pascal-D93ZcfNL.js +1 -0
  202. package/dist/client/assets/perl-C0TMdlhV.js +1 -0
  203. package/dist/client/assets/php-Dhbhpdrm.js +1 -0
  204. package/dist/client/assets/pierre-dark-DF2SEV7i.js +1 -0
  205. package/dist/client/assets/pierre-light-DOlZxES8.js +1 -0
  206. package/dist/client/assets/pkl-u5AG7uiY.js +1 -0
  207. package/dist/client/assets/plastic-3e1v2bzS.js +1 -0
  208. package/dist/client/assets/plsql-ChMvpjG-.js +1 -0
  209. package/dist/client/assets/po-BTJTHyun.js +1 -0
  210. package/dist/client/assets/poimandres-CS3Unz2-.js +1 -0
  211. package/dist/client/assets/polar-C0HS_06l.js +1 -0
  212. package/dist/client/assets/postcss-CXtECtnM.js +1 -0
  213. package/dist/client/assets/powerquery-CEu0bR-o.js +1 -0
  214. package/dist/client/assets/powershell-Dpen1YoG.js +1 -0
  215. package/dist/client/assets/prisma-Dd19v3D-.js +1 -0
  216. package/dist/client/assets/prolog-CbFg5uaA.js +1 -0
  217. package/dist/client/assets/proto-C7zT0LnQ.js +1 -0
  218. package/dist/client/assets/pug-CGlum2m_.js +1 -0
  219. package/dist/client/assets/puppet-BMWR74SV.js +1 -0
  220. package/dist/client/assets/purescript-CklMAg4u.js +1 -0
  221. package/dist/client/assets/python-B6aJPvgy.js +1 -0
  222. package/dist/client/assets/qml-3beO22l8.js +1 -0
  223. package/dist/client/assets/qmldir-C8lEn-DE.js +1 -0
  224. package/dist/client/assets/qss-IeuSbFQv.js +1 -0
  225. package/dist/client/assets/r-Dspwwk_N.js +1 -0
  226. package/dist/client/assets/racket-BqYA7rlc.js +1 -0
  227. package/dist/client/assets/raku-DXvB9xmW.js +1 -0
  228. package/dist/client/assets/razor-Uh8Bk_45.js +1 -0
  229. package/dist/client/assets/red-bN70gL4F.js +1 -0
  230. package/dist/client/assets/reg-C-SQnVFl.js +1 -0
  231. package/dist/client/assets/regexp-CDVJQ6XC.js +1 -0
  232. package/dist/client/assets/rel-C3B-1QV4.js +1 -0
  233. package/dist/client/assets/riscv-BM1_JUlF.js +1 -0
  234. package/dist/client/assets/ron-D8l8udqQ.js +1 -0
  235. package/dist/client/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
  236. package/dist/client/assets/rose-pine-moon-D4_iv3hh.js +1 -0
  237. package/dist/client/assets/rose-pine-qdsjHGoJ.js +1 -0
  238. package/dist/client/assets/rosmsg-BJDFO7_C.js +1 -0
  239. package/dist/client/assets/rst-BrH8l1NY.js +1 -0
  240. package/dist/client/assets/ruby-Dw2BHqvy.js +1 -0
  241. package/dist/client/assets/rust-B1yitclQ.js +1 -0
  242. package/dist/client/assets/sas-cz2c8ADy.js +1 -0
  243. package/dist/client/assets/sass-Cj5Yp3dK.js +1 -0
  244. package/dist/client/assets/scala-C151Ov-r.js +1 -0
  245. package/dist/client/assets/scheme-C98Dy4si.js +1 -0
  246. package/dist/client/assets/scss-OYdSNvt2.js +1 -0
  247. package/dist/client/assets/sdbl-DVxCFoDh.js +1 -0
  248. package/dist/client/assets/shaderlab-Dg9Lc6iA.js +1 -0
  249. package/dist/client/assets/shellscript-Yzrsuije.js +1 -0
  250. package/dist/client/assets/shellsession-BADoaaVG.js +1 -0
  251. package/dist/client/assets/slack-dark-BthQWCQV.js +1 -0
  252. package/dist/client/assets/slack-ochin-DqwNpetd.js +1 -0
  253. package/dist/client/assets/smalltalk-BERRCDM3.js +1 -0
  254. package/dist/client/assets/snazzy-light-Bw305WKR.js +1 -0
  255. package/dist/client/assets/solarized-dark-DXbdFlpD.js +1 -0
  256. package/dist/client/assets/solarized-light-L9t79GZl.js +1 -0
  257. package/dist/client/assets/solidity-rGO070M0.js +1 -0
  258. package/dist/client/assets/soy-Brmx7dQM.js +1 -0
  259. package/dist/client/assets/sparql-rVzFXLq3.js +1 -0
  260. package/dist/client/assets/splunk-BtCnVYZw.js +1 -0
  261. package/dist/client/assets/sql-BLtJtn59.js +1 -0
  262. package/dist/client/assets/ssh-config-_ykCGR6B.js +1 -0
  263. package/dist/client/assets/stata-BH5u7GGu.js +1 -0
  264. package/dist/client/assets/stylus-BEDo0Tqx.js +1 -0
  265. package/dist/client/assets/surrealql-Bq5Q-fJD.js +1 -0
  266. package/dist/client/assets/svelte-C_ipcX3V.js +1 -0
  267. package/dist/client/assets/swift-D82vCrfD.js +1 -0
  268. package/dist/client/assets/synthwave-84-CbfX1IO0.js +1 -0
  269. package/dist/client/assets/system-verilog-CnnmHF94.js +1 -0
  270. package/dist/client/assets/systemd-4A_iFExJ.js +1 -0
  271. package/dist/client/assets/talonscript-CkByrt1z.js +1 -0
  272. package/dist/client/assets/tasl-QIJgUcNo.js +1 -0
  273. package/dist/client/assets/tcl-dwOrl1Do.js +1 -0
  274. package/dist/client/assets/templ-P3uqSqPl.js +1 -0
  275. package/dist/client/assets/terraform-BETggiCN.js +1 -0
  276. package/dist/client/assets/tex-idrVyKtj.js +1 -0
  277. package/dist/client/assets/tokyo-night-hegEt444.js +1 -0
  278. package/dist/client/assets/toml-vGWfd6FD.js +1 -0
  279. package/dist/client/assets/ts-tags-zn1MmPIZ.js +1 -0
  280. package/dist/client/assets/tsv-B_m7g4N7.js +1 -0
  281. package/dist/client/assets/tsx-COt5Ahok.js +1 -0
  282. package/dist/client/assets/turtle-BsS91CYL.js +1 -0
  283. package/dist/client/assets/twig-DNn4PbVi.js +1 -0
  284. package/dist/client/assets/typescript-BPQ3VLAy.js +1 -0
  285. package/dist/client/assets/typespec-BGHnOYBU.js +1 -0
  286. package/dist/client/assets/typst-DHCkPAjA.js +1 -0
  287. package/dist/client/assets/v-BcVCzyr7.js +1 -0
  288. package/dist/client/assets/vala-CsfeWuGM.js +1 -0
  289. package/dist/client/assets/vb-D17OF-Vu.js +1 -0
  290. package/dist/client/assets/verilog-BQ8w6xss.js +1 -0
  291. package/dist/client/assets/vesper-DU1UobuO.js +1 -0
  292. package/dist/client/assets/vhdl-CeAyd5Ju.js +1 -0
  293. package/dist/client/assets/viml-CJc9bBzg.js +1 -0
  294. package/dist/client/assets/vitesse-black-Bkuqu6BP.js +1 -0
  295. package/dist/client/assets/vitesse-dark-D0r3Knsf.js +1 -0
  296. package/dist/client/assets/vitesse-light-CVO1_9PV.js +1 -0
  297. package/dist/client/assets/vue-DN_0RTcg.js +1 -0
  298. package/dist/client/assets/vue-html-AaS7Mt5G.js +1 -0
  299. package/dist/client/assets/vue-vine-CQOfvN7w.js +1 -0
  300. package/dist/client/assets/vyper-CDx5xZoG.js +1 -0
  301. package/dist/client/assets/wasm-CG6Dc4jp.js +1 -0
  302. package/dist/client/assets/wasm-MzD3tlZU.js +1 -0
  303. package/dist/client/assets/wenyan-BV7otONQ.js +1 -0
  304. package/dist/client/assets/wgsl-Dx-B1_4e.js +1 -0
  305. package/dist/client/assets/wikitext-BhOHFoWU.js +1 -0
  306. package/dist/client/assets/wit-5i3qLPDT.js +1 -0
  307. package/dist/client/assets/wolfram-lXgVvXCa.js +1 -0
  308. package/dist/client/assets/xml-sdJ4AIDG.js +1 -0
  309. package/dist/client/assets/xsl-CtQFsRM5.js +1 -0
  310. package/dist/client/assets/yaml-Buea-lGh.js +1 -0
  311. package/dist/client/assets/zenscript-DVFEvuxE.js +1 -0
  312. package/dist/client/assets/zig-VOosw3JB.js +1 -0
  313. package/dist/client/chat-sounds/Blow.mp3 +0 -0
  314. package/dist/client/chat-sounds/Bottle.mp3 +0 -0
  315. package/dist/client/chat-sounds/Frog.mp3 +0 -0
  316. package/dist/client/chat-sounds/Funk.mp3 +0 -0
  317. package/dist/client/chat-sounds/Glass.mp3 +0 -0
  318. package/dist/client/chat-sounds/Ping.mp3 +0 -0
  319. package/dist/client/chat-sounds/Pop.mp3 +0 -0
  320. package/dist/client/chat-sounds/Purr.mp3 +0 -0
  321. package/dist/client/chat-sounds/Tink.mp3 +0 -0
  322. package/dist/client/editor-icons/cursor.png +0 -0
  323. package/dist/client/editor-icons/custom.png +0 -0
  324. package/dist/client/editor-icons/default-app.png +0 -0
  325. package/dist/client/editor-icons/finder.png +0 -0
  326. package/dist/client/editor-icons/preview.png +0 -0
  327. package/dist/client/editor-icons/terminal.png +0 -0
  328. package/dist/client/editor-icons/windsurf.png +0 -0
  329. package/dist/client/editor-icons/xcode.png +0 -0
  330. package/dist/client/favicon.png +0 -0
  331. package/dist/client/fonts/body-medium.woff2 +0 -0
  332. package/dist/client/fonts/body-regular-italic.woff2 +0 -0
  333. package/dist/client/fonts/body-regular.woff2 +0 -0
  334. package/dist/client/fonts/body-semibold.woff2 +0 -0
  335. package/dist/client/icon-192.png +0 -0
  336. package/dist/client/icon-512.png +0 -0
  337. package/dist/client/icon-maskable-512.png +0 -0
  338. package/dist/client/icon.svg +4 -0
  339. package/dist/client/index.html +34 -0
  340. package/dist/client/manifest.webmanifest +46 -0
  341. package/dist/client/screenshot-light.png +0 -0
  342. package/dist/client/screenshot.png +0 -0
  343. package/dist/export-viewer/assets/bricolage-grotesque-latin-ext-wght-normal-CcLUaPy7.woff2 +0 -0
  344. package/dist/export-viewer/assets/bricolage-grotesque-latin-wght-normal-DLoelf7F.woff2 +0 -0
  345. package/dist/export-viewer/assets/bricolage-grotesque-vietnamese-wght-normal-BUzh504Q.woff2 +0 -0
  346. package/dist/export-viewer/assets/index-D1qUumZR.js +410 -0
  347. package/dist/export-viewer/assets/index-gG2nMW51.css +1 -0
  348. package/dist/export-viewer/editor-icons/cursor.png +0 -0
  349. package/dist/export-viewer/editor-icons/custom.png +0 -0
  350. package/dist/export-viewer/editor-icons/default-app.png +0 -0
  351. package/dist/export-viewer/editor-icons/finder.png +0 -0
  352. package/dist/export-viewer/editor-icons/preview.png +0 -0
  353. package/dist/export-viewer/editor-icons/terminal.png +0 -0
  354. package/dist/export-viewer/editor-icons/windsurf.png +0 -0
  355. package/dist/export-viewer/editor-icons/xcode.png +0 -0
  356. package/dist/export-viewer/fonts/body-medium.woff2 +0 -0
  357. package/dist/export-viewer/fonts/body-regular-italic.woff2 +0 -0
  358. package/dist/export-viewer/fonts/body-regular.woff2 +0 -0
  359. package/dist/export-viewer/fonts/body-semibold.woff2 +0 -0
  360. package/dist/export-viewer/index.html +14 -0
  361. package/package.json +99 -0
  362. package/src/server/__fixtures__/claude-session-empty.jsonl +0 -0
  363. package/src/server/__fixtures__/claude-session-malformed.jsonl +3 -0
  364. package/src/server/__fixtures__/claude-session-valid.jsonl +6 -0
  365. package/src/server/agent.test.ts +2369 -0
  366. package/src/server/agent.ts +1927 -0
  367. package/src/server/analytics.test.ts +313 -0
  368. package/src/server/analytics.ts +131 -0
  369. package/src/server/app-settings.test.ts +233 -0
  370. package/src/server/app-settings.ts +548 -0
  371. package/src/server/auth.test.ts +329 -0
  372. package/src/server/auth.ts +204 -0
  373. package/src/server/auto-continue/e2e.test.ts +215 -0
  374. package/src/server/auto-continue/events.test.ts +30 -0
  375. package/src/server/auto-continue/events.ts +35 -0
  376. package/src/server/auto-continue/limit-detector.test.ts +153 -0
  377. package/src/server/auto-continue/limit-detector.ts +159 -0
  378. package/src/server/auto-continue/read-model.test.ts +109 -0
  379. package/src/server/auto-continue/read-model.ts +83 -0
  380. package/src/server/auto-continue/schedule-manager.test.ts +155 -0
  381. package/src/server/auto-continue/schedule-manager.ts +116 -0
  382. package/src/server/claude-session-importer.test.ts +214 -0
  383. package/src/server/claude-session-importer.ts +187 -0
  384. package/src/server/claude-session-mapper.test.ts +88 -0
  385. package/src/server/claude-session-mapper.ts +106 -0
  386. package/src/server/claude-session-parser.test.ts +38 -0
  387. package/src/server/claude-session-parser.ts +67 -0
  388. package/src/server/claude-session-scanner.test.ts +49 -0
  389. package/src/server/claude-session-scanner.ts +24 -0
  390. package/src/server/claude-session-types.ts +61 -0
  391. package/src/server/cli-runtime.test.ts +523 -0
  392. package/src/server/cli-runtime.ts +405 -0
  393. package/src/server/cli-supervisor.ts +102 -0
  394. package/src/server/cli.ts +64 -0
  395. package/src/server/cloudflare-tunnel/agent-integration.test.ts +76 -0
  396. package/src/server/cloudflare-tunnel/agent-integration.ts +55 -0
  397. package/src/server/cloudflare-tunnel/detector.test.ts +72 -0
  398. package/src/server/cloudflare-tunnel/detector.ts +44 -0
  399. package/src/server/cloudflare-tunnel/e2e.test.ts +194 -0
  400. package/src/server/cloudflare-tunnel/events.test.ts +43 -0
  401. package/src/server/cloudflare-tunnel/events.ts +31 -0
  402. package/src/server/cloudflare-tunnel/gateway.ts +143 -0
  403. package/src/server/cloudflare-tunnel/lifecycle.test.ts +48 -0
  404. package/src/server/cloudflare-tunnel/lifecycle.ts +62 -0
  405. package/src/server/cloudflare-tunnel/read-model.test.ts +69 -0
  406. package/src/server/cloudflare-tunnel/read-model.ts +80 -0
  407. package/src/server/cloudflare-tunnel/tunnel-manager.test.ts +116 -0
  408. package/src/server/cloudflare-tunnel/tunnel-manager.ts +165 -0
  409. package/src/server/codex-app-server-protocol.ts +487 -0
  410. package/src/server/codex-app-server.test.ts +1816 -0
  411. package/src/server/codex-app-server.ts +1475 -0
  412. package/src/server/diff-store.test.ts +737 -0
  413. package/src/server/diff-store.ts +2199 -0
  414. package/src/server/discovery.test.ts +211 -0
  415. package/src/server/discovery.ts +301 -0
  416. package/src/server/event-store.test.ts +797 -0
  417. package/src/server/event-store.ts +1421 -0
  418. package/src/server/events.ts +217 -0
  419. package/src/server/external-open.test.ts +112 -0
  420. package/src/server/external-open.ts +345 -0
  421. package/src/server/generate-commit-message.test.ts +79 -0
  422. package/src/server/generate-commit-message.ts +126 -0
  423. package/src/server/generate-title.ts +76 -0
  424. package/src/server/harness-types.ts +19 -0
  425. package/src/server/keybindings.test.ts +144 -0
  426. package/src/server/keybindings.ts +178 -0
  427. package/src/server/llm-provider.test.ts +134 -0
  428. package/src/server/llm-provider.ts +207 -0
  429. package/src/server/machine-name.ts +22 -0
  430. package/src/server/paths-route.test.ts +64 -0
  431. package/src/server/paths.ts +35 -0
  432. package/src/server/process-utils.test.ts +12 -0
  433. package/src/server/process-utils.ts +47 -0
  434. package/src/server/project-paths.test.ts +95 -0
  435. package/src/server/project-paths.ts +191 -0
  436. package/src/server/provider-catalog.test.ts +69 -0
  437. package/src/server/provider-catalog.ts +87 -0
  438. package/src/server/quick-response.test.ts +440 -0
  439. package/src/server/quick-response.ts +300 -0
  440. package/src/server/read-models.test.ts +509 -0
  441. package/src/server/read-models.ts +230 -0
  442. package/src/server/restart.test.ts +27 -0
  443. package/src/server/restart.ts +30 -0
  444. package/src/server/server.ts +616 -0
  445. package/src/server/share.test.ts +180 -0
  446. package/src/server/share.ts +150 -0
  447. package/src/server/standalone-export.test.ts +224 -0
  448. package/src/server/standalone-export.ts +419 -0
  449. package/src/server/terminal-manager.test.ts +315 -0
  450. package/src/server/terminal-manager.ts +350 -0
  451. package/src/server/test-helpers/async-event-queue.ts +52 -0
  452. package/src/server/test-helpers/wait-for.ts +14 -0
  453. package/src/server/title-generation.live.test.ts +44 -0
  454. package/src/server/update-manager.test.ts +158 -0
  455. package/src/server/update-manager.ts +222 -0
  456. package/src/server/update-strategy.test.ts +237 -0
  457. package/src/server/update-strategy.ts +241 -0
  458. package/src/server/uploads.test.ts +292 -0
  459. package/src/server/uploads.ts +131 -0
  460. package/src/server/ws-router.test.ts +2292 -0
  461. package/src/server/ws-router.ts +1465 -0
  462. package/src/shared/analytics.ts +30 -0
  463. package/src/shared/branding.test.ts +31 -0
  464. package/src/shared/branding.ts +77 -0
  465. package/src/shared/dev-ports.test.ts +113 -0
  466. package/src/shared/dev-ports.ts +134 -0
  467. package/src/shared/ports.ts +2 -0
  468. package/src/shared/protocol.ts +257 -0
  469. package/src/shared/share.ts +27 -0
  470. package/src/shared/tools.test.ts +164 -0
  471. package/src/shared/tools.ts +327 -0
  472. package/src/shared/types.test.ts +25 -0
  473. package/src/shared/types.ts +1088 -0
@@ -0,0 +1,737 @@
1
+ import { afterEach, describe, expect, test } from "bun:test"
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
3
+ import { tmpdir } from "node:os"
4
+ import path from "node:path"
5
+ import { appendGitIgnoreEntry, DiffStore, extractGitHubRepoSlug, fetchGitHubPullRequests } from "./diff-store"
6
+
7
+ async function run(command: string[], cwd: string) {
8
+ const process = Bun.spawn(command, {
9
+ cwd,
10
+ stdout: "pipe",
11
+ stderr: "pipe",
12
+ })
13
+ const [stdout, stderr, exitCode] = await Promise.all([
14
+ new Response(process.stdout).text(),
15
+ new Response(process.stderr).text(),
16
+ process.exited,
17
+ ])
18
+
19
+ if (exitCode !== 0) {
20
+ throw new Error(stderr || stdout || `Command failed: ${command.join(" ")}`)
21
+ }
22
+
23
+ return stdout
24
+ }
25
+
26
+ async function createRepo() {
27
+ const root = await mkdtemp(path.join(tmpdir(), "kanna-diff-store-"))
28
+ await run(["git", "init", "-b", "main"], root)
29
+ await run(["git", "config", "user.email", "kanna@example.com"], root)
30
+ await run(["git", "config", "user.name", "Kanna"], root)
31
+ return root
32
+ }
33
+
34
+ async function createBareRemote() {
35
+ const root = await mkdtemp(path.join(tmpdir(), "kanna-diff-remote-"))
36
+ await run(["git", "init", "--bare", "-b", "main"], root)
37
+ return root
38
+ }
39
+
40
+ const tempDirs: string[] = []
41
+
42
+ describe("DiffStore", () => {
43
+ afterEach(async () => {
44
+ await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })))
45
+ })
46
+
47
+ test("returns current worktree diffs for modified files", async () => {
48
+ const repoRoot = await createRepo()
49
+ tempDirs.push(repoRoot)
50
+ await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
51
+ await run(["git", "add", "."], repoRoot)
52
+ await run(["git", "commit", "-m", "init"], repoRoot)
53
+ await writeFile(path.join(repoRoot, "app.txt"), "changed\n", "utf8")
54
+
55
+ const store = new DiffStore(repoRoot)
56
+ await store.initialize()
57
+ await store.refreshSnapshot("project-1", repoRoot)
58
+
59
+ const snapshot = store.getProjectSnapshot("project-1")
60
+ expect(snapshot.status).toBe("ready")
61
+ expect(snapshot.files).toHaveLength(1)
62
+ expect(snapshot.files[0]?.path).toBe("app.txt")
63
+ expect(snapshot.files[0]?.isUntracked).toBe(false)
64
+ expect(snapshot.files[0]?.additions).toBe(1)
65
+ expect(snapshot.files[0]?.deletions).toBe(1)
66
+ await expect(store.readPatch({ projectPath: repoRoot, path: "app.txt" })).resolves.toMatchObject({
67
+ patch: expect.stringContaining("-base"),
68
+ })
69
+ })
70
+
71
+ test("returns no_repo outside a git repository", async () => {
72
+ const root = await mkdtemp(path.join(tmpdir(), "kanna-no-repo-"))
73
+ tempDirs.push(root)
74
+
75
+ const store = new DiffStore(root)
76
+ await store.initialize()
77
+ await store.refreshSnapshot("project-1", root)
78
+
79
+ expect(store.getProjectSnapshot("project-1")).toEqual({
80
+ status: "no_repo",
81
+ branchName: undefined,
82
+ files: [],
83
+ branchHistory: { entries: [] },
84
+ })
85
+ })
86
+
87
+ test("commits only the selected files and refreshes the snapshot", async () => {
88
+ const repoRoot = await createRepo()
89
+ tempDirs.push(repoRoot)
90
+ await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
91
+ await writeFile(path.join(repoRoot, "notes.txt"), "keep\n", "utf8")
92
+ await run(["git", "add", "."], repoRoot)
93
+ await run(["git", "commit", "-m", "init"], repoRoot)
94
+
95
+ await writeFile(path.join(repoRoot, "app.txt"), "changed\n", "utf8")
96
+ await writeFile(path.join(repoRoot, "notes.txt"), "changed too\n", "utf8")
97
+
98
+ const store = new DiffStore(repoRoot)
99
+ await store.initialize()
100
+ await store.refreshSnapshot("project-1", repoRoot)
101
+
102
+ await store.commitFiles({
103
+ projectId: "project-1",
104
+ projectPath: repoRoot,
105
+ paths: ["app.txt"],
106
+ summary: "Update app",
107
+ description: "Only app changes",
108
+ mode: "commit_only",
109
+ })
110
+
111
+ const snapshot = store.getProjectSnapshot("project-1")
112
+ expect(snapshot.status).toBe("ready")
113
+ expect(snapshot.files).toHaveLength(1)
114
+ expect(snapshot.files[0]?.path).toBe("notes.txt")
115
+
116
+ const lastMessage = (await run(["git", "log", "-1", "--pretty=%B"], repoRoot)).trim()
117
+ expect(lastMessage).toBe("Update app\n\nOnly app changes")
118
+ })
119
+
120
+ test("commit_and_push publishes an unpublished branch", async () => {
121
+ const repoRoot = await createRepo()
122
+ const remoteRoot = await createBareRemote()
123
+ tempDirs.push(repoRoot, remoteRoot)
124
+ await run(["git", "remote", "add", "origin", remoteRoot], repoRoot)
125
+ await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
126
+ await run(["git", "add", "."], repoRoot)
127
+ await run(["git", "commit", "-m", "init"], repoRoot)
128
+ await run(["git", "switch", "-c", "feature/publish-me"], repoRoot)
129
+ await writeFile(path.join(repoRoot, "app.txt"), "changed\n", "utf8")
130
+
131
+ const store = new DiffStore(repoRoot)
132
+ await store.initialize()
133
+ await store.refreshSnapshot("project-1", repoRoot)
134
+
135
+ const result = await store.commitFiles({
136
+ projectId: "project-1",
137
+ projectPath: repoRoot,
138
+ paths: ["app.txt"],
139
+ summary: "Publish branch",
140
+ mode: "commit_and_push",
141
+ })
142
+
143
+ expect(result).toMatchObject({
144
+ ok: true,
145
+ mode: "commit_and_push",
146
+ pushed: true,
147
+ })
148
+ expect((await run(["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], repoRoot)).trim()).toBe("origin/feature/publish-me")
149
+ })
150
+
151
+ test("commit_and_push degrades to a local commit when origin is missing", async () => {
152
+ const repoRoot = await createRepo()
153
+ tempDirs.push(repoRoot)
154
+ await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
155
+ await run(["git", "add", "."], repoRoot)
156
+ await run(["git", "commit", "-m", "init"], repoRoot)
157
+ await writeFile(path.join(repoRoot, "app.txt"), "changed\n", "utf8")
158
+
159
+ const store = new DiffStore(repoRoot)
160
+ await store.initialize()
161
+ await store.refreshSnapshot("project-1", repoRoot)
162
+
163
+ const result = await store.commitFiles({
164
+ projectId: "project-1",
165
+ projectPath: repoRoot,
166
+ paths: ["app.txt"],
167
+ summary: "Local only",
168
+ mode: "commit_and_push",
169
+ })
170
+
171
+ expect(result).toMatchObject({
172
+ ok: true,
173
+ mode: "commit_and_push",
174
+ pushed: false,
175
+ })
176
+ expect((await run(["git", "log", "-1", "--pretty=%s"], repoRoot)).trim()).toBe("Local only")
177
+ })
178
+
179
+ test("commits tracked files inside newly ignored directories", async () => {
180
+ const repoRoot = await createRepo()
181
+ tempDirs.push(repoRoot)
182
+ await mkdir(path.join(repoRoot, "build", ".wrangler"), { recursive: true })
183
+ await writeFile(path.join(repoRoot, "build", ".wrangler", "state.sqlite"), "base\n", "utf8")
184
+ await run(["git", "add", "."], repoRoot)
185
+ await run(["git", "commit", "-m", "init"], repoRoot)
186
+
187
+ await writeFile(path.join(repoRoot, "build", ".gitignore"), ".wrangler/\n", "utf8")
188
+ await writeFile(path.join(repoRoot, "build", ".wrangler", "state.sqlite"), "changed\n", "utf8")
189
+
190
+ const store = new DiffStore(repoRoot)
191
+ await store.initialize()
192
+ await store.refreshSnapshot("project-1", repoRoot)
193
+
194
+ const result = await store.commitFiles({
195
+ projectId: "project-1",
196
+ projectPath: repoRoot,
197
+ paths: ["build/.wrangler/state.sqlite"],
198
+ summary: "Commit tracked ignored file",
199
+ mode: "commit_only",
200
+ })
201
+
202
+ expect(result).toMatchObject({
203
+ ok: true,
204
+ mode: "commit_only",
205
+ pushed: false,
206
+ })
207
+ expect((await run(["git", "log", "-1", "--pretty=%s"], repoRoot)).trim()).toBe("Commit tracked ignored file")
208
+
209
+ const snapshot = store.getProjectSnapshot("project-1")
210
+ expect(snapshot.files).toHaveLength(1)
211
+ expect(snapshot.files[0]?.path).toBe("build/.gitignore")
212
+ })
213
+
214
+ test("refreshSnapshot reports origin presence before the first commit", async () => {
215
+ const repoRoot = await createRepo()
216
+ tempDirs.push(repoRoot)
217
+ await run(["git", "remote", "add", "origin", "https://github.com/jakemor/test224.git"], repoRoot)
218
+ await writeFile(path.join(repoRoot, "poem.md"), "rose\n", "utf8")
219
+
220
+ const store = new DiffStore(repoRoot)
221
+ await store.initialize()
222
+ await store.refreshSnapshot("project-1", repoRoot)
223
+
224
+ expect(store.getProjectSnapshot("project-1")).toMatchObject({
225
+ status: "ready",
226
+ branchName: "main",
227
+ hasOriginRemote: true,
228
+ originRepoSlug: "jakemor/test224",
229
+ })
230
+ })
231
+
232
+ test("detects renamed files", async () => {
233
+ const repoRoot = await createRepo()
234
+ tempDirs.push(repoRoot)
235
+ await writeFile(path.join(repoRoot, "before.txt"), "same\n", "utf8")
236
+ await run(["git", "add", "."], repoRoot)
237
+ await run(["git", "commit", "-m", "init"], repoRoot)
238
+ await run(["git", "mv", "before.txt", "after.txt"], repoRoot)
239
+
240
+ const store = new DiffStore(repoRoot)
241
+ await store.initialize()
242
+ await store.refreshSnapshot("project-1", repoRoot)
243
+
244
+ const snapshot = store.getProjectSnapshot("project-1")
245
+ expect(snapshot.status).toBe("ready")
246
+ expect(snapshot.files).toHaveLength(1)
247
+ expect(snapshot.files[0]?.path).toBe("after.txt")
248
+ expect(snapshot.files[0]?.changeType).toBe("renamed")
249
+ expect(snapshot.files[0]?.isUntracked).toBe(false)
250
+ })
251
+
252
+ test("marks untracked files so they can be ignored", async () => {
253
+ const repoRoot = await createRepo()
254
+ tempDirs.push(repoRoot)
255
+ await writeFile(path.join(repoRoot, "tracked.txt"), "base\n", "utf8")
256
+ await run(["git", "add", "."], repoRoot)
257
+ await run(["git", "commit", "-m", "init"], repoRoot)
258
+ await writeFile(path.join(repoRoot, "scratch.log"), "tmp\n", "utf8")
259
+
260
+ const store = new DiffStore(repoRoot)
261
+ await store.initialize()
262
+ await store.refreshSnapshot("project-1", repoRoot)
263
+
264
+ const snapshot = store.getProjectSnapshot("project-1")
265
+ expect(snapshot.files).toHaveLength(1)
266
+ expect(snapshot.files[0]).toMatchObject({
267
+ path: "scratch.log",
268
+ changeType: "added",
269
+ isUntracked: true,
270
+ })
271
+ })
272
+
273
+ test("refreshSnapshot tolerates tracked files replaced by directories", async () => {
274
+ const repoRoot = await createRepo()
275
+ tempDirs.push(repoRoot)
276
+ await writeFile(path.join(repoRoot, "thing"), "base\n", "utf8")
277
+ await run(["git", "add", "."], repoRoot)
278
+ await run(["git", "commit", "-m", "init"], repoRoot)
279
+
280
+ await rm(path.join(repoRoot, "thing"), { force: true })
281
+ await mkdir(path.join(repoRoot, "thing"), { recursive: true })
282
+ await writeFile(path.join(repoRoot, "thing", "file.txt"), "nested\n", "utf8")
283
+
284
+ const store = new DiffStore(repoRoot)
285
+ await store.initialize()
286
+ await expect(store.refreshSnapshot("project-1", repoRoot)).resolves.toBe(true)
287
+
288
+ const snapshot = store.getProjectSnapshot("project-1")
289
+ expect(snapshot.status).toBe("ready")
290
+ expect(snapshot.files).toHaveLength(2)
291
+ expect(snapshot.files.map((file) => file.path)).toEqual(["thing", "thing/file.txt"])
292
+ expect(snapshot.files.map((file) => file.changeType)).toEqual(["deleted", "added"])
293
+ })
294
+
295
+ test("discardFile reverts a tracked modified file", async () => {
296
+ const repoRoot = await createRepo()
297
+ tempDirs.push(repoRoot)
298
+ await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
299
+ await run(["git", "add", "."], repoRoot)
300
+ await run(["git", "commit", "-m", "init"], repoRoot)
301
+ await writeFile(path.join(repoRoot, "app.txt"), "changed\n", "utf8")
302
+
303
+ const store = new DiffStore(repoRoot)
304
+ await store.initialize()
305
+ await store.refreshSnapshot("project-1", repoRoot)
306
+ await store.discardFile({
307
+ projectId: "project-1",
308
+ projectPath: repoRoot,
309
+ path: "app.txt",
310
+ })
311
+
312
+ expect(await readFile(path.join(repoRoot, "app.txt"), "utf8")).toBe("base\n")
313
+ expect(store.getProjectSnapshot("project-1").files).toHaveLength(0)
314
+ })
315
+
316
+ test("discardFile deletes an untracked file", async () => {
317
+ const repoRoot = await createRepo()
318
+ tempDirs.push(repoRoot)
319
+ await writeFile(path.join(repoRoot, "tracked.txt"), "base\n", "utf8")
320
+ await run(["git", "add", "."], repoRoot)
321
+ await run(["git", "commit", "-m", "init"], repoRoot)
322
+ await writeFile(path.join(repoRoot, "scratch.log"), "tmp\n", "utf8")
323
+
324
+ const store = new DiffStore(repoRoot)
325
+ await store.initialize()
326
+ await store.refreshSnapshot("project-1", repoRoot)
327
+ await store.discardFile({
328
+ projectId: "project-1",
329
+ projectPath: repoRoot,
330
+ path: "scratch.log",
331
+ })
332
+
333
+ expect(await Bun.file(path.join(repoRoot, "scratch.log")).exists()).toBe(false)
334
+ expect(store.getProjectSnapshot("project-1").files).toHaveLength(0)
335
+ })
336
+
337
+ test("discardFile reverts a renamed file", async () => {
338
+ const repoRoot = await createRepo()
339
+ tempDirs.push(repoRoot)
340
+ await writeFile(path.join(repoRoot, "before.txt"), "same\n", "utf8")
341
+ await run(["git", "add", "."], repoRoot)
342
+ await run(["git", "commit", "-m", "init"], repoRoot)
343
+ await run(["git", "mv", "before.txt", "after.txt"], repoRoot)
344
+
345
+ const store = new DiffStore(repoRoot)
346
+ await store.initialize()
347
+ await store.refreshSnapshot("project-1", repoRoot)
348
+ await store.discardFile({
349
+ projectId: "project-1",
350
+ projectPath: repoRoot,
351
+ path: "after.txt",
352
+ })
353
+
354
+ expect(await Bun.file(path.join(repoRoot, "before.txt")).exists()).toBe(true)
355
+ expect(await Bun.file(path.join(repoRoot, "after.txt")).exists()).toBe(false)
356
+ expect(store.getProjectSnapshot("project-1").files).toHaveLength(0)
357
+ })
358
+
359
+ test("ignoreFile appends a .gitignore entry once", async () => {
360
+ const repoRoot = await createRepo()
361
+ tempDirs.push(repoRoot)
362
+ await writeFile(path.join(repoRoot, "tracked.txt"), "base\n", "utf8")
363
+ await run(["git", "add", "."], repoRoot)
364
+ await run(["git", "commit", "-m", "init"], repoRoot)
365
+ await writeFile(path.join(repoRoot, "scratch.log"), "tmp\n", "utf8")
366
+
367
+ const store = new DiffStore(repoRoot)
368
+ await store.initialize()
369
+ await store.refreshSnapshot("project-1", repoRoot)
370
+ await store.ignoreFile({
371
+ projectId: "project-1",
372
+ projectPath: repoRoot,
373
+ path: "scratch.log",
374
+ })
375
+
376
+ expect(await readFile(path.join(repoRoot, ".gitignore"), "utf8")).toBe("scratch.log\n")
377
+ })
378
+
379
+ test("ignoreFile accepts a folder entry for an untracked diff", async () => {
380
+ const repoRoot = await createRepo()
381
+ tempDirs.push(repoRoot)
382
+ await writeFile(path.join(repoRoot, "tracked.txt"), "base\n", "utf8")
383
+ await run(["git", "add", "."], repoRoot)
384
+ await run(["git", "commit", "-m", "init"], repoRoot)
385
+ await mkdir(path.join(repoRoot, "tmp/cache"), { recursive: true })
386
+ await writeFile(path.join(repoRoot, "tmp/cache/output.log"), "tmp\n", "utf8")
387
+
388
+ const store = new DiffStore(repoRoot)
389
+ await store.initialize()
390
+ await store.refreshSnapshot("project-1", repoRoot)
391
+ await store.ignoreFile({
392
+ projectId: "project-1",
393
+ projectPath: repoRoot,
394
+ path: "tmp/cache/",
395
+ })
396
+
397
+ expect(await readFile(path.join(repoRoot, ".gitignore"), "utf8")).toBe("tmp/cache/\n")
398
+ })
399
+
400
+ test("appendGitIgnoreEntry does not duplicate an existing identical entry", () => {
401
+ expect(appendGitIgnoreEntry("scratch.log\n", "scratch.log")).toBe("scratch.log\n")
402
+ expect(appendGitIgnoreEntry("scratch.log", "scratch.log")).toBe("scratch.log\n")
403
+ })
404
+
405
+ test("extractGitHubRepoSlug supports common remote URL formats", () => {
406
+ expect(extractGitHubRepoSlug("git@github.com:acme/repo.git")).toBe("acme/repo")
407
+ expect(extractGitHubRepoSlug("ssh://git@github.com/acme/repo.git")).toBe("acme/repo")
408
+ expect(extractGitHubRepoSlug("https://github.com/acme/repo.git")).toBe("acme/repo")
409
+ expect(extractGitHubRepoSlug("https://gitlab.com/acme/repo.git")).toBeNull()
410
+ })
411
+
412
+ test("refreshSnapshot includes recent branch history with tags and github URLs", async () => {
413
+ const repoRoot = await createRepo()
414
+ tempDirs.push(repoRoot)
415
+ await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
416
+ await run(["git", "add", "."], repoRoot)
417
+ await run(["git", "commit", "-m", "Initial commit"], repoRoot)
418
+ await run(["git", "tag", "v1.0.0"], repoRoot)
419
+ await run(["git", "remote", "add", "origin", "git@github.com:acme/repo.git"], repoRoot)
420
+
421
+ const store = new DiffStore(repoRoot)
422
+ await store.initialize()
423
+ await store.refreshSnapshot("project-1", repoRoot)
424
+
425
+ const snapshot = store.getProjectSnapshot("project-1")
426
+ expect(snapshot.branchHistory?.entries).toHaveLength(1)
427
+ expect(snapshot.branchHistory?.entries[0]).toMatchObject({
428
+ summary: "Initial commit",
429
+ authorName: "Kanna",
430
+ tags: ["v1.0.0"],
431
+ githubUrl: expect.stringContaining("https://github.com/acme/repo/commit/"),
432
+ })
433
+ })
434
+
435
+ test("ignoreFile rejects tracked files", async () => {
436
+ const repoRoot = await createRepo()
437
+ tempDirs.push(repoRoot)
438
+ await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
439
+ await run(["git", "add", "."], repoRoot)
440
+ await run(["git", "commit", "-m", "init"], repoRoot)
441
+ await writeFile(path.join(repoRoot, "app.txt"), "changed\n", "utf8")
442
+
443
+ const store = new DiffStore(repoRoot)
444
+ await store.initialize()
445
+ await store.refreshSnapshot("project-1", repoRoot)
446
+
447
+ await expect(store.ignoreFile({
448
+ projectId: "project-1",
449
+ projectPath: repoRoot,
450
+ path: "app.txt",
451
+ })).rejects.toThrow("Only untracked files can be ignored from the diff viewer")
452
+ })
453
+
454
+ test("fetchGitHubPullRequests prefers gh api when available", async () => {
455
+ let requestedPath = ""
456
+
457
+ const pulls = await fetchGitHubPullRequests("acme/repo", {
458
+ ghApiImpl: async (path) => {
459
+ requestedPath = path
460
+ return [{ number: 7, title: "Fix bug", head: { ref: "feature/fix" } }]
461
+ },
462
+ fetchImpl: async () => {
463
+ throw new Error("fetch should not be used when gh succeeds")
464
+ },
465
+ })
466
+
467
+ expect(requestedPath).toBe("repos/acme/repo/pulls?state=open&per_page=50")
468
+ expect(pulls).toHaveLength(1)
469
+ })
470
+
471
+ test("fetchGitHubPullRequests falls back to fetch and sends the GitHub accept header", async () => {
472
+ let requestedUrl = ""
473
+ let requestedAcceptHeader = ""
474
+
475
+ const pulls = await fetchGitHubPullRequests("acme/repo", {
476
+ ghApiImpl: async () => null,
477
+ fetchImpl: async (input, init) => {
478
+ requestedUrl = String(input)
479
+ requestedAcceptHeader = String(new Headers(init?.headers).get("Accept"))
480
+ return new Response(JSON.stringify([{ number: 7, title: "Fix bug", head: { ref: "feature/fix" } }]), {
481
+ status: 200,
482
+ headers: { "Content-Type": "application/json" },
483
+ })
484
+ },
485
+ })
486
+
487
+ expect(requestedUrl).toBe("https://api.github.com/repos/acme/repo/pulls?state=open&per_page=50")
488
+ expect(requestedAcceptHeader).toBe("application/vnd.github+json")
489
+ expect(pulls).toHaveLength(1)
490
+ })
491
+
492
+ test("listBranches includes default branch, local and remote branches, and recent branches", async () => {
493
+ const repoRoot = await createRepo()
494
+ tempDirs.push(repoRoot)
495
+ await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
496
+ await run(["git", "add", "."], repoRoot)
497
+ await run(["git", "commit", "-m", "init"], repoRoot)
498
+ await run(["git", "switch", "-c", "feature/recent"], repoRoot)
499
+ await run(["git", "switch", "-c", "feature/other"], repoRoot)
500
+ await run(["git", "switch", "feature/recent"], repoRoot)
501
+ await run(["git", "switch", "main"], repoRoot).catch(async () => run(["git", "switch", "master"], repoRoot))
502
+ await run(["git", "update-ref", "refs/remotes/origin/main", "HEAD"], repoRoot).catch(() => {})
503
+ await run(["git", "symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/main"], repoRoot).catch(() => {})
504
+ await run(["git", "update-ref", "refs/remotes/origin/feature/remote", "HEAD"], repoRoot)
505
+
506
+ const store = new DiffStore(repoRoot)
507
+ await store.initialize()
508
+
509
+ const result = await store.listBranches({ projectPath: repoRoot })
510
+ expect(result.defaultBranchName).toBe("main")
511
+ expect(result.local.some((entry) => entry.name === "feature/recent")).toBe(true)
512
+ expect(result.remote.some((entry) => entry.remoteRef === "origin/feature/remote")).toBe(true)
513
+ expect(result.recent.some((entry) => entry.name === "feature/recent")).toBe(true)
514
+ })
515
+
516
+ test("listBranches hides remote PR head refs from the remote section", async () => {
517
+ const repoRoot = await createRepo()
518
+ tempDirs.push(repoRoot)
519
+ await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
520
+ await run(["git", "add", "."], repoRoot)
521
+ await run(["git", "commit", "-m", "init"], repoRoot)
522
+ await run(["git", "remote", "add", "origin", "git@github.com:acme/repo.git"], repoRoot)
523
+ await run(["git", "remote", "add", "github-desktop-jane", "git@github.com:jane/repo.git"], repoRoot)
524
+ await run(["git", "update-ref", "refs/remotes/origin/main", "HEAD"], repoRoot).catch(() => {})
525
+ await run(["git", "symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/main"], repoRoot).catch(() => {})
526
+ await run(["git", "update-ref", "refs/remotes/github-desktop-jane/feature/pr-branch", "HEAD"], repoRoot)
527
+ await run(["git", "update-ref", "refs/remotes/origin/feature/pr-branch", "HEAD"], repoRoot)
528
+ await run(["git", "update-ref", "refs/remotes/origin/feature/non-pr", "HEAD"], repoRoot)
529
+
530
+ const originalFetch = globalThis.fetch
531
+ globalThis.fetch = Object.assign(
532
+ async () => new Response(JSON.stringify([
533
+ {
534
+ number: 42,
535
+ title: "PR branch",
536
+ head: {
537
+ ref: "feature/pr-branch",
538
+ label: "jane:feature/pr-branch",
539
+ repo: {
540
+ clone_url: "git@github.com:jane/repo.git",
541
+ full_name: "jane/repo",
542
+ },
543
+ },
544
+ base: {
545
+ ref: "main",
546
+ },
547
+ },
548
+ ]), {
549
+ status: 200,
550
+ headers: { "Content-Type": "application/json" },
551
+ }),
552
+ { preconnect: originalFetch.preconnect.bind(originalFetch) }
553
+ ) as typeof fetch
554
+
555
+ try {
556
+ const store = new DiffStore(repoRoot)
557
+ await store.initialize()
558
+
559
+ const result = await store.listBranches({ projectPath: repoRoot })
560
+ expect(result.pullRequests.some((entry) => entry.prNumber === 42)).toBe(true)
561
+ expect(result.remote.some((entry) => entry.remoteRef === "github-desktop-jane/feature/pr-branch")).toBe(false)
562
+ expect(result.remote.some((entry) => entry.remoteRef === "origin/feature/pr-branch")).toBe(false)
563
+ expect(result.remote.some((entry) => entry.remoteRef === "origin/feature/non-pr")).toBe(true)
564
+ } finally {
565
+ globalThis.fetch = originalFetch
566
+ }
567
+ })
568
+
569
+ test("checkoutBranch creates a local tracking branch from a remote branch", async () => {
570
+ const repoRoot = await createRepo()
571
+ tempDirs.push(repoRoot)
572
+ await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
573
+ await run(["git", "add", "."], repoRoot)
574
+ await run(["git", "commit", "-m", "init"], repoRoot)
575
+ await run(["git", "remote", "add", "origin", "git@github.com:acme/repo.git"], repoRoot)
576
+ await run(["git", "update-ref", "refs/remotes/origin/feature/remote", "HEAD"], repoRoot)
577
+
578
+ const store = new DiffStore(repoRoot)
579
+ await store.initialize()
580
+ const result = await store.checkoutBranch({
581
+ projectId: "project-1",
582
+ projectPath: repoRoot,
583
+ branch: { kind: "remote", name: "feature/remote", remoteRef: "origin/feature/remote" },
584
+ })
585
+
586
+ expect(result.ok).toBe(true)
587
+ expect((await run(["git", "branch", "--show-current"], repoRoot)).trim()).toBe("feature/remote")
588
+ })
589
+
590
+ test("checkoutBranch cancels when changes exist and bringChanges is false", async () => {
591
+ const repoRoot = await createRepo()
592
+ tempDirs.push(repoRoot)
593
+ await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
594
+ await run(["git", "add", "."], repoRoot)
595
+ await run(["git", "commit", "-m", "init"], repoRoot)
596
+ await run(["git", "switch", "-c", "feature/other"], repoRoot)
597
+ await run(["git", "switch", "main"], repoRoot).catch(async () => run(["git", "switch", "master"], repoRoot))
598
+ await writeFile(path.join(repoRoot, "app.txt"), "changed\n", "utf8")
599
+
600
+ const store = new DiffStore(repoRoot)
601
+ await store.initialize()
602
+ const result = await store.checkoutBranch({
603
+ projectId: "project-1",
604
+ projectPath: repoRoot,
605
+ branch: { kind: "local", name: "feature/other" },
606
+ bringChanges: false,
607
+ })
608
+
609
+ expect(result.ok).toBe(false)
610
+ if (!result.ok) {
611
+ expect(result.cancelled).toBe(true)
612
+ }
613
+ })
614
+
615
+ test("createBranch creates and checks out a branch from a chosen base", async () => {
616
+ const repoRoot = await createRepo()
617
+ tempDirs.push(repoRoot)
618
+ await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
619
+ await run(["git", "add", "."], repoRoot)
620
+ await run(["git", "commit", "-m", "init"], repoRoot)
621
+ await run(["git", "switch", "-c", "feature/base"], repoRoot)
622
+
623
+ const store = new DiffStore(repoRoot)
624
+ await store.initialize()
625
+ const result = await store.createBranch({
626
+ projectId: "project-1",
627
+ projectPath: repoRoot,
628
+ name: "feature/new",
629
+ baseBranchName: "feature/base",
630
+ })
631
+
632
+ expect(result.ok).toBe(true)
633
+ expect((await run(["git", "branch", "--show-current"], repoRoot)).trim()).toBe("feature/new")
634
+ })
635
+
636
+ test("previewMergeBranch reports up-to-date and mergeable states", async () => {
637
+ const repoRoot = await createRepo()
638
+ tempDirs.push(repoRoot)
639
+ await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
640
+ await run(["git", "add", "."], repoRoot)
641
+ await run(["git", "commit", "-m", "init"], repoRoot)
642
+ await run(["git", "switch", "-c", "feature/preview"], repoRoot)
643
+
644
+ const store = new DiffStore(repoRoot)
645
+ await store.initialize()
646
+
647
+ const upToDatePreview = await store.previewMergeBranch({
648
+ projectPath: repoRoot,
649
+ branch: { kind: "local", name: "main" },
650
+ })
651
+
652
+ expect(upToDatePreview.status).toBe("up_to_date")
653
+ expect(upToDatePreview.commitCount).toBe(0)
654
+
655
+ await writeFile(path.join(repoRoot, "app.txt"), "feature\n", "utf8")
656
+ await run(["git", "commit", "-am", "feature"], repoRoot)
657
+ await run(["git", "switch", "main"], repoRoot)
658
+
659
+ const mergeablePreview = await store.previewMergeBranch({
660
+ projectPath: repoRoot,
661
+ branch: { kind: "local", name: "feature/preview" },
662
+ })
663
+
664
+ expect(mergeablePreview.status).toBe("mergeable")
665
+ expect(mergeablePreview.commitCount).toBe(1)
666
+ expect(mergeablePreview.hasConflicts).toBe(false)
667
+ })
668
+
669
+ test("previewMergeBranch detects likely conflicts", async () => {
670
+ const repoRoot = await createRepo()
671
+ tempDirs.push(repoRoot)
672
+ await writeFile(path.join(repoRoot, "conflict.txt"), "base\n", "utf8")
673
+ await run(["git", "add", "."], repoRoot)
674
+ await run(["git", "commit", "-m", "init"], repoRoot)
675
+ await run(["git", "switch", "-c", "feature/conflict"], repoRoot)
676
+ await writeFile(path.join(repoRoot, "conflict.txt"), "feature\n", "utf8")
677
+ await run(["git", "commit", "-am", "feature"], repoRoot)
678
+ await run(["git", "switch", "main"], repoRoot)
679
+ await writeFile(path.join(repoRoot, "conflict.txt"), "main\n", "utf8")
680
+ await run(["git", "commit", "-am", "main"], repoRoot)
681
+
682
+ const store = new DiffStore(repoRoot)
683
+ await store.initialize()
684
+
685
+ const preview = await store.previewMergeBranch({
686
+ projectPath: repoRoot,
687
+ branch: { kind: "local", name: "feature/conflict" },
688
+ })
689
+
690
+ expect(preview.status).toBe("conflicts")
691
+ expect(preview.hasConflicts).toBe(true)
692
+ expect(preview.commitCount).toBe(1)
693
+ })
694
+
695
+ test("mergeBranch blocks dirty worktrees and merges clean branches", async () => {
696
+ const repoRoot = await createRepo()
697
+ tempDirs.push(repoRoot)
698
+ await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
699
+ await run(["git", "add", "."], repoRoot)
700
+ await run(["git", "commit", "-m", "init"], repoRoot)
701
+ await run(["git", "switch", "-c", "feature/merge"], repoRoot)
702
+ await writeFile(path.join(repoRoot, "app.txt"), "feature\n", "utf8")
703
+ await run(["git", "commit", "-am", "feature"], repoRoot)
704
+ await run(["git", "switch", "main"], repoRoot)
705
+ await writeFile(path.join(repoRoot, "scratch.txt"), "dirty\n", "utf8")
706
+
707
+ const store = new DiffStore(repoRoot)
708
+ await store.initialize()
709
+
710
+ const blockedResult = await store.mergeBranch({
711
+ projectId: "project-1",
712
+ projectPath: repoRoot,
713
+ branch: { kind: "local", name: "feature/merge" },
714
+ })
715
+
716
+ expect(blockedResult).toMatchObject({
717
+ ok: false,
718
+ title: "Merge blocked",
719
+ snapshotChanged: false,
720
+ })
721
+
722
+ await rm(path.join(repoRoot, "scratch.txt"))
723
+
724
+ const mergeResult = await store.mergeBranch({
725
+ projectId: "project-1",
726
+ projectPath: repoRoot,
727
+ branch: { kind: "local", name: "feature/merge" },
728
+ })
729
+
730
+ expect(mergeResult).toMatchObject({
731
+ ok: true,
732
+ snapshotChanged: true,
733
+ })
734
+ expect((await run(["git", "branch", "--show-current"], repoRoot)).trim()).toBe("main")
735
+ expect((await run(["git", "log", "--format=%s", "-1"], repoRoot)).trim()).toBe("feature")
736
+ })
737
+ })