@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
package/dist/git/run.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { execFile, spawn } from 'node:child_process';
|
|
2
|
+
import { badRequest } from '../routes/http.js';
|
|
3
|
+
/**
|
|
4
|
+
* The only module that spawns git.
|
|
5
|
+
*
|
|
6
|
+
* Every invocation is `execFile('git', argv)` (or `spawn` when stdout is
|
|
7
|
+
* streamed) — an argv array, never a shell — with prompts disabled and stable,
|
|
8
|
+
* untranslated output. Callers hand over arguments as data; pathspecs are
|
|
9
|
+
* validated with {@link assertPathspec} and always placed after a `--`,
|
|
10
|
+
* revisions with {@link assertRevision}, so no user-controlled string can ever
|
|
11
|
+
* be read as an option.
|
|
12
|
+
*/
|
|
13
|
+
/** 64 MiB: enough for any patch or file we are willing to hold in memory. */
|
|
14
|
+
export const DEFAULT_MAX_BUFFER = 64 * 1024 * 1024;
|
|
15
|
+
/** Stderr kept on a GitError is capped so a runaway process cannot bloat error responses. */
|
|
16
|
+
const MAX_STDERR_CHARS = 4000;
|
|
17
|
+
/** A git invocation that failed: non-zero exit, timeout, abort, or git missing. */
|
|
18
|
+
export class GitError extends Error {
|
|
19
|
+
args;
|
|
20
|
+
exitCode;
|
|
21
|
+
stderr;
|
|
22
|
+
name = 'GitError';
|
|
23
|
+
constructor(
|
|
24
|
+
/** The argv git was started with (never includes secrets: credentials come from helpers). */
|
|
25
|
+
args,
|
|
26
|
+
/** Exit code, or null when git did not exit normally (killed, aborted, not found). */
|
|
27
|
+
exitCode, stderr, message) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.args = args;
|
|
30
|
+
this.exitCode = exitCode;
|
|
31
|
+
this.stderr = stderr;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Run `git -C <repoPath> <args>` and resolve with its output. Rejects with a
|
|
36
|
+
* GitError, or with a 400 HttpError when `repoPath` could be read as an option.
|
|
37
|
+
*/
|
|
38
|
+
export async function runGit(repoPath, args, opts = {}) {
|
|
39
|
+
return spawnGit(['-C', assertRepoPath(repoPath), ...args], opts);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Run `git <args>` without a repository (used for `git clone` and `git --version`). Everything
|
|
43
|
+
* else operates on an existing repository and must use {@link runGit}.
|
|
44
|
+
*/
|
|
45
|
+
export async function runGitGlobal(args, opts = {}) {
|
|
46
|
+
return spawnGit(args, opts);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Run `git -C <repoPath> <args>` and hand stdout to `sink` chunk by chunk
|
|
50
|
+
* instead of buffering it, for output that may not fit in memory (counting the
|
|
51
|
+
* lines of a huge blob). Resolves once git exited successfully; rejects with a
|
|
52
|
+
* GitError exactly like {@link runGit}.
|
|
53
|
+
*/
|
|
54
|
+
export async function runGitStreaming(repoPath, args, sink, opts = {}) {
|
|
55
|
+
const argv = ['-C', assertRepoPath(repoPath), ...args];
|
|
56
|
+
await new Promise((resolve, reject) => {
|
|
57
|
+
const child = spawn('git', argv, {
|
|
58
|
+
env: gitEnv(opts.env),
|
|
59
|
+
windowsHide: true,
|
|
60
|
+
signal: opts.signal,
|
|
61
|
+
timeout: opts.timeoutMs ?? 0,
|
|
62
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
63
|
+
});
|
|
64
|
+
let stderr = '';
|
|
65
|
+
let failure;
|
|
66
|
+
let settled = false;
|
|
67
|
+
const fail = (err) => {
|
|
68
|
+
if (settled)
|
|
69
|
+
return;
|
|
70
|
+
settled = true;
|
|
71
|
+
reject(err);
|
|
72
|
+
};
|
|
73
|
+
child.stdout.on('data', (chunk) => {
|
|
74
|
+
try {
|
|
75
|
+
sink(chunk);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
child.kill();
|
|
79
|
+
fail(new GitError(argv, null, redact(stderr), err instanceof Error ? err.message : String(err)));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
child.stderr.on('data', (chunk) => {
|
|
83
|
+
if (stderr.length < MAX_STDERR_CHARS)
|
|
84
|
+
stderr += chunk.toString('utf8');
|
|
85
|
+
});
|
|
86
|
+
child.on('error', (err) => {
|
|
87
|
+
failure = err;
|
|
88
|
+
// A spawn failure (git missing) never reaches 'close'; an abort does, once git has exited.
|
|
89
|
+
if (child.pid === undefined)
|
|
90
|
+
fail(toGitError(argv, failure, redact(stderr)));
|
|
91
|
+
});
|
|
92
|
+
child.on('close', (code, signal) => {
|
|
93
|
+
if (settled)
|
|
94
|
+
return;
|
|
95
|
+
if (failure)
|
|
96
|
+
return fail(toGitError(argv, failure, redact(stderr)));
|
|
97
|
+
if (code === 0) {
|
|
98
|
+
settled = true;
|
|
99
|
+
resolve();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
fail(toGitError(argv, { code, killed: child.killed, signal }, redact(stderr)));
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function spawnGit(argv, opts) {
|
|
107
|
+
const maxBuffer = opts.maxBuffer ?? DEFAULT_MAX_BUFFER;
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const child = execFile('git', [...argv], {
|
|
110
|
+
encoding: 'buffer',
|
|
111
|
+
maxBuffer,
|
|
112
|
+
timeout: opts.timeoutMs ?? 0,
|
|
113
|
+
signal: opts.signal,
|
|
114
|
+
windowsHide: true,
|
|
115
|
+
env: gitEnv(opts.env),
|
|
116
|
+
}, (error, stdout, stderr) => {
|
|
117
|
+
const settle = () => {
|
|
118
|
+
const stderrText = redact(stderr.toString('utf8'));
|
|
119
|
+
if (!error) {
|
|
120
|
+
resolve({ stdout, stderr: stderrText, exitCode: 0, truncated: false });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const err = error;
|
|
124
|
+
if (err.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER' && opts.truncateStdout) {
|
|
125
|
+
resolve({ stdout, stderr: stderrText, exitCode: 0, truncated: true });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (typeof err.code === 'number' && opts.okExitCodes?.includes(err.code)) {
|
|
129
|
+
resolve({ stdout, stderr: stderrText, exitCode: err.code, truncated: false });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
reject(toGitError(argv, err, stderrText));
|
|
133
|
+
};
|
|
134
|
+
// On abort, execFile reports before git has actually exited. Wait for it, so a caller
|
|
135
|
+
// that removes a half-written clone afterwards is not racing a still-running git.
|
|
136
|
+
if (error &&
|
|
137
|
+
isAbort(error) &&
|
|
138
|
+
child.exitCode === null &&
|
|
139
|
+
child.signalCode === null) {
|
|
140
|
+
child.once('close', settle);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
settle();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
// git may exit before reading stdin (EPIPE); that is not an error for us.
|
|
147
|
+
child.stdin?.on('error', () => { });
|
|
148
|
+
if (opts.input === undefined)
|
|
149
|
+
child.stdin?.end();
|
|
150
|
+
else
|
|
151
|
+
child.stdin?.end(opts.input);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/** The environment every git child gets: the caller's, plus the fixed settings we rely on. */
|
|
155
|
+
function gitEnv(extra) {
|
|
156
|
+
return {
|
|
157
|
+
...process.env,
|
|
158
|
+
...extra,
|
|
159
|
+
// Never block on a username/password prompt; fail instead.
|
|
160
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
161
|
+
// Untranslated, byte-stable messages and output.
|
|
162
|
+
LC_ALL: 'C',
|
|
163
|
+
// Read-only commands must not take the index lock in the user's checkout.
|
|
164
|
+
GIT_OPTIONAL_LOCKS: '0',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const isAbort = (err) => err.code === 'ABORT_ERR' || err.name === 'AbortError';
|
|
168
|
+
function toGitError(argv, err, stderr) {
|
|
169
|
+
const cmd = `git ${subcommand(argv)}`;
|
|
170
|
+
const detail = stderr.trim().split('\n').filter(Boolean).pop();
|
|
171
|
+
const suffix = detail ? `: ${detail}` : '';
|
|
172
|
+
if (err.code === 'ENOENT') {
|
|
173
|
+
return new GitError(argv, null, stderr, 'git is not installed or not on PATH');
|
|
174
|
+
}
|
|
175
|
+
if (isAbort(err)) {
|
|
176
|
+
return new GitError(argv, null, stderr, `${cmd} was aborted`);
|
|
177
|
+
}
|
|
178
|
+
if (err.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
|
|
179
|
+
return new GitError(argv, null, stderr, `${cmd} produced more output than allowed`);
|
|
180
|
+
}
|
|
181
|
+
if (err.killed || (err.signal && typeof err.code !== 'number')) {
|
|
182
|
+
return new GitError(argv, null, stderr, `${cmd} timed out (${err.signal ?? 'killed'})${suffix}`);
|
|
183
|
+
}
|
|
184
|
+
const exitCode = typeof err.code === 'number' ? err.code : null;
|
|
185
|
+
return new GitError(argv, exitCode, stderr, `${cmd} failed (exit ${exitCode ?? '?'})${suffix}`);
|
|
186
|
+
}
|
|
187
|
+
/** The git subcommand in an argv, skipping global options such as `-C <path>` and `-c k=v`. */
|
|
188
|
+
function subcommand(argv) {
|
|
189
|
+
for (let i = 0; i < argv.length; i++) {
|
|
190
|
+
const arg = argv[i];
|
|
191
|
+
if (arg === '-C' || arg === '-c') {
|
|
192
|
+
i++;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (arg === undefined || arg.startsWith('-'))
|
|
196
|
+
continue;
|
|
197
|
+
return arg;
|
|
198
|
+
}
|
|
199
|
+
return '';
|
|
200
|
+
}
|
|
201
|
+
/** Strip `user:password@` from URLs and cap the length; stderr ends up in API responses. */
|
|
202
|
+
function redact(text) {
|
|
203
|
+
const clean = text.replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi, '$1***@');
|
|
204
|
+
return clean.length > MAX_STDERR_CHARS ? `${clean.slice(0, MAX_STDERR_CHARS)}…` : clean;
|
|
205
|
+
}
|
|
206
|
+
function assertRepoPath(repoPath) {
|
|
207
|
+
if (repoPath.startsWith('-'))
|
|
208
|
+
throw badRequest('repository path must not start with "-"');
|
|
209
|
+
return repoPath;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Validate a repository-relative path before it is handed to git as a pathspec
|
|
213
|
+
* or as the `<path>` in `<rev>:<path>`. Returns the path unchanged.
|
|
214
|
+
*
|
|
215
|
+
* Rejects (HTTP 400): empty, control characters, a leading `-` (would be an
|
|
216
|
+
* option), a leading `:` (pathspec magic), absolute paths, and `.`/`..`
|
|
217
|
+
* segments. A single trailing slash is allowed (directory pathspecs).
|
|
218
|
+
*/
|
|
219
|
+
export function assertPathspec(p) {
|
|
220
|
+
if (typeof p !== 'string' || p.length === 0)
|
|
221
|
+
throw badRequest('path is required');
|
|
222
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: rejecting control characters is the point
|
|
223
|
+
if (/[\0-\x1f\x7f]/.test(p))
|
|
224
|
+
throw badRequest('path contains control characters');
|
|
225
|
+
if (p.startsWith('-'))
|
|
226
|
+
throw badRequest(`path must not start with "-": ${p}`);
|
|
227
|
+
if (p.startsWith(':'))
|
|
228
|
+
throw badRequest(`path must not start with ":": ${p}`);
|
|
229
|
+
if (p.startsWith('/') || p.startsWith('\\') || /^[A-Za-z]:[\\/]/.test(p)) {
|
|
230
|
+
throw badRequest(`path must be relative to the repository root: ${p}`);
|
|
231
|
+
}
|
|
232
|
+
const segments = p.split('/');
|
|
233
|
+
segments.forEach((segment, i) => {
|
|
234
|
+
if (segment === '..')
|
|
235
|
+
throw badRequest(`path must not contain "..": ${p}`);
|
|
236
|
+
if (segment === '.')
|
|
237
|
+
throw badRequest(`path must not contain "." segments: ${p}`);
|
|
238
|
+
if (segment === '' && i !== segments.length - 1) {
|
|
239
|
+
throw badRequest(`path must not contain empty segments: ${p}`);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
return p;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Validate a revision argument (an oid, a ref name, or a range such as `a..b`)
|
|
246
|
+
* so it can never be parsed as an option. Returns it unchanged.
|
|
247
|
+
*/
|
|
248
|
+
export function assertRevision(rev) {
|
|
249
|
+
if (typeof rev !== 'string' || rev.length === 0)
|
|
250
|
+
throw badRequest('revision is required');
|
|
251
|
+
if (/[\0-\x20\x7f]/.test(rev))
|
|
252
|
+
throw badRequest('revision contains whitespace or control characters');
|
|
253
|
+
if (rev.startsWith('-'))
|
|
254
|
+
throw badRequest(`revision must not start with "-": ${rev}`);
|
|
255
|
+
return rev;
|
|
256
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Open `url` in the user's default browser without any dependency.
|
|
4
|
+
* Best effort: failures are swallowed, the URL is printed anyway.
|
|
5
|
+
*/
|
|
6
|
+
export function openBrowser(url) {
|
|
7
|
+
const [command, args] = commandFor(process.platform, url);
|
|
8
|
+
try {
|
|
9
|
+
const child = spawn(command, args, { stdio: 'ignore', detached: true });
|
|
10
|
+
child.on('error', () => { });
|
|
11
|
+
child.unref();
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// ignore: the user can still open the printed URL by hand
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function commandFor(platform, url) {
|
|
18
|
+
switch (platform) {
|
|
19
|
+
case 'darwin':
|
|
20
|
+
return ['open', [url]];
|
|
21
|
+
case 'win32':
|
|
22
|
+
// The empty string is the window title `start` would otherwise steal the URL for.
|
|
23
|
+
return ['cmd', ['/c', 'start', '', url]];
|
|
24
|
+
default:
|
|
25
|
+
return ['xdg-open', [url]];
|
|
26
|
+
}
|
|
27
|
+
}
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
/**
|
|
5
|
+
* Absolute path of the `coja` package root (the directory holding package.json).
|
|
6
|
+
* Both `src/` (run via tsx) and `dist/` (built) sit one level below it, so the
|
|
7
|
+
* same relative hop works in either case.
|
|
8
|
+
*/
|
|
9
|
+
export const packageRoot = fileURLToPath(new URL('..', import.meta.url));
|
|
10
|
+
/** Where the built web UI lives inside the published package. */
|
|
11
|
+
export function resolvePublicDir() {
|
|
12
|
+
return path.join(packageRoot, 'public');
|
|
13
|
+
}
|
|
14
|
+
export function packageVersion() {
|
|
15
|
+
const raw = readFileSync(path.join(packageRoot, 'package.json'), 'utf8');
|
|
16
|
+
const pkg = JSON.parse(raw);
|
|
17
|
+
return pkg.version;
|
|
18
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { nowIso } from '../db.js';
|
|
2
|
+
import { fetch as gitFetch, mergeBase, revParse } from '../git/plumbing.js';
|
|
3
|
+
import { GitError } from '../git/run.js';
|
|
4
|
+
import { githubPullHeadRef, prBaseRef, prHeadRef } from './refs.js';
|
|
5
|
+
const REMOTE = 'origin';
|
|
6
|
+
const key = (projectId, number) => `${projectId}#${number}`;
|
|
7
|
+
/**
|
|
8
|
+
* Brings a pull request's objects into a project's repository, lazily and in
|
|
9
|
+
* the background (design.md: "Fetch is lazy and non-blocking").
|
|
10
|
+
*
|
|
11
|
+
* `ensure` compares the namespaced refs with the oids GitHub reports and only
|
|
12
|
+
* fetches when they differ, so re-opening a PR whose head moved re-fetches
|
|
13
|
+
* while an unchanged PR is `ready` without touching the network. Status is
|
|
14
|
+
* kept in memory per `${projectId}#${number}`; the refs themselves persist in
|
|
15
|
+
* the repository.
|
|
16
|
+
*
|
|
17
|
+
* Requests are never dropped: when `ensure` is asked for other oids while a
|
|
18
|
+
* fetch is in flight, the newest oids are decided again as soon as that fetch
|
|
19
|
+
* finishes, so a head that moves mid-fetch still ends `ready` at the new oids.
|
|
20
|
+
*/
|
|
21
|
+
export class PrFetcher {
|
|
22
|
+
statuses = new Map();
|
|
23
|
+
/** Background fetches, by key. */
|
|
24
|
+
inflight = new Map();
|
|
25
|
+
/** `ensure` calls still deciding whether to fetch, by key; concurrent callers share one. */
|
|
26
|
+
ensuring = new Map();
|
|
27
|
+
status(projectId, number) {
|
|
28
|
+
return this.statuses.get(key(projectId, number)) ?? { state: 'idle' };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Make sure `refs` are fetched. Returns immediately: `ready` when the
|
|
32
|
+
* repository already holds exactly these oids, otherwise `fetching` after
|
|
33
|
+
* starting a background fetch (or while one is running).
|
|
34
|
+
*/
|
|
35
|
+
ensure(project, refs) {
|
|
36
|
+
const k = key(project.id, refs.number);
|
|
37
|
+
const running = this.inflight.get(k);
|
|
38
|
+
if (running) {
|
|
39
|
+
// The newest request wins; the oids already being fetched need nothing more.
|
|
40
|
+
running.next = sameOids(running.refs, refs) ? undefined : refs;
|
|
41
|
+
return Promise.resolve(this.status(project.id, refs.number));
|
|
42
|
+
}
|
|
43
|
+
const deciding = this.ensuring.get(k);
|
|
44
|
+
if (deciding) {
|
|
45
|
+
if (sameOids(deciding.refs, refs))
|
|
46
|
+
return deciding.done;
|
|
47
|
+
return deciding.done.then(() => this.ensure(project, refs));
|
|
48
|
+
}
|
|
49
|
+
return this.beginDecision(project, refs, k);
|
|
50
|
+
}
|
|
51
|
+
/** Resolve once nothing is pending or running for the PR (tests and eager callers). */
|
|
52
|
+
async waitFor(projectId, number) {
|
|
53
|
+
const k = key(projectId, number);
|
|
54
|
+
for (;;) {
|
|
55
|
+
const deciding = this.ensuring.get(k);
|
|
56
|
+
if (deciding) {
|
|
57
|
+
await deciding.done;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const running = this.inflight.get(k);
|
|
61
|
+
if (running) {
|
|
62
|
+
await running.done;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
return this.status(projectId, number);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
beginDecision(project, refs, k) {
|
|
69
|
+
const entry = {
|
|
70
|
+
refs,
|
|
71
|
+
done: this.decide(project, refs, k).finally(() => {
|
|
72
|
+
if (this.ensuring.get(k) === entry)
|
|
73
|
+
this.ensuring.delete(k);
|
|
74
|
+
}),
|
|
75
|
+
};
|
|
76
|
+
this.ensuring.set(k, entry);
|
|
77
|
+
return entry.done;
|
|
78
|
+
}
|
|
79
|
+
/** Never rejects: a failing check (e.g. no merge base) becomes an `error` status, like a failed fetch. */
|
|
80
|
+
async decide(project, refs, k) {
|
|
81
|
+
try {
|
|
82
|
+
const repo = project.path;
|
|
83
|
+
const n = refs.number;
|
|
84
|
+
const [head, base] = await Promise.all([
|
|
85
|
+
revParse(repo, prHeadRef(n)),
|
|
86
|
+
revParse(repo, prBaseRef(n)),
|
|
87
|
+
]);
|
|
88
|
+
if (head && base && head === refs.headRefOid && base === refs.baseRefOid) {
|
|
89
|
+
const current = this.statuses.get(k);
|
|
90
|
+
if (current?.state === 'ready' && current.headOid === head && current.baseOid === base) {
|
|
91
|
+
return current;
|
|
92
|
+
}
|
|
93
|
+
const ready = {
|
|
94
|
+
state: 'ready',
|
|
95
|
+
headOid: head,
|
|
96
|
+
baseOid: base,
|
|
97
|
+
mergeBaseOid: await mergeBase(repo, base, head),
|
|
98
|
+
...(current?.startedAt ? { startedAt: current.startedAt } : {}),
|
|
99
|
+
finishedAt: current?.finishedAt ?? nowIso(),
|
|
100
|
+
};
|
|
101
|
+
this.statuses.set(k, ready);
|
|
102
|
+
return ready;
|
|
103
|
+
}
|
|
104
|
+
return this.start(project, refs, k);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
const failed = { state: 'error', error: describe(err), finishedAt: nowIso() };
|
|
108
|
+
this.statuses.set(k, failed);
|
|
109
|
+
return failed;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
start(project, refs, k) {
|
|
113
|
+
const startedAt = nowIso();
|
|
114
|
+
const fetching = { state: 'fetching', startedAt };
|
|
115
|
+
this.statuses.set(k, fetching);
|
|
116
|
+
const entry = {
|
|
117
|
+
refs,
|
|
118
|
+
done: this.runFetch(project, refs, startedAt).then((final) => {
|
|
119
|
+
this.statuses.set(k, final);
|
|
120
|
+
if (this.inflight.get(k) === entry)
|
|
121
|
+
this.inflight.delete(k);
|
|
122
|
+
// The oids changed while we fetched: check the repository against the newest ones.
|
|
123
|
+
if (entry.next)
|
|
124
|
+
void this.beginDecision(project, entry.next, k);
|
|
125
|
+
return final;
|
|
126
|
+
}),
|
|
127
|
+
};
|
|
128
|
+
this.inflight.set(k, entry);
|
|
129
|
+
return fetching;
|
|
130
|
+
}
|
|
131
|
+
/** Never rejects: failures become an `error` status. */
|
|
132
|
+
async runFetch(project, refs, startedAt) {
|
|
133
|
+
const repo = project.path;
|
|
134
|
+
const n = refs.number;
|
|
135
|
+
const headSpec = `+${githubPullHeadRef(n)}:${prHeadRef(n)}`;
|
|
136
|
+
try {
|
|
137
|
+
try {
|
|
138
|
+
// GitHub serves any reachable commit by full oid, so the base is pinned to
|
|
139
|
+
// exactly the oid the PR was compared against.
|
|
140
|
+
await gitFetch(repo, REMOTE, [headSpec, `+${refs.baseRefOid}:${prBaseRef(n)}`]);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
if (!(err instanceof GitError) || !isValidRefName(refs.baseRefName))
|
|
144
|
+
throw err;
|
|
145
|
+
// Servers that refuse wants by oid still serve the branch tip.
|
|
146
|
+
await gitFetch(repo, REMOTE, [headSpec, `+refs/heads/${refs.baseRefName}:${prBaseRef(n)}`]);
|
|
147
|
+
}
|
|
148
|
+
const [headOid, baseOid] = await Promise.all([
|
|
149
|
+
revParse(repo, prHeadRef(n)),
|
|
150
|
+
revParse(repo, prBaseRef(n)),
|
|
151
|
+
]);
|
|
152
|
+
if (!headOid || !baseOid)
|
|
153
|
+
throw new Error(`refs for PR #${n} are missing after the fetch`);
|
|
154
|
+
const mergeBaseOid = await mergeBase(repo, baseOid, headOid);
|
|
155
|
+
return { state: 'ready', headOid, baseOid, mergeBaseOid, startedAt, finishedAt: nowIso() };
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
return { state: 'error', error: describe(err), startedAt, finishedAt: nowIso() };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const sameOids = (a, b) => a.headRefOid === b.headRefOid && a.baseRefOid === b.baseRefOid;
|
|
163
|
+
function isValidRefName(name) {
|
|
164
|
+
return (name.length > 0 && !name.startsWith('-') && !/[\s~^:?*[\\]/.test(name) && !name.includes('..'));
|
|
165
|
+
}
|
|
166
|
+
/** Human-readable failure: git's stderr (already redacted by the runner) or the error message. */
|
|
167
|
+
function describe(err) {
|
|
168
|
+
if (err instanceof GitError) {
|
|
169
|
+
const stderr = err.stderr.trim();
|
|
170
|
+
return stderr ? `${err.message}\n${stderr}`.slice(0, 2000) : err.message;
|
|
171
|
+
}
|
|
172
|
+
return err instanceof Error ? err.message : String(err);
|
|
173
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { blobInfo, countLines, diff, diffNameStatus, showFile } from '../git/plumbing.js';
|
|
2
|
+
import { assertPathspec } from '../git/run.js';
|
|
3
|
+
import { badRequest, notFound } from '../routes/http.js';
|
|
4
|
+
/**
|
|
5
|
+
* Read-side views of a fetched pull request, all computed from git objects.
|
|
6
|
+
* `mergeBase` is the diff base (decisions.md), matching GitHub's own PR diff.
|
|
7
|
+
*/
|
|
8
|
+
export function getChangedFiles(repo, mergeBase, head) {
|
|
9
|
+
return diffNameStatus(repo, mergeBase, head);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The unified patch for one file. Pass `previousPath` for renames so git sees
|
|
13
|
+
* both sides and can pair them; without it a rename shows as delete + add.
|
|
14
|
+
*/
|
|
15
|
+
export async function getFileDiff(repo, mergeBase, head, path, previousPath) {
|
|
16
|
+
assertPathspec(path);
|
|
17
|
+
const paths = [path];
|
|
18
|
+
if (previousPath !== undefined && previousPath !== path)
|
|
19
|
+
paths.push(assertPathspec(previousPath));
|
|
20
|
+
const { patch, truncated } = await diff(repo, mergeBase, head, { paths });
|
|
21
|
+
const binary = !truncated && isBinaryPatch(patch);
|
|
22
|
+
return {
|
|
23
|
+
path,
|
|
24
|
+
...(previousPath !== undefined ? { previousPath } : {}),
|
|
25
|
+
patch: binary || truncated ? '' : patch,
|
|
26
|
+
binary,
|
|
27
|
+
tooLarge: truncated,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const isBinaryPatch = (patch) => /^Binary files .* differ$/m.test(patch) || /^GIT binary patch$/m.test(patch);
|
|
31
|
+
/**
|
|
32
|
+
* A file's text at `oid`, optionally a 1-based inclusive line range (clamped
|
|
33
|
+
* to the file). 404 when the path does not exist at that commit; 400 for
|
|
34
|
+
* directories and binary files.
|
|
35
|
+
*
|
|
36
|
+
* Only the first `maxBytes` of a blob are read. When that cuts the file short,
|
|
37
|
+
* `text` keeps whole lines only (the cut line is dropped), `lineCount` is
|
|
38
|
+
* counted from the full blob, and `truncated` tells whether the requested text
|
|
39
|
+
* — the whole file or the range — reached past what was read.
|
|
40
|
+
*/
|
|
41
|
+
export async function getBlob(repo, ref, oid, path, start, end, opts = {}) {
|
|
42
|
+
assertPathspec(path);
|
|
43
|
+
const info = await blobInfo(repo, oid, path);
|
|
44
|
+
if (!info.exists)
|
|
45
|
+
throw notFound(`${path} at ${ref}`);
|
|
46
|
+
if (info.type !== 'blob')
|
|
47
|
+
throw badRequest(`${path} is a ${info.type ?? 'non-file'}, not a file`);
|
|
48
|
+
const file = await showFile(repo, oid, path, opts.maxBytes === undefined ? {} : { maxBytes: opts.maxBytes });
|
|
49
|
+
if (file.binary)
|
|
50
|
+
throw badRequest(`${path} is a binary file`);
|
|
51
|
+
const lines = splitLines(file.text);
|
|
52
|
+
if (file.truncated && !file.text.endsWith('\n'))
|
|
53
|
+
lines.pop(); // the cut fell inside this line
|
|
54
|
+
const lineCount = file.truncated ? await countLines(repo, oid, path) : lines.length;
|
|
55
|
+
const base = { ref, oid, path, size: file.size, lineCount };
|
|
56
|
+
if (start === undefined && end === undefined) {
|
|
57
|
+
const text = file.truncated ? joinLines(lines) : file.text;
|
|
58
|
+
return { ...base, text, truncated: file.truncated };
|
|
59
|
+
}
|
|
60
|
+
if (lineCount === 0)
|
|
61
|
+
return { ...base, text: '', truncated: false };
|
|
62
|
+
const startLine = clamp(start ?? 1, 1, lineCount);
|
|
63
|
+
const endLine = clamp(end ?? lineCount, startLine, lineCount);
|
|
64
|
+
return {
|
|
65
|
+
...base,
|
|
66
|
+
startLine,
|
|
67
|
+
endLine,
|
|
68
|
+
text: lines.slice(startLine - 1, endLine).join('\n'),
|
|
69
|
+
truncated: endLine > lines.length,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Lines of `text`; a trailing newline does not start an extra empty line. */
|
|
73
|
+
function splitLines(text) {
|
|
74
|
+
if (text === '')
|
|
75
|
+
return [];
|
|
76
|
+
const lines = text.split('\n');
|
|
77
|
+
if (lines[lines.length - 1] === '')
|
|
78
|
+
lines.pop();
|
|
79
|
+
return lines;
|
|
80
|
+
}
|
|
81
|
+
/** Inverse of {@link splitLines} for complete lines: each one newline-terminated. */
|
|
82
|
+
const joinLines = (lines) => (lines.length === 0 ? '' : `${lines.join('\n')}\n`);
|
|
83
|
+
const clamp = (n, min, max) => Math.min(Math.max(n, min), max);
|
package/dist/pr/refs.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where fetched pull requests live inside a project's repository.
|
|
3
|
+
*
|
|
4
|
+
* Refs are namespaced under `refs/coja/` (decisions.md): the objects stay
|
|
5
|
+
* reachable (no gc), while the user's branches, remote-tracking refs and
|
|
6
|
+
* FETCH_HEAD are never written.
|
|
7
|
+
*/
|
|
8
|
+
export const prHeadRef = (number) => `refs/coja/pull/${assertPrNumber(number)}/head`;
|
|
9
|
+
export const prBaseRef = (number) => `refs/coja/pull/${assertPrNumber(number)}/base`;
|
|
10
|
+
/** GitHub's read-only ref for a pull request's head commit (works for fork PRs). */
|
|
11
|
+
export const githubPullHeadRef = (number) => `refs/pull/${assertPrNumber(number)}/head`;
|
|
12
|
+
function assertPrNumber(number) {
|
|
13
|
+
if (!Number.isSafeInteger(number) || number <= 0) {
|
|
14
|
+
throw new RangeError(`invalid pull request number: ${String(number)}`);
|
|
15
|
+
}
|
|
16
|
+
return number;
|
|
17
|
+
}
|