relay-ide 0.1.0
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/README.md +259 -0
- package/dist/bin/claude-remote-cli.js +390 -0
- package/dist/bin/relay-ide.js +390 -0
- package/dist/frontend/assets/abap-BdImnpbu.js +1 -0
- package/dist/frontend/assets/actionscript-3-CoDkCxhg.js +1 -0
- package/dist/frontend/assets/ada-bCR0ucgS.js +1 -0
- package/dist/frontend/assets/andromeeda-C4gqWexZ.js +1 -0
- package/dist/frontend/assets/angular-html-DA-rfuFy.js +1 -0
- package/dist/frontend/assets/angular-ts-BrjP3tb8.js +1 -0
- package/dist/frontend/assets/apache-Pmp26Uib.js +1 -0
- package/dist/frontend/assets/apex-D8_7TLub.js +1 -0
- package/dist/frontend/assets/apl-CORt7UWP.js +1 -0
- package/dist/frontend/assets/applescript-Co6uUVPk.js +1 -0
- package/dist/frontend/assets/ara-BRHolxvo.js +1 -0
- package/dist/frontend/assets/asciidoc-Ve4PFQV2.js +1 -0
- package/dist/frontend/assets/asm-D_Q5rh1f.js +1 -0
- package/dist/frontend/assets/astro-HNnZUWAn.js +1 -0
- package/dist/frontend/assets/aurora-x-D-2ljcwZ.js +1 -0
- package/dist/frontend/assets/awk-DMzUqQB5.js +1 -0
- package/dist/frontend/assets/ayu-dark-DYE7WIF3.js +1 -0
- package/dist/frontend/assets/ayu-light-BA47KaF1.js +1 -0
- package/dist/frontend/assets/ayu-mirage-32ctXXKs.js +1 -0
- package/dist/frontend/assets/ballerina-BFfxhgS-.js +1 -0
- package/dist/frontend/assets/bat-BkioyH1T.js +1 -0
- package/dist/frontend/assets/beancount-k_qm7-4y.js +1 -0
- package/dist/frontend/assets/berry-uYugtg8r.js +1 -0
- package/dist/frontend/assets/bibtex-CHM0blh-.js +1 -0
- package/dist/frontend/assets/bicep-Bmn6On1c.js +1 -0
- package/dist/frontend/assets/bird2-BIv1doCn.js +1 -0
- package/dist/frontend/assets/blade-BjGOyj-B.js +1 -0
- package/dist/frontend/assets/bsl-BO_Y6i37.js +1 -0
- package/dist/frontend/assets/c-BIGW1oBm.js +1 -0
- package/dist/frontend/assets/c3-eo99z4R2.js +1 -0
- package/dist/frontend/assets/cadence-Bv_4Rxtq.js +1 -0
- package/dist/frontend/assets/cairo-KRGpt6FW.js +1 -0
- package/dist/frontend/assets/catppuccin-frappe-DFWUc33u.js +1 -0
- package/dist/frontend/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
- package/dist/frontend/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
- package/dist/frontend/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
- package/dist/frontend/assets/clarity-D53aC0YG.js +1 -0
- package/dist/frontend/assets/clojure-P80f7IUj.js +1 -0
- package/dist/frontend/assets/cmake-D1j8_8rp.js +1 -0
- package/dist/frontend/assets/cobol-nBiQ_Alo.js +1 -0
- package/dist/frontend/assets/codeowners-Bp6g37R7.js +1 -0
- package/dist/frontend/assets/codeql-DsOJ9woJ.js +1 -0
- package/dist/frontend/assets/coffee-Ch7k5sss.js +1 -0
- package/dist/frontend/assets/common-lisp-Cg-RD9OK.js +1 -0
- package/dist/frontend/assets/coq-DkFqJrB1.js +1 -0
- package/dist/frontend/assets/cpp-CofmeUqb.js +1 -0
- package/dist/frontend/assets/crystal-DNxU26gB.js +1 -0
- package/dist/frontend/assets/csharp-COcwbKMJ.js +1 -0
- package/dist/frontend/assets/css-CLj8gQPS.js +1 -0
- package/dist/frontend/assets/csv-fuZLfV_i.js +1 -0
- package/dist/frontend/assets/cue-D82EKSYY.js +1 -0
- package/dist/frontend/assets/cypher-COkxafJQ.js +1 -0
- package/dist/frontend/assets/d-85-TOEBH.js +1 -0
- package/dist/frontend/assets/dark-plus-C3mMm8J8.js +1 -0
- package/dist/frontend/assets/dart-bE4Kk8sk.js +1 -0
- package/dist/frontend/assets/dax-CEL-wOlO.js +1 -0
- package/dist/frontend/assets/desktop-BmXAJ9_W.js +1 -0
- package/dist/frontend/assets/diff-D97Zzqfu.js +1 -0
- package/dist/frontend/assets/docker-BcOcwvcX.js +1 -0
- package/dist/frontend/assets/dotenv-Da5cRb03.js +1 -0
- package/dist/frontend/assets/dracula-BzJJZx-M.js +1 -0
- package/dist/frontend/assets/dracula-soft-BXkSAIEj.js +1 -0
- package/dist/frontend/assets/dream-maker-BtqSS_iP.js +1 -0
- package/dist/frontend/assets/edge-FbVlp4U3.js +1 -0
- package/dist/frontend/assets/elixir-CkH2-t6x.js +1 -0
- package/dist/frontend/assets/elm-DbKCFpqz.js +1 -0
- package/dist/frontend/assets/emacs-lisp-CXvaQtF9.js +1 -0
- package/dist/frontend/assets/erb-BYCe7drp.js +1 -0
- package/dist/frontend/assets/erlang-DsQrWhSR.js +1 -0
- package/dist/frontend/assets/everforest-dark-BgDCqdQA.js +1 -0
- package/dist/frontend/assets/everforest-light-C8M2exoo.js +1 -0
- package/dist/frontend/assets/fennel-BYunw83y.js +1 -0
- package/dist/frontend/assets/fish-BvzEVeQv.js +1 -0
- package/dist/frontend/assets/fluent-C4IJs8-o.js +1 -0
- package/dist/frontend/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
- package/dist/frontend/assets/fortran-free-form-BxgE0vQu.js +1 -0
- package/dist/frontend/assets/fsharp-CXgrBDvD.js +1 -0
- package/dist/frontend/assets/gdresource-BOOCDP_w.js +1 -0
- package/dist/frontend/assets/gdscript-C5YyOfLZ.js +1 -0
- package/dist/frontend/assets/gdshader-DkwncUOv.js +1 -0
- package/dist/frontend/assets/genie-D0YGMca9.js +1 -0
- package/dist/frontend/assets/gherkin-DyxjwDmM.js +1 -0
- package/dist/frontend/assets/git-commit-F4YmCXRG.js +1 -0
- package/dist/frontend/assets/git-rebase-r7XF79zn.js +1 -0
- package/dist/frontend/assets/github-dark-DHJKELXO.js +1 -0
- package/dist/frontend/assets/github-dark-default-Cuk6v7N8.js +1 -0
- package/dist/frontend/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
- package/dist/frontend/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
- package/dist/frontend/assets/github-light-DAi9KRSo.js +1 -0
- package/dist/frontend/assets/github-light-default-D7oLnXFd.js +1 -0
- package/dist/frontend/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
- package/dist/frontend/assets/gleam-BspZqrRM.js +1 -0
- package/dist/frontend/assets/glimmer-js-ByusRIyA.js +1 -0
- package/dist/frontend/assets/glimmer-ts-BfAWNZQY.js +1 -0
- package/dist/frontend/assets/glsl-DplSGwfg.js +1 -0
- package/dist/frontend/assets/gn-n2N0HUVH.js +1 -0
- package/dist/frontend/assets/gnuplot-DdkO51Og.js +1 -0
- package/dist/frontend/assets/go-C27-OAKa.js +1 -0
- package/dist/frontend/assets/graphql-ChdNCCLP.js +1 -0
- package/dist/frontend/assets/groovy-gcz8RCvz.js +1 -0
- package/dist/frontend/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
- package/dist/frontend/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
- package/dist/frontend/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
- package/dist/frontend/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
- package/dist/frontend/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
- package/dist/frontend/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
- package/dist/frontend/assets/hack-i7_Ulhet.js +1 -0
- package/dist/frontend/assets/haml-D5jkg6IW.js +1 -0
- package/dist/frontend/assets/handlebars-BpdQsYii.js +1 -0
- package/dist/frontend/assets/haskell-Df6bDoY_.js +1 -0
- package/dist/frontend/assets/haxe-CzTSHFRz.js +1 -0
- package/dist/frontend/assets/hcl-BWvSN4gD.js +1 -0
- package/dist/frontend/assets/hjson-D5-asLiD.js +1 -0
- package/dist/frontend/assets/hlsl-D3lLCCz7.js +1 -0
- package/dist/frontend/assets/horizon-BUw7H-hv.js +1 -0
- package/dist/frontend/assets/horizon-bright-CUuTKBJd.js +1 -0
- package/dist/frontend/assets/houston-DnULxvSX.js +1 -0
- package/dist/frontend/assets/html-derivative-DlHx6ybY.js +1 -0
- package/dist/frontend/assets/html-pp8916En.js +1 -0
- package/dist/frontend/assets/http-jrhK8wxY.js +1 -0
- package/dist/frontend/assets/hurl-irOxFIW8.js +1 -0
- package/dist/frontend/assets/hxml-Bvhsp5Yf.js +1 -0
- package/dist/frontend/assets/hy-DFXneXwc.js +1 -0
- package/dist/frontend/assets/imba-DGztddWO.js +1 -0
- package/dist/frontend/assets/ini-BEwlwnbL.js +1 -0
- package/dist/frontend/assets/java-CylS5w8V.js +1 -0
- package/dist/frontend/assets/javascript-wDzz0qaB.js +1 -0
- package/dist/frontend/assets/jinja-f2NsQr07.js +1 -0
- package/dist/frontend/assets/jison-wvAkD_A8.js +1 -0
- package/dist/frontend/assets/json-Cp-IABpG.js +1 -0
- package/dist/frontend/assets/json5-C9tS-k6U.js +1 -0
- package/dist/frontend/assets/jsonc-Des-eS-w.js +1 -0
- package/dist/frontend/assets/jsonl-DcaNXYhu.js +1 -0
- package/dist/frontend/assets/jsonnet-DFQXde-d.js +1 -0
- package/dist/frontend/assets/jssm-C2t-YnRu.js +1 -0
- package/dist/frontend/assets/jsx-g9-lgVsj.js +1 -0
- package/dist/frontend/assets/julia-CxzCAyBv.js +1 -0
- package/dist/frontend/assets/just-VxiPbLrw.js +1 -0
- package/dist/frontend/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
- package/dist/frontend/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
- package/dist/frontend/assets/kanagawa-wave-DWedfzmr.js +1 -0
- package/dist/frontend/assets/kdl-DV7GczEv.js +1 -0
- package/dist/frontend/assets/kotlin-BdnUsdx6.js +1 -0
- package/dist/frontend/assets/kusto-wEQ09or8.js +1 -0
- package/dist/frontend/assets/laserwave-DUszq2jm.js +1 -0
- package/dist/frontend/assets/latex-CWtU0Tv5.js +1 -0
- package/dist/frontend/assets/lean-BZvkOJ9d.js +1 -0
- package/dist/frontend/assets/less-B1dDrJ26.js +1 -0
- package/dist/frontend/assets/light-plus-B7mTdjB0.js +1 -0
- package/dist/frontend/assets/liquid-C0sCDyMI.js +1 -0
- package/dist/frontend/assets/llvm-DjAJT7YJ.js +1 -0
- package/dist/frontend/assets/log-2UxHyX5q.js +1 -0
- package/dist/frontend/assets/logo-BtOb2qkB.js +1 -0
- package/dist/frontend/assets/lua-BaeVxFsk.js +1 -0
- package/dist/frontend/assets/luau-C-HG3fhB.js +1 -0
- package/dist/frontend/assets/main-CL5_Wlhv.css +32 -0
- package/dist/frontend/assets/main-Czet4Z1x.js +371 -0
- package/dist/frontend/assets/make-CHLpvVh8.js +1 -0
- package/dist/frontend/assets/markdown-Cvjx9yec.js +1 -0
- package/dist/frontend/assets/marko-DjSrsDqO.js +1 -0
- package/dist/frontend/assets/material-theme-D5KoaKCx.js +1 -0
- package/dist/frontend/assets/material-theme-darker-BfHTSMKl.js +1 -0
- package/dist/frontend/assets/material-theme-lighter-B0m2ddpp.js +1 -0
- package/dist/frontend/assets/material-theme-ocean-CyktbL80.js +1 -0
- package/dist/frontend/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
- package/dist/frontend/assets/matlab-D7o27uSR.js +1 -0
- package/dist/frontend/assets/mdc-DTYItulj.js +1 -0
- package/dist/frontend/assets/mdx-Cmh6b_Ma.js +1 -0
- package/dist/frontend/assets/mermaid-mWjccvbQ.js +1 -0
- package/dist/frontend/assets/min-dark-CafNBF8u.js +1 -0
- package/dist/frontend/assets/min-light-CTRr51gU.js +1 -0
- package/dist/frontend/assets/mipsasm-CKIfxQSi.js +1 -0
- package/dist/frontend/assets/mojo-rZm6bMo-.js +1 -0
- package/dist/frontend/assets/monokai-D4h5O-jR.js +1 -0
- package/dist/frontend/assets/moonbit-_H4v1dQx.js +1 -0
- package/dist/frontend/assets/move-IF9eRakj.js +1 -0
- package/dist/frontend/assets/narrat-DRg8JJMk.js +1 -0
- package/dist/frontend/assets/nextflow-C-mBbutL.js +1 -0
- package/dist/frontend/assets/nextflow-groovy-vE_lwT2v.js +1 -0
- package/dist/frontend/assets/nginx-BpAMiNFr.js +1 -0
- package/dist/frontend/assets/night-owl-C39BiMTA.js +1 -0
- package/dist/frontend/assets/night-owl-light-CMTm3GFP.js +1 -0
- package/dist/frontend/assets/nim-BIad80T-.js +1 -0
- package/dist/frontend/assets/nix-CwoSXNpI.js +1 -0
- package/dist/frontend/assets/nord-Ddv68eIx.js +1 -0
- package/dist/frontend/assets/nushell-Cz2AlsmD.js +1 -0
- package/dist/frontend/assets/objective-c-DXmwc3jG.js +1 -0
- package/dist/frontend/assets/objective-cpp-CLxacb5B.js +1 -0
- package/dist/frontend/assets/ocaml-C0hk2d4L.js +1 -0
- package/dist/frontend/assets/odin-BBf5iR-q.js +1 -0
- package/dist/frontend/assets/one-dark-pro-DVMEJ2y_.js +1 -0
- package/dist/frontend/assets/one-light-C3Wv6jpd.js +1 -0
- package/dist/frontend/assets/openscad-C4EeE6gA.js +1 -0
- package/dist/frontend/assets/pascal-D93ZcfNL.js +1 -0
- package/dist/frontend/assets/perl-NvoQZIq0.js +1 -0
- package/dist/frontend/assets/php-R6g_5hLQ.js +1 -0
- package/dist/frontend/assets/pkl-u5AG7uiY.js +1 -0
- package/dist/frontend/assets/plastic-3e1v2bzS.js +1 -0
- package/dist/frontend/assets/plsql-ChMvpjG-.js +1 -0
- package/dist/frontend/assets/po-BTJTHyun.js +1 -0
- package/dist/frontend/assets/poimandres-CS3Unz2-.js +1 -0
- package/dist/frontend/assets/polar-C0HS_06l.js +1 -0
- package/dist/frontend/assets/postcss-CXtECtnM.js +1 -0
- package/dist/frontend/assets/powerquery-CEu0bR-o.js +1 -0
- package/dist/frontend/assets/powershell-Dpen1YoG.js +1 -0
- package/dist/frontend/assets/prisma-Dd19v3D-.js +1 -0
- package/dist/frontend/assets/prolog-CbFg5uaA.js +1 -0
- package/dist/frontend/assets/proto-C7zT0LnQ.js +1 -0
- package/dist/frontend/assets/pug-DKIMFp6K.js +1 -0
- package/dist/frontend/assets/puppet-BMWR74SV.js +1 -0
- package/dist/frontend/assets/purescript-CklMAg4u.js +1 -0
- package/dist/frontend/assets/python-B6aJPvgy.js +1 -0
- package/dist/frontend/assets/qml-3beO22l8.js +1 -0
- package/dist/frontend/assets/qmldir-C8lEn-DE.js +1 -0
- package/dist/frontend/assets/qss-IeuSbFQv.js +1 -0
- package/dist/frontend/assets/r-Dspwwk_N.js +1 -0
- package/dist/frontend/assets/racket-BqYA7rlc.js +1 -0
- package/dist/frontend/assets/raku-DXvB9xmW.js +1 -0
- package/dist/frontend/assets/razor-BDqjjVU7.js +1 -0
- package/dist/frontend/assets/red-bN70gL4F.js +1 -0
- package/dist/frontend/assets/reg-C-SQnVFl.js +1 -0
- package/dist/frontend/assets/regexp-CDVJQ6XC.js +1 -0
- package/dist/frontend/assets/rel-C3B-1QV4.js +1 -0
- package/dist/frontend/assets/riscv-BM1_JUlF.js +1 -0
- package/dist/frontend/assets/ron-D8l8udqQ.js +1 -0
- package/dist/frontend/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
- package/dist/frontend/assets/rose-pine-moon-D4_iv3hh.js +1 -0
- package/dist/frontend/assets/rose-pine-qdsjHGoJ.js +1 -0
- package/dist/frontend/assets/rosmsg-BJDFO7_C.js +1 -0
- package/dist/frontend/assets/rst-CRjBmOyv.js +1 -0
- package/dist/frontend/assets/ruby-Wjq7vjNf.js +1 -0
- package/dist/frontend/assets/rust-B1yitclQ.js +1 -0
- package/dist/frontend/assets/sas-cz2c8ADy.js +1 -0
- package/dist/frontend/assets/sass-Cj5Yp3dK.js +1 -0
- package/dist/frontend/assets/scala-C151Ov-r.js +1 -0
- package/dist/frontend/assets/scheme-C98Dy4si.js +1 -0
- package/dist/frontend/assets/scss-D5BDwBP9.js +1 -0
- package/dist/frontend/assets/sdbl-DVxCFoDh.js +1 -0
- package/dist/frontend/assets/shaderlab-Dg9Lc6iA.js +1 -0
- package/dist/frontend/assets/shellscript-Yzrsuije.js +1 -0
- package/dist/frontend/assets/shellsession-BADoaaVG.js +1 -0
- package/dist/frontend/assets/slack-dark-BthQWCQV.js +1 -0
- package/dist/frontend/assets/slack-ochin-DqwNpetd.js +1 -0
- package/dist/frontend/assets/smalltalk-BERRCDM3.js +1 -0
- package/dist/frontend/assets/snazzy-light-Bw305WKR.js +1 -0
- package/dist/frontend/assets/solarized-dark-DXbdFlpD.js +1 -0
- package/dist/frontend/assets/solarized-light-L9t79GZl.js +1 -0
- package/dist/frontend/assets/solidity-rGO070M0.js +1 -0
- package/dist/frontend/assets/soy-8wufbnw4.js +1 -0
- package/dist/frontend/assets/sparql-rVzFXLq3.js +1 -0
- package/dist/frontend/assets/splunk-BtCnVYZw.js +1 -0
- package/dist/frontend/assets/sql-BLtJtn59.js +1 -0
- package/dist/frontend/assets/ssh-config-_ykCGR6B.js +1 -0
- package/dist/frontend/assets/stata-BH5u7GGu.js +1 -0
- package/dist/frontend/assets/stylus-BEDo0Tqx.js +1 -0
- package/dist/frontend/assets/surrealql-Bq5Q-fJD.js +1 -0
- package/dist/frontend/assets/svelte-Cy7k_4gC.js +1 -0
- package/dist/frontend/assets/swift-D82vCrfD.js +1 -0
- package/dist/frontend/assets/synthwave-84-CbfX1IO0.js +1 -0
- package/dist/frontend/assets/system-verilog-CnnmHF94.js +1 -0
- package/dist/frontend/assets/systemd-4A_iFExJ.js +1 -0
- package/dist/frontend/assets/talonscript-CkByrt1z.js +1 -0
- package/dist/frontend/assets/tasl-QIJgUcNo.js +1 -0
- package/dist/frontend/assets/tcl-dwOrl1Do.js +1 -0
- package/dist/frontend/assets/templ-DhtptRzy.js +1 -0
- package/dist/frontend/assets/terraform-BETggiCN.js +1 -0
- package/dist/frontend/assets/tex-idrVyKtj.js +1 -0
- package/dist/frontend/assets/tokyo-night-hegEt444.js +1 -0
- package/dist/frontend/assets/toml-vGWfd6FD.js +1 -0
- package/dist/frontend/assets/ts-tags-DQrlYJgV.js +1 -0
- package/dist/frontend/assets/tsv-B_m7g4N7.js +1 -0
- package/dist/frontend/assets/tsx-COt5Ahok.js +1 -0
- package/dist/frontend/assets/turtle-BsS91CYL.js +1 -0
- package/dist/frontend/assets/twig-xg9kU7Mw.js +1 -0
- package/dist/frontend/assets/typescript-BPQ3VLAy.js +1 -0
- package/dist/frontend/assets/typespec-CAFt9gP4.js +1 -0
- package/dist/frontend/assets/typst-DHCkPAjA.js +1 -0
- package/dist/frontend/assets/v-BcVCzyr7.js +1 -0
- package/dist/frontend/assets/vala-CsfeWuGM.js +1 -0
- package/dist/frontend/assets/vb-D17OF-Vu.js +1 -0
- package/dist/frontend/assets/verilog-BQ8w6xss.js +1 -0
- package/dist/frontend/assets/vesper-DU1UobuO.js +1 -0
- package/dist/frontend/assets/vhdl-CeAyd5Ju.js +1 -0
- package/dist/frontend/assets/viml-CJc9bBzg.js +1 -0
- package/dist/frontend/assets/vitesse-black-Bkuqu6BP.js +1 -0
- package/dist/frontend/assets/vitesse-dark-D0r3Knsf.js +1 -0
- package/dist/frontend/assets/vitesse-light-CVO1_9PV.js +1 -0
- package/dist/frontend/assets/vue-D2xRrEX4.js +1 -0
- package/dist/frontend/assets/vue-html-AaS7Mt5G.js +1 -0
- package/dist/frontend/assets/vue-vine-BoDAl6tE.js +1 -0
- package/dist/frontend/assets/vyper-CDx5xZoG.js +1 -0
- package/dist/frontend/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/frontend/assets/wasm-MzD3tlZU.js +1 -0
- package/dist/frontend/assets/wenyan-BV7otONQ.js +1 -0
- package/dist/frontend/assets/wgsl-Dx-B1_4e.js +1 -0
- package/dist/frontend/assets/wikitext-BhOHFoWU.js +1 -0
- package/dist/frontend/assets/wit-5i3qLPDT.js +1 -0
- package/dist/frontend/assets/wolfram-lXgVvXCa.js +1 -0
- package/dist/frontend/assets/xml-sdJ4AIDG.js +1 -0
- package/dist/frontend/assets/xsl-CtQFsRM5.js +1 -0
- package/dist/frontend/assets/yaml-Buea-lGh.js +1 -0
- package/dist/frontend/assets/zenscript-DVFEvuxE.js +1 -0
- package/dist/frontend/assets/zig-VOosw3JB.js +1 -0
- package/dist/frontend/icon-192.png +0 -0
- package/dist/frontend/icon-512.png +0 -0
- package/dist/frontend/icon.svg +8 -0
- package/dist/frontend/index.html +30 -0
- package/dist/frontend/manifest.json +25 -0
- package/dist/frontend/sw.js +66 -0
- package/dist/server/agent-events.js +39 -0
- package/dist/server/analytics.js +885 -0
- package/dist/server/auth.js +65 -0
- package/dist/server/belayer/executor.js +200 -0
- package/dist/server/belayer/intake.js +27 -0
- package/dist/server/belayer/pipeline.js +97 -0
- package/dist/server/belayer/pr-lifecycle.js +69 -0
- package/dist/server/belayer/prompts.js +154 -0
- package/dist/server/belayer/types.js +23 -0
- package/dist/server/branch-linker.js +137 -0
- package/dist/server/browser-content.js +145 -0
- package/dist/server/clipboard.js +63 -0
- package/dist/server/codex-hooks-adapter.js +93 -0
- package/dist/server/config.js +325 -0
- package/dist/server/gh-routes.js +163 -0
- package/dist/server/gh.js +276 -0
- package/dist/server/git-routes.js +154 -0
- package/dist/server/git.js +694 -0
- package/dist/server/github-app.js +218 -0
- package/dist/server/github-graphql.js +178 -0
- package/dist/server/hooks.js +373 -0
- package/dist/server/index.js +1549 -0
- package/dist/server/integration-github.js +137 -0
- package/dist/server/integration-jira.js +210 -0
- package/dist/server/integration-linear.js +176 -0
- package/dist/server/logger.js +18 -0
- package/dist/server/mobile-input-pipeline.js +129 -0
- package/dist/server/opencode-relay.js +53 -0
- package/dist/server/org-dashboard.js +241 -0
- package/dist/server/output-parsers/claude-parser.js +56 -0
- package/dist/server/output-parsers/codex-parser.js +13 -0
- package/dist/server/output-parsers/index.js +14 -0
- package/dist/server/output-parsers/null-parser.js +12 -0
- package/dist/server/output-parsers/opencode-parser.js +77 -0
- package/dist/server/pty-handler.js +586 -0
- package/dist/server/push.js +84 -0
- package/dist/server/review-poller.js +237 -0
- package/dist/server/sdk-handler.js +539 -0
- package/dist/server/service.js +189 -0
- package/dist/server/sessions.js +638 -0
- package/dist/server/telemetry.js +236 -0
- package/dist/server/ticket-transitions.js +166 -0
- package/dist/server/types.js +146 -0
- package/dist/server/utils.js +23 -0
- package/dist/server/watcher.js +661 -0
- package/dist/server/webhook-manager.js +547 -0
- package/dist/server/webhooks.js +73 -0
- package/dist/server/workspace-groups.js +363 -0
- package/dist/server/workspaces.js +1207 -0
- package/dist/server/ws.js +192 -0
- package/dist/test/EmptyState.spec.js +51 -0
- package/dist/test/action-coverage.test.js +139 -0
- package/dist/test/actions/registry.test.js +59 -0
- package/dist/test/actions/shortcuts.test.js +79 -0
- package/dist/test/agent-events.test.js +151 -0
- package/dist/test/analytics.test.js +158 -0
- package/dist/test/attention.test.js +91 -0
- package/dist/test/auth.test.js +105 -0
- package/dist/test/backend-state.test.js +47 -0
- package/dist/test/belayer-executor.test.js +33 -0
- package/dist/test/belayer-intake.test.js +44 -0
- package/dist/test/belayer-pipeline.test.js +113 -0
- package/dist/test/belayer-pr-lifecycle.test.js +26 -0
- package/dist/test/belayer-prompts.test.js +60 -0
- package/dist/test/belayer-types.test.js +69 -0
- package/dist/test/bin/claude-remote-cli.js +214 -0
- package/dist/test/boot-state.test.js +133 -0
- package/dist/test/branch-lifecycle.test.js +75 -0
- package/dist/test/branch-linker.test.js +236 -0
- package/dist/test/branch-rename.test.js +45 -0
- package/dist/test/branch-watcher.test.js +115 -0
- package/dist/test/browser-cli.test.js +91 -0
- package/dist/test/browser-content.test.js +93 -0
- package/dist/test/browser-tabs-ui.test.js +39 -0
- package/dist/test/changed-files-api.test.js +140 -0
- package/dist/test/clipboard.test.js +12 -0
- package/dist/test/codex-hooks-adapter.test.js +237 -0
- package/dist/test/components/EmptyState.spec.js +51 -0
- package/dist/test/components/ErrorToast.spec.js +65 -0
- package/dist/test/components/TuiCheckbox.spec.js +120 -0
- package/dist/test/components/TuiInput.spec.js +186 -0
- package/dist/test/components/leaf-component-migration.spec.js +104 -0
- package/dist/test/config-freshness.test.js +63 -0
- package/dist/test/config.test.js +813 -0
- package/dist/test/diff-summary.test.js +98 -0
- package/dist/test/display-state.test.js +179 -0
- package/dist/test/event-message-types.test.js +32 -0
- package/dist/test/file-tree-utils.test.js +167 -0
- package/dist/test/framework-types.test.js +183 -0
- package/dist/test/frameworks-api.test.js +93 -0
- package/dist/test/frontend/src/lib/pr-state.js +114 -0
- package/dist/test/fs-browse.test.js +246 -0
- package/dist/test/fuzzy-scorer.test.js +145 -0
- package/dist/test/gh-routes.test.js +156 -0
- package/dist/test/git-changed-files.test.js +152 -0
- package/dist/test/git-routes.test.js +146 -0
- package/dist/test/git-utils.test.js +68 -0
- package/dist/test/git-watcher.test.js +110 -0
- package/dist/test/git.test.js +140 -0
- package/dist/test/github-app.test.js +455 -0
- package/dist/test/github-graphql.test.js +301 -0
- package/dist/test/greetings.test.js +83 -0
- package/dist/test/hooks-agent-event.test.js +412 -0
- package/dist/test/hooks.test.js +149 -0
- package/dist/test/integration-github.test.js +220 -0
- package/dist/test/integration-jira.test.js +238 -0
- package/dist/test/integration-linear.test.js +293 -0
- package/dist/test/mobile-input.test.js +235 -0
- package/dist/test/opencode-relay.test.js +107 -0
- package/dist/test/org-dashboard.test.js +349 -0
- package/dist/test/output-parser.test.js +217 -0
- package/dist/test/paths.test.js +32 -0
- package/dist/test/pr-state.test.js +407 -0
- package/dist/test/pr-status.test.js +82 -0
- package/dist/test/presets.test.js +242 -0
- package/dist/test/pty-handler-multi-agent.test.js +149 -0
- package/dist/test/pty-handler.test.js +146 -0
- package/dist/test/pull-requests.test.js +78 -0
- package/dist/test/review-poller.test.js +349 -0
- package/dist/test/server/analytics.js +121 -0
- package/dist/test/server/auth.js +63 -0
- package/dist/test/server/branch-linker.js +124 -0
- package/dist/test/server/clipboard.js +56 -0
- package/dist/test/server/config.js +137 -0
- package/dist/test/server/git.js +308 -0
- package/dist/test/server/hooks.js +196 -0
- package/dist/test/server/index.js +1124 -0
- package/dist/test/server/integration-github.js +117 -0
- package/dist/test/server/integration-jira.js +164 -0
- package/dist/test/server/integration-linear.js +176 -0
- package/dist/test/server/mobile-input-pipeline.js +123 -0
- package/dist/test/server/org-dashboard.js +184 -0
- package/dist/test/server/output-parsers/claude-parser.js +54 -0
- package/dist/test/server/output-parsers/codex-parser.js +13 -0
- package/dist/test/server/output-parsers/index.js +7 -0
- package/dist/test/server/pty-handler.js +310 -0
- package/dist/test/server/push.js +80 -0
- package/dist/test/server/review-poller.js +218 -0
- package/dist/test/server/service.js +169 -0
- package/dist/test/server/sessions.js +434 -0
- package/dist/test/server/ticket-transitions.js +216 -0
- package/dist/test/server/types.js +20 -0
- package/dist/test/server/utils.js +22 -0
- package/dist/test/server/watcher.js +139 -0
- package/dist/test/server/workspaces.js +657 -0
- package/dist/test/server/ws.js +152 -0
- package/dist/test/server-startup.test.js +62 -0
- package/dist/test/service.test.js +43 -0
- package/dist/test/session-analytics-api.test.js +123 -0
- package/dist/test/session-analytics.test.js +425 -0
- package/dist/test/session-intent.test.js +249 -0
- package/dist/test/sessions.test.js +1152 -0
- package/dist/test/sidebar-items.test.js +164 -0
- package/dist/test/stores/boot-state-store.test.js +165 -0
- package/dist/test/stores/sessions-logic.test.js +191 -0
- package/dist/test/stores/toasts-store.test.js +66 -0
- package/dist/test/stores/ui-store.test.js +203 -0
- package/dist/test/stores/unread-store.test.js +97 -0
- package/dist/test/telemetry-api.test.js +54 -0
- package/dist/test/telemetry-sync.test.js +68 -0
- package/dist/test/telemetry.test.js +295 -0
- package/dist/test/terminal-zoom.test.js +102 -0
- package/dist/test/test/analytics.test.js +152 -0
- package/dist/test/test/auth.test.js +95 -0
- package/dist/test/test/branch-linker.test.js +231 -0
- package/dist/test/test/branch-rename.test.js +45 -0
- package/dist/test/test/clipboard.test.js +12 -0
- package/dist/test/test/config.test.js +281 -0
- package/dist/test/test/fs-browse.test.js +202 -0
- package/dist/test/test/git.test.js +67 -0
- package/dist/test/test/hooks.test.js +139 -0
- package/dist/test/test/integration-github.test.js +203 -0
- package/dist/test/test/integration-jira.test.js +294 -0
- package/dist/test/test/integration-linear.test.js +293 -0
- package/dist/test/test/mobile-input.test.js +193 -0
- package/dist/test/test/org-dashboard.test.js +240 -0
- package/dist/test/test/output-parser.test.js +95 -0
- package/dist/test/test/paths.test.js +32 -0
- package/dist/test/test/pr-state.test.js +220 -0
- package/dist/test/test/pull-requests.test.js +67 -0
- package/dist/test/test/review-poller.test.js +235 -0
- package/dist/test/test/service.test.js +43 -0
- package/dist/test/test/sessions.test.js +750 -0
- package/dist/test/test/ticket-transitions.test.js +130 -0
- package/dist/test/test/version.test.js +34 -0
- package/dist/test/test/worktrees.test.js +256 -0
- package/dist/test/ticket-transitions.test.js +312 -0
- package/dist/test/unread.test.js +23 -0
- package/dist/test/version.test.js +34 -0
- package/dist/test/webhook-manager.test.js +484 -0
- package/dist/test/webhooks.test.js +208 -0
- package/dist/test/workspace-groups.test.js +377 -0
- package/dist/test/worktrees.test.js +531 -0
- package/package.json +88 -0
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
import { describe, it, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import * as sessions from '../server/sessions.js';
|
|
7
|
+
import { resolveTmuxSpawn, generateTmuxSessionName } from '../server/pty-handler.js';
|
|
8
|
+
import { serializeAll, restoreFromDisk } from '../server/sessions.js';
|
|
9
|
+
// Track created session IDs so we can clean up after each test
|
|
10
|
+
const createdIds = [];
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
// Kill any remaining sessions created during tests
|
|
13
|
+
for (const id of createdIds) {
|
|
14
|
+
try {
|
|
15
|
+
const session = sessions.get(id);
|
|
16
|
+
if (session) {
|
|
17
|
+
sessions.kill(id);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Already killed or exited, ignore
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
createdIds.length = 0;
|
|
25
|
+
});
|
|
26
|
+
describe('sessions', () => {
|
|
27
|
+
it('list returns empty array initially', () => {
|
|
28
|
+
const result = sessions.list();
|
|
29
|
+
assert.ok(Array.isArray(result));
|
|
30
|
+
assert.strictEqual(result.length, 0);
|
|
31
|
+
});
|
|
32
|
+
it('create spawns PTY and adds session to registry', () => {
|
|
33
|
+
const result = sessions.create({
|
|
34
|
+
repoName: 'test-repo',
|
|
35
|
+
repoPath: '/tmp',
|
|
36
|
+
command: '/bin/echo',
|
|
37
|
+
args: ['hello'],
|
|
38
|
+
cols: 80,
|
|
39
|
+
rows: 24,
|
|
40
|
+
});
|
|
41
|
+
createdIds.push(result.id);
|
|
42
|
+
assert.ok(result.id, 'should have an id');
|
|
43
|
+
assert.strictEqual(result.repoName, 'test-repo');
|
|
44
|
+
assert.strictEqual(result.repoPath, '/tmp');
|
|
45
|
+
assert.ok(typeof result.pid === 'number', 'should have a numeric pid');
|
|
46
|
+
assert.ok(result.createdAt, 'should have a createdAt timestamp');
|
|
47
|
+
assert.strictEqual('pty' in result, false, 'should not expose pty object');
|
|
48
|
+
const list = sessions.list();
|
|
49
|
+
assert.strictEqual(list.length, 1);
|
|
50
|
+
assert.strictEqual(list[0]?.id, result.id);
|
|
51
|
+
});
|
|
52
|
+
it('get returns session by id', () => {
|
|
53
|
+
const result = sessions.create({
|
|
54
|
+
repoName: 'test-repo',
|
|
55
|
+
repoPath: '/tmp',
|
|
56
|
+
command: '/bin/echo',
|
|
57
|
+
args: ['hello'],
|
|
58
|
+
});
|
|
59
|
+
createdIds.push(result.id);
|
|
60
|
+
const session = sessions.get(result.id);
|
|
61
|
+
assert.ok(session, 'should return the session');
|
|
62
|
+
assert.strictEqual(session.id, result.id);
|
|
63
|
+
assert.strictEqual(session.repoName, 'test-repo');
|
|
64
|
+
assert.strictEqual(session.mode, 'pty');
|
|
65
|
+
assert.ok(session.pty, 'get should include the pty object');
|
|
66
|
+
});
|
|
67
|
+
it('get returns undefined for nonexistent id', () => {
|
|
68
|
+
const session = sessions.get('nonexistent-id-12345');
|
|
69
|
+
assert.strictEqual(session, undefined);
|
|
70
|
+
});
|
|
71
|
+
it('kill removes session from registry', () => {
|
|
72
|
+
const result = sessions.create({
|
|
73
|
+
repoName: 'test-repo',
|
|
74
|
+
repoPath: '/tmp',
|
|
75
|
+
command: '/bin/echo',
|
|
76
|
+
args: ['hello'],
|
|
77
|
+
});
|
|
78
|
+
createdIds.push(result.id);
|
|
79
|
+
sessions.kill(result.id);
|
|
80
|
+
// Remove from tracking since it's already killed
|
|
81
|
+
createdIds.splice(createdIds.indexOf(result.id), 1);
|
|
82
|
+
const session = sessions.get(result.id);
|
|
83
|
+
assert.strictEqual(session, undefined, 'session should be removed after kill');
|
|
84
|
+
const list = sessions.list();
|
|
85
|
+
assert.ok(!list.some((s) => s.id === result.id), 'killed session should not appear in list');
|
|
86
|
+
});
|
|
87
|
+
it('kill throws for nonexistent session', () => {
|
|
88
|
+
assert.throws(() => sessions.kill('nonexistent-id'), /Session not found/);
|
|
89
|
+
});
|
|
90
|
+
it('resize throws for nonexistent session', () => {
|
|
91
|
+
assert.throws(() => sessions.resize('nonexistent-id', 100, 40), /Session not found/);
|
|
92
|
+
});
|
|
93
|
+
it('write sends data to PTY stdin', (_, done) => {
|
|
94
|
+
const result = sessions.create({
|
|
95
|
+
repoName: 'test-repo',
|
|
96
|
+
repoPath: '/tmp',
|
|
97
|
+
command: '/bin/cat',
|
|
98
|
+
args: [],
|
|
99
|
+
cols: 80,
|
|
100
|
+
rows: 24,
|
|
101
|
+
});
|
|
102
|
+
createdIds.push(result.id);
|
|
103
|
+
const session = sessions.get(result.id);
|
|
104
|
+
assert.ok(session);
|
|
105
|
+
assert.strictEqual(session.mode, 'pty');
|
|
106
|
+
const ptySession = session;
|
|
107
|
+
let output = '';
|
|
108
|
+
ptySession.pty.onData((data) => {
|
|
109
|
+
output += data;
|
|
110
|
+
if (output.includes('hello')) {
|
|
111
|
+
done();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
sessions.write(result.id, 'hello');
|
|
115
|
+
});
|
|
116
|
+
it('write throws for nonexistent session', () => {
|
|
117
|
+
assert.throws(() => sessions.write('nonexistent-id', 'data'), /Session not found/);
|
|
118
|
+
});
|
|
119
|
+
it('session starts as not idle', () => {
|
|
120
|
+
const result = sessions.create({
|
|
121
|
+
repoName: 'test-repo',
|
|
122
|
+
repoPath: '/tmp',
|
|
123
|
+
command: '/bin/cat',
|
|
124
|
+
args: [],
|
|
125
|
+
});
|
|
126
|
+
createdIds.push(result.id);
|
|
127
|
+
const session = sessions.get(result.id);
|
|
128
|
+
assert.ok(session);
|
|
129
|
+
assert.strictEqual(session.idle, false);
|
|
130
|
+
});
|
|
131
|
+
it('list includes idle field', () => {
|
|
132
|
+
const result = sessions.create({
|
|
133
|
+
repoName: 'test-repo',
|
|
134
|
+
repoPath: '/tmp',
|
|
135
|
+
command: '/bin/cat',
|
|
136
|
+
args: [],
|
|
137
|
+
});
|
|
138
|
+
createdIds.push(result.id);
|
|
139
|
+
const list = sessions.list();
|
|
140
|
+
assert.strictEqual(list.length, 1);
|
|
141
|
+
assert.strictEqual(list[0]?.idle, false);
|
|
142
|
+
});
|
|
143
|
+
it('type defaults to worktree when not specified', () => {
|
|
144
|
+
const result = sessions.create({
|
|
145
|
+
repoName: 'test-repo',
|
|
146
|
+
repoPath: '/tmp',
|
|
147
|
+
command: '/bin/echo',
|
|
148
|
+
args: ['hello'],
|
|
149
|
+
});
|
|
150
|
+
createdIds.push(result.id);
|
|
151
|
+
assert.strictEqual(result.type, 'worktree');
|
|
152
|
+
const session = sessions.get(result.id);
|
|
153
|
+
assert.ok(session);
|
|
154
|
+
assert.strictEqual(session.type, 'worktree');
|
|
155
|
+
});
|
|
156
|
+
it('type is set to repo when specified', () => {
|
|
157
|
+
const result = sessions.create({
|
|
158
|
+
type: 'repo',
|
|
159
|
+
repoName: 'test-repo',
|
|
160
|
+
repoPath: '/tmp',
|
|
161
|
+
command: '/bin/echo',
|
|
162
|
+
args: ['hello'],
|
|
163
|
+
});
|
|
164
|
+
createdIds.push(result.id);
|
|
165
|
+
assert.strictEqual(result.type, 'repo');
|
|
166
|
+
const session = sessions.get(result.id);
|
|
167
|
+
assert.ok(session);
|
|
168
|
+
assert.strictEqual(session.type, 'repo');
|
|
169
|
+
});
|
|
170
|
+
it('list includes type field', () => {
|
|
171
|
+
const r1 = sessions.create({
|
|
172
|
+
type: 'repo',
|
|
173
|
+
repoName: 'repo-a',
|
|
174
|
+
repoPath: '/tmp/a',
|
|
175
|
+
command: '/bin/echo',
|
|
176
|
+
args: ['hello'],
|
|
177
|
+
});
|
|
178
|
+
createdIds.push(r1.id);
|
|
179
|
+
const r2 = sessions.create({
|
|
180
|
+
type: 'worktree',
|
|
181
|
+
repoName: 'repo-b',
|
|
182
|
+
repoPath: '/tmp/b',
|
|
183
|
+
command: '/bin/echo',
|
|
184
|
+
args: ['hello'],
|
|
185
|
+
});
|
|
186
|
+
createdIds.push(r2.id);
|
|
187
|
+
const list = sessions.list();
|
|
188
|
+
const repoSession = list.find(function (s) { return s.id === r1.id; });
|
|
189
|
+
const wtSession = list.find(function (s) { return s.id === r2.id; });
|
|
190
|
+
assert.ok(repoSession);
|
|
191
|
+
assert.strictEqual(repoSession.type, 'repo');
|
|
192
|
+
assert.ok(wtSession);
|
|
193
|
+
assert.strictEqual(wtSession.type, 'worktree');
|
|
194
|
+
});
|
|
195
|
+
it('findRepoSession returns undefined when no repo sessions exist', () => {
|
|
196
|
+
const result = sessions.findRepoSession('/tmp');
|
|
197
|
+
assert.strictEqual(result, undefined);
|
|
198
|
+
});
|
|
199
|
+
it('findRepoSession returns repo session matching repoPath', () => {
|
|
200
|
+
const created = sessions.create({
|
|
201
|
+
type: 'repo',
|
|
202
|
+
repoName: 'test-repo',
|
|
203
|
+
repoPath: '/tmp/my-repo',
|
|
204
|
+
command: '/bin/echo',
|
|
205
|
+
args: ['hello'],
|
|
206
|
+
});
|
|
207
|
+
createdIds.push(created.id);
|
|
208
|
+
const found = sessions.findRepoSession('/tmp/my-repo');
|
|
209
|
+
assert.ok(found, 'should find the repo session');
|
|
210
|
+
assert.strictEqual(found.id, created.id);
|
|
211
|
+
assert.strictEqual(found.type, 'repo');
|
|
212
|
+
});
|
|
213
|
+
it('findRepoSession ignores worktree sessions at same path', () => {
|
|
214
|
+
const created = sessions.create({
|
|
215
|
+
type: 'worktree',
|
|
216
|
+
repoName: 'test-repo',
|
|
217
|
+
repoPath: '/tmp/my-repo',
|
|
218
|
+
command: '/bin/echo',
|
|
219
|
+
args: ['hello'],
|
|
220
|
+
});
|
|
221
|
+
createdIds.push(created.id);
|
|
222
|
+
const found = sessions.findRepoSession('/tmp/my-repo');
|
|
223
|
+
assert.strictEqual(found, undefined, 'should not match worktree sessions');
|
|
224
|
+
});
|
|
225
|
+
it('branchName defaults to worktreeName when not specified', () => {
|
|
226
|
+
const result = sessions.create({
|
|
227
|
+
repoName: 'test-repo',
|
|
228
|
+
repoPath: '/tmp',
|
|
229
|
+
worktreeName: 'dy-feat-my-feature',
|
|
230
|
+
command: '/bin/echo',
|
|
231
|
+
args: ['hello'],
|
|
232
|
+
});
|
|
233
|
+
createdIds.push(result.id);
|
|
234
|
+
assert.strictEqual(result.branchName, 'dy-feat-my-feature');
|
|
235
|
+
});
|
|
236
|
+
it('branchName is set independently from worktreeName', () => {
|
|
237
|
+
const result = sessions.create({
|
|
238
|
+
repoName: 'test-repo',
|
|
239
|
+
repoPath: '/tmp',
|
|
240
|
+
worktreeName: 'dy-feat-my-feature',
|
|
241
|
+
branchName: 'dy/feat/my-feature',
|
|
242
|
+
command: '/bin/echo',
|
|
243
|
+
args: ['hello'],
|
|
244
|
+
});
|
|
245
|
+
createdIds.push(result.id);
|
|
246
|
+
assert.strictEqual(result.worktreeName, 'dy-feat-my-feature');
|
|
247
|
+
assert.strictEqual(result.branchName, 'dy/feat/my-feature');
|
|
248
|
+
});
|
|
249
|
+
it('list includes branchName field', () => {
|
|
250
|
+
const result = sessions.create({
|
|
251
|
+
repoName: 'test-repo',
|
|
252
|
+
repoPath: '/tmp',
|
|
253
|
+
worktreeName: 'my-wt',
|
|
254
|
+
branchName: 'feat/my-branch',
|
|
255
|
+
command: '/bin/echo',
|
|
256
|
+
args: ['hello'],
|
|
257
|
+
});
|
|
258
|
+
createdIds.push(result.id);
|
|
259
|
+
const list = sessions.list();
|
|
260
|
+
const session = list.find(s => s.id === result.id);
|
|
261
|
+
assert.ok(session);
|
|
262
|
+
assert.strictEqual(session.branchName, 'feat/my-branch');
|
|
263
|
+
});
|
|
264
|
+
it('branchName defaults to empty string when neither branchName nor worktreeName provided', () => {
|
|
265
|
+
const result = sessions.create({
|
|
266
|
+
type: 'repo',
|
|
267
|
+
repoName: 'test-repo',
|
|
268
|
+
repoPath: '/tmp',
|
|
269
|
+
command: '/bin/echo',
|
|
270
|
+
args: ['hello'],
|
|
271
|
+
});
|
|
272
|
+
createdIds.push(result.id);
|
|
273
|
+
assert.strictEqual(result.branchName, '');
|
|
274
|
+
});
|
|
275
|
+
it('resolveTmuxSpawn returns correct tmux command and args', () => {
|
|
276
|
+
const result = resolveTmuxSpawn('claude', ['--continue'], 'test-session');
|
|
277
|
+
assert.deepStrictEqual(result, {
|
|
278
|
+
command: 'tmux',
|
|
279
|
+
args: [
|
|
280
|
+
'-u', 'new-session', '-s', 'test-session', '--', 'claude', '--continue',
|
|
281
|
+
';', 'set', 'set-clipboard', 'on',
|
|
282
|
+
';', 'set', 'allow-passthrough', 'on',
|
|
283
|
+
';', 'set', 'mode-keys', 'vi',
|
|
284
|
+
],
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
it('generateTmuxSessionName has crc- prefix', () => {
|
|
288
|
+
const name = generateTmuxSessionName('my-session', 'abcdef1234567890');
|
|
289
|
+
assert.ok(name.startsWith('crc-'), `expected crc- prefix, got: ${name}`);
|
|
290
|
+
});
|
|
291
|
+
it('generateTmuxSessionName sanitizes special characters', () => {
|
|
292
|
+
const name = generateTmuxSessionName('feat/auth-flow', 'abcdef1234567890');
|
|
293
|
+
assert.ok(name.startsWith('crc-feat-auth-flow-'), `expected sanitized name, got: ${name}`);
|
|
294
|
+
});
|
|
295
|
+
it('generateTmuxSessionName limits display name to 30 chars', () => {
|
|
296
|
+
const longName = 'a-very-long-display-name-that-exceeds-thirty-characters';
|
|
297
|
+
const id = 'abcdef1234567890';
|
|
298
|
+
const name = generateTmuxSessionName(longName, id);
|
|
299
|
+
// Format is crc-<sanitized up to 30>-<8 char id>
|
|
300
|
+
// The sanitized portion should be at most 30 chars
|
|
301
|
+
const withoutPrefix = name.slice('crc-'.length);
|
|
302
|
+
const parts = withoutPrefix.split('-');
|
|
303
|
+
const idPart = parts[parts.length - 1];
|
|
304
|
+
const displayPart = withoutPrefix.slice(0, withoutPrefix.length - idPart.length - 1);
|
|
305
|
+
assert.ok(displayPart.length <= 30, `display portion should be <= 30 chars, got: ${displayPart.length}`);
|
|
306
|
+
});
|
|
307
|
+
it('generateTmuxSessionName uses 8 chars from the provided id', () => {
|
|
308
|
+
const id = 'abcdef1234567890';
|
|
309
|
+
const name = generateTmuxSessionName('my-session', id);
|
|
310
|
+
assert.ok(name.endsWith(id.slice(0, 8)), `expected name to end with ${id.slice(0, 8)}, got: ${name}`);
|
|
311
|
+
});
|
|
312
|
+
it('agent defaults to claude when not specified', () => {
|
|
313
|
+
const result = sessions.create({
|
|
314
|
+
repoName: 'test-repo',
|
|
315
|
+
repoPath: '/tmp',
|
|
316
|
+
command: '/bin/echo',
|
|
317
|
+
args: ['hello'],
|
|
318
|
+
});
|
|
319
|
+
createdIds.push(result.id);
|
|
320
|
+
assert.strictEqual(result.agent, 'claude');
|
|
321
|
+
});
|
|
322
|
+
it('agent is set when specified', () => {
|
|
323
|
+
const result = sessions.create({
|
|
324
|
+
repoName: 'test-repo',
|
|
325
|
+
repoPath: '/tmp',
|
|
326
|
+
agent: 'codex',
|
|
327
|
+
command: '/bin/echo',
|
|
328
|
+
args: ['hello'],
|
|
329
|
+
});
|
|
330
|
+
createdIds.push(result.id);
|
|
331
|
+
assert.strictEqual(result.agent, 'codex');
|
|
332
|
+
});
|
|
333
|
+
it('list includes agent field', () => {
|
|
334
|
+
const result = sessions.create({
|
|
335
|
+
repoName: 'test-repo',
|
|
336
|
+
repoPath: '/tmp',
|
|
337
|
+
agent: 'codex',
|
|
338
|
+
command: '/bin/echo',
|
|
339
|
+
args: ['hello'],
|
|
340
|
+
});
|
|
341
|
+
createdIds.push(result.id);
|
|
342
|
+
const list = sessions.list();
|
|
343
|
+
const session = list.find(s => s.id === result.id);
|
|
344
|
+
assert.ok(session);
|
|
345
|
+
assert.strictEqual(session.agent, 'codex');
|
|
346
|
+
});
|
|
347
|
+
it('useTmux defaults to false when not specified', () => {
|
|
348
|
+
const result = sessions.create({
|
|
349
|
+
repoName: 'test-repo',
|
|
350
|
+
repoPath: '/tmp',
|
|
351
|
+
command: '/bin/echo',
|
|
352
|
+
args: ['hello'],
|
|
353
|
+
});
|
|
354
|
+
createdIds.push(result.id);
|
|
355
|
+
assert.strictEqual(result.useTmux, false);
|
|
356
|
+
assert.strictEqual(result.tmuxSessionName, '');
|
|
357
|
+
});
|
|
358
|
+
it('useTmux is disabled when custom command is provided even if useTmux is true', () => {
|
|
359
|
+
const result = sessions.create({
|
|
360
|
+
repoName: 'test-repo',
|
|
361
|
+
repoPath: '/tmp',
|
|
362
|
+
command: '/bin/echo',
|
|
363
|
+
args: ['hello'],
|
|
364
|
+
useTmux: true,
|
|
365
|
+
});
|
|
366
|
+
createdIds.push(result.id);
|
|
367
|
+
// Custom command sessions should never use tmux
|
|
368
|
+
assert.strictEqual(result.useTmux, false);
|
|
369
|
+
assert.strictEqual(result.tmuxSessionName, '');
|
|
370
|
+
});
|
|
371
|
+
it('list includes useTmux and tmuxSessionName fields', () => {
|
|
372
|
+
const result = sessions.create({
|
|
373
|
+
repoName: 'test-repo',
|
|
374
|
+
repoPath: '/tmp',
|
|
375
|
+
command: '/bin/echo',
|
|
376
|
+
args: ['hello'],
|
|
377
|
+
});
|
|
378
|
+
createdIds.push(result.id);
|
|
379
|
+
const list = sessions.list();
|
|
380
|
+
const session = list.find(s => s.id === result.id);
|
|
381
|
+
assert.ok(session);
|
|
382
|
+
assert.strictEqual(session.useTmux, false);
|
|
383
|
+
assert.strictEqual(session.tmuxSessionName, '');
|
|
384
|
+
});
|
|
385
|
+
it('calls onPtyReplaced when continue-arg process fails quickly', (_, done) => {
|
|
386
|
+
const result = sessions.create({
|
|
387
|
+
repoName: 'test-repo',
|
|
388
|
+
repoPath: '/tmp',
|
|
389
|
+
command: '/bin/false',
|
|
390
|
+
args: [...sessions.AGENT_CONTINUE_ARGS.claude],
|
|
391
|
+
});
|
|
392
|
+
createdIds.push(result.id);
|
|
393
|
+
const session = sessions.get(result.id);
|
|
394
|
+
assert.ok(session);
|
|
395
|
+
assert.strictEqual(session.mode, 'pty');
|
|
396
|
+
const ptySession = session;
|
|
397
|
+
ptySession.onPtyReplacedCallbacks.push((newPty) => {
|
|
398
|
+
assert.ok(newPty, 'should receive new PTY');
|
|
399
|
+
assert.strictEqual(ptySession.pty, newPty, 'session.pty should be updated to new PTY');
|
|
400
|
+
done();
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
it('session survives after continue-arg retry', (_, done) => {
|
|
404
|
+
const result = sessions.create({
|
|
405
|
+
repoName: 'test-repo',
|
|
406
|
+
repoPath: '/tmp',
|
|
407
|
+
command: '/bin/false',
|
|
408
|
+
args: [...sessions.AGENT_CONTINUE_ARGS.claude],
|
|
409
|
+
});
|
|
410
|
+
createdIds.push(result.id);
|
|
411
|
+
const session = sessions.get(result.id);
|
|
412
|
+
assert.ok(session);
|
|
413
|
+
assert.strictEqual(session.mode, 'pty');
|
|
414
|
+
const ptySession = session;
|
|
415
|
+
ptySession.onPtyReplacedCallbacks.push(() => {
|
|
416
|
+
const stillExists = sessions.get(result.id);
|
|
417
|
+
assert.ok(stillExists, 'session should still exist after retry');
|
|
418
|
+
done();
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
it('retries when continue-arg process exits quickly with code 0 (tmux behavior)', (_, done) => {
|
|
422
|
+
const result = sessions.create({
|
|
423
|
+
repoName: 'test-repo',
|
|
424
|
+
repoPath: '/tmp',
|
|
425
|
+
command: '/bin/sh',
|
|
426
|
+
args: ['-c', 'exit 0', ...sessions.AGENT_CONTINUE_ARGS.claude],
|
|
427
|
+
});
|
|
428
|
+
createdIds.push(result.id);
|
|
429
|
+
const session = sessions.get(result.id);
|
|
430
|
+
assert.ok(session);
|
|
431
|
+
assert.strictEqual(session.mode, 'pty');
|
|
432
|
+
const ptySession = session;
|
|
433
|
+
ptySession.onPtyReplacedCallbacks.push((newPty) => {
|
|
434
|
+
assert.ok(newPty, 'should receive new PTY even with exit code 0');
|
|
435
|
+
assert.strictEqual(ptySession.pty, newPty, 'session.pty should be updated');
|
|
436
|
+
const stillExists = sessions.get(result.id);
|
|
437
|
+
assert.ok(stillExists, 'session should still exist after retry');
|
|
438
|
+
done();
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
it('create accepts a predetermined id', () => {
|
|
442
|
+
const result = sessions.create({
|
|
443
|
+
id: 'custom-id-12345678',
|
|
444
|
+
repoName: 'test-repo',
|
|
445
|
+
repoPath: '/tmp',
|
|
446
|
+
command: '/bin/echo',
|
|
447
|
+
args: ['hello'],
|
|
448
|
+
});
|
|
449
|
+
createdIds.push(result.id);
|
|
450
|
+
assert.strictEqual(result.id, 'custom-id-12345678');
|
|
451
|
+
const session = sessions.get('custom-id-12345678');
|
|
452
|
+
assert.ok(session);
|
|
453
|
+
});
|
|
454
|
+
it('create accepts initialScrollback', () => {
|
|
455
|
+
const result = sessions.create({
|
|
456
|
+
repoName: 'test-repo',
|
|
457
|
+
repoPath: '/tmp',
|
|
458
|
+
command: '/bin/echo',
|
|
459
|
+
args: ['hello'],
|
|
460
|
+
initialScrollback: ['prior output\r\n'],
|
|
461
|
+
});
|
|
462
|
+
createdIds.push(result.id);
|
|
463
|
+
const session = sessions.get(result.id);
|
|
464
|
+
assert.ok(session);
|
|
465
|
+
assert.strictEqual(session.mode, 'pty');
|
|
466
|
+
assert.ok(session.scrollback.length >= 1);
|
|
467
|
+
assert.strictEqual(session.scrollback[0], 'prior output\r\n');
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
describe('session persistence', () => {
|
|
471
|
+
let tmpDir;
|
|
472
|
+
afterEach(() => {
|
|
473
|
+
// Clean up any sessions created during tests
|
|
474
|
+
for (const s of sessions.list()) {
|
|
475
|
+
try {
|
|
476
|
+
sessions.kill(s.id);
|
|
477
|
+
}
|
|
478
|
+
catch { /* ignore */ }
|
|
479
|
+
}
|
|
480
|
+
// Clean up temp directory
|
|
481
|
+
if (tmpDir) {
|
|
482
|
+
try {
|
|
483
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
484
|
+
}
|
|
485
|
+
catch { /* ignore */ }
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
function createTmpDir() {
|
|
489
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'crc-test-'));
|
|
490
|
+
return tmpDir;
|
|
491
|
+
}
|
|
492
|
+
it('serializeAll writes pending-sessions.json and scrollback files', () => {
|
|
493
|
+
const configDir = createTmpDir();
|
|
494
|
+
const s = sessions.create({
|
|
495
|
+
repoName: 'test-repo',
|
|
496
|
+
repoPath: '/tmp',
|
|
497
|
+
command: '/bin/cat',
|
|
498
|
+
args: [],
|
|
499
|
+
});
|
|
500
|
+
// Manually push some scrollback
|
|
501
|
+
const session = sessions.get(s.id);
|
|
502
|
+
assert.ok(session);
|
|
503
|
+
assert.strictEqual(session.mode, 'pty');
|
|
504
|
+
session.scrollback.push('hello world');
|
|
505
|
+
serializeAll(configDir);
|
|
506
|
+
// Check pending-sessions.json
|
|
507
|
+
const pendingPath = path.join(configDir, 'pending-sessions.json');
|
|
508
|
+
assert.ok(fs.existsSync(pendingPath), 'pending-sessions.json should exist');
|
|
509
|
+
const pending = JSON.parse(fs.readFileSync(pendingPath, 'utf-8'));
|
|
510
|
+
assert.strictEqual(pending.version, 1);
|
|
511
|
+
assert.ok(pending.timestamp);
|
|
512
|
+
assert.strictEqual(pending.sessions.length, 1);
|
|
513
|
+
assert.strictEqual(pending.sessions[0].id, s.id);
|
|
514
|
+
assert.strictEqual(pending.sessions[0].repoPath, '/tmp');
|
|
515
|
+
// Check scrollback file
|
|
516
|
+
const scrollbackPath = path.join(configDir, 'scrollback', s.id + '.buf');
|
|
517
|
+
assert.ok(fs.existsSync(scrollbackPath), 'scrollback file should exist');
|
|
518
|
+
const scrollbackData = fs.readFileSync(scrollbackPath, 'utf-8');
|
|
519
|
+
assert.ok(scrollbackData.includes('hello world'));
|
|
520
|
+
});
|
|
521
|
+
it('restoreFromDisk restores sessions with original IDs', async () => {
|
|
522
|
+
const configDir = createTmpDir();
|
|
523
|
+
// Create and serialize a session
|
|
524
|
+
const s = sessions.create({
|
|
525
|
+
repoName: 'test-repo',
|
|
526
|
+
repoPath: '/tmp',
|
|
527
|
+
command: '/bin/cat',
|
|
528
|
+
args: [],
|
|
529
|
+
displayName: 'my-session',
|
|
530
|
+
});
|
|
531
|
+
const originalId = s.id;
|
|
532
|
+
const session = sessions.get(originalId);
|
|
533
|
+
assert.ok(session);
|
|
534
|
+
assert.strictEqual(session.mode, 'pty');
|
|
535
|
+
session.scrollback.push('saved output');
|
|
536
|
+
serializeAll(configDir);
|
|
537
|
+
// Kill the original session
|
|
538
|
+
sessions.kill(originalId);
|
|
539
|
+
assert.strictEqual(sessions.get(originalId), undefined);
|
|
540
|
+
// Restore
|
|
541
|
+
const restored = await restoreFromDisk(configDir);
|
|
542
|
+
assert.strictEqual(restored, 1);
|
|
543
|
+
// Verify session exists with original ID
|
|
544
|
+
const restoredSession = sessions.get(originalId);
|
|
545
|
+
assert.ok(restoredSession, 'restored session should exist');
|
|
546
|
+
assert.strictEqual(restoredSession.repoPath, '/tmp');
|
|
547
|
+
assert.strictEqual(restoredSession.displayName, 'my-session');
|
|
548
|
+
// Scrollback should be restored
|
|
549
|
+
assert.strictEqual(restoredSession.mode, 'pty');
|
|
550
|
+
assert.ok(restoredSession.scrollback.length >= 1);
|
|
551
|
+
assert.strictEqual(restoredSession.scrollback[0], 'saved output');
|
|
552
|
+
// pending-sessions.json should be cleaned up
|
|
553
|
+
assert.ok(!fs.existsSync(path.join(configDir, 'pending-sessions.json')));
|
|
554
|
+
});
|
|
555
|
+
it('restoreFromDisk ignores stale files (>5 min old)', async () => {
|
|
556
|
+
const configDir = createTmpDir();
|
|
557
|
+
// Write a stale pending file
|
|
558
|
+
const staleTime = new Date(Date.now() - 6 * 60 * 1000).toISOString();
|
|
559
|
+
const pending = {
|
|
560
|
+
version: 1,
|
|
561
|
+
timestamp: staleTime,
|
|
562
|
+
sessions: [{ id: 'stale-id', type: 'repo', agent: 'claude', root: '', repoName: 'test', repoPath: '/tmp', worktreeName: '', branchName: '', displayName: 'test', createdAt: staleTime, lastActivity: staleTime, useTmux: false, tmuxSessionName: '', customCommand: null, cwd: '/tmp' }],
|
|
563
|
+
};
|
|
564
|
+
fs.writeFileSync(path.join(configDir, 'pending-sessions.json'), JSON.stringify(pending));
|
|
565
|
+
const restored = await restoreFromDisk(configDir);
|
|
566
|
+
assert.strictEqual(restored, 0, 'should not restore stale sessions');
|
|
567
|
+
assert.ok(!fs.existsSync(path.join(configDir, 'pending-sessions.json')), 'stale file should be deleted');
|
|
568
|
+
});
|
|
569
|
+
it('restoreFromDisk handles missing scrollback gracefully', async () => {
|
|
570
|
+
const configDir = createTmpDir();
|
|
571
|
+
// Create a session, serialize, then delete scrollback file
|
|
572
|
+
const s = sessions.create({
|
|
573
|
+
repoName: 'test-repo',
|
|
574
|
+
repoPath: '/tmp',
|
|
575
|
+
command: '/bin/cat',
|
|
576
|
+
args: [],
|
|
577
|
+
});
|
|
578
|
+
serializeAll(configDir);
|
|
579
|
+
sessions.kill(s.id);
|
|
580
|
+
// Delete scrollback file
|
|
581
|
+
const scrollbackPath = path.join(configDir, 'scrollback', s.id + '.buf');
|
|
582
|
+
try {
|
|
583
|
+
fs.unlinkSync(scrollbackPath);
|
|
584
|
+
}
|
|
585
|
+
catch { /* ignore */ }
|
|
586
|
+
const restored = await restoreFromDisk(configDir);
|
|
587
|
+
assert.strictEqual(restored, 1, 'should still restore without scrollback');
|
|
588
|
+
});
|
|
589
|
+
it('restoreFromDisk returns 0 when no pending file exists', async () => {
|
|
590
|
+
const configDir = createTmpDir();
|
|
591
|
+
const restored = await restoreFromDisk(configDir);
|
|
592
|
+
assert.strictEqual(restored, 0);
|
|
593
|
+
});
|
|
594
|
+
it('restoreFromDisk preserves tmuxSessionName for tmux sessions', async () => {
|
|
595
|
+
const configDir = createTmpDir();
|
|
596
|
+
// Write a pending file with a tmux session
|
|
597
|
+
const pending = {
|
|
598
|
+
version: 1,
|
|
599
|
+
timestamp: new Date().toISOString(),
|
|
600
|
+
sessions: [{
|
|
601
|
+
id: 'tmux-test-id',
|
|
602
|
+
type: 'worktree',
|
|
603
|
+
agent: 'claude',
|
|
604
|
+
root: '',
|
|
605
|
+
repoName: 'test-repo',
|
|
606
|
+
repoPath: '/tmp',
|
|
607
|
+
worktreeName: 'my-wt',
|
|
608
|
+
branchName: 'my-branch',
|
|
609
|
+
displayName: 'my-session',
|
|
610
|
+
createdAt: new Date().toISOString(),
|
|
611
|
+
lastActivity: new Date().toISOString(),
|
|
612
|
+
useTmux: true,
|
|
613
|
+
tmuxSessionName: 'crc-my-session-tmux-tes',
|
|
614
|
+
customCommand: '/bin/cat', // Use /bin/cat to avoid spawning real claude binary in test
|
|
615
|
+
cwd: '/tmp',
|
|
616
|
+
}],
|
|
617
|
+
};
|
|
618
|
+
fs.writeFileSync(path.join(configDir, 'pending-sessions.json'), JSON.stringify(pending));
|
|
619
|
+
const restored = await restoreFromDisk(configDir);
|
|
620
|
+
assert.strictEqual(restored, 1);
|
|
621
|
+
const session = sessions.get('tmux-test-id');
|
|
622
|
+
assert.ok(session, 'restored session should exist');
|
|
623
|
+
assert.strictEqual(session.mode, 'pty');
|
|
624
|
+
assert.strictEqual(session.tmuxSessionName, 'crc-my-session-tmux-tes', 'tmuxSessionName should be preserved from serialized data');
|
|
625
|
+
});
|
|
626
|
+
it('restored session remains in list after PTY exits (disconnected status)', async () => {
|
|
627
|
+
const configDir = createTmpDir();
|
|
628
|
+
const pending = {
|
|
629
|
+
version: 1,
|
|
630
|
+
timestamp: new Date().toISOString(),
|
|
631
|
+
sessions: [{
|
|
632
|
+
id: 'restore-exit-test',
|
|
633
|
+
type: 'worktree',
|
|
634
|
+
agent: 'claude',
|
|
635
|
+
root: '',
|
|
636
|
+
repoName: 'test-repo',
|
|
637
|
+
repoPath: '/tmp',
|
|
638
|
+
worktreeName: 'my-wt',
|
|
639
|
+
branchName: 'my-branch',
|
|
640
|
+
displayName: 'restored-session',
|
|
641
|
+
createdAt: new Date().toISOString(),
|
|
642
|
+
lastActivity: new Date().toISOString(),
|
|
643
|
+
useTmux: false,
|
|
644
|
+
tmuxSessionName: '',
|
|
645
|
+
customCommand: '/bin/false',
|
|
646
|
+
cwd: '/tmp',
|
|
647
|
+
}],
|
|
648
|
+
};
|
|
649
|
+
fs.writeFileSync(path.join(configDir, 'pending-sessions.json'), JSON.stringify(pending));
|
|
650
|
+
await restoreFromDisk(configDir);
|
|
651
|
+
// Wait for PTY to exit
|
|
652
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
653
|
+
// Session should still be in the list with disconnected status
|
|
654
|
+
const list = sessions.list();
|
|
655
|
+
const found = list.find(s => s.id === 'restore-exit-test');
|
|
656
|
+
assert.ok(found, 'restored session should remain in list after PTY exit');
|
|
657
|
+
assert.strictEqual(found.status, 'disconnected');
|
|
658
|
+
});
|
|
659
|
+
it('full serialize-restore round trip preserves all session fields including tmuxSessionName', async () => {
|
|
660
|
+
const configDir = createTmpDir();
|
|
661
|
+
// Create sessions of different types
|
|
662
|
+
const repo = sessions.create({
|
|
663
|
+
type: 'repo',
|
|
664
|
+
repoName: 'my-repo',
|
|
665
|
+
repoPath: '/tmp/repo',
|
|
666
|
+
command: '/bin/cat',
|
|
667
|
+
args: [],
|
|
668
|
+
displayName: 'My Repo',
|
|
669
|
+
});
|
|
670
|
+
const terminal = sessions.create({
|
|
671
|
+
type: 'terminal',
|
|
672
|
+
repoPath: '/tmp',
|
|
673
|
+
command: '/bin/sh',
|
|
674
|
+
args: [],
|
|
675
|
+
displayName: 'Terminal 1',
|
|
676
|
+
});
|
|
677
|
+
// Serialize all
|
|
678
|
+
serializeAll(configDir);
|
|
679
|
+
// Kill originals
|
|
680
|
+
sessions.kill(repo.id);
|
|
681
|
+
sessions.kill(terminal.id);
|
|
682
|
+
assert.strictEqual(sessions.list().length, 0);
|
|
683
|
+
// Also inject a tmux-style session into the pending file to test tmuxSessionName round-trip.
|
|
684
|
+
// Use customCommand so restore spawns that instead of claude --continue (which would exit instantly).
|
|
685
|
+
const pendingPath = path.join(configDir, 'pending-sessions.json');
|
|
686
|
+
const pending = JSON.parse(fs.readFileSync(pendingPath, 'utf-8'));
|
|
687
|
+
pending.sessions.push({
|
|
688
|
+
id: 'tmux-roundtrip-id',
|
|
689
|
+
type: 'worktree',
|
|
690
|
+
agent: 'claude',
|
|
691
|
+
root: '',
|
|
692
|
+
repoName: 'tmux-repo',
|
|
693
|
+
repoPath: '/tmp',
|
|
694
|
+
worktreeName: 'tmux-wt',
|
|
695
|
+
branchName: 'feat/tmux',
|
|
696
|
+
displayName: 'Tmux Session',
|
|
697
|
+
createdAt: new Date().toISOString(),
|
|
698
|
+
lastActivity: new Date().toISOString(),
|
|
699
|
+
useTmux: true,
|
|
700
|
+
tmuxSessionName: 'crc-tmux-session-tmux-rou',
|
|
701
|
+
customCommand: '/bin/cat',
|
|
702
|
+
cwd: '/tmp',
|
|
703
|
+
});
|
|
704
|
+
fs.writeFileSync(pendingPath, JSON.stringify(pending));
|
|
705
|
+
// Restore
|
|
706
|
+
const restored = await restoreFromDisk(configDir);
|
|
707
|
+
assert.strictEqual(restored, 3);
|
|
708
|
+
// Verify all sessions exist
|
|
709
|
+
const list = sessions.list();
|
|
710
|
+
assert.strictEqual(list.length, 3);
|
|
711
|
+
const restoredRepo = list.find(s => s.id === repo.id);
|
|
712
|
+
assert.ok(restoredRepo);
|
|
713
|
+
assert.strictEqual(restoredRepo.type, 'repo');
|
|
714
|
+
assert.strictEqual(restoredRepo.displayName, 'My Repo');
|
|
715
|
+
assert.strictEqual(restoredRepo.status, 'active');
|
|
716
|
+
const restoredTerminal = list.find(s => s.id === terminal.id);
|
|
717
|
+
assert.ok(restoredTerminal);
|
|
718
|
+
assert.strictEqual(restoredTerminal.type, 'terminal');
|
|
719
|
+
assert.strictEqual(restoredTerminal.displayName, 'Terminal 1');
|
|
720
|
+
// Verify tmux session name survived the round trip
|
|
721
|
+
const restoredTmux = sessions.get('tmux-roundtrip-id');
|
|
722
|
+
assert.ok(restoredTmux);
|
|
723
|
+
assert.strictEqual(restoredTmux.mode, 'pty');
|
|
724
|
+
assert.strictEqual(restoredTmux.tmuxSessionName, 'crc-tmux-session-tmux-rou');
|
|
725
|
+
assert.strictEqual(restoredTmux.displayName, 'Tmux Session');
|
|
726
|
+
});
|
|
727
|
+
it('serializeAll captures session state before kill', () => {
|
|
728
|
+
const configDir = createTmpDir();
|
|
729
|
+
const s = sessions.create({
|
|
730
|
+
repoName: 'test-repo',
|
|
731
|
+
repoPath: '/tmp',
|
|
732
|
+
command: '/bin/cat',
|
|
733
|
+
args: [],
|
|
734
|
+
displayName: 'before-kill',
|
|
735
|
+
});
|
|
736
|
+
const session = sessions.get(s.id);
|
|
737
|
+
assert.ok(session);
|
|
738
|
+
assert.strictEqual(session.mode, 'pty');
|
|
739
|
+
session.scrollback.push('important output');
|
|
740
|
+
serializeAll(configDir);
|
|
741
|
+
// Kill after serialize (mimics gracefulShutdown sequence)
|
|
742
|
+
sessions.kill(s.id);
|
|
743
|
+
// Verify data is on disk
|
|
744
|
+
const pendingPath = path.join(configDir, 'pending-sessions.json');
|
|
745
|
+
assert.ok(fs.existsSync(pendingPath));
|
|
746
|
+
const pending = JSON.parse(fs.readFileSync(pendingPath, 'utf-8'));
|
|
747
|
+
assert.strictEqual(pending.sessions.length, 1);
|
|
748
|
+
assert.strictEqual(pending.sessions[0].displayName, 'before-kill');
|
|
749
|
+
});
|
|
750
|
+
});
|