@ayushporwal/coja 0.0.1
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/bin/coja.js +2 -0
- package/dist/ai/chat-handler.js +115 -0
- package/dist/ai/chats.js +92 -0
- package/dist/ai/history.js +70 -0
- package/dist/ai/prompt.js +73 -0
- package/dist/ai/providers.js +81 -0
- package/dist/ai/title.js +77 -0
- package/dist/ai/tools.js +495 -0
- package/dist/ai/types.js +22 -0
- package/dist/app.js +142 -0
- package/dist/cli.js +99 -0
- package/dist/codex/connection.js +293 -0
- package/dist/codex/errors.js +52 -0
- package/dist/codex/models.js +97 -0
- package/dist/codex/oauth.js +217 -0
- package/dist/codex/protocol.js +49 -0
- package/dist/codex/provider.js +209 -0
- package/dist/context.js +8 -0
- package/dist/custom-providers.js +134 -0
- package/dist/data-dir.js +25 -0
- package/dist/db.js +65 -0
- package/dist/forge/forge.js +11 -0
- package/dist/forge/gh-cli.js +99 -0
- package/dist/forge/github.js +627 -0
- package/dist/forge/graphql.js +106 -0
- package/dist/forge/queries.js +258 -0
- package/dist/git/plumbing.js +519 -0
- package/dist/git/remote.js +47 -0
- package/dist/git/run.js +256 -0
- package/dist/open-browser.js +27 -0
- package/dist/paths.js +18 -0
- package/dist/pr/fetcher.js +173 -0
- package/dist/pr/objects.js +83 -0
- package/dist/pr/refs.js +17 -0
- package/dist/projects/add.js +111 -0
- package/dist/projects/store.js +37 -0
- package/dist/routes/chat.js +187 -0
- package/dist/routes/fs.js +65 -0
- package/dist/routes/git.js +75 -0
- package/dist/routes/guard.js +96 -0
- package/dist/routes/http.js +35 -0
- package/dist/routes/projects.js +51 -0
- package/dist/routes/pulls.js +64 -0
- package/dist/routes/review.js +113 -0
- package/dist/routes/setup.js +285 -0
- package/dist/secrets/providers.js +27 -0
- package/dist/secrets/store.js +190 -0
- package/dist/server.js +51 -0
- package/dist/services.js +15 -0
- package/dist/shared/api.js +186 -0
- package/package.json +35 -0
- package/public/assets/abap-CLvhMVsD.js +1 -0
- package/public/assets/actionscript-3-Cs6JJXT5.js +1 -0
- package/public/assets/ada-C5qYipkI.js +1 -0
- package/public/assets/ahk-CA9gdOdF.js +1 -0
- package/public/assets/ahk2-_6rzW9BS.js +1 -0
- package/public/assets/andromeeda-vGVdxbeo.js +1 -0
- package/public/assets/angular-html-BvGMhoM4.js +1 -0
- package/public/assets/angular-ts-D4cJlfHk.js +1 -0
- package/public/assets/apache-U0d_L8uA.js +1 -0
- package/public/assets/apex-KFGFyKwG.js +1 -0
- package/public/assets/apl-BAzDJNhF.js +1 -0
- package/public/assets/applescript-CCn79oCD.js +1 -0
- package/public/assets/ara-4CJ0cIlV.js +1 -0
- package/public/assets/asciidoc-SCjQUq34.js +1 -0
- package/public/assets/asm-Cmm7eHzH.js +1 -0
- package/public/assets/astro-tbl65Kw6.js +1 -0
- package/public/assets/aurora-x-CDeNXAV0.js +1 -0
- package/public/assets/awk-BWXHIvNe.js +1 -0
- package/public/assets/ayu-dark-DluEY0Gj.js +1 -0
- package/public/assets/ayu-light-C3h-C4tm.js +1 -0
- package/public/assets/ayu-mirage-Bqwy1Gya.js +1 -0
- package/public/assets/ballerina-B7ZEbQpA.js +1 -0
- package/public/assets/bat-qdxAdBWQ.js +1 -0
- package/public/assets/beancount-D-usSTwE.js +1 -0
- package/public/assets/berry-DKpUyyne.js +1 -0
- package/public/assets/bibtex-Ci_nEsc7.js +1 -0
- package/public/assets/bicep-CUHmPFLl.js +1 -0
- package/public/assets/bird2-CYIUgYqB.js +1 -0
- package/public/assets/blade-Cy5u_P69.js +1 -0
- package/public/assets/bsl-_VjlB9mv.js +1 -0
- package/public/assets/c-xsYtGME5.js +1 -0
- package/public/assets/c3-D0apG41V.js +1 -0
- package/public/assets/cadence-CQ2zXKGN.js +1 -0
- package/public/assets/cairo-DLTphjLi.js +1 -0
- package/public/assets/catppuccin-frappe-C9Q5zVZY.js +1 -0
- package/public/assets/catppuccin-latte-CtsyrtPq.js +1 -0
- package/public/assets/catppuccin-macchiato-R8nAf1vR.js +1 -0
- package/public/assets/catppuccin-mocha-DjuaAJKi.js +1 -0
- package/public/assets/chapel-DlXZg0HF.js +1 -0
- package/public/assets/clarity-CtfBBspg.js +1 -0
- package/public/assets/clojure-DqKBuwfJ.js +1 -0
- package/public/assets/cmake-Bj61d0ZC.js +1 -0
- package/public/assets/cobol-NNONwiOX.js +1 -0
- package/public/assets/codeowners-C8r90Shi.js +1 -0
- package/public/assets/codeql-oeQT6MSM.js +1 -0
- package/public/assets/coffee-O853MBTp.js +1 -0
- package/public/assets/common-lisp-Cv5bFMCO.js +1 -0
- package/public/assets/coq-DuBRPxYg.js +1 -0
- package/public/assets/cpp-DtWY3seq.js +1 -0
- package/public/assets/crystal-CcZ3zkyx.js +1 -0
- package/public/assets/csharp-oqKa8noW.js +1 -0
- package/public/assets/css-mSNQLTOb.js +1 -0
- package/public/assets/csv-Dx-8-gkx.js +1 -0
- package/public/assets/cue-CE9AQfxI.js +1 -0
- package/public/assets/cypher-ClKdZ_lG.js +1 -0
- package/public/assets/d-qD-0Kul2.js +1 -0
- package/public/assets/dark-plus-Cs2F2srj.js +1 -0
- package/public/assets/dart-CnvKMtbv.js +1 -0
- package/public/assets/dax-BkyTk9wS.js +1 -0
- package/public/assets/desktop-Dlh5hvp9.js +1 -0
- package/public/assets/diff-woXpYk--.js +1 -0
- package/public/assets/docker-IyjqRm3v.js +1 -0
- package/public/assets/dotenv-_5a1GRtc.js +1 -0
- package/public/assets/dracula-BHWKrbxM.js +1 -0
- package/public/assets/dracula-soft-5eyTD99u.js +1 -0
- package/public/assets/dream-maker-DW3nJb8Q.js +1 -0
- package/public/assets/edge-BA0HOmx0.js +1 -0
- package/public/assets/elixir-Buj0VxhK.js +1 -0
- package/public/assets/elm-lVH2iZsu.js +1 -0
- package/public/assets/emacs-lisp-c_oH4hRZ.js +1 -0
- package/public/assets/erb-CbQPls-2.js +1 -0
- package/public/assets/erlang-Cphh6RMH.js +1 -0
- package/public/assets/everforest-dark-sB-x3p7T.js +1 -0
- package/public/assets/everforest-light-Df2xbC6M.js +1 -0
- package/public/assets/fennel-DQxkIbk2.js +1 -0
- package/public/assets/fish-BJitypiv.js +1 -0
- package/public/assets/fluent-C03EYrpw.js +1 -0
- package/public/assets/fortran-fixed-form-DEKoE2YW.js +1 -0
- package/public/assets/fortran-free-form-CYNrtFtB.js +1 -0
- package/public/assets/fsharp-D13ZGOAj.js +1 -0
- package/public/assets/gdresource-xMC7jr7x.js +1 -0
- package/public/assets/gdscript-DYvRD3hg.js +1 -0
- package/public/assets/gdshader-CBce3t8t.js +1 -0
- package/public/assets/genie-CV2tkWYe.js +1 -0
- package/public/assets/gherkin-DExj1W_8.js +1 -0
- package/public/assets/git-commit-BSykSTBG.js +1 -0
- package/public/assets/git-rebase-DidgtTB0.js +1 -0
- package/public/assets/github-dark-C-LZuMrd.js +1 -0
- package/public/assets/github-dark-default-DXG-b-1a.js +1 -0
- package/public/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
- package/public/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
- package/public/assets/github-light-EUqPIrTm.js +1 -0
- package/public/assets/github-light-default-BXViO-2h.js +1 -0
- package/public/assets/github-light-high-contrast-B68TUdTA.js +1 -0
- package/public/assets/gleam-CSRkHgEL.js +1 -0
- package/public/assets/glimmer-js-Cck5MdS-.js +1 -0
- package/public/assets/glimmer-ts-DMfQxyYr.js +1 -0
- package/public/assets/glsl-DPBFrb1h.js +1 -0
- package/public/assets/gn-ilITqXS6.js +1 -0
- package/public/assets/gnuplot-7GGW24-e.js +1 -0
- package/public/assets/go-rLFTqkRN.js +1 -0
- package/public/assets/graphql-BvPubeCr.js +1 -0
- package/public/assets/groovy-CacY0gHj.js +1 -0
- package/public/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
- package/public/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
- package/public/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
- package/public/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
- package/public/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
- package/public/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
- package/public/assets/hack-Drv5y81z.js +1 -0
- package/public/assets/haml-DcpINEk-.js +1 -0
- package/public/assets/handlebars-sj4UAwHW.js +1 -0
- package/public/assets/haskell-D8IpX4py.js +1 -0
- package/public/assets/haxe-C8oCnwJJ.js +1 -0
- package/public/assets/hcl-Dh228itO.js +1 -0
- package/public/assets/hjson-CxZEssPk.js +1 -0
- package/public/assets/hlsl-Cvrh5tZx.js +1 -0
- package/public/assets/horizon-CE9ld1lL.js +1 -0
- package/public/assets/horizon-bright-DSNQnXHK.js +1 -0
- package/public/assets/houston-CsvMBhTu.js +1 -0
- package/public/assets/html-DQjDPZbg.js +1 -0
- package/public/assets/html-derivative-DscSYlel.js +1 -0
- package/public/assets/http-CCrfmrUy.js +1 -0
- package/public/assets/hurl-DLm6Xdy2.js +1 -0
- package/public/assets/hxml-Dcm88srW.js +1 -0
- package/public/assets/hy-CZbG8q4J.js +1 -0
- package/public/assets/imba-DsUTQ-LC.js +1 -0
- package/public/assets/index-BDRMh7Hy.js +2918 -0
- package/public/assets/index-Cw-82hD0.css +1 -0
- package/public/assets/ini-B5eOa1yu.js +1 -0
- package/public/assets/java-CzpiG9dn.js +1 -0
- package/public/assets/javascript-2eedadNh.js +1 -0
- package/public/assets/jinja-Cch1Cycs.js +1 -0
- package/public/assets/jison-C3DQW9A-.js +1 -0
- package/public/assets/json-BhBCwuxI.js +1 -0
- package/public/assets/json5-BR5RXkoi.js +1 -0
- package/public/assets/jsonc-CYpm1nAK.js +1 -0
- package/public/assets/jsonl-CmCQp5Yx.js +1 -0
- package/public/assets/jsonnet-CJTPZ8u_.js +1 -0
- package/public/assets/jssm-DXw9l8Rf.js +1 -0
- package/public/assets/jsx-De1vU88o.js +1 -0
- package/public/assets/julia-BlJzw_Rq.js +1 -0
- package/public/assets/just-A2qsO4cM.js +1 -0
- package/public/assets/kanagawa-dragon-Bozd7klA.js +1 -0
- package/public/assets/kanagawa-lotus-BmuOG9ZW.js +1 -0
- package/public/assets/kanagawa-wave-BML5mF2J.js +1 -0
- package/public/assets/kdl-CsD5j6eV.js +1 -0
- package/public/assets/kotlin-DhhofPvG.js +1 -0
- package/public/assets/kusto-C7mF5XQf.js +1 -0
- package/public/assets/laserwave-C_8bwKvT.js +1 -0
- package/public/assets/latex-CDaOJk0y.js +1 -0
- package/public/assets/lean-CewbzKMR.js +1 -0
- package/public/assets/less-DVTAwKKz.js +1 -0
- package/public/assets/light-plus-DVQuIRkW.js +1 -0
- package/public/assets/liquid-BOnT7KYX.js +1 -0
- package/public/assets/llvm-DqHOMS-s.js +1 -0
- package/public/assets/log-BNLmms1o.js +1 -0
- package/public/assets/logo-Cluzi2Zq.js +1 -0
- package/public/assets/lottie-BT9QSO-G.js +2 -0
- package/public/assets/lua-CoLL9qBE.js +1 -0
- package/public/assets/luau-C2pBCSgt.js +1 -0
- package/public/assets/make-Dixweg8N.js +1 -0
- package/public/assets/markdown-BYOwaDjH.js +1 -0
- package/public/assets/marko-D8Xuri3Y.js +1 -0
- package/public/assets/material-theme-Bm3Qr25_.js +1 -0
- package/public/assets/material-theme-darker-2IIEA8gg.js +1 -0
- package/public/assets/material-theme-lighter-uhdI0v04.js +1 -0
- package/public/assets/material-theme-ocean-CHQ94UKr.js +1 -0
- package/public/assets/material-theme-palenight-B5W6OYN7.js +1 -0
- package/public/assets/matlab-D7qyCx1q.js +1 -0
- package/public/assets/mdc-BhVsbv7E.js +1 -0
- package/public/assets/mdx-DQZ5AkYe.js +1 -0
- package/public/assets/mermaid-BxpfTIzt.js +1 -0
- package/public/assets/min-dark-BSWPekZh.js +1 -0
- package/public/assets/min-light-DDpmG2fV.js +1 -0
- package/public/assets/mipsasm-BMqwQI7S.js +1 -0
- package/public/assets/mojo-fg40_ntQ.js +1 -0
- package/public/assets/monokai-B_bV9PQ9.js +1 -0
- package/public/assets/moonbit-DgjMoLWr.js +1 -0
- package/public/assets/move-BYnGhZoM.js +1 -0
- package/public/assets/narrat-_X_XdTYD.js +1 -0
- package/public/assets/nextflow-BJtWHP5T.js +1 -0
- package/public/assets/nextflow-groovy-DJMQeKeT.js +1 -0
- package/public/assets/nginx-CN1bxnHs.js +1 -0
- package/public/assets/night-owl-DhmEMT88.js +1 -0
- package/public/assets/night-owl-light-eJ-hLW7d.js +1 -0
- package/public/assets/nim-BKDc-Eam.js +1 -0
- package/public/assets/nix-IvuFDN5E.js +1 -0
- package/public/assets/nord-Cb4Vim4T.js +1 -0
- package/public/assets/nsis-CjNLYCLg.js +1 -0
- package/public/assets/nushell-DHYfO66s.js +1 -0
- package/public/assets/objective-c-D1A_Heim.js +1 -0
- package/public/assets/objective-cpp-BsSzOQcm.js +1 -0
- package/public/assets/ocaml-O90oeIOV.js +1 -0
- package/public/assets/odin-B1RWQWA5.js +1 -0
- package/public/assets/one-dark-pro-CLwyXe_n.js +1 -0
- package/public/assets/one-light-D7Lr4KcI.js +1 -0
- package/public/assets/openscad-BUDT5pXO.js +1 -0
- package/public/assets/org-CBldiceC.js +1 -0
- package/public/assets/pascal-4ZHwLPI5.js +1 -0
- package/public/assets/perl-KiFGXvwB.js +1 -0
- package/public/assets/php-CjFYgW_Q.js +1 -0
- package/public/assets/pierre-dark-CpLgRqie.js +1 -0
- package/public/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
- package/public/assets/pierre-dark-soft-kZQmAZld.js +1 -0
- package/public/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
- package/public/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
- package/public/assets/pierre-light-CoaEpmwp.js +1 -0
- package/public/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
- package/public/assets/pierre-light-soft-lWLdNTOI.js +1 -0
- package/public/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
- package/public/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
- package/public/assets/pkl-ot-7Btpt.js +1 -0
- package/public/assets/plastic-DQwYfKfQ.js +1 -0
- package/public/assets/plsql-DGHpHOYJ.js +1 -0
- package/public/assets/po-BiJDBrnU.js +1 -0
- package/public/assets/poimandres-DRFjx7u4.js +1 -0
- package/public/assets/polar-C7UOKdEL.js +1 -0
- package/public/assets/postcss-BXeXVLqQ.js +1 -0
- package/public/assets/powerquery-DNMTfnFr.js +1 -0
- package/public/assets/powershell-CCSX75yM.js +1 -0
- package/public/assets/prisma-DMgNhnYS.js +1 -0
- package/public/assets/prolog-iXnhIJG7.js +1 -0
- package/public/assets/proto-DB4EqR-F.js +1 -0
- package/public/assets/pug-CiQNzZtF.js +1 -0
- package/public/assets/puppet-CDv2pdJW.js +1 -0
- package/public/assets/purescript-9MfHhQsQ.js +1 -0
- package/public/assets/python-gzcpVVnB.js +1 -0
- package/public/assets/qml-BynhyZxj.js +1 -0
- package/public/assets/qmldir-DCQb3MpD.js +1 -0
- package/public/assets/qss-Fe1Jh2GI.js +1 -0
- package/public/assets/r-BydfSESc.js +1 -0
- package/public/assets/racket-DcIDlBhZ.js +1 -0
- package/public/assets/raku-B3gFvitq.js +1 -0
- package/public/assets/razor-Bfi-c6ag.js +1 -0
- package/public/assets/rbs-C0MpOrj4.js +1 -0
- package/public/assets/red-DX1BQOs8.js +1 -0
- package/public/assets/reg-CRGYupPL.js +1 -0
- package/public/assets/regexp-BFpups_T.js +1 -0
- package/public/assets/rel-BtDbiS_P.js +1 -0
- package/public/assets/riscv-Ckw8ddFX.js +1 -0
- package/public/assets/ron-VUp2lXgN.js +1 -0
- package/public/assets/rose-pine-aMsaMD1D.js +1 -0
- package/public/assets/rose-pine-dawn-8PwUyIlP.js +1 -0
- package/public/assets/rose-pine-moon-DO-qXg-I.js +1 -0
- package/public/assets/rosmsg-CAekHB0j.js +1 -0
- package/public/assets/rst-Z5_5o5cf.js +1 -0
- package/public/assets/ruby-DyP6QZ5x.js +1 -0
- package/public/assets/rust-Cfkwpbl8.js +1 -0
- package/public/assets/sas-DzcEtbFW.js +1 -0
- package/public/assets/sass-DXrisJhu.js +1 -0
- package/public/assets/scala-BiFt_7mu.js +1 -0
- package/public/assets/scheme-DQCgrYNe.js +1 -0
- package/public/assets/scss-BVi50TCh.js +1 -0
- package/public/assets/sdbl-bTVj8UrX.js +1 -0
- package/public/assets/shaderlab-TOUzSsQk.js +1 -0
- package/public/assets/shellscript-DwBEbT4l.js +1 -0
- package/public/assets/shellsession-DtCkQKX0.js +1 -0
- package/public/assets/slack-dark-DnToyrRv.js +1 -0
- package/public/assets/slack-ochin-B2OO5cIa.js +1 -0
- package/public/assets/smalltalk-BHpQb5nM.js +1 -0
- package/public/assets/smithy-Dgaz7aur.js +1 -0
- package/public/assets/snazzy-light-4G7pJPwS.js +1 -0
- package/public/assets/solarized-dark-UdsmMDCL.js +1 -0
- package/public/assets/solarized-light-C1IUL_tW.js +1 -0
- package/public/assets/solidity-Bj7E1mRn.js +1 -0
- package/public/assets/soy-Col_eH-0.js +1 -0
- package/public/assets/sparql-D_iOobhT.js +1 -0
- package/public/assets/splunk-BC2Px7Mm.js +1 -0
- package/public/assets/sql-CF1geVQJ.js +1 -0
- package/public/assets/ssh-config-BgfXC-Er.js +1 -0
- package/public/assets/stata-JT7HPvq1.js +1 -0
- package/public/assets/stylus-B6D30XZt.js +1 -0
- package/public/assets/surrealql-kYIUYZug.js +1 -0
- package/public/assets/svelte-CsWZn_FS.js +1 -0
- package/public/assets/swift-CyEgAFGc.js +1 -0
- package/public/assets/synthwave-84-nFMaYfgc.js +1 -0
- package/public/assets/system-verilog-Dh5ihWbf.js +1 -0
- package/public/assets/systemd-BxMlprV5.js +1 -0
- package/public/assets/talonscript-CohzipZa.js +1 -0
- package/public/assets/tasl-DMoTqEGO.js +1 -0
- package/public/assets/tcl-CZd0xW_V.js +1 -0
- package/public/assets/templ-DCsT8WEJ.js +1 -0
- package/public/assets/terraform-DswuEJGm.js +1 -0
- package/public/assets/tex-ZyN9TW16.js +1 -0
- package/public/assets/tokyo-night-oM2G3aXe.js +1 -0
- package/public/assets/toml-CcmNWLt0.js +1 -0
- package/public/assets/ts-tags-ArLnwaTi.js +1 -0
- package/public/assets/tsv-sltzmVWM.js +1 -0
- package/public/assets/tsx-CXwa5__r.js +1 -0
- package/public/assets/turtle-ByJddavk.js +1 -0
- package/public/assets/twig-BsBKj8BU.js +1 -0
- package/public/assets/typescript-Brm8t4pO.js +1 -0
- package/public/assets/typespec-BRdr0IET.js +1 -0
- package/public/assets/typst-vNjrO6QE.js +1 -0
- package/public/assets/v-DBj4K9d_.js +1 -0
- package/public/assets/vala-zf12oZj6.js +1 -0
- package/public/assets/vb-DOwzaHZ0.js +1 -0
- package/public/assets/verilog-B2gwz-sJ.js +1 -0
- package/public/assets/vesper-DdrHHSXu.js +1 -0
- package/public/assets/vhdl-BroJfC0k.js +1 -0
- package/public/assets/viml-DvXPmvsu.js +1 -0
- package/public/assets/vitesse-black-fwtXNY1n.js +1 -0
- package/public/assets/vitesse-dark-BZCL-v6S.js +1 -0
- package/public/assets/vitesse-light-VbXTXTou.js +1 -0
- package/public/assets/vue-CcPq89qx.js +1 -0
- package/public/assets/vue-html-B5qW2N7E.js +1 -0
- package/public/assets/vue-vine-LYElP1mf.js +1 -0
- package/public/assets/vyper-CgoNMtux.js +1 -0
- package/public/assets/wasm-BnjxR4X6.js +1 -0
- package/public/assets/wasm-ByWQv1Qj.js +1 -0
- package/public/assets/wenyan-C8pVoKbM.js +1 -0
- package/public/assets/wgsl-BsKzXJz4.js +1 -0
- package/public/assets/wikitext-ClFFjSW2.js +1 -0
- package/public/assets/wit-DdvCle-K.js +1 -0
- package/public/assets/wolfram-DLL8P-h_.js +1 -0
- package/public/assets/xml-D5hvN78Q.js +1 -0
- package/public/assets/xsl-CBSZ35CX.js +1 -0
- package/public/assets/yaml-D4FTZq40.js +1 -0
- package/public/assets/zenscript-BnlCZFoB.js +1 -0
- package/public/assets/zig-CMLA9XwU.js +1 -0
- package/public/brand/coja/cj-dark.svg +1 -0
- package/public/brand/coja/cj-light.svg +1 -0
- package/public/brand/coja/cj-themeable.svg +1 -0
- package/public/brand/coja/lottie/cj-huge-dark.json +311 -0
- package/public/brand/coja/lottie/cj-huge-light.json +311 -0
- package/public/brand/coja/lottie/cj-large-dark.json +311 -0
- package/public/brand/coja/lottie/cj-large-light.json +311 -0
- package/public/brand/coja/lottie/cj-medium-dark.json +311 -0
- package/public/brand/coja/lottie/cj-medium-light.json +311 -0
- package/public/brand/coja/lottie/cj-small-dark.json +311 -0
- package/public/brand/coja/lottie/cj-small-light.json +311 -0
- package/public/brand/coja/manifest.json +172 -0
- package/public/brand/coja/themes/ember/dark/cj-huge.json +311 -0
- package/public/brand/coja/themes/ember/dark/cj-large.json +311 -0
- package/public/brand/coja/themes/ember/dark/cj-medium.json +311 -0
- package/public/brand/coja/themes/ember/dark/cj-small.json +311 -0
- package/public/brand/coja/themes/ember/dark/cj.svg +1 -0
- package/public/brand/coja/themes/ember/light/cj-huge.json +311 -0
- package/public/brand/coja/themes/ember/light/cj-large.json +311 -0
- package/public/brand/coja/themes/ember/light/cj-medium.json +311 -0
- package/public/brand/coja/themes/ember/light/cj-small.json +311 -0
- package/public/brand/coja/themes/ember/light/cj.svg +1 -0
- package/public/brand/coja/themes/grove/dark/cj-huge.json +311 -0
- package/public/brand/coja/themes/grove/dark/cj-large.json +311 -0
- package/public/brand/coja/themes/grove/dark/cj-medium.json +311 -0
- package/public/brand/coja/themes/grove/dark/cj-small.json +311 -0
- package/public/brand/coja/themes/grove/dark/cj.svg +1 -0
- package/public/brand/coja/themes/grove/light/cj-huge.json +311 -0
- package/public/brand/coja/themes/grove/light/cj-large.json +311 -0
- package/public/brand/coja/themes/grove/light/cj-medium.json +311 -0
- package/public/brand/coja/themes/grove/light/cj-small.json +311 -0
- package/public/brand/coja/themes/grove/light/cj.svg +1 -0
- package/public/brand/coja/themes/iris/dark/cj-huge.json +311 -0
- package/public/brand/coja/themes/iris/dark/cj-large.json +311 -0
- package/public/brand/coja/themes/iris/dark/cj-medium.json +311 -0
- package/public/brand/coja/themes/iris/dark/cj-small.json +311 -0
- package/public/brand/coja/themes/iris/dark/cj.svg +1 -0
- package/public/brand/coja/themes/iris/light/cj-huge.json +311 -0
- package/public/brand/coja/themes/iris/light/cj-large.json +311 -0
- package/public/brand/coja/themes/iris/light/cj-medium.json +311 -0
- package/public/brand/coja/themes/iris/light/cj-small.json +311 -0
- package/public/brand/coja/themes/iris/light/cj.svg +1 -0
- package/public/brand/coja/themes/mulberry/dark/cj-huge.json +311 -0
- package/public/brand/coja/themes/mulberry/dark/cj-large.json +311 -0
- package/public/brand/coja/themes/mulberry/dark/cj-medium.json +311 -0
- package/public/brand/coja/themes/mulberry/dark/cj-small.json +311 -0
- package/public/brand/coja/themes/mulberry/dark/cj.svg +1 -0
- package/public/brand/coja/themes/mulberry/light/cj-huge.json +311 -0
- package/public/brand/coja/themes/mulberry/light/cj-large.json +311 -0
- package/public/brand/coja/themes/mulberry/light/cj-medium.json +311 -0
- package/public/brand/coja/themes/mulberry/light/cj-small.json +311 -0
- package/public/brand/coja/themes/mulberry/light/cj.svg +1 -0
- package/public/brand/coja/themes/ocean/dark/cj-huge.json +311 -0
- package/public/brand/coja/themes/ocean/dark/cj-large.json +311 -0
- package/public/brand/coja/themes/ocean/dark/cj-medium.json +311 -0
- package/public/brand/coja/themes/ocean/dark/cj-small.json +311 -0
- package/public/brand/coja/themes/ocean/dark/cj.svg +1 -0
- package/public/brand/coja/themes/ocean/light/cj-huge.json +311 -0
- package/public/brand/coja/themes/ocean/light/cj-large.json +311 -0
- package/public/brand/coja/themes/ocean/light/cj-medium.json +311 -0
- package/public/brand/coja/themes/ocean/light/cj-small.json +311 -0
- package/public/brand/coja/themes/ocean/light/cj.svg +1 -0
- package/public/fonts/ibm-plex-sans/IBMPlexSans-Bold.woff2 +0 -0
- package/public/fonts/ibm-plex-sans/IBMPlexSans-BoldItalic.woff2 +0 -0
- package/public/fonts/ibm-plex-sans/IBMPlexSans-Italic.woff2 +0 -0
- package/public/fonts/ibm-plex-sans/IBMPlexSans-Medium.woff2 +0 -0
- package/public/fonts/ibm-plex-sans/IBMPlexSans-MediumItalic.woff2 +0 -0
- package/public/fonts/ibm-plex-sans/IBMPlexSans-Regular.woff2 +0 -0
- package/public/fonts/ibm-plex-sans/IBMPlexSans-SemiBold.woff2 +0 -0
- package/public/fonts/ibm-plex-sans/IBMPlexSans-SemiBoldItalic.woff2 +0 -0
- package/public/fonts/ibm-plex-sans/LICENSE.txt +93 -0
- package/public/fonts/ibm-plex-sans/PROVENANCE.md +10 -0
- package/public/fonts/maple-mono-nf/LICENSE.txt +93 -0
- package/public/fonts/maple-mono-nf/MapleMono-NF-Bold.woff2 +0 -0
- package/public/fonts/maple-mono-nf/MapleMono-NF-BoldItalic.woff2 +0 -0
- package/public/fonts/maple-mono-nf/MapleMono-NF-Italic.woff2 +0 -0
- package/public/fonts/maple-mono-nf/MapleMono-NF-Medium.woff2 +0 -0
- package/public/fonts/maple-mono-nf/MapleMono-NF-MediumItalic.woff2 +0 -0
- package/public/fonts/maple-mono-nf/MapleMono-NF-Regular.woff2 +0 -0
- package/public/fonts/maple-mono-nf/MapleMono-NF-SemiBold.woff2 +0 -0
- package/public/fonts/maple-mono-nf/MapleMono-NF-SemiBoldItalic.woff2 +0 -0
- package/public/fonts/maple-mono-nf/PROVENANCE.md +12 -0
- package/public/index.html +14 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
import { HttpError } from '../routes/http.js';
|
|
2
|
+
import { isEmptyFilter } from '../shared/api.js';
|
|
3
|
+
import { ForgeError } from './forge.js';
|
|
4
|
+
import * as q from './queries.js';
|
|
5
|
+
/**
|
|
6
|
+
* The GitHub implementation of `Forge`, on top of the GraphQL API. GitHub is
|
|
7
|
+
* the source of truth for all review state (design.md, "Sync model"): nothing
|
|
8
|
+
* here is cached except the viewer's identity.
|
|
9
|
+
*
|
|
10
|
+
* Every mutation in this class is reached only from an HTTP route handler that
|
|
11
|
+
* a human triggered in the UI. Nothing under server/src/ai may import it.
|
|
12
|
+
*/
|
|
13
|
+
/** Open-PR list pages of 100; a repo with more than this many open PRs is truncated. */
|
|
14
|
+
/** One API page = one GraphQL page; also the contract's `perPage`. */
|
|
15
|
+
export const PR_PAGE_SIZE = 100;
|
|
16
|
+
/** GitHub caps a PR's commit list at 250 regardless of paging (100 + 100 + 50). */
|
|
17
|
+
const MAX_COMMIT_PAGES = 3;
|
|
18
|
+
/** Runaway guard for the other connections (files are capped by GitHub at 3000 = 30 pages). */
|
|
19
|
+
const MAX_PAGES = 50;
|
|
20
|
+
export const COMMENT_LOCATION_REJECTED = 'GitHub rejected the comment location: the line must be part of the pull request diff.';
|
|
21
|
+
export const SUMMARY_REQUIRED = 'Write a summary or add at least one comment first.';
|
|
22
|
+
export class GitHubForge {
|
|
23
|
+
gql;
|
|
24
|
+
viewerPromise;
|
|
25
|
+
/** `owner/repo` + filter → page number → that page's start cursor (page 1 needs none). */
|
|
26
|
+
prCursors = new Map();
|
|
27
|
+
/** Per-PR tail of the `addPendingComment` chain, so first comments cannot race (see below). */
|
|
28
|
+
commentQueues = new Map();
|
|
29
|
+
constructor(opts) {
|
|
30
|
+
this.gql = opts.gql;
|
|
31
|
+
}
|
|
32
|
+
viewer() {
|
|
33
|
+
if (!this.viewerPromise) {
|
|
34
|
+
const p = this.gql(q.VIEWER).then((d) => mapActor(d.viewer));
|
|
35
|
+
// Do not cache a failure: the next call retries.
|
|
36
|
+
p.catch(() => {
|
|
37
|
+
if (this.viewerPromise === p)
|
|
38
|
+
this.viewerPromise = undefined;
|
|
39
|
+
});
|
|
40
|
+
this.viewerPromise = p;
|
|
41
|
+
}
|
|
42
|
+
return this.viewerPromise;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* One page of open PRs. A page is exactly one GraphQL page (100 nodes), so
|
|
46
|
+
* serving page N needs the start cursor of page N: cursors are cached per
|
|
47
|
+
* repo as they are seen, and a jump past the deepest cached page walks
|
|
48
|
+
* forward one 100-node query at a time, caching as it goes — later visits
|
|
49
|
+
* to any known page are a single query again.
|
|
50
|
+
*/
|
|
51
|
+
async listPullRequestPage(repo, page, filter) {
|
|
52
|
+
const wanted = Math.max(1, Math.trunc(page));
|
|
53
|
+
const { login } = await this.viewer();
|
|
54
|
+
const filtered = filter !== undefined && !isEmptyFilter(filter);
|
|
55
|
+
// Cursors are cached per repo AND per filter (each filter pages independently).
|
|
56
|
+
const cursors = this.prPageCursors(repo, filter);
|
|
57
|
+
// Deepest page at or before the wanted one whose start cursor we know:
|
|
58
|
+
// walking begins there (a lower cached page needs no walk at all).
|
|
59
|
+
let start = 1;
|
|
60
|
+
while (start < wanted && cursors.has(start + 1))
|
|
61
|
+
start += 1;
|
|
62
|
+
let items = [];
|
|
63
|
+
let total = 0;
|
|
64
|
+
for (let p = start; p <= wanted; p += 1) {
|
|
65
|
+
const after = cursors.get(p);
|
|
66
|
+
const connection = filtered
|
|
67
|
+
? this.searchConnection(await this.gql(q.PR_SEARCH, {
|
|
68
|
+
query: buildSearchQuery(repo, filter),
|
|
69
|
+
login,
|
|
70
|
+
after,
|
|
71
|
+
}), repo)
|
|
72
|
+
: this.listConnection(await this.gql(q.PR_LIST, {
|
|
73
|
+
owner: repo.owner,
|
|
74
|
+
name: repo.repo,
|
|
75
|
+
login,
|
|
76
|
+
after,
|
|
77
|
+
}), repo);
|
|
78
|
+
total = connection.totalCount ?? 0;
|
|
79
|
+
const end = connection.pageInfo?.endCursor;
|
|
80
|
+
const hasNext = connection.pageInfo?.hasNextPage;
|
|
81
|
+
// Cache the next page's start cursor even when this is the page being
|
|
82
|
+
// served, so a later visit to page+1 needs no re-walk.
|
|
83
|
+
if (end && hasNext)
|
|
84
|
+
cursors.set(p + 1, end);
|
|
85
|
+
if (p === wanted) {
|
|
86
|
+
items = (connection.nodes ?? []).filter(nonNull).map(mapSummary);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
if (!end || !hasNext) {
|
|
90
|
+
// The list is shorter than the requested page: serve it empty.
|
|
91
|
+
return this.prPage([], wanted, total);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return this.prPage(items, wanted, total);
|
|
95
|
+
}
|
|
96
|
+
listConnection(d, repo) {
|
|
97
|
+
const connection = d.repository?.pullRequests;
|
|
98
|
+
if (!connection)
|
|
99
|
+
throw repoNotFound(repo);
|
|
100
|
+
return connection;
|
|
101
|
+
}
|
|
102
|
+
searchConnection(d, repo) {
|
|
103
|
+
if (!d.search)
|
|
104
|
+
throw new ForgeError(`GitHub search failed for ${repo.owner}/${repo.repo}`);
|
|
105
|
+
return { ...d.search, totalCount: d.search.issueCount ?? d.search.totalCount };
|
|
106
|
+
}
|
|
107
|
+
prPageCursors(repo, filter) {
|
|
108
|
+
const key = filterKey(repo, filter);
|
|
109
|
+
let cache = this.prCursors.get(key);
|
|
110
|
+
if (!cache) {
|
|
111
|
+
cache = new Map();
|
|
112
|
+
this.prCursors.set(key, cache);
|
|
113
|
+
}
|
|
114
|
+
return cache;
|
|
115
|
+
}
|
|
116
|
+
prPage(items, page, total) {
|
|
117
|
+
return {
|
|
118
|
+
items,
|
|
119
|
+
page,
|
|
120
|
+
perPage: PR_PAGE_SIZE,
|
|
121
|
+
total,
|
|
122
|
+
totalPages: Math.max(1, Math.ceil(total / PR_PAGE_SIZE)),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async getPullRequestRefs(repo, number) {
|
|
126
|
+
const d = await this.gql(q.PR_REFS, {
|
|
127
|
+
owner: repo.owner,
|
|
128
|
+
name: repo.repo,
|
|
129
|
+
number,
|
|
130
|
+
});
|
|
131
|
+
const pr = d.repository?.pullRequest;
|
|
132
|
+
if (!pr)
|
|
133
|
+
throw prNotFound(repo, number);
|
|
134
|
+
return {
|
|
135
|
+
id: pr.id,
|
|
136
|
+
number: pr.number,
|
|
137
|
+
headRefOid: pr.headRefOid,
|
|
138
|
+
baseRefOid: pr.baseRefOid,
|
|
139
|
+
baseRefName: pr.baseRefName,
|
|
140
|
+
headRefName: pr.headRefName,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async getPullRequest(repo, number) {
|
|
144
|
+
const viewer = await this.viewer();
|
|
145
|
+
const vars = { owner: repo.owner, name: repo.repo, number, login: viewer.login };
|
|
146
|
+
const data = await this.gql(q.PR_DETAIL, vars);
|
|
147
|
+
const pr = data.repository?.pullRequest;
|
|
148
|
+
if (!pr)
|
|
149
|
+
throw prNotFound(repo, number);
|
|
150
|
+
const page = (doc, pick) => {
|
|
151
|
+
return async (after) => pick(await this.gql(doc, { ...vars, after })) ?? EMPTY;
|
|
152
|
+
};
|
|
153
|
+
const [commits, files, rawThreads, reviews, issueComments] = await Promise.all([
|
|
154
|
+
drain(pr.commits, page(q.PR_COMMITS_PAGE, (d) => d.repository?.pullRequest?.commits), MAX_COMMIT_PAGES),
|
|
155
|
+
drain(pr.files, page(q.PR_FILES_PAGE, (d) => d.repository?.pullRequest?.files)),
|
|
156
|
+
drain(pr.reviewThreads, page(q.PR_THREADS_PAGE, (d) => d.repository?.pullRequest?.reviewThreads)),
|
|
157
|
+
drain(pr.reviews, page(q.PR_REVIEWS_PAGE, (d) => d.repository?.pullRequest?.reviews)),
|
|
158
|
+
drain(pr.comments, page(q.PR_ISSUE_COMMENTS_PAGE, (d) => d.repository?.pullRequest?.comments)),
|
|
159
|
+
]);
|
|
160
|
+
const threads = await Promise.all(rawThreads.map(async (t) => {
|
|
161
|
+
const comments = await drain(t.comments, async (after) => {
|
|
162
|
+
const d = await this.gql(q.THREAD_COMMENTS_PAGE, {
|
|
163
|
+
id: t.id,
|
|
164
|
+
after,
|
|
165
|
+
});
|
|
166
|
+
return d.node?.comments ?? EMPTY;
|
|
167
|
+
});
|
|
168
|
+
return mapThread(t, comments.map((c) => mapComment(c, viewer.login)));
|
|
169
|
+
}));
|
|
170
|
+
return {
|
|
171
|
+
...mapSummary(pr),
|
|
172
|
+
body: pr.body,
|
|
173
|
+
bodyHTML: pr.bodyHTML,
|
|
174
|
+
commits: commits.map(mapCommit),
|
|
175
|
+
files: files.map(mapFile),
|
|
176
|
+
threads,
|
|
177
|
+
conversation: buildConversation(issueComments, reviews),
|
|
178
|
+
pendingReview: mapPendingReview(pr.pendingReview?.nodes),
|
|
179
|
+
viewer,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Serialized per pull request: two quick first comments would otherwise both
|
|
184
|
+
* see "no pending review" and each create one, leaving the second invisible
|
|
185
|
+
* to the UI. A failed comment does not block the ones queued behind it.
|
|
186
|
+
*/
|
|
187
|
+
addPendingComment(repo, pr, input) {
|
|
188
|
+
const k = `${repo.owner}/${repo.repo}#${pr.number}`.toLowerCase();
|
|
189
|
+
const previous = this.commentQueues.get(k) ?? Promise.resolve();
|
|
190
|
+
const run = previous.then(() => this.addPendingCommentNow(repo, pr, input));
|
|
191
|
+
const settled = run.then(noop, noop).finally(() => {
|
|
192
|
+
if (this.commentQueues.get(k) === settled)
|
|
193
|
+
this.commentQueues.delete(k);
|
|
194
|
+
});
|
|
195
|
+
this.commentQueues.set(k, settled);
|
|
196
|
+
return run;
|
|
197
|
+
}
|
|
198
|
+
async addPendingCommentNow(repo, pr, input) {
|
|
199
|
+
const viewer = await this.viewer();
|
|
200
|
+
let pending = await this.findPendingReview(repo, pr.number, viewer.login);
|
|
201
|
+
if (!pending) {
|
|
202
|
+
const d = await this.gql(q.START_REVIEW, {
|
|
203
|
+
pullRequestId: pr.id,
|
|
204
|
+
commitOID: pr.headRefOid,
|
|
205
|
+
});
|
|
206
|
+
const review = d.addPullRequestReview?.pullRequestReview;
|
|
207
|
+
if (!review)
|
|
208
|
+
throw new ForgeError('GitHub did not create a pending review.', 502);
|
|
209
|
+
pending = { id: review.id, commentCount: 0 };
|
|
210
|
+
}
|
|
211
|
+
const threadInput = {
|
|
212
|
+
pullRequestReviewId: pending.id,
|
|
213
|
+
path: input.path,
|
|
214
|
+
body: input.body,
|
|
215
|
+
line: input.line,
|
|
216
|
+
side: input.side,
|
|
217
|
+
};
|
|
218
|
+
// A range is anything that does not start exactly where it ends (line and side).
|
|
219
|
+
const startLine = input.startLine ?? input.line;
|
|
220
|
+
const startSide = input.startSide ?? input.side;
|
|
221
|
+
if (startLine !== input.line || startSide !== input.side) {
|
|
222
|
+
threadInput.startLine = startLine;
|
|
223
|
+
threadInput.startSide = startSide;
|
|
224
|
+
}
|
|
225
|
+
const d = await this.gql(q.ADD_THREAD, { input: threadInput });
|
|
226
|
+
const thread = d.addPullRequestReviewThread?.thread;
|
|
227
|
+
if (!thread)
|
|
228
|
+
throw new ForgeError(COMMENT_LOCATION_REJECTED, 422);
|
|
229
|
+
const comments = compact(thread.comments.nodes).map((c) => ({
|
|
230
|
+
...mapComment(c, viewer.login),
|
|
231
|
+
reviewId: c.pullRequestReview?.id ?? pending.id,
|
|
232
|
+
reviewState: 'PENDING',
|
|
233
|
+
isPending: true,
|
|
234
|
+
isMine: true,
|
|
235
|
+
}));
|
|
236
|
+
const commentCount = compact(thread.firstComment?.nodes)[0]?.pullRequestReview?.comments?.totalCount ??
|
|
237
|
+
pending.commentCount + 1;
|
|
238
|
+
return {
|
|
239
|
+
thread: mapThread(thread, comments),
|
|
240
|
+
pendingReview: { id: pending.id, commentCount },
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Reply without a pending-review id. Published immediately when the viewer has no pending
|
|
245
|
+
* review; when one is open GitHub attaches the reply to it instead (observed live), and the
|
|
246
|
+
* returned comment says so through `isPending` / `reviewId`.
|
|
247
|
+
*/
|
|
248
|
+
async replyToThread(threadId, body) {
|
|
249
|
+
const viewer = await this.viewer();
|
|
250
|
+
const d = await this.gql(q.REPLY, { threadId, body });
|
|
251
|
+
const comment = d.addPullRequestReviewThreadReply?.comment;
|
|
252
|
+
if (!comment)
|
|
253
|
+
throw new ForgeError('GitHub did not return the posted reply.', 502);
|
|
254
|
+
const mapped = mapComment(comment, viewer.login);
|
|
255
|
+
const review = comment.pullRequestReview;
|
|
256
|
+
const pendingReview = mapped.isPending && review
|
|
257
|
+
? { id: review.id, commentCount: review.comments?.totalCount ?? 1 }
|
|
258
|
+
: undefined;
|
|
259
|
+
return { comment: mapped, threadId, ...(pendingReview ? { pendingReview } : {}) };
|
|
260
|
+
}
|
|
261
|
+
async updateComment(commentId, body) {
|
|
262
|
+
const viewer = await this.viewer();
|
|
263
|
+
const d = await this.gql(q.UPDATE_COMMENT, { id: commentId, body });
|
|
264
|
+
const comment = d.updatePullRequestReviewComment?.pullRequestReviewComment;
|
|
265
|
+
if (!comment)
|
|
266
|
+
throw new ForgeError('GitHub did not return the updated comment.', 502);
|
|
267
|
+
return mapComment(comment, viewer.login);
|
|
268
|
+
}
|
|
269
|
+
async deleteComment(commentId) {
|
|
270
|
+
await this.gql(q.DELETE_COMMENT, { id: commentId });
|
|
271
|
+
}
|
|
272
|
+
assertCommentInPullRequest(commentId, repo, number) {
|
|
273
|
+
return this.assertNodeInPullRequest(q.COMMENT_OWNER, commentId, repo, number, 'comment');
|
|
274
|
+
}
|
|
275
|
+
assertThreadInPullRequest(threadId, repo, number) {
|
|
276
|
+
return this.assertNodeInPullRequest(q.THREAD_OWNER, threadId, repo, number, 'thread');
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* 404 (`not_found`) unless the node resolves, is of the expected type, and
|
|
280
|
+
* belongs to PR `number` of `repo`. GitHub's own "could not resolve node" is
|
|
281
|
+
* folded into the same 404 so a bad id is indistinguishable from another PR's.
|
|
282
|
+
*/
|
|
283
|
+
async assertNodeInPullRequest(doc, id, repo, number, what) {
|
|
284
|
+
let data;
|
|
285
|
+
try {
|
|
286
|
+
data = await this.gql(doc, { id });
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
if (err instanceof ForgeError && err.status === 404)
|
|
290
|
+
throw notInPullRequest(what);
|
|
291
|
+
throw err;
|
|
292
|
+
}
|
|
293
|
+
const pr = data.node?.pullRequest;
|
|
294
|
+
if (!pr ||
|
|
295
|
+
pr.number !== number ||
|
|
296
|
+
!sameSlug(pr.repository.owner.login, repo.owner) ||
|
|
297
|
+
!sameSlug(pr.repository.name, repo.repo)) {
|
|
298
|
+
throw notInPullRequest(what);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async setFileViewed(prId, path, viewed) {
|
|
302
|
+
if (viewed) {
|
|
303
|
+
await this.gql(q.MARK_VIEWED, { pullRequestId: prId, path });
|
|
304
|
+
return 'VIEWED';
|
|
305
|
+
}
|
|
306
|
+
await this.gql(q.UNMARK_VIEWED, { pullRequestId: prId, path });
|
|
307
|
+
return 'UNVIEWED';
|
|
308
|
+
}
|
|
309
|
+
async submitReview(repo, pr, input) {
|
|
310
|
+
const viewer = await this.viewer();
|
|
311
|
+
const pending = await this.findPendingReview(repo, pr.number, viewer.login);
|
|
312
|
+
const body = input.body.trim() === '' ? undefined : input.body;
|
|
313
|
+
if (input.event !== 'APPROVE' && body === undefined && !(pending && pending.commentCount > 0)) {
|
|
314
|
+
throw new HttpError(400, SUMMARY_REQUIRED, 'bad_request');
|
|
315
|
+
}
|
|
316
|
+
let review;
|
|
317
|
+
if (pending) {
|
|
318
|
+
const d = await this.gql(q.SUBMIT_REVIEW, {
|
|
319
|
+
reviewId: pending.id,
|
|
320
|
+
event: input.event,
|
|
321
|
+
...(body === undefined ? {} : { body }),
|
|
322
|
+
});
|
|
323
|
+
review = d.submitPullRequestReview?.pullRequestReview;
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
const d = await this.gql(q.ADD_AND_SUBMIT_REVIEW, {
|
|
327
|
+
pullRequestId: pr.id,
|
|
328
|
+
commitOID: pr.headRefOid,
|
|
329
|
+
event: input.event,
|
|
330
|
+
...(body === undefined ? {} : { body }),
|
|
331
|
+
});
|
|
332
|
+
review = d.addPullRequestReview?.pullRequestReview;
|
|
333
|
+
}
|
|
334
|
+
if (!review)
|
|
335
|
+
throw new ForgeError('GitHub did not return the submitted review.', 502);
|
|
336
|
+
return {
|
|
337
|
+
reviewId: review.id,
|
|
338
|
+
state: asReviewState(review.state) ?? 'COMMENTED',
|
|
339
|
+
...(review.url ? { url: review.url } : {}),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
async discardPendingReview(repo, number) {
|
|
343
|
+
const viewer = await this.viewer();
|
|
344
|
+
const pending = await this.findPendingReview(repo, number, viewer.login);
|
|
345
|
+
if (!pending)
|
|
346
|
+
return;
|
|
347
|
+
await this.gql(q.DISCARD_REVIEW, { reviewId: pending.id });
|
|
348
|
+
}
|
|
349
|
+
/** The viewer's pending review on the PR, or null. */
|
|
350
|
+
async findPendingReview(repo, number, login) {
|
|
351
|
+
const d = await this.gql(q.PENDING_REVIEW, {
|
|
352
|
+
owner: repo.owner,
|
|
353
|
+
name: repo.repo,
|
|
354
|
+
number,
|
|
355
|
+
login,
|
|
356
|
+
});
|
|
357
|
+
const pr = d.repository?.pullRequest;
|
|
358
|
+
if (!pr)
|
|
359
|
+
throw prNotFound(repo, number);
|
|
360
|
+
return mapPendingReview(pr.reviews?.nodes);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
// Paging
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
function nonNull(value) {
|
|
367
|
+
return value !== null;
|
|
368
|
+
}
|
|
369
|
+
const EMPTY = { pageInfo: { hasNextPage: false, endCursor: null }, nodes: [] };
|
|
370
|
+
function compact(nodes) {
|
|
371
|
+
return (nodes ?? []).filter((n) => n != null);
|
|
372
|
+
}
|
|
373
|
+
/** All nodes of a connection: the page already in hand plus every following page. */
|
|
374
|
+
async function drain(first, more, maxPages = MAX_PAGES) {
|
|
375
|
+
const out = compact(first.nodes);
|
|
376
|
+
let page = first;
|
|
377
|
+
for (let n = 1; n < maxPages && page.pageInfo.hasNextPage && page.pageInfo.endCursor; n++) {
|
|
378
|
+
page = await more(page.pageInfo.endCursor);
|
|
379
|
+
out.push(...compact(page.nodes));
|
|
380
|
+
}
|
|
381
|
+
return out;
|
|
382
|
+
}
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
// Mapping GitHub's shapes onto the wire types
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
const REVIEW_STATES = new Set([
|
|
387
|
+
'PENDING',
|
|
388
|
+
'COMMENTED',
|
|
389
|
+
'APPROVED',
|
|
390
|
+
'CHANGES_REQUESTED',
|
|
391
|
+
'DISMISSED',
|
|
392
|
+
]);
|
|
393
|
+
const CHANGE_TYPES = new Set([
|
|
394
|
+
'ADDED',
|
|
395
|
+
'DELETED',
|
|
396
|
+
'MODIFIED',
|
|
397
|
+
'RENAMED',
|
|
398
|
+
'COPIED',
|
|
399
|
+
'CHANGED',
|
|
400
|
+
]);
|
|
401
|
+
const VIEWED_STATES = new Set([
|
|
402
|
+
'VIEWED',
|
|
403
|
+
'UNVIEWED',
|
|
404
|
+
'DISMISSED',
|
|
405
|
+
]);
|
|
406
|
+
const asReviewState = (s) => s && REVIEW_STATES.has(s) ? s : undefined;
|
|
407
|
+
const asSide = (s) => (s === 'LEFT' ? 'LEFT' : 'RIGHT');
|
|
408
|
+
const asOptionalSide = (s) => s === 'LEFT' || s === 'RIGHT' ? s : null;
|
|
409
|
+
const asChangeType = (s) => CHANGE_TYPES.has(s) ? s : 'MODIFIED';
|
|
410
|
+
const asViewedState = (s) => VIEWED_STATES.has(s) ? s : 'UNVIEWED';
|
|
411
|
+
/** GitHub renders deleted accounts as "ghost"; a null author maps the same way. */
|
|
412
|
+
export function mapActor(a) {
|
|
413
|
+
if (!a)
|
|
414
|
+
return { login: 'ghost' };
|
|
415
|
+
return a.avatarUrl ? { login: a.login, avatarUrl: a.avatarUrl } : { login: a.login };
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* The viewer's own state on a PR: a PENDING review wins; otherwise the most
|
|
419
|
+
* recently submitted review decides (DISMISSED counts as a plain comment).
|
|
420
|
+
*/
|
|
421
|
+
export function deriveMyReviewState(pending, reviews) {
|
|
422
|
+
if (compact(pending).length > 0)
|
|
423
|
+
return 'pending';
|
|
424
|
+
// GitHub's rule: the newest APPROVED / CHANGES_REQUESTED review is the reviewer's
|
|
425
|
+
// standing opinion, even when later COMMENTED reviews exist (a published reply
|
|
426
|
+
// creates one). Only when there is no opinionated review does "commented" apply.
|
|
427
|
+
let opinion;
|
|
428
|
+
let commented = false;
|
|
429
|
+
for (const r of compact(reviews)) {
|
|
430
|
+
if (r.state === 'PENDING')
|
|
431
|
+
continue;
|
|
432
|
+
if (r.state === 'APPROVED' || r.state === 'CHANGES_REQUESTED') {
|
|
433
|
+
if (!opinion || (r.submittedAt ?? '') > (opinion.submittedAt ?? ''))
|
|
434
|
+
opinion = r;
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
commented = true;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (opinion?.state === 'APPROVED')
|
|
441
|
+
return 'approved';
|
|
442
|
+
if (opinion?.state === 'CHANGES_REQUESTED')
|
|
443
|
+
return 'changes_requested';
|
|
444
|
+
return commented ? 'commented' : 'none';
|
|
445
|
+
}
|
|
446
|
+
function mapPendingReview(nodes) {
|
|
447
|
+
const node = compact(nodes)[0];
|
|
448
|
+
return node ? { id: node.id, commentCount: node.comments?.totalCount ?? 0 } : null;
|
|
449
|
+
}
|
|
450
|
+
export function mapSummary(pr) {
|
|
451
|
+
return {
|
|
452
|
+
id: pr.id,
|
|
453
|
+
number: pr.number,
|
|
454
|
+
title: pr.title,
|
|
455
|
+
author: mapActor(pr.author),
|
|
456
|
+
headRefName: pr.headRefName,
|
|
457
|
+
baseRefName: pr.baseRefName,
|
|
458
|
+
headRefOid: pr.headRefOid,
|
|
459
|
+
baseRefOid: pr.baseRefOid,
|
|
460
|
+
updatedAt: pr.updatedAt,
|
|
461
|
+
createdAt: pr.createdAt,
|
|
462
|
+
isDraft: pr.isDraft,
|
|
463
|
+
url: pr.url,
|
|
464
|
+
additions: pr.additions,
|
|
465
|
+
deletions: pr.deletions,
|
|
466
|
+
changedFiles: pr.changedFiles,
|
|
467
|
+
myReviewState: deriveMyReviewState(pr.pendingReview?.nodes, pr.viewerReviews?.nodes),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
export function mapCommit(c) {
|
|
471
|
+
const login = c.commit.author?.user?.login;
|
|
472
|
+
return {
|
|
473
|
+
oid: c.commit.oid,
|
|
474
|
+
abbreviatedOid: c.commit.abbreviatedOid,
|
|
475
|
+
messageHeadline: c.commit.messageHeadline,
|
|
476
|
+
messageBody: c.commit.messageBody,
|
|
477
|
+
authorName: c.commit.author?.name ?? login ?? 'unknown',
|
|
478
|
+
...(login ? { authorLogin: login } : {}),
|
|
479
|
+
committedDate: c.commit.committedDate,
|
|
480
|
+
url: c.url || c.commit.url,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
export function mapFile(f) {
|
|
484
|
+
return {
|
|
485
|
+
path: f.path,
|
|
486
|
+
additions: f.additions,
|
|
487
|
+
deletions: f.deletions,
|
|
488
|
+
changeType: asChangeType(f.changeType),
|
|
489
|
+
viewedState: asViewedState(f.viewerViewedState),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
export function mapComment(c, viewerLogin) {
|
|
493
|
+
const review = c.pullRequestReview;
|
|
494
|
+
return {
|
|
495
|
+
id: c.id,
|
|
496
|
+
...(typeof c.databaseId === 'number' ? { databaseId: c.databaseId } : {}),
|
|
497
|
+
body: c.body,
|
|
498
|
+
bodyHTML: c.bodyHTML,
|
|
499
|
+
author: mapActor(c.author),
|
|
500
|
+
createdAt: c.createdAt,
|
|
501
|
+
...(c.url ? { url: c.url } : {}),
|
|
502
|
+
...(review ? { reviewId: review.id } : {}),
|
|
503
|
+
...(asReviewState(review?.state) ? { reviewState: asReviewState(review?.state) } : {}),
|
|
504
|
+
isPending: c.state === 'PENDING' || review?.state === 'PENDING',
|
|
505
|
+
isMine: c.author?.login === viewerLogin,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
export function mapThread(t, comments) {
|
|
509
|
+
return {
|
|
510
|
+
id: t.id,
|
|
511
|
+
path: t.path,
|
|
512
|
+
line: t.line,
|
|
513
|
+
startLine: t.startLine,
|
|
514
|
+
side: asSide(t.diffSide),
|
|
515
|
+
startSide: asOptionalSide(t.startDiffSide),
|
|
516
|
+
originalLine: t.originalLine,
|
|
517
|
+
isResolved: t.isResolved,
|
|
518
|
+
isOutdated: t.isOutdated,
|
|
519
|
+
comments,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* The Overview conversation: issue comments plus submitted reviews, oldest
|
|
524
|
+
* first. The viewer's PENDING review is excluded (it is not public), and so
|
|
525
|
+
* are reviews that carry neither a body nor inline comments (pure approvals
|
|
526
|
+
* still have a state and are kept).
|
|
527
|
+
*/
|
|
528
|
+
export function buildConversation(comments, reviews) {
|
|
529
|
+
const items = [];
|
|
530
|
+
for (const c of comments) {
|
|
531
|
+
items.push({
|
|
532
|
+
at: Date.parse(c.createdAt),
|
|
533
|
+
item: {
|
|
534
|
+
kind: 'comment',
|
|
535
|
+
id: c.id,
|
|
536
|
+
author: mapActor(c.author),
|
|
537
|
+
body: c.body,
|
|
538
|
+
bodyHTML: c.bodyHTML,
|
|
539
|
+
createdAt: c.createdAt,
|
|
540
|
+
...(c.url ? { url: c.url } : {}),
|
|
541
|
+
},
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
for (const r of reviews) {
|
|
545
|
+
if (r.state === 'PENDING')
|
|
546
|
+
continue;
|
|
547
|
+
const commentCount = r.comments?.totalCount ?? 0;
|
|
548
|
+
if (r.body.trim() === '' && commentCount === 0 && r.state === 'COMMENTED')
|
|
549
|
+
continue;
|
|
550
|
+
items.push({
|
|
551
|
+
at: Date.parse(r.submittedAt ?? r.createdAt),
|
|
552
|
+
item: {
|
|
553
|
+
kind: 'review',
|
|
554
|
+
id: r.id,
|
|
555
|
+
author: mapActor(r.author),
|
|
556
|
+
body: r.body,
|
|
557
|
+
bodyHTML: r.bodyHTML,
|
|
558
|
+
state: asReviewState(r.state) ?? 'COMMENTED',
|
|
559
|
+
submittedAt: r.submittedAt,
|
|
560
|
+
...(r.url ? { url: r.url } : {}),
|
|
561
|
+
commentCount,
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
items.sort((a, b) => a.at - b.at);
|
|
566
|
+
return items.map((x) => x.item);
|
|
567
|
+
}
|
|
568
|
+
const noop = () => { };
|
|
569
|
+
/** GitHub owner and repository names are case-insensitive. */
|
|
570
|
+
const sameSlug = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
571
|
+
const notInPullRequest = (what) => new HttpError(404, `${what} not found in this pull request`, 'not_found');
|
|
572
|
+
const repoNotFound = (repo) => new ForgeError(`Repository ${repo.owner}/${repo.repo} was not found on GitHub.`, 404);
|
|
573
|
+
const prNotFound = (repo, number) => new ForgeError(`Pull request #${number} was not found in ${repo.owner}/${repo.repo}.`, 404);
|
|
574
|
+
// ---------------------------------------------------------------------------
|
|
575
|
+
// Search-query building (GitHub search API qualifiers)
|
|
576
|
+
// ---------------------------------------------------------------------------
|
|
577
|
+
/** Logins are safe bare; anything else gets quoted so `/` and `:` stay literal. */
|
|
578
|
+
const SAFE_BARE = /^[A-Za-z0-9_.-]+$/;
|
|
579
|
+
const quote = (value) => SAFE_BARE.test(value) ? value : `"${value.replace(/"/g, '')}"`;
|
|
580
|
+
/**
|
|
581
|
+
* The search string for a filter: `repo:o/r is:pr is:open` plus qualifiers.
|
|
582
|
+
* Title text is passed as tokens with `in:title`; text containing `:` or
|
|
583
|
+
* quotes becomes a quoted phrase so it can never inject qualifiers. A bare
|
|
584
|
+
* hex token doubles as a commit-SHA search, which GitHub handles natively.
|
|
585
|
+
*/
|
|
586
|
+
export function buildSearchQuery(repo, filter) {
|
|
587
|
+
const parts = [`repo:${repo.owner}/${repo.repo}`, 'is:pr', 'is:open'];
|
|
588
|
+
const text = filter?.text?.trim();
|
|
589
|
+
if (text) {
|
|
590
|
+
// A bare hex SHA must go WITHOUT `in:title`: GitHub matches it to the PR
|
|
591
|
+
// by commit instead. Text containing `:` or quotes becomes a phrase so it
|
|
592
|
+
// can never inject qualifiers.
|
|
593
|
+
if (/^[0-9a-f]{7,40}$/i.test(text)) {
|
|
594
|
+
parts.push(text.toLowerCase());
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
const safe = /["':]/.test(text) ? `"${text.replace(/"/g, '')}"` : text;
|
|
598
|
+
parts.push(`${safe} in:title`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const author = filter?.author?.trim();
|
|
602
|
+
if (author)
|
|
603
|
+
parts.push(`author:${quote(author)}`);
|
|
604
|
+
const head = filter?.head?.trim();
|
|
605
|
+
if (head)
|
|
606
|
+
parts.push(`head:${quote(head)}`);
|
|
607
|
+
const base = filter?.base?.trim();
|
|
608
|
+
if (base)
|
|
609
|
+
parts.push(`base:${quote(base)}`);
|
|
610
|
+
if (filter?.draft !== undefined)
|
|
611
|
+
parts.push(`draft:${filter.draft ? 'true' : 'false'}`);
|
|
612
|
+
return parts.join(' ');
|
|
613
|
+
}
|
|
614
|
+
/** Cache key: repo plus the filter's own identity (stable field order). */
|
|
615
|
+
export function filterKey(repo, filter) {
|
|
616
|
+
if (filter === undefined || isEmptyFilter(filter))
|
|
617
|
+
return `${repo.owner}/${repo.repo}`;
|
|
618
|
+
const f = filter;
|
|
619
|
+
return [
|
|
620
|
+
`${repo.owner}/${repo.repo}`,
|
|
621
|
+
f.text ?? '',
|
|
622
|
+
f.author ?? '',
|
|
623
|
+
f.head ?? '',
|
|
624
|
+
f.base ?? '',
|
|
625
|
+
f.draft === undefined ? '' : String(f.draft),
|
|
626
|
+
].join('|');
|
|
627
|
+
}
|