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,547 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { loadConfig, saveConfig } from './config.js';
|
|
6
|
+
import { extractOwnerRepo, buildRepoMap } from './git.js';
|
|
7
|
+
import { createLogger } from './logger.js';
|
|
8
|
+
// ── Smee singleton state ───────────────────────────────────────────────────────
|
|
9
|
+
let smeeHandle = null;
|
|
10
|
+
let smeeConnected = false;
|
|
11
|
+
let lastEventAt = null;
|
|
12
|
+
const logger = createLogger('webhook');
|
|
13
|
+
// ── Smart polling state ────────────────────────────────────────────────────────
|
|
14
|
+
let pollingTimer = null;
|
|
15
|
+
function stopSmartPolling() {
|
|
16
|
+
if (pollingTimer !== null) {
|
|
17
|
+
clearInterval(pollingTimer);
|
|
18
|
+
pollingTimer = null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Starts a 30-second polling interval that broadcasts `pr-updated` and
|
|
23
|
+
* `ci-updated` events only for workspaces that do NOT have a working webhook
|
|
24
|
+
* configured (`webhookEnabled !== true` or `webhookError` is set).
|
|
25
|
+
*
|
|
26
|
+
* Calling this again replaces any existing polling timer.
|
|
27
|
+
*/
|
|
28
|
+
export function startSmartPolling(configPath, broadcastEvent) {
|
|
29
|
+
stopSmartPolling();
|
|
30
|
+
const POLL_INTERVAL_MS = 30_000;
|
|
31
|
+
const tick = () => {
|
|
32
|
+
const config = loadConfig(configPath);
|
|
33
|
+
const workspacePaths = config.repos ?? [];
|
|
34
|
+
if (workspacePaths.length === 0)
|
|
35
|
+
return;
|
|
36
|
+
const repoSettings = config.repoSettings ?? {};
|
|
37
|
+
// Collect paths that need polling (no webhook or webhook has an error)
|
|
38
|
+
const unwebhookedPaths = workspacePaths.filter((wsPath) => {
|
|
39
|
+
const ws = repoSettings[wsPath];
|
|
40
|
+
return !ws?.webhookEnabled || ws?.webhookError;
|
|
41
|
+
});
|
|
42
|
+
if (unwebhookedPaths.length === 0)
|
|
43
|
+
return;
|
|
44
|
+
// Resolve owner/repo for each unwebhooked path synchronously via git config cache
|
|
45
|
+
// We use the async buildRepoMap but fire-and-forget inside the interval
|
|
46
|
+
void (async () => {
|
|
47
|
+
const execFn = (file, args, opts) => execFileAsync(file, args, opts);
|
|
48
|
+
const repoMap = await buildRepoMap(unwebhookedPaths, execFn);
|
|
49
|
+
// Single broadcast per poll cycle — frontend debounces invalidation
|
|
50
|
+
if (repoMap.size > 0) {
|
|
51
|
+
broadcastEvent('pr-updated', { repos: [...repoMap.keys()] });
|
|
52
|
+
broadcastEvent('ci-updated', { repos: [...repoMap.keys()] });
|
|
53
|
+
}
|
|
54
|
+
})();
|
|
55
|
+
};
|
|
56
|
+
pollingTimer = setInterval(tick, POLL_INTERVAL_MS);
|
|
57
|
+
}
|
|
58
|
+
function stopSmee() {
|
|
59
|
+
if (smeeHandle) {
|
|
60
|
+
try {
|
|
61
|
+
smeeHandle.close();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Best-effort
|
|
65
|
+
}
|
|
66
|
+
smeeHandle = null;
|
|
67
|
+
}
|
|
68
|
+
smeeConnected = false;
|
|
69
|
+
}
|
|
70
|
+
function startSmee(smeeUrl, targetPort) {
|
|
71
|
+
stopSmee();
|
|
72
|
+
// Dynamic import — smee-client may not be installed
|
|
73
|
+
void (async () => {
|
|
74
|
+
try {
|
|
75
|
+
// Import as unknown first to avoid type-mismatch with varying smee-client versions
|
|
76
|
+
const smeeModule = (await import('smee-client'));
|
|
77
|
+
const client = new smeeModule.default({
|
|
78
|
+
source: smeeUrl,
|
|
79
|
+
target: `http://localhost:${targetPort}/webhooks`,
|
|
80
|
+
logger: {
|
|
81
|
+
info: (message, ...args) => logger.info(String(message), ...args),
|
|
82
|
+
error: (message, ...args) => logger.error(String(message), ...args),
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
// Hook connectivity events via client setters (available before start)
|
|
86
|
+
client.onmessage = () => {
|
|
87
|
+
lastEventAt = new Date().toISOString();
|
|
88
|
+
};
|
|
89
|
+
client.onerror = () => {
|
|
90
|
+
smeeConnected = false;
|
|
91
|
+
};
|
|
92
|
+
client.onopen = () => {
|
|
93
|
+
smeeConnected = true;
|
|
94
|
+
};
|
|
95
|
+
// start() returns Promise<EventSource> — await it to get the handle for close()
|
|
96
|
+
const es = await client.start();
|
|
97
|
+
smeeHandle = { close: () => void es.close() };
|
|
98
|
+
smeeConnected = true;
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
logger.warn('smee-client not available or failed to start:', err);
|
|
102
|
+
smeeConnected = false;
|
|
103
|
+
}
|
|
104
|
+
})();
|
|
105
|
+
}
|
|
106
|
+
export function reloadSmee(configPath, port) {
|
|
107
|
+
const config = loadConfig(configPath);
|
|
108
|
+
const smeeUrl = config.github?.smeeUrl;
|
|
109
|
+
stopSmee();
|
|
110
|
+
if (smeeUrl) {
|
|
111
|
+
startSmee(smeeUrl, port);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export function getSmeeStatus() {
|
|
115
|
+
return { smeeConnected, lastEventAt };
|
|
116
|
+
}
|
|
117
|
+
function makeGithubApi(fetchFn) {
|
|
118
|
+
return async function githubApi(method, path, token, body) {
|
|
119
|
+
const init = {
|
|
120
|
+
method,
|
|
121
|
+
headers: {
|
|
122
|
+
Authorization: `Bearer ${token}`,
|
|
123
|
+
Accept: 'application/vnd.github+json',
|
|
124
|
+
'Content-Type': 'application/json',
|
|
125
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
if (body !== undefined) {
|
|
129
|
+
init.body = JSON.stringify(body);
|
|
130
|
+
}
|
|
131
|
+
return fetchFn(`https://api.github.com${path}`, init);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// ── Webhook CRUD helper ────────────────────────────────────────────────────────
|
|
135
|
+
const execFileAsync = promisify(execFile);
|
|
136
|
+
async function getOwnerRepoForPath(repoPath) {
|
|
137
|
+
try {
|
|
138
|
+
const { stdout } = await execFileAsync('git', ['remote', 'get-url', 'origin'], {
|
|
139
|
+
cwd: repoPath,
|
|
140
|
+
timeout: 10_000,
|
|
141
|
+
});
|
|
142
|
+
return extractOwnerRepo(stdout.trim());
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function createWebhookForPath(repoPath, configPath, config, githubApi) {
|
|
149
|
+
const token = config.github?.accessToken;
|
|
150
|
+
const secret = config.github?.webhookSecret;
|
|
151
|
+
const smeeUrl = config.github?.smeeUrl;
|
|
152
|
+
if (!token)
|
|
153
|
+
return { ok: false, error: 'not_authenticated', webhookError: null };
|
|
154
|
+
if (!secret || !smeeUrl)
|
|
155
|
+
return { ok: false, error: 'not_configured', webhookError: null };
|
|
156
|
+
const ownerRepo = await getOwnerRepoForPath(repoPath);
|
|
157
|
+
if (!ownerRepo)
|
|
158
|
+
return { ok: false, error: 'no_remote', webhookError: null };
|
|
159
|
+
const parts = ownerRepo.split('/');
|
|
160
|
+
const owner = parts[0];
|
|
161
|
+
const repo = parts[1];
|
|
162
|
+
if (!owner || !repo)
|
|
163
|
+
return { ok: false, error: 'invalid_remote', webhookError: null };
|
|
164
|
+
let apiRes;
|
|
165
|
+
try {
|
|
166
|
+
apiRes = await githubApi('POST', `/repos/${owner}/${repo}/hooks`, token, {
|
|
167
|
+
name: 'web',
|
|
168
|
+
active: true,
|
|
169
|
+
events: ['*'],
|
|
170
|
+
config: {
|
|
171
|
+
url: smeeUrl,
|
|
172
|
+
content_type: 'json',
|
|
173
|
+
secret,
|
|
174
|
+
insecure_ssl: '0',
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
error: `fetch_failed: ${String(err)}`,
|
|
182
|
+
webhookError: null,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
// 422 — webhook already exists; try to find the existing webhook ID
|
|
186
|
+
if (apiRes.status === 422) {
|
|
187
|
+
try {
|
|
188
|
+
const listRes = await githubApi('GET', `/repos/${owner}/${repo}/hooks`, token);
|
|
189
|
+
if (listRes.ok) {
|
|
190
|
+
const hooks = (await listRes.json());
|
|
191
|
+
const existing = hooks.find((h) => h.config?.url === smeeUrl);
|
|
192
|
+
if (existing) {
|
|
193
|
+
persistWebhookSuccess(configPath, config, repoPath, existing.id);
|
|
194
|
+
return { ok: true, webhookId: existing.id, ownerRepo };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
logger.warn('Could not retrieve existing webhook ID for', ownerRepo, err);
|
|
200
|
+
}
|
|
201
|
+
// Webhook exists on GitHub but we couldn't find its ID — don't persist a fake ID
|
|
202
|
+
return { ok: true, webhookId: 0, ownerRepo };
|
|
203
|
+
}
|
|
204
|
+
if (apiRes.status === 403) {
|
|
205
|
+
persistWebhookError(configPath, config, repoPath, 'not-admin');
|
|
206
|
+
return { ok: false, error: 'forbidden', webhookError: 'not-admin' };
|
|
207
|
+
}
|
|
208
|
+
if (apiRes.status === 401) {
|
|
209
|
+
return { ok: false, error: 'unauthorized', webhookError: null };
|
|
210
|
+
}
|
|
211
|
+
if (apiRes.status === 404) {
|
|
212
|
+
persistWebhookError(configPath, config, repoPath, 'not-found');
|
|
213
|
+
return { ok: false, error: 'not_found', webhookError: 'not-found' };
|
|
214
|
+
}
|
|
215
|
+
if (!apiRes.ok) {
|
|
216
|
+
return {
|
|
217
|
+
ok: false,
|
|
218
|
+
error: `github_error_${apiRes.status}`,
|
|
219
|
+
webhookError: null,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const created = (await apiRes.json());
|
|
223
|
+
persistWebhookSuccess(configPath, config, repoPath, created.id);
|
|
224
|
+
return { ok: true, webhookId: created.id, ownerRepo };
|
|
225
|
+
}
|
|
226
|
+
function persistWebhookSuccess(configPath, config, repoPath, webhookId) {
|
|
227
|
+
if (!config.repoSettings)
|
|
228
|
+
config.repoSettings = {};
|
|
229
|
+
if (!config.repoSettings[repoPath])
|
|
230
|
+
config.repoSettings[repoPath] = {};
|
|
231
|
+
const ws = config.repoSettings[repoPath];
|
|
232
|
+
ws.webhookId = webhookId;
|
|
233
|
+
ws.webhookEnabled = true;
|
|
234
|
+
delete ws.webhookError;
|
|
235
|
+
saveConfig(configPath, config);
|
|
236
|
+
}
|
|
237
|
+
function persistWebhookError(configPath, config, repoPath, errorCode) {
|
|
238
|
+
if (!config.repoSettings)
|
|
239
|
+
config.repoSettings = {};
|
|
240
|
+
if (!config.repoSettings[repoPath])
|
|
241
|
+
config.repoSettings[repoPath] = {};
|
|
242
|
+
config.repoSettings[repoPath].webhookError = errorCode;
|
|
243
|
+
saveConfig(configPath, config);
|
|
244
|
+
}
|
|
245
|
+
// ── Router factory ─────────────────────────────────────────────────────────────
|
|
246
|
+
/**
|
|
247
|
+
* Creates and returns an Express Router for webhook management routes.
|
|
248
|
+
*
|
|
249
|
+
* Mount with:
|
|
250
|
+
* app.use('/webhooks/manage', createWebhookManagerRouter({ configPath, broadcastEvent, requireAuth }));
|
|
251
|
+
*
|
|
252
|
+
* Distinct from the `/webhooks` receiver in webhooks.ts — this router handles CRUD and lifecycle.
|
|
253
|
+
*/
|
|
254
|
+
export function createWebhookManagerRouter(deps) {
|
|
255
|
+
const { configPath, requireAuth } = deps;
|
|
256
|
+
const fetchFn = deps.fetchFn ?? globalThis.fetch;
|
|
257
|
+
const githubApi = makeGithubApi(fetchFn);
|
|
258
|
+
const router = Router();
|
|
259
|
+
function getConfig() {
|
|
260
|
+
return loadConfig(configPath);
|
|
261
|
+
}
|
|
262
|
+
// All routes require authentication
|
|
263
|
+
router.use(requireAuth);
|
|
264
|
+
// ── POST /setup — Generate smee channel + secret, save config, start smee ──
|
|
265
|
+
router.post('/setup', async (_req, res) => {
|
|
266
|
+
// Create smee channel via redirect
|
|
267
|
+
let channelUrl;
|
|
268
|
+
try {
|
|
269
|
+
const smeeRes = await fetchFn('https://smee.io/new', {
|
|
270
|
+
redirect: 'manual',
|
|
271
|
+
});
|
|
272
|
+
const location = smeeRes.headers.get('location');
|
|
273
|
+
if (location) {
|
|
274
|
+
channelUrl = location;
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
// No location header — try following redirect
|
|
278
|
+
const followRes = await fetchFn('https://smee.io/new', {
|
|
279
|
+
redirect: 'follow',
|
|
280
|
+
});
|
|
281
|
+
channelUrl = followRes.url;
|
|
282
|
+
}
|
|
283
|
+
if (!channelUrl || !channelUrl.startsWith('https://smee.io/')) {
|
|
284
|
+
res.status(502).json({ error: 'smee_channel_failed' });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
res.status(502).json({ error: 'smee_unreachable', detail: String(err) });
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const secret = crypto.randomBytes(20).toString('hex');
|
|
293
|
+
const config = getConfig();
|
|
294
|
+
if (!config.github)
|
|
295
|
+
config.github = {};
|
|
296
|
+
config.github.webhookSecret = secret;
|
|
297
|
+
config.github.smeeUrl = channelUrl;
|
|
298
|
+
config.github.backfillOffered = false; // reset so backfill banner shows after fresh setup
|
|
299
|
+
saveConfig(configPath, config);
|
|
300
|
+
// Start smee client — non-blocking
|
|
301
|
+
startSmee(channelUrl, config.port ?? 3456);
|
|
302
|
+
res.json({ ok: true, smeeUrl: channelUrl });
|
|
303
|
+
});
|
|
304
|
+
// ── DELETE /setup — Teardown: delete all tracked webhooks, clear config, stop smee ──
|
|
305
|
+
router.delete('/setup', async (_req, res) => {
|
|
306
|
+
const config = getConfig();
|
|
307
|
+
const token = config.github?.accessToken;
|
|
308
|
+
let deleted = 0;
|
|
309
|
+
if (token && config.repoSettings) {
|
|
310
|
+
const entries = Object.entries(config.repoSettings);
|
|
311
|
+
for (const [repoPath, ws] of entries) {
|
|
312
|
+
const webhookId = ws.webhookId;
|
|
313
|
+
if (!webhookId)
|
|
314
|
+
continue;
|
|
315
|
+
const ownerRepo = await getOwnerRepoForPath(repoPath);
|
|
316
|
+
if (!ownerRepo)
|
|
317
|
+
continue;
|
|
318
|
+
const parts = ownerRepo.split('/');
|
|
319
|
+
const owner = parts[0];
|
|
320
|
+
const repo = parts[1];
|
|
321
|
+
if (!owner || !repo)
|
|
322
|
+
continue;
|
|
323
|
+
try {
|
|
324
|
+
await githubApi('DELETE', `/repos/${owner}/${repo}/hooks/${webhookId}`, token);
|
|
325
|
+
deleted++;
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
logger.warn(`Failed to delete webhook ${webhookId} for ${ownerRepo}:`, err);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// Clear webhook fields from all repo settings
|
|
332
|
+
for (const ws of Object.values(config.repoSettings)) {
|
|
333
|
+
delete ws.webhookId;
|
|
334
|
+
delete ws.webhookEnabled;
|
|
335
|
+
delete ws.webhookError;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// Clear github webhook config fields
|
|
339
|
+
if (config.github) {
|
|
340
|
+
delete config.github.webhookSecret;
|
|
341
|
+
delete config.github.smeeUrl;
|
|
342
|
+
delete config.github.autoProvision;
|
|
343
|
+
delete config.github.backfillOffered;
|
|
344
|
+
}
|
|
345
|
+
saveConfig(configPath, config);
|
|
346
|
+
stopSmee();
|
|
347
|
+
res.json({ ok: true, deleted });
|
|
348
|
+
});
|
|
349
|
+
// ── GET /status — Health endpoint ──
|
|
350
|
+
router.get('/status', (_req, res) => {
|
|
351
|
+
const config = getConfig();
|
|
352
|
+
const github = config.github;
|
|
353
|
+
const configured = Boolean(github?.webhookSecret && github?.smeeUrl);
|
|
354
|
+
const { smeeConnected: sc, lastEventAt: lea } = getSmeeStatus();
|
|
355
|
+
const secret = github?.webhookSecret ?? null;
|
|
356
|
+
const secretPreview = secret ? `****${secret.slice(-4)}` : null;
|
|
357
|
+
res.json({
|
|
358
|
+
configured,
|
|
359
|
+
smeeConnected: sc,
|
|
360
|
+
lastEventAt: lea,
|
|
361
|
+
autoProvision: github?.autoProvision ?? false,
|
|
362
|
+
secretPreview,
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
// ── POST /reload — Hot-reload smee client ──
|
|
366
|
+
router.post('/reload', (_req, res) => {
|
|
367
|
+
const config = getConfig();
|
|
368
|
+
const smeeUrl = config.github?.smeeUrl;
|
|
369
|
+
stopSmee();
|
|
370
|
+
if (smeeUrl) {
|
|
371
|
+
startSmee(smeeUrl, config.port ?? 3456);
|
|
372
|
+
}
|
|
373
|
+
res.json({ ok: true });
|
|
374
|
+
});
|
|
375
|
+
// ── POST /ping — Test connection via GitHub ping API ──
|
|
376
|
+
router.post('/ping', async (_req, res) => {
|
|
377
|
+
const config = getConfig();
|
|
378
|
+
const token = config.github?.accessToken;
|
|
379
|
+
if (!token) {
|
|
380
|
+
res.status(400).json({ error: 'not_authenticated' });
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
// Find first workspace with a webhookId
|
|
384
|
+
let foundOwnerRepo = null;
|
|
385
|
+
let foundWebhookId = null;
|
|
386
|
+
if (config.repoSettings) {
|
|
387
|
+
for (const [repoPath, ws] of Object.entries(config.repoSettings)) {
|
|
388
|
+
if (!ws.webhookId)
|
|
389
|
+
continue;
|
|
390
|
+
const ownerRepo = await getOwnerRepoForPath(repoPath);
|
|
391
|
+
if (ownerRepo) {
|
|
392
|
+
foundOwnerRepo = ownerRepo;
|
|
393
|
+
foundWebhookId = ws.webhookId;
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (!foundOwnerRepo || !foundWebhookId) {
|
|
399
|
+
res.json({ error: 'no_webhook' });
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const parts = foundOwnerRepo.split('/');
|
|
403
|
+
const owner = parts[0];
|
|
404
|
+
const repo = parts[1];
|
|
405
|
+
if (!owner || !repo) {
|
|
406
|
+
res.json({ error: 'no_webhook' });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
const pingRes = await githubApi('POST', `/repos/${owner}/${repo}/hooks/${foundWebhookId}/pings`, token);
|
|
411
|
+
if (pingRes.ok || pingRes.status === 204) {
|
|
412
|
+
res.json({ ok: true });
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
res
|
|
416
|
+
.status(pingRes.status)
|
|
417
|
+
.json({ error: 'ping_failed', status: pingRes.status });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch (err) {
|
|
421
|
+
res.status(502).json({ error: 'ping_failed', detail: String(err) });
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
// ── POST /repos — Create webhook for a specific repo ──
|
|
425
|
+
router.post('/repos', async (req, res) => {
|
|
426
|
+
const body = req.body;
|
|
427
|
+
const repoPath = body.repoPath;
|
|
428
|
+
if (!repoPath || typeof repoPath !== 'string') {
|
|
429
|
+
res.status(400).json({ error: 'missing_repo_path' });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const config = getConfig();
|
|
433
|
+
const result = await createWebhookForPath(repoPath, configPath, config, githubApi);
|
|
434
|
+
if (result.ok) {
|
|
435
|
+
res.json({ ok: true, webhookId: result.webhookId });
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
const status = result.webhookError === 'not-admin' ? 403 : 400;
|
|
439
|
+
res
|
|
440
|
+
.status(status)
|
|
441
|
+
.json({ error: result.error, webhookError: result.webhookError });
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
// ── POST /repos/remove — Remove webhook for a specific repo ──
|
|
445
|
+
router.post('/repos/remove', async (req, res) => {
|
|
446
|
+
const body = req.body;
|
|
447
|
+
const repoPath = body.repoPath;
|
|
448
|
+
if (!repoPath || typeof repoPath !== 'string') {
|
|
449
|
+
res.status(400).json({ error: 'missing_repo_path' });
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const config = getConfig();
|
|
453
|
+
const token = config.github?.accessToken;
|
|
454
|
+
const ws = config.repoSettings?.[repoPath];
|
|
455
|
+
const webhookId = ws?.webhookId;
|
|
456
|
+
if (!webhookId) {
|
|
457
|
+
res.json({ ok: true });
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (token) {
|
|
461
|
+
const ownerRepo = await getOwnerRepoForPath(repoPath);
|
|
462
|
+
if (ownerRepo) {
|
|
463
|
+
const parts = ownerRepo.split('/');
|
|
464
|
+
const owner = parts[0];
|
|
465
|
+
const repo = parts[1];
|
|
466
|
+
if (owner && repo) {
|
|
467
|
+
try {
|
|
468
|
+
const delRes = await githubApi('DELETE', `/repos/${owner}/${repo}/hooks/${webhookId}`, token);
|
|
469
|
+
// 404 is fine — webhook already gone
|
|
470
|
+
if (!delRes.ok && delRes.status !== 404) {
|
|
471
|
+
logger.warn(`DELETE hook returned ${delRes.status}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
catch (err) {
|
|
475
|
+
logger.warn('Failed to delete webhook via API:', err);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// Clear local webhook state regardless of API result
|
|
481
|
+
if (config.repoSettings?.[repoPath]) {
|
|
482
|
+
const wsEntry = config.repoSettings[repoPath];
|
|
483
|
+
delete wsEntry.webhookId;
|
|
484
|
+
delete wsEntry.webhookEnabled;
|
|
485
|
+
delete wsEntry.webhookError;
|
|
486
|
+
}
|
|
487
|
+
saveConfig(configPath, config);
|
|
488
|
+
res.json({ ok: true });
|
|
489
|
+
});
|
|
490
|
+
// ── POST /backfill — Create webhooks for all workspaces ──
|
|
491
|
+
router.post('/backfill', async (_req, res) => {
|
|
492
|
+
const config = getConfig();
|
|
493
|
+
const workspacePaths = config.repos ?? [];
|
|
494
|
+
if (workspacePaths.length === 0) {
|
|
495
|
+
res.json({ total: 0, success: 0, failed: 0, results: [] });
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
// Build repo map to confirm which paths have valid git remotes
|
|
499
|
+
const execFn = (file, args, opts) => execFileAsync(file, args, opts);
|
|
500
|
+
const repoMap = await buildRepoMap(workspacePaths, execFn);
|
|
501
|
+
const results = [];
|
|
502
|
+
// Bounded concurrency: 5 at a time
|
|
503
|
+
const CONCURRENCY = 5;
|
|
504
|
+
const paths = [...workspacePaths];
|
|
505
|
+
for (let i = 0; i < paths.length; i += CONCURRENCY) {
|
|
506
|
+
const batch = paths.slice(i, i + CONCURRENCY);
|
|
507
|
+
const batchResults = await Promise.all(batch.map(async (wsPath) => {
|
|
508
|
+
// Find ownerRepo from the map by value
|
|
509
|
+
let ownerRepo = null;
|
|
510
|
+
for (const [key, val] of repoMap) {
|
|
511
|
+
if (val === wsPath) {
|
|
512
|
+
ownerRepo = key;
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (!ownerRepo) {
|
|
517
|
+
return {
|
|
518
|
+
path: wsPath,
|
|
519
|
+
ownerRepo: null,
|
|
520
|
+
ok: false,
|
|
521
|
+
error: 'no_remote',
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
// Reload config for each path to pick up writes from previous batch items
|
|
525
|
+
const freshConfig = getConfig();
|
|
526
|
+
const result = await createWebhookForPath(wsPath, configPath, freshConfig, githubApi);
|
|
527
|
+
if (result.ok) {
|
|
528
|
+
return { path: wsPath, ownerRepo, ok: true };
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
return { path: wsPath, ownerRepo, ok: false, error: result.error };
|
|
532
|
+
}
|
|
533
|
+
}));
|
|
534
|
+
results.push(...batchResults);
|
|
535
|
+
}
|
|
536
|
+
const success = results.filter((r) => r.ok).length;
|
|
537
|
+
const failed = results.filter((r) => !r.ok).length;
|
|
538
|
+
// Mark backfill as offered so the banner doesn't re-show
|
|
539
|
+
const updatedConfig = getConfig();
|
|
540
|
+
if (updatedConfig.github) {
|
|
541
|
+
updatedConfig.github.backfillOffered = true;
|
|
542
|
+
saveConfig(configPath, updatedConfig);
|
|
543
|
+
}
|
|
544
|
+
res.json({ total: paths.length, success, failed, results });
|
|
545
|
+
});
|
|
546
|
+
return router;
|
|
547
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { Router } from 'express';
|
|
3
|
+
import express from 'express';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Helpers
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
function verifySignature(secret, payload, signature) {
|
|
8
|
+
const expected = 'sha256=' +
|
|
9
|
+
crypto.createHmac('sha256', secret).update(payload).digest('hex');
|
|
10
|
+
try {
|
|
11
|
+
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Factory
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
export function createWebhookRouter(deps) {
|
|
21
|
+
const router = Router();
|
|
22
|
+
// Middleware: parse JSON and preserve raw body for signature verification
|
|
23
|
+
router.use(express.json({
|
|
24
|
+
verify: (req, _res, buf) => {
|
|
25
|
+
req.rawBody =
|
|
26
|
+
buf.toString('utf8');
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
// POST / — receive GitHub webhook events
|
|
30
|
+
router.post('/', (req, res) => {
|
|
31
|
+
const secret = deps.secret();
|
|
32
|
+
// If no secret configured, webhooks are not set up yet
|
|
33
|
+
if (!secret) {
|
|
34
|
+
res.status(401).json({ error: 'Webhooks not configured' });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const signature = req.headers['x-hub-signature-256'];
|
|
38
|
+
// Reject if signature header is missing
|
|
39
|
+
if (!signature || typeof signature !== 'string') {
|
|
40
|
+
res.status(401).json({ error: 'Missing signature' });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Verify signature against raw body
|
|
44
|
+
const rawBody = req.rawBody ?? '';
|
|
45
|
+
if (!verifySignature(secret, rawBody, signature)) {
|
|
46
|
+
res.status(401).json({ error: 'Invalid signature' });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Route based on event type
|
|
50
|
+
const event = req.headers['x-github-event'];
|
|
51
|
+
const repoFullName = req.body?.repository
|
|
52
|
+
? req.body.repository?.full_name
|
|
53
|
+
: undefined;
|
|
54
|
+
if (event === 'pull_request' || event === 'pull_request_review') {
|
|
55
|
+
deps.broadcastEvent('pr-updated', repoFullName ? { repo: repoFullName } : undefined);
|
|
56
|
+
// If PR was merged, also broadcast worktrees-changed so sidebar refreshes with branchState: 'merged'
|
|
57
|
+
if (event === 'pull_request') {
|
|
58
|
+
const body = req.body;
|
|
59
|
+
const action = body.action;
|
|
60
|
+
const pr = body.pull_request;
|
|
61
|
+
if (action === 'closed' && pr?.merged === true) {
|
|
62
|
+
deps.broadcastEvent('worktrees-changed');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (event === 'check_suite' || event === 'check_run') {
|
|
67
|
+
deps.broadcastEvent('ci-updated', repoFullName ? { repo: repoFullName } : undefined);
|
|
68
|
+
}
|
|
69
|
+
// Unknown events: ignore, return 200 OK
|
|
70
|
+
res.json({ ok: true });
|
|
71
|
+
});
|
|
72
|
+
return router;
|
|
73
|
+
}
|