@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,14 @@
1
+ /** Polls until `condition()` returns true or `timeoutMs` elapses. */
2
+ export async function waitFor(
3
+ condition: () => boolean,
4
+ timeoutMs = 2000,
5
+ label = "condition",
6
+ ): Promise<void> {
7
+ const start = Date.now()
8
+ while (!condition()) {
9
+ if (Date.now() - start > timeoutMs) {
10
+ throw new Error(`Timed out waiting for ${label}`)
11
+ }
12
+ await new Promise((resolve) => setTimeout(resolve, 10))
13
+ }
14
+ }
@@ -0,0 +1,44 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { CodexAppServerManager } from "./codex-app-server"
3
+ import { fallbackTitleFromMessage, generateTitleForChatDetailed } from "./generate-title"
4
+ import { QuickResponseAdapter, runClaudeStructured, runCodexStructured } from "./quick-response"
5
+
6
+ const shouldRunLiveTests = process.env.KANNA_RUN_LIVE_TITLE_TESTS === "1"
7
+ const LIVE_MESSAGE = "Please help me debug a websocket reconnection issue in a Bun server app"
8
+
9
+ if (shouldRunLiveTests) {
10
+ describe("live title generation", () => {
11
+ test("generates a title with Claude", async () => {
12
+ const adapter = new QuickResponseAdapter({
13
+ runClaudeStructured,
14
+ runCodexStructured: async () => {
15
+ throw new Error("Codex fallback should not be used in the Claude live title test")
16
+ },
17
+ })
18
+
19
+ const result = await generateTitleForChatDetailed(LIVE_MESSAGE, process.cwd(), adapter)
20
+
21
+ expect(result.usedFallback).toBe(false)
22
+ expect(result.failureMessage).toBeNull()
23
+ expect(typeof result.title).toBe("string")
24
+ expect(result.title).not.toBe(fallbackTitleFromMessage(LIVE_MESSAGE))
25
+ }, 15_000)
26
+
27
+ test("generates a title with Codex", async () => {
28
+ const codexManager = new CodexAppServerManager()
29
+ const adapter = new QuickResponseAdapter({
30
+ runClaudeStructured: async () => {
31
+ throw new Error("Claude should not be used in the Codex live title test")
32
+ },
33
+ runCodexStructured: async (args) => runCodexStructured(codexManager, args),
34
+ })
35
+
36
+ const result = await generateTitleForChatDetailed(LIVE_MESSAGE, process.cwd(), adapter)
37
+
38
+ expect(result.usedFallback).toBe(false)
39
+ expect(result.failureMessage).toBeNull()
40
+ expect(typeof result.title).toBe("string")
41
+ expect(result.title).not.toBe(fallbackTitleFromMessage(LIVE_MESSAGE))
42
+ }, 15_000)
43
+ })
44
+ }
@@ -0,0 +1,158 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { UpdateManager } from "./update-manager"
3
+ import { UpdateInstallError, type UpdateChecker, type UpdateReloader } from "./update-strategy"
4
+
5
+ class FakeChecker implements UpdateChecker {
6
+ calls = 0
7
+ constructor(private results: Array<{ latestVersion: string; updateAvailable: boolean }>) {}
8
+ async check() {
9
+ const result = this.results[Math.min(this.calls, this.results.length - 1)]
10
+ this.calls += 1
11
+ return result
12
+ }
13
+ }
14
+
15
+ class FakeReloader implements UpdateReloader {
16
+ calls = 0
17
+ constructor(private onReload: () => Promise<void> = async () => {}) {}
18
+ async reload() {
19
+ this.calls += 1
20
+ await this.onReload()
21
+ }
22
+ }
23
+
24
+ describe("UpdateManager", () => {
25
+ test("tracks update lifecycle events", async () => {
26
+ const events: Array<{ name: string; properties?: Record<string, unknown> }> = []
27
+ const manager = new UpdateManager({
28
+ currentVersion: "0.12.0",
29
+ checker: new FakeChecker([{ latestVersion: "0.13.0", updateAvailable: true }]),
30
+ reloader: new FakeReloader(),
31
+ trackEvent: (eventName, properties) => {
32
+ events.push({ name: eventName, properties })
33
+ },
34
+ })
35
+
36
+ await manager.checkForUpdates({ force: true })
37
+ await manager.installUpdate()
38
+
39
+ expect(events).toEqual([
40
+ {
41
+ name: "update_checked",
42
+ properties: {
43
+ latest_version: "0.13.0",
44
+ },
45
+ },
46
+ {
47
+ name: "update_installed",
48
+ properties: {
49
+ latest_version: "0.13.0",
50
+ },
51
+ },
52
+ ])
53
+ })
54
+
55
+ test("detects available updates", async () => {
56
+ const manager = new UpdateManager({
57
+ currentVersion: "0.12.0",
58
+ checker: new FakeChecker([{ latestVersion: "0.13.0", updateAvailable: true }]),
59
+ reloader: new FakeReloader(),
60
+ })
61
+ const snapshot = await manager.checkForUpdates({ force: true })
62
+ expect(snapshot.status).toBe("available")
63
+ expect(snapshot.updateAvailable).toBe(true)
64
+ expect(snapshot.latestVersion).toBe("0.13.0")
65
+ expect(snapshot.installAction).toBe("restart")
66
+ expect(snapshot.reloadRequestedAt).toBeNull()
67
+ })
68
+
69
+ test("bypasses cache when force is true", async () => {
70
+ const checker = new FakeChecker([
71
+ { latestVersion: "0.12.1", updateAvailable: true },
72
+ { latestVersion: "0.13.0", updateAvailable: true },
73
+ ])
74
+ const manager = new UpdateManager({
75
+ currentVersion: "0.12.0",
76
+ checker,
77
+ reloader: new FakeReloader(),
78
+ })
79
+ await manager.checkForUpdates()
80
+ await manager.checkForUpdates({ force: true })
81
+ expect(checker.calls).toBe(2)
82
+ expect(manager.getSnapshot().latestVersion).toBe("0.13.0")
83
+ })
84
+
85
+ test("surfaces reloader failures without clearing the running version", async () => {
86
+ const reloader = new FakeReloader(async () => {
87
+ throw new UpdateInstallError(
88
+ "This update is still propagating. Try again in a few minutes.",
89
+ "version_not_live_yet",
90
+ "Update not live yet",
91
+ )
92
+ })
93
+ const manager = new UpdateManager({
94
+ currentVersion: "0.12.0",
95
+ checker: new FakeChecker([{ latestVersion: "0.13.0", updateAvailable: true }]),
96
+ reloader,
97
+ })
98
+ await manager.checkForUpdates({ force: true })
99
+ const result = await manager.installUpdate()
100
+ expect(result).toEqual({
101
+ ok: false,
102
+ action: "restart",
103
+ errorCode: "version_not_live_yet",
104
+ userTitle: "Update not live yet",
105
+ userMessage: "This update is still propagating. Try again in a few minutes.",
106
+ })
107
+ expect(reloader.calls).toBe(1)
108
+ expect(manager.getSnapshot().status).toBe("error")
109
+ expect(manager.getSnapshot().currentVersion).toBe("0.12.0")
110
+ })
111
+
112
+ test("falls back to generic errorCode/userTitle when reloader throws a plain Error", async () => {
113
+ const reloader = new FakeReloader(async () => {
114
+ throw new Error("random failure")
115
+ })
116
+ const manager = new UpdateManager({
117
+ currentVersion: "0.12.0",
118
+ checker: new FakeChecker([{ latestVersion: "0.13.0", updateAvailable: true }]),
119
+ reloader,
120
+ })
121
+ await manager.checkForUpdates({ force: true })
122
+ const result = await manager.installUpdate()
123
+ expect(result).toEqual({
124
+ ok: false,
125
+ action: "restart",
126
+ errorCode: "install_failed",
127
+ userTitle: "Update failed",
128
+ userMessage: "random failure",
129
+ })
130
+ expect(manager.getSnapshot().status).toBe("error")
131
+ expect(manager.getSnapshot().error).toBe("random failure")
132
+ })
133
+
134
+ test("always exposes an available reload action in dev mode", async () => {
135
+ const manager = new UpdateManager({
136
+ currentVersion: "0.12.0",
137
+ checker: new FakeChecker([{ latestVersion: "9.9.9", updateAvailable: true }]),
138
+ reloader: new FakeReloader(),
139
+ devMode: true,
140
+ })
141
+ expect(manager.getSnapshot()).toMatchObject({
142
+ status: "available",
143
+ updateAvailable: true,
144
+ installAction: "restart",
145
+ reloadRequestedAt: null,
146
+ })
147
+ const result = await manager.installUpdate()
148
+ expect(result).toEqual({
149
+ ok: true,
150
+ action: "restart",
151
+ errorCode: null,
152
+ userTitle: null,
153
+ userMessage: null,
154
+ })
155
+ expect(manager.getSnapshot().status).toBe("restart_pending")
156
+ expect(typeof manager.getSnapshot().reloadRequestedAt).toBe("number")
157
+ })
158
+ })
@@ -0,0 +1,222 @@
1
+ import type { UpdateInstallResult, UpdateSnapshot } from "../shared/types"
2
+ import { UpdateInstallError, type UpdateChecker, type UpdateReloader } from "./update-strategy"
3
+
4
+ const UPDATE_CACHE_TTL_MS = 5 * 60 * 1000
5
+
6
+ export interface UpdateManagerDeps {
7
+ currentVersion: string
8
+ checker: UpdateChecker
9
+ reloader: UpdateReloader
10
+ devMode?: boolean
11
+ trackEvent?: (eventName: string, properties?: Record<string, unknown>) => void
12
+ }
13
+
14
+ export class UpdateManager {
15
+ private readonly deps: UpdateManagerDeps
16
+ private readonly listeners = new Set<(snapshot: UpdateSnapshot) => void>()
17
+ private snapshot: UpdateSnapshot
18
+ private checkPromise: Promise<UpdateSnapshot> | null = null
19
+ private installPromise: Promise<UpdateInstallResult> | null = null
20
+
21
+ constructor(deps: UpdateManagerDeps) {
22
+ this.deps = deps
23
+ this.snapshot = {
24
+ currentVersion: deps.currentVersion,
25
+ latestVersion: deps.devMode ? `${deps.currentVersion}-dev` : null,
26
+ status: deps.devMode ? "available" : "idle",
27
+ updateAvailable: Boolean(deps.devMode),
28
+ lastCheckedAt: deps.devMode ? Date.now() : null,
29
+ error: null,
30
+ installAction: "restart",
31
+ reloadRequestedAt: null,
32
+ }
33
+ }
34
+
35
+ getSnapshot() {
36
+ return this.snapshot
37
+ }
38
+
39
+ onChange(listener: (snapshot: UpdateSnapshot) => void) {
40
+ this.listeners.add(listener)
41
+ return () => {
42
+ this.listeners.delete(listener)
43
+ }
44
+ }
45
+
46
+ async checkForUpdates(options: { force?: boolean } = {}) {
47
+ if (this.deps.devMode) return this.snapshot
48
+ if (this.snapshot.status === "updating" || this.snapshot.status === "restart_pending") return this.snapshot
49
+ if (this.checkPromise) return this.checkPromise
50
+ if (!options.force && this.snapshot.lastCheckedAt && Date.now() - this.snapshot.lastCheckedAt < UPDATE_CACHE_TTL_MS) {
51
+ return this.snapshot
52
+ }
53
+
54
+ this.setSnapshot({ ...this.snapshot, status: "checking", error: null, reloadRequestedAt: null })
55
+
56
+ const checkPromise = this.runCheck()
57
+ this.checkPromise = checkPromise
58
+ try {
59
+ return await checkPromise
60
+ } finally {
61
+ if (this.checkPromise === checkPromise) this.checkPromise = null
62
+ }
63
+ }
64
+
65
+ async forceReload(): Promise<UpdateInstallResult> {
66
+ if (this.deps.devMode) {
67
+ this.setSnapshot({ ...this.snapshot, status: "restart_pending", reloadRequestedAt: Date.now(), error: null })
68
+ return { ok: true, action: "restart", errorCode: null, userTitle: null, userMessage: null }
69
+ }
70
+
71
+ if (this.snapshot.status === "updating" || this.snapshot.status === "restart_pending") {
72
+ return { ok: true, action: "restart", errorCode: null, userTitle: null, userMessage: null }
73
+ }
74
+
75
+ this.setSnapshot({ ...this.snapshot, status: "updating", error: null, reloadRequestedAt: null })
76
+
77
+ try {
78
+ await this.deps.reloader.reload()
79
+ } catch (error) {
80
+ const installError = error instanceof UpdateInstallError ? error : null
81
+ const message = error instanceof Error ? error.message : String(error)
82
+ this.setSnapshot({ ...this.snapshot, status: "error", error: message, reloadRequestedAt: null })
83
+ return {
84
+ ok: false,
85
+ action: "restart",
86
+ errorCode: installError?.errorCode ?? "install_failed",
87
+ userTitle: installError?.userTitle ?? "Re-deploy failed",
88
+ userMessage: installError?.message ?? message,
89
+ }
90
+ }
91
+
92
+ this.setSnapshot({
93
+ ...this.snapshot,
94
+ status: "restart_pending",
95
+ error: null,
96
+ reloadRequestedAt: Date.now(),
97
+ })
98
+ return { ok: true, action: "restart", errorCode: null, userTitle: null, userMessage: null }
99
+ }
100
+
101
+ async installUpdate(): Promise<UpdateInstallResult> {
102
+ if (this.deps.devMode) {
103
+ this.deps.trackEvent?.("update_installed", {
104
+ latest_version: this.snapshot.latestVersion,
105
+ })
106
+ this.setSnapshot({ ...this.snapshot, status: "updating", error: null, reloadRequestedAt: null })
107
+ this.setSnapshot({
108
+ ...this.snapshot,
109
+ status: "restart_pending",
110
+ updateAvailable: false,
111
+ error: null,
112
+ reloadRequestedAt: Date.now(),
113
+ })
114
+ return { ok: true, action: "restart", errorCode: null, userTitle: null, userMessage: null }
115
+ }
116
+
117
+ if (this.snapshot.status === "updating" || this.snapshot.status === "restart_pending") {
118
+ return {
119
+ ok: this.snapshot.updateAvailable,
120
+ action: "restart",
121
+ errorCode: null,
122
+ userTitle: null,
123
+ userMessage: null,
124
+ }
125
+ }
126
+
127
+ if (this.installPromise) return this.installPromise
128
+
129
+ const installPromise = this.runInstall()
130
+ this.installPromise = installPromise
131
+ try {
132
+ return await installPromise
133
+ } finally {
134
+ if (this.installPromise === installPromise) this.installPromise = null
135
+ }
136
+ }
137
+
138
+ private async runCheck() {
139
+ try {
140
+ const { latestVersion, updateAvailable } = await this.deps.checker.check()
141
+ const nextSnapshot: UpdateSnapshot = {
142
+ ...this.snapshot,
143
+ latestVersion,
144
+ updateAvailable,
145
+ status: updateAvailable ? "available" : "up_to_date",
146
+ lastCheckedAt: Date.now(),
147
+ error: null,
148
+ reloadRequestedAt: null,
149
+ }
150
+ this.setSnapshot(nextSnapshot)
151
+ this.deps.trackEvent?.("update_checked", {
152
+ latest_version: latestVersion,
153
+ })
154
+ return nextSnapshot
155
+ } catch (error) {
156
+ const nextSnapshot: UpdateSnapshot = {
157
+ ...this.snapshot,
158
+ status: "error",
159
+ lastCheckedAt: Date.now(),
160
+ error: error instanceof Error ? error.message : String(error),
161
+ reloadRequestedAt: null,
162
+ }
163
+ this.setSnapshot(nextSnapshot)
164
+ this.deps.trackEvent?.("update_failed", {
165
+ latest_version: this.snapshot.latestVersion,
166
+ })
167
+ return nextSnapshot
168
+ }
169
+ }
170
+
171
+ private async runInstall(): Promise<UpdateInstallResult> {
172
+ if (!this.snapshot.updateAvailable) {
173
+ const snapshot = await this.checkForUpdates({ force: true })
174
+ if (!snapshot.updateAvailable) {
175
+ return { ok: false, action: "restart", errorCode: null, userTitle: null, userMessage: null }
176
+ }
177
+ }
178
+
179
+ this.setSnapshot({ ...this.snapshot, status: "updating", error: null, reloadRequestedAt: null })
180
+
181
+ try {
182
+ await this.deps.reloader.reload()
183
+ } catch (error) {
184
+ const installError = error instanceof UpdateInstallError ? error : null
185
+ const message = error instanceof Error ? error.message : String(error)
186
+ this.setSnapshot({
187
+ ...this.snapshot,
188
+ status: "error",
189
+ error: message,
190
+ reloadRequestedAt: null,
191
+ })
192
+ this.deps.trackEvent?.("update_failed", {
193
+ latest_version: null,
194
+ })
195
+ return {
196
+ ok: false,
197
+ action: "restart",
198
+ errorCode: installError?.errorCode ?? "install_failed",
199
+ userTitle: installError?.userTitle ?? "Update failed",
200
+ userMessage: installError?.message ?? message,
201
+ }
202
+ }
203
+
204
+ this.setSnapshot({
205
+ ...this.snapshot,
206
+ currentVersion: this.snapshot.latestVersion ?? this.snapshot.currentVersion,
207
+ status: "restart_pending",
208
+ updateAvailable: false,
209
+ error: null,
210
+ reloadRequestedAt: Date.now(),
211
+ })
212
+ this.deps.trackEvent?.("update_installed", {
213
+ latest_version: this.snapshot.latestVersion,
214
+ })
215
+ return { ok: true, action: "restart", errorCode: null, userTitle: null, userMessage: null }
216
+ }
217
+
218
+ private setSnapshot(snapshot: UpdateSnapshot) {
219
+ this.snapshot = snapshot
220
+ for (const listener of this.listeners) listener(snapshot)
221
+ }
222
+ }
@@ -0,0 +1,237 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { NpmChecker, SupervisorExitReloader, UpdateInstallError, createUpdateStrategy, GitChecker, Pm2Reloader } from "./update-strategy"
3
+
4
+ describe("NpmChecker", () => {
5
+ test("reports update available when latest is newer", async () => {
6
+ const checker = new NpmChecker({
7
+ currentVersion: "0.12.0",
8
+ fetchLatestVersion: async () => "0.13.0",
9
+ })
10
+ const result = await checker.check()
11
+ expect(result).toEqual({ latestVersion: "0.13.0", updateAvailable: true })
12
+ })
13
+
14
+ test("reports no update when versions match", async () => {
15
+ const checker = new NpmChecker({
16
+ currentVersion: "0.13.0",
17
+ fetchLatestVersion: async () => "0.13.0",
18
+ })
19
+ const result = await checker.check()
20
+ expect(result).toEqual({ latestVersion: "0.13.0", updateAvailable: false })
21
+ })
22
+
23
+ test("propagates fetch errors", async () => {
24
+ const checker = new NpmChecker({
25
+ currentVersion: "0.12.0",
26
+ fetchLatestVersion: async () => { throw new Error("registry down") },
27
+ })
28
+ await expect(checker.check()).rejects.toThrow("registry down")
29
+ })
30
+ })
31
+
32
+ describe("SupervisorExitReloader", () => {
33
+ test("installs target version when invoked", async () => {
34
+ const calls: Array<{ packageName: string; version: string }> = []
35
+ const reloader = new SupervisorExitReloader({
36
+ targetVersion: () => "0.13.0",
37
+ installVersion: (packageName, version) => {
38
+ calls.push({ packageName, version })
39
+ return { ok: true, errorCode: null, userTitle: null, userMessage: null }
40
+ },
41
+ })
42
+ await reloader.reload()
43
+ expect(calls).toEqual([{ packageName: "@cuongtran001/kanna", version: "0.13.0" }])
44
+ })
45
+
46
+ test("throws UpdateInstallError with structured fields when install fails", async () => {
47
+ const reloader = new SupervisorExitReloader({
48
+ targetVersion: () => "0.13.0",
49
+ installVersion: () => ({
50
+ ok: false,
51
+ errorCode: "version_not_live_yet",
52
+ userTitle: "Update not live yet",
53
+ userMessage: "This update is still propagating. Try again in a few minutes.",
54
+ }),
55
+ })
56
+ await expect(reloader.reload()).rejects.toBeInstanceOf(UpdateInstallError)
57
+ await expect(reloader.reload()).rejects.toMatchObject({
58
+ message: "This update is still propagating. Try again in a few minutes.",
59
+ errorCode: "version_not_live_yet",
60
+ userTitle: "Update not live yet",
61
+ })
62
+ })
63
+
64
+ test("throws when target version cannot be resolved", async () => {
65
+ const reloader = new SupervisorExitReloader({
66
+ targetVersion: () => null,
67
+ installVersion: () => ({ ok: true, errorCode: null, userTitle: null, userMessage: null }),
68
+ })
69
+ await expect(reloader.reload()).rejects.toThrow(/target version/i)
70
+ })
71
+ })
72
+
73
+ describe("GitChecker", () => {
74
+ const makeRunGit = (responses: Record<string, string>) => async (args: string[]) => {
75
+ const key = args.join(" ")
76
+ if (!(key in responses)) throw new Error(`unexpected git call: ${key}`)
77
+ return responses[key]
78
+ }
79
+
80
+ test("reports update when HEAD differs from upstream", async () => {
81
+ const checker = new GitChecker({
82
+ repoDir: "/tmp/repo",
83
+ branch: "main",
84
+ runGit: makeRunGit({
85
+ "fetch origin main": "",
86
+ "rev-parse HEAD": "abc123def456\n",
87
+ "rev-parse origin/main": "deadbeef99887\n",
88
+ }),
89
+ })
90
+ const result = await checker.check()
91
+ expect(result).toEqual({ latestVersion: "deadbee", updateAvailable: true })
92
+ })
93
+
94
+ test("reports no update when HEAD matches upstream", async () => {
95
+ const checker = new GitChecker({
96
+ repoDir: "/tmp/repo",
97
+ branch: "main",
98
+ runGit: makeRunGit({
99
+ "fetch origin main": "",
100
+ "rev-parse HEAD": "abc123def456\n",
101
+ "rev-parse origin/main": "abc123def456\n",
102
+ }),
103
+ })
104
+ const result = await checker.check()
105
+ expect(result).toEqual({ latestVersion: "abc123d", updateAvailable: false })
106
+ })
107
+
108
+ test("propagates git fetch errors", async () => {
109
+ const checker = new GitChecker({
110
+ repoDir: "/tmp/repo",
111
+ branch: "main",
112
+ runGit: async () => { throw new Error("fetch failed: network") },
113
+ })
114
+ await expect(checker.check()).rejects.toThrow(/fetch failed/)
115
+ })
116
+ })
117
+
118
+ describe("createUpdateStrategy", () => {
119
+ const baseDeps = {
120
+ currentVersion: "0.12.0",
121
+ fetchLatestVersion: async () => "0.13.0",
122
+ installVersion: () => ({ ok: true, errorCode: null, userTitle: null, userMessage: null }),
123
+ latestVersionHint: () => "0.13.0",
124
+ } as const
125
+
126
+ test("defaults to npm + supervisor-exit when env unset", () => {
127
+ const strategy = createUpdateStrategy({ reloaderEnv: undefined, ...baseDeps })
128
+ expect(strategy.checker).toBeInstanceOf(NpmChecker)
129
+ expect(strategy.reloader).toBeInstanceOf(SupervisorExitReloader)
130
+ })
131
+
132
+ test("uses npm + supervisor-exit when env=supervisor", () => {
133
+ const strategy = createUpdateStrategy({ reloaderEnv: "supervisor", ...baseDeps })
134
+ expect(strategy.checker).toBeInstanceOf(NpmChecker)
135
+ expect(strategy.reloader).toBeInstanceOf(SupervisorExitReloader)
136
+ })
137
+
138
+ test("throws on unknown reloader value", () => {
139
+ expect(() => createUpdateStrategy({ reloaderEnv: "bogus", ...baseDeps })).toThrow(/unknown.*reloader/i)
140
+ })
141
+ })
142
+
143
+ describe("Pm2Reloader", () => {
144
+ function makeReloader(overrides: {
145
+ lockfileChanged?: boolean
146
+ commandErrors?: Record<string, string>
147
+ reloadError?: Error | null
148
+ } = {}) {
149
+ const calls: string[] = []
150
+ const reloader = new Pm2Reloader({
151
+ repoDir: "/tmp/repo",
152
+ processName: "kanna",
153
+ runCommand: async (command, args) => {
154
+ const line = [command, ...args].join(" ")
155
+ calls.push(line)
156
+ if (overrides.commandErrors?.[line]) {
157
+ throw new Error(overrides.commandErrors[line])
158
+ }
159
+ },
160
+ lockfileChanged: async () => overrides.lockfileChanged ?? false,
161
+ triggerPm2Reload: async () => {
162
+ calls.push("pm2.reload kanna")
163
+ if (overrides.reloadError) throw overrides.reloadError
164
+ },
165
+ })
166
+ return { reloader, calls }
167
+ }
168
+
169
+ test("runs git pull, build, then pm2 reload when lockfile unchanged", async () => {
170
+ const { reloader, calls } = makeReloader({ lockfileChanged: false })
171
+ await reloader.reload()
172
+ expect(calls).toEqual([
173
+ "git pull --ff-only",
174
+ "bun run build",
175
+ "pm2.reload kanna",
176
+ ])
177
+ })
178
+
179
+ test("inserts bun install when lockfile changed", async () => {
180
+ const { reloader, calls } = makeReloader({ lockfileChanged: true })
181
+ await reloader.reload()
182
+ expect(calls).toEqual([
183
+ "git pull --ff-only",
184
+ "bun install",
185
+ "bun run build",
186
+ "pm2.reload kanna",
187
+ ])
188
+ })
189
+
190
+ test("aborts before reload when git pull fails", async () => {
191
+ const { reloader, calls } = makeReloader({
192
+ commandErrors: { "git pull --ff-only": "merge conflict in src/foo.ts" },
193
+ })
194
+ await expect(reloader.reload()).rejects.toThrow(/git pull failed/i)
195
+ expect(calls).toEqual(["git pull --ff-only"])
196
+ })
197
+
198
+ test("aborts before reload when build fails", async () => {
199
+ const { reloader, calls } = makeReloader({
200
+ commandErrors: { "bun run build": "tsc error TS2345" },
201
+ })
202
+ await expect(reloader.reload()).rejects.toThrow(/bun run build failed/i)
203
+ expect(calls).toEqual(["git pull --ff-only", "bun run build"])
204
+ })
205
+
206
+ test("surfaces pm2 reload failures", async () => {
207
+ const { reloader } = makeReloader({ reloadError: new Error("pm2 daemon not running") })
208
+ await expect(reloader.reload()).rejects.toThrow(/pm2 reload failed/i)
209
+ })
210
+ })
211
+
212
+ describe("createUpdateStrategy pm2 branch", () => {
213
+ test("returns GitChecker + Pm2Reloader for KANNA_RELOADER=pm2", () => {
214
+ const strategy = createUpdateStrategy({
215
+ reloaderEnv: "pm2",
216
+ currentVersion: "0.12.0",
217
+ fetchLatestVersion: async () => "ignored",
218
+ installVersion: () => ({ ok: true, errorCode: null, userTitle: null, userMessage: null }),
219
+ latestVersionHint: () => null,
220
+ repoDir: "/tmp/repo",
221
+ })
222
+ expect(strategy.checker).toBeInstanceOf(GitChecker)
223
+ expect(strategy.reloader).toBeInstanceOf(Pm2Reloader)
224
+ })
225
+
226
+ test("throws when pm2 mode selected without repoDir", () => {
227
+ expect(() =>
228
+ createUpdateStrategy({
229
+ reloaderEnv: "pm2",
230
+ currentVersion: "0.12.0",
231
+ fetchLatestVersion: async () => "ignored",
232
+ installVersion: () => ({ ok: true, errorCode: null, userTitle: null, userMessage: null }),
233
+ latestVersionHint: () => null,
234
+ }),
235
+ ).toThrow(/KANNA_REPO_DIR/)
236
+ })
237
+ })