@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.
- package/LICENSE +22 -0
- package/README.md +355 -0
- package/bin/kanna +9 -0
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/abap-BdImnpbu.js +1 -0
- package/dist/client/assets/actionscript-3-CoDkCxhg.js +1 -0
- package/dist/client/assets/ada-bCR0ucgS.js +1 -0
- package/dist/client/assets/andromeeda-C4gqWexZ.js +1 -0
- package/dist/client/assets/angular-html-CU67Zn6k.js +1 -0
- package/dist/client/assets/angular-ts-BwZT4LLn.js +1 -0
- package/dist/client/assets/apache-Pmp26Uib.js +1 -0
- package/dist/client/assets/apex-D8_7TLub.js +1 -0
- package/dist/client/assets/apl-dKokRX4l.js +1 -0
- package/dist/client/assets/applescript-Co6uUVPk.js +1 -0
- package/dist/client/assets/ara-BRHolxvo.js +1 -0
- package/dist/client/assets/asciidoc-Ve4PFQV2.js +1 -0
- package/dist/client/assets/asm-D_Q5rh1f.js +1 -0
- package/dist/client/assets/astro-CbQHKStN.js +1 -0
- package/dist/client/assets/aurora-x-D-2ljcwZ.js +1 -0
- package/dist/client/assets/awk-DMzUqQB5.js +1 -0
- package/dist/client/assets/ayu-dark-DYE7WIF3.js +1 -0
- package/dist/client/assets/ayu-light-BA47KaF1.js +1 -0
- package/dist/client/assets/ayu-mirage-32ctXXKs.js +1 -0
- package/dist/client/assets/ballerina-BFfxhgS-.js +1 -0
- package/dist/client/assets/bat-BkioyH1T.js +1 -0
- package/dist/client/assets/beancount-k_qm7-4y.js +1 -0
- package/dist/client/assets/berry-uYugtg8r.js +1 -0
- package/dist/client/assets/bibtex-CHM0blh-.js +1 -0
- package/dist/client/assets/bicep-Bmn6On1c.js +1 -0
- package/dist/client/assets/bird2-DPOp833l.js +1 -0
- package/dist/client/assets/blade-D4QpJJKB.js +1 -0
- package/dist/client/assets/bricolage-grotesque-latin-ext-wght-normal-CcLUaPy7.woff2 +0 -0
- package/dist/client/assets/bricolage-grotesque-latin-wght-normal-DLoelf7F.woff2 +0 -0
- package/dist/client/assets/bricolage-grotesque-vietnamese-wght-normal-BUzh504Q.woff2 +0 -0
- package/dist/client/assets/bsl-BO_Y6i37.js +1 -0
- package/dist/client/assets/c-BIGW1oBm.js +1 -0
- package/dist/client/assets/c3-eo99z4R2.js +1 -0
- package/dist/client/assets/cadence-Bv_4Rxtq.js +1 -0
- package/dist/client/assets/cairo-KRGpt6FW.js +1 -0
- package/dist/client/assets/catppuccin-frappe-DFWUc33u.js +1 -0
- package/dist/client/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
- package/dist/client/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
- package/dist/client/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
- package/dist/client/assets/clarity-D53aC0YG.js +1 -0
- package/dist/client/assets/clojure-P80f7IUj.js +1 -0
- package/dist/client/assets/cmake-D1j8_8rp.js +1 -0
- package/dist/client/assets/cobol-nwyudZeR.js +1 -0
- package/dist/client/assets/codeowners-Bp6g37R7.js +1 -0
- package/dist/client/assets/codeql-DsOJ9woJ.js +1 -0
- package/dist/client/assets/coffee-Ch7k5sss.js +1 -0
- package/dist/client/assets/common-lisp-Cg-RD9OK.js +1 -0
- package/dist/client/assets/coq-DkFqJrB1.js +1 -0
- package/dist/client/assets/cpp-CofmeUqb.js +1 -0
- package/dist/client/assets/crystal-tKQVLTB8.js +1 -0
- package/dist/client/assets/csharp-COcwbKMJ.js +1 -0
- package/dist/client/assets/css-DPfMkruS.js +1 -0
- package/dist/client/assets/csv-fuZLfV_i.js +1 -0
- package/dist/client/assets/cue-D82EKSYY.js +1 -0
- package/dist/client/assets/cypher-COkxafJQ.js +1 -0
- package/dist/client/assets/d-85-TOEBH.js +1 -0
- package/dist/client/assets/dark-plus-C3mMm8J8.js +1 -0
- package/dist/client/assets/dart-CF10PKvl.js +1 -0
- package/dist/client/assets/dax-CEL-wOlO.js +1 -0
- package/dist/client/assets/desktop-BmXAJ9_W.js +1 -0
- package/dist/client/assets/diff-D97Zzqfu.js +1 -0
- package/dist/client/assets/docker-BcOcwvcX.js +1 -0
- package/dist/client/assets/dotenv-Da5cRb03.js +1 -0
- package/dist/client/assets/dracula-BzJJZx-M.js +1 -0
- package/dist/client/assets/dracula-soft-BXkSAIEj.js +1 -0
- package/dist/client/assets/dream-maker-BtqSS_iP.js +1 -0
- package/dist/client/assets/edge-BkV0erSs.js +1 -0
- package/dist/client/assets/elixir-CDX3lj18.js +1 -0
- package/dist/client/assets/elm-DbKCFpqz.js +1 -0
- package/dist/client/assets/emacs-lisp-C9XAeP06.js +1 -0
- package/dist/client/assets/erb-B12qg9BL.js +1 -0
- package/dist/client/assets/erlang-DsQrWhSR.js +1 -0
- package/dist/client/assets/everforest-dark-BgDCqdQA.js +1 -0
- package/dist/client/assets/everforest-light-C8M2exoo.js +1 -0
- package/dist/client/assets/fennel-BYunw83y.js +1 -0
- package/dist/client/assets/fish-BvzEVeQv.js +1 -0
- package/dist/client/assets/fluent-C4IJs8-o.js +1 -0
- package/dist/client/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
- package/dist/client/assets/fortran-free-form-BxgE0vQu.js +1 -0
- package/dist/client/assets/fsharp-CXgrBDvD.js +1 -0
- package/dist/client/assets/gdresource-BOOCDP_w.js +1 -0
- package/dist/client/assets/gdscript-C5YyOfLZ.js +1 -0
- package/dist/client/assets/gdshader-DkwncUOv.js +1 -0
- package/dist/client/assets/genie-D0YGMca9.js +1 -0
- package/dist/client/assets/gherkin-DyxjwDmM.js +1 -0
- package/dist/client/assets/git-commit-F4YmCXRG.js +1 -0
- package/dist/client/assets/git-rebase-r7XF79zn.js +1 -0
- package/dist/client/assets/github-dark-DHJKELXO.js +1 -0
- package/dist/client/assets/github-dark-default-Cuk6v7N8.js +1 -0
- package/dist/client/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
- package/dist/client/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
- package/dist/client/assets/github-light-DAi9KRSo.js +1 -0
- package/dist/client/assets/github-light-default-D7oLnXFd.js +1 -0
- package/dist/client/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
- package/dist/client/assets/gleam-BspZqrRM.js +1 -0
- package/dist/client/assets/glimmer-js-Rg0-pVw9.js +1 -0
- package/dist/client/assets/glimmer-ts-U6CK756n.js +1 -0
- package/dist/client/assets/glsl-DplSGwfg.js +1 -0
- package/dist/client/assets/gn-n2N0HUVH.js +1 -0
- package/dist/client/assets/gnuplot-DdkO51Og.js +1 -0
- package/dist/client/assets/go-CxLEBnE3.js +1 -0
- package/dist/client/assets/graphql-ChdNCCLP.js +1 -0
- package/dist/client/assets/groovy-gcz8RCvz.js +1 -0
- package/dist/client/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
- package/dist/client/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
- package/dist/client/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
- package/dist/client/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
- package/dist/client/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
- package/dist/client/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
- package/dist/client/assets/hack-CaT9iCJl.js +1 -0
- package/dist/client/assets/haml-B8DHNrY2.js +1 -0
- package/dist/client/assets/handlebars-BL8al0AC.js +1 -0
- package/dist/client/assets/haskell-Df6bDoY_.js +1 -0
- package/dist/client/assets/haxe-CzTSHFRz.js +1 -0
- package/dist/client/assets/hcl-BWvSN4gD.js +1 -0
- package/dist/client/assets/hjson-D5-asLiD.js +1 -0
- package/dist/client/assets/hlsl-D3lLCCz7.js +1 -0
- package/dist/client/assets/horizon-BUw7H-hv.js +1 -0
- package/dist/client/assets/horizon-bright-Cn-bp-IR.js +1 -0
- package/dist/client/assets/houston-DnULxvSX.js +1 -0
- package/dist/client/assets/html-GMplVEZG.js +1 -0
- package/dist/client/assets/html-derivative-BFtXZ54Q.js +1 -0
- package/dist/client/assets/http-jrhK8wxY.js +1 -0
- package/dist/client/assets/hurl-irOxFIW8.js +1 -0
- package/dist/client/assets/hxml-Bvhsp5Yf.js +1 -0
- package/dist/client/assets/hy-DFXneXwc.js +1 -0
- package/dist/client/assets/imba-DGztddWO.js +1 -0
- package/dist/client/assets/index-Do7324M0.css +32 -0
- package/dist/client/assets/index-ktE9DLCD.js +2620 -0
- package/dist/client/assets/ini-BEwlwnbL.js +1 -0
- package/dist/client/assets/java-CylS5w8V.js +1 -0
- package/dist/client/assets/javascript-wDzz0qaB.js +1 -0
- package/dist/client/assets/jinja-4LBKfQ-Z.js +1 -0
- package/dist/client/assets/jison-wvAkD_A8.js +1 -0
- package/dist/client/assets/json-Cp-IABpG.js +1 -0
- package/dist/client/assets/json5-C9tS-k6U.js +1 -0
- package/dist/client/assets/jsonc-Des-eS-w.js +1 -0
- package/dist/client/assets/jsonl-DcaNXYhu.js +1 -0
- package/dist/client/assets/jsonnet-DFQXde-d.js +1 -0
- package/dist/client/assets/jssm-C2t-YnRu.js +1 -0
- package/dist/client/assets/jsx-g9-lgVsj.js +1 -0
- package/dist/client/assets/julia-CxzCAyBv.js +1 -0
- package/dist/client/assets/just-Cw27pwNe.js +1 -0
- package/dist/client/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
- package/dist/client/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
- package/dist/client/assets/kanagawa-wave-DWedfzmr.js +1 -0
- package/dist/client/assets/kdl-DV7GczEv.js +1 -0
- package/dist/client/assets/kotlin-BdnUsdx6.js +1 -0
- package/dist/client/assets/kusto-DZf3V79B.js +1 -0
- package/dist/client/assets/laserwave-DUszq2jm.js +1 -0
- package/dist/client/assets/latex-CWtU0Tv5.js +1 -0
- package/dist/client/assets/lean-BZvkOJ9d.js +1 -0
- package/dist/client/assets/less-B1dDrJ26.js +1 -0
- package/dist/client/assets/light-plus-B7mTdjB0.js +1 -0
- package/dist/client/assets/liquid-DYVedYrR.js +1 -0
- package/dist/client/assets/llvm-DjAJT7YJ.js +1 -0
- package/dist/client/assets/log-2UxHyX5q.js +1 -0
- package/dist/client/assets/logo-BtOb2qkB.js +1 -0
- package/dist/client/assets/lua-BaeVxFsk.js +1 -0
- package/dist/client/assets/luau-C-HG3fhB.js +1 -0
- package/dist/client/assets/make-CHLpvVh8.js +1 -0
- package/dist/client/assets/markdown-Cvjx9yec.js +1 -0
- package/dist/client/assets/marko-CnJfTvn9.js +1 -0
- package/dist/client/assets/material-theme-D5KoaKCx.js +1 -0
- package/dist/client/assets/material-theme-darker-BfHTSMKl.js +1 -0
- package/dist/client/assets/material-theme-lighter-B0m2ddpp.js +1 -0
- package/dist/client/assets/material-theme-ocean-CyktbL80.js +1 -0
- package/dist/client/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
- package/dist/client/assets/matlab-D7o27uSR.js +1 -0
- package/dist/client/assets/mdc-BMNejdWA.js +1 -0
- package/dist/client/assets/mdx-Cmh6b_Ma.js +1 -0
- package/dist/client/assets/mermaid-mWjccvbQ.js +1 -0
- package/dist/client/assets/min-dark-CafNBF8u.js +1 -0
- package/dist/client/assets/min-light-CTRr51gU.js +1 -0
- package/dist/client/assets/mipsasm-CKIfxQSi.js +1 -0
- package/dist/client/assets/mojo-rZm6bMo-.js +1 -0
- package/dist/client/assets/monokai-D4h5O-jR.js +1 -0
- package/dist/client/assets/moonbit-_H4v1dQx.js +1 -0
- package/dist/client/assets/move-IF9eRakj.js +1 -0
- package/dist/client/assets/narrat-DRg8JJMk.js +1 -0
- package/dist/client/assets/nextflow-Zz6hmt5N.js +1 -0
- package/dist/client/assets/nextflow-groovy-BeH2EWoN.js +1 -0
- package/dist/client/assets/nginx-BpAMiNFr.js +1 -0
- package/dist/client/assets/night-owl-C39BiMTA.js +1 -0
- package/dist/client/assets/night-owl-light-CMTm3GFP.js +1 -0
- package/dist/client/assets/nim-CVrawwO9.js +1 -0
- package/dist/client/assets/nix-CwoSXNpI.js +1 -0
- package/dist/client/assets/nord-Ddv68eIx.js +1 -0
- package/dist/client/assets/nushell-Cz2AlsmD.js +1 -0
- package/dist/client/assets/objective-c-DXmwc3jG.js +1 -0
- package/dist/client/assets/objective-cpp-CLxacb5B.js +1 -0
- package/dist/client/assets/ocaml-C0hk2d4L.js +1 -0
- package/dist/client/assets/odin-BBf5iR-q.js +1 -0
- package/dist/client/assets/one-dark-pro-DVMEJ2y_.js +1 -0
- package/dist/client/assets/one-light-C3Wv6jpd.js +1 -0
- package/dist/client/assets/openscad-C4EeE6gA.js +1 -0
- package/dist/client/assets/pascal-D93ZcfNL.js +1 -0
- package/dist/client/assets/perl-C0TMdlhV.js +1 -0
- package/dist/client/assets/php-Dhbhpdrm.js +1 -0
- package/dist/client/assets/pierre-dark-DF2SEV7i.js +1 -0
- package/dist/client/assets/pierre-light-DOlZxES8.js +1 -0
- package/dist/client/assets/pkl-u5AG7uiY.js +1 -0
- package/dist/client/assets/plastic-3e1v2bzS.js +1 -0
- package/dist/client/assets/plsql-ChMvpjG-.js +1 -0
- package/dist/client/assets/po-BTJTHyun.js +1 -0
- package/dist/client/assets/poimandres-CS3Unz2-.js +1 -0
- package/dist/client/assets/polar-C0HS_06l.js +1 -0
- package/dist/client/assets/postcss-CXtECtnM.js +1 -0
- package/dist/client/assets/powerquery-CEu0bR-o.js +1 -0
- package/dist/client/assets/powershell-Dpen1YoG.js +1 -0
- package/dist/client/assets/prisma-Dd19v3D-.js +1 -0
- package/dist/client/assets/prolog-CbFg5uaA.js +1 -0
- package/dist/client/assets/proto-C7zT0LnQ.js +1 -0
- package/dist/client/assets/pug-CGlum2m_.js +1 -0
- package/dist/client/assets/puppet-BMWR74SV.js +1 -0
- package/dist/client/assets/purescript-CklMAg4u.js +1 -0
- package/dist/client/assets/python-B6aJPvgy.js +1 -0
- package/dist/client/assets/qml-3beO22l8.js +1 -0
- package/dist/client/assets/qmldir-C8lEn-DE.js +1 -0
- package/dist/client/assets/qss-IeuSbFQv.js +1 -0
- package/dist/client/assets/r-Dspwwk_N.js +1 -0
- package/dist/client/assets/racket-BqYA7rlc.js +1 -0
- package/dist/client/assets/raku-DXvB9xmW.js +1 -0
- package/dist/client/assets/razor-Uh8Bk_45.js +1 -0
- package/dist/client/assets/red-bN70gL4F.js +1 -0
- package/dist/client/assets/reg-C-SQnVFl.js +1 -0
- package/dist/client/assets/regexp-CDVJQ6XC.js +1 -0
- package/dist/client/assets/rel-C3B-1QV4.js +1 -0
- package/dist/client/assets/riscv-BM1_JUlF.js +1 -0
- package/dist/client/assets/ron-D8l8udqQ.js +1 -0
- package/dist/client/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
- package/dist/client/assets/rose-pine-moon-D4_iv3hh.js +1 -0
- package/dist/client/assets/rose-pine-qdsjHGoJ.js +1 -0
- package/dist/client/assets/rosmsg-BJDFO7_C.js +1 -0
- package/dist/client/assets/rst-BrH8l1NY.js +1 -0
- package/dist/client/assets/ruby-Dw2BHqvy.js +1 -0
- package/dist/client/assets/rust-B1yitclQ.js +1 -0
- package/dist/client/assets/sas-cz2c8ADy.js +1 -0
- package/dist/client/assets/sass-Cj5Yp3dK.js +1 -0
- package/dist/client/assets/scala-C151Ov-r.js +1 -0
- package/dist/client/assets/scheme-C98Dy4si.js +1 -0
- package/dist/client/assets/scss-OYdSNvt2.js +1 -0
- package/dist/client/assets/sdbl-DVxCFoDh.js +1 -0
- package/dist/client/assets/shaderlab-Dg9Lc6iA.js +1 -0
- package/dist/client/assets/shellscript-Yzrsuije.js +1 -0
- package/dist/client/assets/shellsession-BADoaaVG.js +1 -0
- package/dist/client/assets/slack-dark-BthQWCQV.js +1 -0
- package/dist/client/assets/slack-ochin-DqwNpetd.js +1 -0
- package/dist/client/assets/smalltalk-BERRCDM3.js +1 -0
- package/dist/client/assets/snazzy-light-Bw305WKR.js +1 -0
- package/dist/client/assets/solarized-dark-DXbdFlpD.js +1 -0
- package/dist/client/assets/solarized-light-L9t79GZl.js +1 -0
- package/dist/client/assets/solidity-rGO070M0.js +1 -0
- package/dist/client/assets/soy-Brmx7dQM.js +1 -0
- package/dist/client/assets/sparql-rVzFXLq3.js +1 -0
- package/dist/client/assets/splunk-BtCnVYZw.js +1 -0
- package/dist/client/assets/sql-BLtJtn59.js +1 -0
- package/dist/client/assets/ssh-config-_ykCGR6B.js +1 -0
- package/dist/client/assets/stata-BH5u7GGu.js +1 -0
- package/dist/client/assets/stylus-BEDo0Tqx.js +1 -0
- package/dist/client/assets/surrealql-Bq5Q-fJD.js +1 -0
- package/dist/client/assets/svelte-C_ipcX3V.js +1 -0
- package/dist/client/assets/swift-D82vCrfD.js +1 -0
- package/dist/client/assets/synthwave-84-CbfX1IO0.js +1 -0
- package/dist/client/assets/system-verilog-CnnmHF94.js +1 -0
- package/dist/client/assets/systemd-4A_iFExJ.js +1 -0
- package/dist/client/assets/talonscript-CkByrt1z.js +1 -0
- package/dist/client/assets/tasl-QIJgUcNo.js +1 -0
- package/dist/client/assets/tcl-dwOrl1Do.js +1 -0
- package/dist/client/assets/templ-P3uqSqPl.js +1 -0
- package/dist/client/assets/terraform-BETggiCN.js +1 -0
- package/dist/client/assets/tex-idrVyKtj.js +1 -0
- package/dist/client/assets/tokyo-night-hegEt444.js +1 -0
- package/dist/client/assets/toml-vGWfd6FD.js +1 -0
- package/dist/client/assets/ts-tags-zn1MmPIZ.js +1 -0
- package/dist/client/assets/tsv-B_m7g4N7.js +1 -0
- package/dist/client/assets/tsx-COt5Ahok.js +1 -0
- package/dist/client/assets/turtle-BsS91CYL.js +1 -0
- package/dist/client/assets/twig-DNn4PbVi.js +1 -0
- package/dist/client/assets/typescript-BPQ3VLAy.js +1 -0
- package/dist/client/assets/typespec-BGHnOYBU.js +1 -0
- package/dist/client/assets/typst-DHCkPAjA.js +1 -0
- package/dist/client/assets/v-BcVCzyr7.js +1 -0
- package/dist/client/assets/vala-CsfeWuGM.js +1 -0
- package/dist/client/assets/vb-D17OF-Vu.js +1 -0
- package/dist/client/assets/verilog-BQ8w6xss.js +1 -0
- package/dist/client/assets/vesper-DU1UobuO.js +1 -0
- package/dist/client/assets/vhdl-CeAyd5Ju.js +1 -0
- package/dist/client/assets/viml-CJc9bBzg.js +1 -0
- package/dist/client/assets/vitesse-black-Bkuqu6BP.js +1 -0
- package/dist/client/assets/vitesse-dark-D0r3Knsf.js +1 -0
- package/dist/client/assets/vitesse-light-CVO1_9PV.js +1 -0
- package/dist/client/assets/vue-DN_0RTcg.js +1 -0
- package/dist/client/assets/vue-html-AaS7Mt5G.js +1 -0
- package/dist/client/assets/vue-vine-CQOfvN7w.js +1 -0
- package/dist/client/assets/vyper-CDx5xZoG.js +1 -0
- package/dist/client/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/client/assets/wasm-MzD3tlZU.js +1 -0
- package/dist/client/assets/wenyan-BV7otONQ.js +1 -0
- package/dist/client/assets/wgsl-Dx-B1_4e.js +1 -0
- package/dist/client/assets/wikitext-BhOHFoWU.js +1 -0
- package/dist/client/assets/wit-5i3qLPDT.js +1 -0
- package/dist/client/assets/wolfram-lXgVvXCa.js +1 -0
- package/dist/client/assets/xml-sdJ4AIDG.js +1 -0
- package/dist/client/assets/xsl-CtQFsRM5.js +1 -0
- package/dist/client/assets/yaml-Buea-lGh.js +1 -0
- package/dist/client/assets/zenscript-DVFEvuxE.js +1 -0
- package/dist/client/assets/zig-VOosw3JB.js +1 -0
- package/dist/client/chat-sounds/Blow.mp3 +0 -0
- package/dist/client/chat-sounds/Bottle.mp3 +0 -0
- package/dist/client/chat-sounds/Frog.mp3 +0 -0
- package/dist/client/chat-sounds/Funk.mp3 +0 -0
- package/dist/client/chat-sounds/Glass.mp3 +0 -0
- package/dist/client/chat-sounds/Ping.mp3 +0 -0
- package/dist/client/chat-sounds/Pop.mp3 +0 -0
- package/dist/client/chat-sounds/Purr.mp3 +0 -0
- package/dist/client/chat-sounds/Tink.mp3 +0 -0
- package/dist/client/editor-icons/cursor.png +0 -0
- package/dist/client/editor-icons/custom.png +0 -0
- package/dist/client/editor-icons/default-app.png +0 -0
- package/dist/client/editor-icons/finder.png +0 -0
- package/dist/client/editor-icons/preview.png +0 -0
- package/dist/client/editor-icons/terminal.png +0 -0
- package/dist/client/editor-icons/windsurf.png +0 -0
- package/dist/client/editor-icons/xcode.png +0 -0
- package/dist/client/favicon.png +0 -0
- package/dist/client/fonts/body-medium.woff2 +0 -0
- package/dist/client/fonts/body-regular-italic.woff2 +0 -0
- package/dist/client/fonts/body-regular.woff2 +0 -0
- package/dist/client/fonts/body-semibold.woff2 +0 -0
- package/dist/client/icon-192.png +0 -0
- package/dist/client/icon-512.png +0 -0
- package/dist/client/icon-maskable-512.png +0 -0
- package/dist/client/icon.svg +4 -0
- package/dist/client/index.html +34 -0
- package/dist/client/manifest.webmanifest +46 -0
- package/dist/client/screenshot-light.png +0 -0
- package/dist/client/screenshot.png +0 -0
- package/dist/export-viewer/assets/bricolage-grotesque-latin-ext-wght-normal-CcLUaPy7.woff2 +0 -0
- package/dist/export-viewer/assets/bricolage-grotesque-latin-wght-normal-DLoelf7F.woff2 +0 -0
- package/dist/export-viewer/assets/bricolage-grotesque-vietnamese-wght-normal-BUzh504Q.woff2 +0 -0
- package/dist/export-viewer/assets/index-D1qUumZR.js +410 -0
- package/dist/export-viewer/assets/index-gG2nMW51.css +1 -0
- package/dist/export-viewer/editor-icons/cursor.png +0 -0
- package/dist/export-viewer/editor-icons/custom.png +0 -0
- package/dist/export-viewer/editor-icons/default-app.png +0 -0
- package/dist/export-viewer/editor-icons/finder.png +0 -0
- package/dist/export-viewer/editor-icons/preview.png +0 -0
- package/dist/export-viewer/editor-icons/terminal.png +0 -0
- package/dist/export-viewer/editor-icons/windsurf.png +0 -0
- package/dist/export-viewer/editor-icons/xcode.png +0 -0
- package/dist/export-viewer/fonts/body-medium.woff2 +0 -0
- package/dist/export-viewer/fonts/body-regular-italic.woff2 +0 -0
- package/dist/export-viewer/fonts/body-regular.woff2 +0 -0
- package/dist/export-viewer/fonts/body-semibold.woff2 +0 -0
- package/dist/export-viewer/index.html +14 -0
- package/package.json +99 -0
- package/src/server/__fixtures__/claude-session-empty.jsonl +0 -0
- package/src/server/__fixtures__/claude-session-malformed.jsonl +3 -0
- package/src/server/__fixtures__/claude-session-valid.jsonl +6 -0
- package/src/server/agent.test.ts +2369 -0
- package/src/server/agent.ts +1927 -0
- package/src/server/analytics.test.ts +313 -0
- package/src/server/analytics.ts +131 -0
- package/src/server/app-settings.test.ts +233 -0
- package/src/server/app-settings.ts +548 -0
- package/src/server/auth.test.ts +329 -0
- package/src/server/auth.ts +204 -0
- package/src/server/auto-continue/e2e.test.ts +215 -0
- package/src/server/auto-continue/events.test.ts +30 -0
- package/src/server/auto-continue/events.ts +35 -0
- package/src/server/auto-continue/limit-detector.test.ts +153 -0
- package/src/server/auto-continue/limit-detector.ts +159 -0
- package/src/server/auto-continue/read-model.test.ts +109 -0
- package/src/server/auto-continue/read-model.ts +83 -0
- package/src/server/auto-continue/schedule-manager.test.ts +155 -0
- package/src/server/auto-continue/schedule-manager.ts +116 -0
- package/src/server/claude-session-importer.test.ts +214 -0
- package/src/server/claude-session-importer.ts +187 -0
- package/src/server/claude-session-mapper.test.ts +88 -0
- package/src/server/claude-session-mapper.ts +106 -0
- package/src/server/claude-session-parser.test.ts +38 -0
- package/src/server/claude-session-parser.ts +67 -0
- package/src/server/claude-session-scanner.test.ts +49 -0
- package/src/server/claude-session-scanner.ts +24 -0
- package/src/server/claude-session-types.ts +61 -0
- package/src/server/cli-runtime.test.ts +523 -0
- package/src/server/cli-runtime.ts +405 -0
- package/src/server/cli-supervisor.ts +102 -0
- package/src/server/cli.ts +64 -0
- package/src/server/cloudflare-tunnel/agent-integration.test.ts +76 -0
- package/src/server/cloudflare-tunnel/agent-integration.ts +55 -0
- package/src/server/cloudflare-tunnel/detector.test.ts +72 -0
- package/src/server/cloudflare-tunnel/detector.ts +44 -0
- package/src/server/cloudflare-tunnel/e2e.test.ts +194 -0
- package/src/server/cloudflare-tunnel/events.test.ts +43 -0
- package/src/server/cloudflare-tunnel/events.ts +31 -0
- package/src/server/cloudflare-tunnel/gateway.ts +143 -0
- package/src/server/cloudflare-tunnel/lifecycle.test.ts +48 -0
- package/src/server/cloudflare-tunnel/lifecycle.ts +62 -0
- package/src/server/cloudflare-tunnel/read-model.test.ts +69 -0
- package/src/server/cloudflare-tunnel/read-model.ts +80 -0
- package/src/server/cloudflare-tunnel/tunnel-manager.test.ts +116 -0
- package/src/server/cloudflare-tunnel/tunnel-manager.ts +165 -0
- package/src/server/codex-app-server-protocol.ts +487 -0
- package/src/server/codex-app-server.test.ts +1816 -0
- package/src/server/codex-app-server.ts +1475 -0
- package/src/server/diff-store.test.ts +737 -0
- package/src/server/diff-store.ts +2199 -0
- package/src/server/discovery.test.ts +211 -0
- package/src/server/discovery.ts +301 -0
- package/src/server/event-store.test.ts +797 -0
- package/src/server/event-store.ts +1421 -0
- package/src/server/events.ts +217 -0
- package/src/server/external-open.test.ts +112 -0
- package/src/server/external-open.ts +345 -0
- package/src/server/generate-commit-message.test.ts +79 -0
- package/src/server/generate-commit-message.ts +126 -0
- package/src/server/generate-title.ts +76 -0
- package/src/server/harness-types.ts +19 -0
- package/src/server/keybindings.test.ts +144 -0
- package/src/server/keybindings.ts +178 -0
- package/src/server/llm-provider.test.ts +134 -0
- package/src/server/llm-provider.ts +207 -0
- package/src/server/machine-name.ts +22 -0
- package/src/server/paths-route.test.ts +64 -0
- package/src/server/paths.ts +35 -0
- package/src/server/process-utils.test.ts +12 -0
- package/src/server/process-utils.ts +47 -0
- package/src/server/project-paths.test.ts +95 -0
- package/src/server/project-paths.ts +191 -0
- package/src/server/provider-catalog.test.ts +69 -0
- package/src/server/provider-catalog.ts +87 -0
- package/src/server/quick-response.test.ts +440 -0
- package/src/server/quick-response.ts +300 -0
- package/src/server/read-models.test.ts +509 -0
- package/src/server/read-models.ts +230 -0
- package/src/server/restart.test.ts +27 -0
- package/src/server/restart.ts +30 -0
- package/src/server/server.ts +616 -0
- package/src/server/share.test.ts +180 -0
- package/src/server/share.ts +150 -0
- package/src/server/standalone-export.test.ts +224 -0
- package/src/server/standalone-export.ts +419 -0
- package/src/server/terminal-manager.test.ts +315 -0
- package/src/server/terminal-manager.ts +350 -0
- package/src/server/test-helpers/async-event-queue.ts +52 -0
- package/src/server/test-helpers/wait-for.ts +14 -0
- package/src/server/title-generation.live.test.ts +44 -0
- package/src/server/update-manager.test.ts +158 -0
- package/src/server/update-manager.ts +222 -0
- package/src/server/update-strategy.test.ts +237 -0
- package/src/server/update-strategy.ts +241 -0
- package/src/server/uploads.test.ts +292 -0
- package/src/server/uploads.ts +131 -0
- package/src/server/ws-router.test.ts +2292 -0
- package/src/server/ws-router.ts +1465 -0
- package/src/shared/analytics.ts +30 -0
- package/src/shared/branding.test.ts +31 -0
- package/src/shared/branding.ts +77 -0
- package/src/shared/dev-ports.test.ts +113 -0
- package/src/shared/dev-ports.ts +134 -0
- package/src/shared/ports.ts +2 -0
- package/src/shared/protocol.ts +257 -0
- package/src/shared/share.ts +27 -0
- package/src/shared/tools.test.ts +164 -0
- package/src/shared/tools.ts +327 -0
- package/src/shared/types.test.ts +25 -0
- 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
|
+
})
|