miko-code 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +179 -0
- package/bin/miko +10 -0
- package/dist/client/antigravity.webp +0 -0
- package/dist/client/assets/abap-BdImnpbu.js +1 -0
- package/dist/client/assets/actionscript-3-CoDkCxhg.js +1 -0
- package/dist/client/assets/ada-bCR0ucgS.js +1 -0
- package/dist/client/assets/andromeeda-C4gqWexZ.js +1 -0
- package/dist/client/assets/angular-html-CU67Zn6k.js +1 -0
- package/dist/client/assets/angular-ts-BwZT4LLn.js +1 -0
- package/dist/client/assets/apache-Pmp26Uib.js +1 -0
- package/dist/client/assets/apex-D8_7TLub.js +1 -0
- package/dist/client/assets/apl-dKokRX4l.js +1 -0
- package/dist/client/assets/applescript-Co6uUVPk.js +1 -0
- package/dist/client/assets/ara-BRHolxvo.js +1 -0
- package/dist/client/assets/asciidoc-Ve4PFQV2.js +1 -0
- package/dist/client/assets/asm-D_Q5rh1f.js +1 -0
- package/dist/client/assets/astro-CbQHKStN.js +1 -0
- package/dist/client/assets/aurora-x-D-2ljcwZ.js +1 -0
- package/dist/client/assets/awk-DMzUqQB5.js +1 -0
- package/dist/client/assets/ayu-dark-DYE7WIF3.js +1 -0
- package/dist/client/assets/ayu-light-BA47KaF1.js +1 -0
- package/dist/client/assets/ayu-mirage-32ctXXKs.js +1 -0
- package/dist/client/assets/ballerina-BFfxhgS-.js +1 -0
- package/dist/client/assets/bat-BkioyH1T.js +1 -0
- package/dist/client/assets/beancount-k_qm7-4y.js +1 -0
- package/dist/client/assets/berry-uYugtg8r.js +1 -0
- package/dist/client/assets/bibtex-CHM0blh-.js +1 -0
- package/dist/client/assets/bicep-Bmn6On1c.js +1 -0
- package/dist/client/assets/bird2-DPOp833l.js +1 -0
- package/dist/client/assets/blade-D4QpJJKB.js +1 -0
- package/dist/client/assets/bsl-BO_Y6i37.js +1 -0
- package/dist/client/assets/c-BIGW1oBm.js +1 -0
- package/dist/client/assets/c3-eo99z4R2.js +1 -0
- package/dist/client/assets/cadence-Bv_4Rxtq.js +1 -0
- package/dist/client/assets/cairo-KRGpt6FW.js +1 -0
- package/dist/client/assets/catppuccin-frappe-DFWUc33u.js +1 -0
- package/dist/client/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
- package/dist/client/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
- package/dist/client/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
- package/dist/client/assets/clarity-D53aC0YG.js +1 -0
- package/dist/client/assets/clojure-P80f7IUj.js +1 -0
- package/dist/client/assets/cmake-D1j8_8rp.js +1 -0
- package/dist/client/assets/cobol-nwyudZeR.js +1 -0
- package/dist/client/assets/codeowners-Bp6g37R7.js +1 -0
- package/dist/client/assets/codeql-DsOJ9woJ.js +1 -0
- package/dist/client/assets/coffee-Ch7k5sss.js +1 -0
- package/dist/client/assets/common-lisp-Cg-RD9OK.js +1 -0
- package/dist/client/assets/coq-DkFqJrB1.js +1 -0
- package/dist/client/assets/cpp-CofmeUqb.js +1 -0
- package/dist/client/assets/crystal-tKQVLTB8.js +1 -0
- package/dist/client/assets/csharp-COcwbKMJ.js +1 -0
- package/dist/client/assets/css-DPfMkruS.js +1 -0
- package/dist/client/assets/csv-fuZLfV_i.js +1 -0
- package/dist/client/assets/cue-D82EKSYY.js +1 -0
- package/dist/client/assets/cypher-COkxafJQ.js +1 -0
- package/dist/client/assets/d-85-TOEBH.js +1 -0
- package/dist/client/assets/dark-plus-C3mMm8J8.js +1 -0
- package/dist/client/assets/dart-CF10PKvl.js +1 -0
- package/dist/client/assets/dax-CEL-wOlO.js +1 -0
- package/dist/client/assets/desktop-BmXAJ9_W.js +1 -0
- package/dist/client/assets/diff-D97Zzqfu.js +1 -0
- package/dist/client/assets/docker-BcOcwvcX.js +1 -0
- package/dist/client/assets/dotenv-Da5cRb03.js +1 -0
- package/dist/client/assets/dracula-BzJJZx-M.js +1 -0
- package/dist/client/assets/dracula-soft-BXkSAIEj.js +1 -0
- package/dist/client/assets/dream-maker-BtqSS_iP.js +1 -0
- package/dist/client/assets/edge-BkV0erSs.js +1 -0
- package/dist/client/assets/elixir-CDX3lj18.js +1 -0
- package/dist/client/assets/elm-DbKCFpqz.js +1 -0
- package/dist/client/assets/emacs-lisp-C9XAeP06.js +1 -0
- package/dist/client/assets/erb-B12qg9BL.js +1 -0
- package/dist/client/assets/erlang-DsQrWhSR.js +1 -0
- package/dist/client/assets/everforest-dark-BgDCqdQA.js +1 -0
- package/dist/client/assets/everforest-light-C8M2exoo.js +1 -0
- package/dist/client/assets/fennel-BYunw83y.js +1 -0
- package/dist/client/assets/fish-BvzEVeQv.js +1 -0
- package/dist/client/assets/fluent-C4IJs8-o.js +1 -0
- package/dist/client/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
- package/dist/client/assets/fortran-free-form-BxgE0vQu.js +1 -0
- package/dist/client/assets/fsharp-CXgrBDvD.js +1 -0
- package/dist/client/assets/gdresource-BOOCDP_w.js +1 -0
- package/dist/client/assets/gdscript-C5YyOfLZ.js +1 -0
- package/dist/client/assets/gdshader-DkwncUOv.js +1 -0
- package/dist/client/assets/genie-D0YGMca9.js +1 -0
- package/dist/client/assets/gherkin-DyxjwDmM.js +1 -0
- package/dist/client/assets/git-commit-F4YmCXRG.js +1 -0
- package/dist/client/assets/git-rebase-r7XF79zn.js +1 -0
- package/dist/client/assets/github-dark-DHJKELXO.js +1 -0
- package/dist/client/assets/github-dark-default-Cuk6v7N8.js +1 -0
- package/dist/client/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
- package/dist/client/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
- package/dist/client/assets/github-light-DAi9KRSo.js +1 -0
- package/dist/client/assets/github-light-default-D7oLnXFd.js +1 -0
- package/dist/client/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
- package/dist/client/assets/gleam-BspZqrRM.js +1 -0
- package/dist/client/assets/glimmer-js-Rg0-pVw9.js +1 -0
- package/dist/client/assets/glimmer-ts-U6CK756n.js +1 -0
- package/dist/client/assets/glsl-DplSGwfg.js +1 -0
- package/dist/client/assets/gn-n2N0HUVH.js +1 -0
- package/dist/client/assets/gnuplot-DdkO51Og.js +1 -0
- package/dist/client/assets/go-CxLEBnE3.js +1 -0
- package/dist/client/assets/graphql-ChdNCCLP.js +1 -0
- package/dist/client/assets/groovy-gcz8RCvz.js +1 -0
- package/dist/client/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
- package/dist/client/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
- package/dist/client/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
- package/dist/client/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
- package/dist/client/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
- package/dist/client/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
- package/dist/client/assets/hack-CaT9iCJl.js +1 -0
- package/dist/client/assets/haml-B8DHNrY2.js +1 -0
- package/dist/client/assets/handlebars-BL8al0AC.js +1 -0
- package/dist/client/assets/haskell-Df6bDoY_.js +1 -0
- package/dist/client/assets/haxe-CzTSHFRz.js +1 -0
- package/dist/client/assets/hcl-BWvSN4gD.js +1 -0
- package/dist/client/assets/hjson-D5-asLiD.js +1 -0
- package/dist/client/assets/hlsl-D3lLCCz7.js +1 -0
- package/dist/client/assets/horizon-BUw7H-hv.js +1 -0
- package/dist/client/assets/horizon-bright-Cn-bp-IR.js +1 -0
- package/dist/client/assets/houston-DnULxvSX.js +1 -0
- package/dist/client/assets/html-GMplVEZG.js +1 -0
- package/dist/client/assets/html-derivative-BFtXZ54Q.js +1 -0
- package/dist/client/assets/http-jrhK8wxY.js +1 -0
- package/dist/client/assets/hurl-irOxFIW8.js +1 -0
- package/dist/client/assets/hxml-Bvhsp5Yf.js +1 -0
- package/dist/client/assets/hy-DFXneXwc.js +1 -0
- package/dist/client/assets/imba-DGztddWO.js +1 -0
- package/dist/client/assets/index-C07zYq_-.css +32 -0
- package/dist/client/assets/index-Ce3hNHfL.js +2351 -0
- package/dist/client/assets/ini-BEwlwnbL.js +1 -0
- package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/client/assets/java-CylS5w8V.js +1 -0
- package/dist/client/assets/javascript-wDzz0qaB.js +1 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/client/assets/jinja-4LBKfQ-Z.js +1 -0
- package/dist/client/assets/jison-wvAkD_A8.js +1 -0
- package/dist/client/assets/json-Cp-IABpG.js +1 -0
- package/dist/client/assets/json5-C9tS-k6U.js +1 -0
- package/dist/client/assets/jsonc-Des-eS-w.js +1 -0
- package/dist/client/assets/jsonl-DcaNXYhu.js +1 -0
- package/dist/client/assets/jsonnet-DFQXde-d.js +1 -0
- package/dist/client/assets/jssm-C2t-YnRu.js +1 -0
- package/dist/client/assets/jsx-g9-lgVsj.js +1 -0
- package/dist/client/assets/julia-CxzCAyBv.js +1 -0
- package/dist/client/assets/just-Cw27pwNe.js +1 -0
- package/dist/client/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
- package/dist/client/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
- package/dist/client/assets/kanagawa-wave-DWedfzmr.js +1 -0
- package/dist/client/assets/kdl-DV7GczEv.js +1 -0
- package/dist/client/assets/kotlin-BdnUsdx6.js +1 -0
- package/dist/client/assets/kusto-DZf3V79B.js +1 -0
- package/dist/client/assets/laserwave-DUszq2jm.js +1 -0
- package/dist/client/assets/latex-CWtU0Tv5.js +1 -0
- package/dist/client/assets/lean-BZvkOJ9d.js +1 -0
- package/dist/client/assets/less-B1dDrJ26.js +1 -0
- package/dist/client/assets/light-plus-B7mTdjB0.js +1 -0
- package/dist/client/assets/liquid-DYVedYrR.js +1 -0
- package/dist/client/assets/llvm-DjAJT7YJ.js +1 -0
- package/dist/client/assets/log-2UxHyX5q.js +1 -0
- package/dist/client/assets/logo-BtOb2qkB.js +1 -0
- package/dist/client/assets/lua-BaeVxFsk.js +1 -0
- package/dist/client/assets/luau-C-HG3fhB.js +1 -0
- package/dist/client/assets/make-CHLpvVh8.js +1 -0
- package/dist/client/assets/markdown-Cvjx9yec.js +1 -0
- package/dist/client/assets/marko-CnJfTvn9.js +1 -0
- package/dist/client/assets/material-theme-D5KoaKCx.js +1 -0
- package/dist/client/assets/material-theme-darker-BfHTSMKl.js +1 -0
- package/dist/client/assets/material-theme-lighter-B0m2ddpp.js +1 -0
- package/dist/client/assets/material-theme-ocean-CyktbL80.js +1 -0
- package/dist/client/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
- package/dist/client/assets/matlab-D7o27uSR.js +1 -0
- package/dist/client/assets/mdc-BMNejdWA.js +1 -0
- package/dist/client/assets/mdx-Cmh6b_Ma.js +1 -0
- package/dist/client/assets/mermaid-mWjccvbQ.js +1 -0
- package/dist/client/assets/min-dark-CafNBF8u.js +1 -0
- package/dist/client/assets/min-light-CTRr51gU.js +1 -0
- package/dist/client/assets/mipsasm-CKIfxQSi.js +1 -0
- package/dist/client/assets/mojo-rZm6bMo-.js +1 -0
- package/dist/client/assets/monokai-D4h5O-jR.js +1 -0
- package/dist/client/assets/moonbit-_H4v1dQx.js +1 -0
- package/dist/client/assets/move-IF9eRakj.js +1 -0
- package/dist/client/assets/narrat-DRg8JJMk.js +1 -0
- package/dist/client/assets/nextflow-Zz6hmt5N.js +1 -0
- package/dist/client/assets/nextflow-groovy-BeH2EWoN.js +1 -0
- package/dist/client/assets/nginx-BpAMiNFr.js +1 -0
- package/dist/client/assets/night-owl-C39BiMTA.js +1 -0
- package/dist/client/assets/night-owl-light-CMTm3GFP.js +1 -0
- package/dist/client/assets/nim-CVrawwO9.js +1 -0
- package/dist/client/assets/nix-CwoSXNpI.js +1 -0
- package/dist/client/assets/nord-Ddv68eIx.js +1 -0
- package/dist/client/assets/nushell-Cz2AlsmD.js +1 -0
- package/dist/client/assets/objective-c-DXmwc3jG.js +1 -0
- package/dist/client/assets/objective-cpp-CLxacb5B.js +1 -0
- package/dist/client/assets/ocaml-C0hk2d4L.js +1 -0
- package/dist/client/assets/odin-BBf5iR-q.js +1 -0
- package/dist/client/assets/one-dark-pro-DVMEJ2y_.js +1 -0
- package/dist/client/assets/one-light-C3Wv6jpd.js +1 -0
- package/dist/client/assets/openscad-C4EeE6gA.js +1 -0
- package/dist/client/assets/pascal-D93ZcfNL.js +1 -0
- package/dist/client/assets/perl-C0TMdlhV.js +1 -0
- package/dist/client/assets/php-Dhbhpdrm.js +1 -0
- package/dist/client/assets/pierre-dark-DF2SEV7i.js +1 -0
- package/dist/client/assets/pierre-light-DOlZxES8.js +1 -0
- package/dist/client/assets/pkl-u5AG7uiY.js +1 -0
- package/dist/client/assets/plastic-3e1v2bzS.js +1 -0
- package/dist/client/assets/plsql-ChMvpjG-.js +1 -0
- package/dist/client/assets/po-BTJTHyun.js +1 -0
- package/dist/client/assets/poimandres-CS3Unz2-.js +1 -0
- package/dist/client/assets/polar-C0HS_06l.js +1 -0
- package/dist/client/assets/postcss-CXtECtnM.js +1 -0
- package/dist/client/assets/powerquery-CEu0bR-o.js +1 -0
- package/dist/client/assets/powershell-Dpen1YoG.js +1 -0
- package/dist/client/assets/prisma-Dd19v3D-.js +1 -0
- package/dist/client/assets/prolog-CbFg5uaA.js +1 -0
- package/dist/client/assets/proto-C7zT0LnQ.js +1 -0
- package/dist/client/assets/pug-CGlum2m_.js +1 -0
- package/dist/client/assets/puppet-BMWR74SV.js +1 -0
- package/dist/client/assets/purescript-CklMAg4u.js +1 -0
- package/dist/client/assets/python-B6aJPvgy.js +1 -0
- package/dist/client/assets/qml-3beO22l8.js +1 -0
- package/dist/client/assets/qmldir-C8lEn-DE.js +1 -0
- package/dist/client/assets/qss-IeuSbFQv.js +1 -0
- package/dist/client/assets/r-Dspwwk_N.js +1 -0
- package/dist/client/assets/racket-BqYA7rlc.js +1 -0
- package/dist/client/assets/raku-DXvB9xmW.js +1 -0
- package/dist/client/assets/razor-Uh8Bk_45.js +1 -0
- package/dist/client/assets/red-bN70gL4F.js +1 -0
- package/dist/client/assets/reg-C-SQnVFl.js +1 -0
- package/dist/client/assets/regexp-CDVJQ6XC.js +1 -0
- package/dist/client/assets/rel-C3B-1QV4.js +1 -0
- package/dist/client/assets/riscv-BM1_JUlF.js +1 -0
- package/dist/client/assets/ron-D8l8udqQ.js +1 -0
- package/dist/client/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
- package/dist/client/assets/rose-pine-moon-D4_iv3hh.js +1 -0
- package/dist/client/assets/rose-pine-qdsjHGoJ.js +1 -0
- package/dist/client/assets/rosmsg-BJDFO7_C.js +1 -0
- package/dist/client/assets/rst-BrH8l1NY.js +1 -0
- package/dist/client/assets/ruby-Dw2BHqvy.js +1 -0
- package/dist/client/assets/rust-B1yitclQ.js +1 -0
- package/dist/client/assets/sas-cz2c8ADy.js +1 -0
- package/dist/client/assets/sass-Cj5Yp3dK.js +1 -0
- package/dist/client/assets/scala-C151Ov-r.js +1 -0
- package/dist/client/assets/scheme-C98Dy4si.js +1 -0
- package/dist/client/assets/scratchpad-page-DcMny6uZ.js +1 -0
- package/dist/client/assets/scss-OYdSNvt2.js +1 -0
- package/dist/client/assets/sdbl-DVxCFoDh.js +1 -0
- package/dist/client/assets/shaderlab-Dg9Lc6iA.js +1 -0
- package/dist/client/assets/shellscript-Yzrsuije.js +1 -0
- package/dist/client/assets/shellsession-BADoaaVG.js +1 -0
- package/dist/client/assets/slack-dark-BthQWCQV.js +1 -0
- package/dist/client/assets/slack-ochin-DqwNpetd.js +1 -0
- package/dist/client/assets/smalltalk-BERRCDM3.js +1 -0
- package/dist/client/assets/snazzy-light-Bw305WKR.js +1 -0
- package/dist/client/assets/solarized-dark-DXbdFlpD.js +1 -0
- package/dist/client/assets/solarized-light-L9t79GZl.js +1 -0
- package/dist/client/assets/solidity-rGO070M0.js +1 -0
- package/dist/client/assets/soy-Brmx7dQM.js +1 -0
- package/dist/client/assets/sparql-rVzFXLq3.js +1 -0
- package/dist/client/assets/splunk-BtCnVYZw.js +1 -0
- package/dist/client/assets/sql-BLtJtn59.js +1 -0
- package/dist/client/assets/ssh-config-_ykCGR6B.js +1 -0
- package/dist/client/assets/stata-BH5u7GGu.js +1 -0
- package/dist/client/assets/stylus-BEDo0Tqx.js +1 -0
- package/dist/client/assets/surrealql-Bq5Q-fJD.js +1 -0
- package/dist/client/assets/svelte-C_ipcX3V.js +1 -0
- package/dist/client/assets/swift-D82vCrfD.js +1 -0
- package/dist/client/assets/synthwave-84-CbfX1IO0.js +1 -0
- package/dist/client/assets/system-verilog-CnnmHF94.js +1 -0
- package/dist/client/assets/systemd-4A_iFExJ.js +1 -0
- package/dist/client/assets/talonscript-CkByrt1z.js +1 -0
- package/dist/client/assets/tasl-QIJgUcNo.js +1 -0
- package/dist/client/assets/tcl-dwOrl1Do.js +1 -0
- package/dist/client/assets/templ-P3uqSqPl.js +1 -0
- package/dist/client/assets/terraform-BETggiCN.js +1 -0
- package/dist/client/assets/tex-idrVyKtj.js +1 -0
- package/dist/client/assets/tokyo-night-hegEt444.js +1 -0
- package/dist/client/assets/toml-vGWfd6FD.js +1 -0
- package/dist/client/assets/ts-tags-zn1MmPIZ.js +1 -0
- package/dist/client/assets/tsv-B_m7g4N7.js +1 -0
- package/dist/client/assets/tsx-COt5Ahok.js +1 -0
- package/dist/client/assets/turtle-BsS91CYL.js +1 -0
- package/dist/client/assets/twig-DNn4PbVi.js +1 -0
- package/dist/client/assets/typescript-BPQ3VLAy.js +1 -0
- package/dist/client/assets/typespec-BGHnOYBU.js +1 -0
- package/dist/client/assets/typst-DHCkPAjA.js +1 -0
- package/dist/client/assets/v-BcVCzyr7.js +1 -0
- package/dist/client/assets/vala-CsfeWuGM.js +1 -0
- package/dist/client/assets/vb-D17OF-Vu.js +1 -0
- package/dist/client/assets/verilog-BQ8w6xss.js +1 -0
- package/dist/client/assets/vesper-DU1UobuO.js +1 -0
- package/dist/client/assets/vhdl-CeAyd5Ju.js +1 -0
- package/dist/client/assets/viml-CJc9bBzg.js +1 -0
- package/dist/client/assets/vitesse-black-Bkuqu6BP.js +1 -0
- package/dist/client/assets/vitesse-dark-D0r3Knsf.js +1 -0
- package/dist/client/assets/vitesse-light-CVO1_9PV.js +1 -0
- package/dist/client/assets/vue-DN_0RTcg.js +1 -0
- package/dist/client/assets/vue-html-AaS7Mt5G.js +1 -0
- package/dist/client/assets/vue-vine-CQOfvN7w.js +1 -0
- package/dist/client/assets/vyper-CDx5xZoG.js +1 -0
- package/dist/client/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/client/assets/wasm-MzD3tlZU.js +1 -0
- package/dist/client/assets/wenyan-BV7otONQ.js +1 -0
- package/dist/client/assets/wgsl-Dx-B1_4e.js +1 -0
- package/dist/client/assets/wikitext-BhOHFoWU.js +1 -0
- package/dist/client/assets/wit-5i3qLPDT.js +1 -0
- package/dist/client/assets/wolfram-lXgVvXCa.js +1 -0
- package/dist/client/assets/xml-sdJ4AIDG.js +1 -0
- package/dist/client/assets/xsl-CtQFsRM5.js +1 -0
- package/dist/client/assets/yaml-Buea-lGh.js +1 -0
- package/dist/client/assets/zenscript-DVFEvuxE.js +1 -0
- package/dist/client/assets/zig-VOosw3JB.js +1 -0
- package/dist/client/cursor.png +0 -0
- package/dist/client/favicon.svg +17 -0
- package/dist/client/finder.png +0 -0
- package/dist/client/icons/claude.svg +1 -0
- package/dist/client/icons/openai.svg +1 -0
- package/dist/client/images/github.png +0 -0
- package/dist/client/index.html +15 -0
- package/dist/client/logo.svg +17 -0
- package/dist/client/terminal.png +0 -0
- package/dist/client/vscode.png +0 -0
- package/dist/client/warp.png +0 -0
- package/package.json +107 -0
- package/src/server/agent-instruction-attachments.ts +458 -0
- package/src/server/agent.ts +1879 -0
- package/src/server/cli-runtime.ts +418 -0
- package/src/server/cli-supervisor.ts +90 -0
- package/src/server/cli.ts +102 -0
- package/src/server/codex-app-server-protocol.ts +478 -0
- package/src/server/codex-app-server.ts +1645 -0
- package/src/server/data-dir-lock.ts +128 -0
- package/src/server/diff-store.ts +1587 -0
- package/src/server/durable-file.ts +74 -0
- package/src/server/event-store.ts +1448 -0
- package/src/server/event.ts +249 -0
- package/src/server/external-file-access.ts +48 -0
- package/src/server/external-open.ts +259 -0
- package/src/server/generate-title.ts +75 -0
- package/src/server/git-refresh-poller.ts +92 -0
- package/src/server/github-rest-client.ts +176 -0
- package/src/server/harness-types.ts +24 -0
- package/src/server/keybindings.ts +203 -0
- package/src/server/machine-name.ts +22 -0
- package/src/server/paths.ts +51 -0
- package/src/server/pr-manager.ts +1204 -0
- package/src/server/pr-refresh-poller.ts +126 -0
- package/src/server/process-utils.ts +18 -0
- package/src/server/provider-catalog.ts +90 -0
- package/src/server/quick-response.ts +274 -0
- package/src/server/read-models.ts +311 -0
- package/src/server/restart.ts +33 -0
- package/src/server/scratchpad-manager.ts +87 -0
- package/src/server/server.ts +759 -0
- package/src/server/share.ts +126 -0
- package/src/server/terminal-manager.ts +371 -0
- package/src/server/update-manager.ts +250 -0
- package/src/server/uploads.ts +191 -0
- package/src/server/workspace-file-search.ts +191 -0
- package/src/server/workspace-manager.ts +627 -0
- package/src/server/workspace-polling.ts +10 -0
- package/src/server/ws-router.ts +1039 -0
- package/src/shared/branding.ts +69 -0
- package/src/shared/dev-ports.ts +100 -0
- package/src/shared/ports.ts +2 -0
- package/src/shared/protocol.ts +217 -0
- package/src/shared/tools.ts +324 -0
- package/src/shared/types.ts +1220 -0
- package/tsconfig.json +35 -0
|
@@ -0,0 +1,1587 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { realpath, rm, stat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import type {
|
|
5
|
+
BranchActionFailure,
|
|
6
|
+
BranchActionSuccess,
|
|
7
|
+
BranchMetadata,
|
|
8
|
+
GitHubRepoAvailabilityResult,
|
|
9
|
+
GithubPublishInfo,
|
|
10
|
+
UpstreamStatus,
|
|
11
|
+
WorkspaceBranchHistoryEntry,
|
|
12
|
+
WorkspaceBranchHistorySnapshot,
|
|
13
|
+
WorkspaceDiffFile,
|
|
14
|
+
WorkspaceDiffPatchResult,
|
|
15
|
+
WorkspaceFileContentsResult,
|
|
16
|
+
WorkspaceGitSnapshot,
|
|
17
|
+
} from '../shared/types';
|
|
18
|
+
import { registerExternalFileAccess } from './external-file-access';
|
|
19
|
+
import { inferWorkspaceFileContentType } from './uploads';
|
|
20
|
+
|
|
21
|
+
interface StoredWorkspaceGitState extends BranchMetadata, UpstreamStatus {
|
|
22
|
+
status: WorkspaceGitSnapshot['status'];
|
|
23
|
+
files: WorkspaceDiffFile[];
|
|
24
|
+
pullRequestFiles?: WorkspaceDiffFile[];
|
|
25
|
+
hasPushedCommits?: boolean;
|
|
26
|
+
branchPublishState?: WorkspaceGitSnapshot['branchPublishState'];
|
|
27
|
+
mainAheadCount?: number;
|
|
28
|
+
branchHistory: WorkspaceBranchHistorySnapshot;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface DirtyPathEntry {
|
|
32
|
+
path: string;
|
|
33
|
+
previousPath?: string;
|
|
34
|
+
changeType: WorkspaceDiffFile['changeType'];
|
|
35
|
+
isUntracked: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const MAX_WORKSPACE_FILE_CONTENT_BYTES = 2 * 1024 * 1024;
|
|
39
|
+
const DEFAULT_BINARY_MIME_TYPE = 'application/octet-stream';
|
|
40
|
+
const TEXT_PLAIN_CONTENT_TYPE = 'text/plain; charset=utf-8';
|
|
41
|
+
|
|
42
|
+
export interface GitHubBackedRepoInspection {
|
|
43
|
+
ok: boolean;
|
|
44
|
+
repoRoot?: string;
|
|
45
|
+
branchName?: string;
|
|
46
|
+
defaultBranchName?: string;
|
|
47
|
+
githubOwner?: string;
|
|
48
|
+
githubRepo?: string;
|
|
49
|
+
originRepoSlug?: string;
|
|
50
|
+
message?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createEmptyState(): StoredWorkspaceGitState {
|
|
54
|
+
return {
|
|
55
|
+
status: 'unknown',
|
|
56
|
+
branchName: undefined,
|
|
57
|
+
defaultBranchName: undefined,
|
|
58
|
+
hasOriginRemote: undefined,
|
|
59
|
+
originRepoSlug: undefined,
|
|
60
|
+
hasUpstream: undefined,
|
|
61
|
+
aheadCount: undefined,
|
|
62
|
+
behindCount: undefined,
|
|
63
|
+
lastFetchedAt: undefined,
|
|
64
|
+
files: [],
|
|
65
|
+
pullRequestFiles: [],
|
|
66
|
+
hasPushedCommits: undefined,
|
|
67
|
+
branchPublishState: 'unknown',
|
|
68
|
+
mainAheadCount: undefined,
|
|
69
|
+
branchHistory: { entries: [] },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function branchMetadataEqual(left: BranchMetadata, right: BranchMetadata) {
|
|
74
|
+
return (
|
|
75
|
+
left.branchName === right.branchName &&
|
|
76
|
+
left.defaultBranchName === right.defaultBranchName &&
|
|
77
|
+
left.hasOriginRemote === right.hasOriginRemote &&
|
|
78
|
+
left.originRepoSlug === right.originRepoSlug &&
|
|
79
|
+
left.hasUpstream === right.hasUpstream
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function upstreamStatusEqual(left: UpstreamStatus, right: UpstreamStatus) {
|
|
84
|
+
return (
|
|
85
|
+
left.aheadCount === right.aheadCount &&
|
|
86
|
+
left.behindCount === right.behindCount &&
|
|
87
|
+
left.lastFetchedAt === right.lastFetchedAt
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function branchHistoryEqual(
|
|
92
|
+
left: WorkspaceBranchHistorySnapshot,
|
|
93
|
+
right: WorkspaceBranchHistorySnapshot,
|
|
94
|
+
) {
|
|
95
|
+
if (left.entries.length !== right.entries.length) return false;
|
|
96
|
+
|
|
97
|
+
return left.entries.every((entry, index) => {
|
|
98
|
+
const other = right.entries[index];
|
|
99
|
+
return (
|
|
100
|
+
Boolean(other) &&
|
|
101
|
+
entry.sha === other.sha &&
|
|
102
|
+
entry.summary === other.summary &&
|
|
103
|
+
entry.description === other.description &&
|
|
104
|
+
entry.authorName === other.authorName &&
|
|
105
|
+
entry.authoredAt === other.authoredAt &&
|
|
106
|
+
entry.githubUrl === other.githubUrl &&
|
|
107
|
+
entry.tags.length === other.tags.length &&
|
|
108
|
+
entry.tags.every((tag, tagIndex) => tag === other.tags[tagIndex])
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function snapshotsEqual(
|
|
114
|
+
left: StoredWorkspaceGitState | undefined,
|
|
115
|
+
right: StoredWorkspaceGitState,
|
|
116
|
+
) {
|
|
117
|
+
if (!left) {
|
|
118
|
+
return (
|
|
119
|
+
right.status === 'unknown' &&
|
|
120
|
+
right.files.length === 0 &&
|
|
121
|
+
(right.pullRequestFiles?.length ?? 0) === 0
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (left.status !== right.status) return false;
|
|
126
|
+
if (!branchMetadataEqual(left, right)) return false;
|
|
127
|
+
if (!upstreamStatusEqual(left, right)) return false;
|
|
128
|
+
if (left.hasPushedCommits !== right.hasPushedCommits) return false;
|
|
129
|
+
if (left.branchPublishState !== right.branchPublishState) return false;
|
|
130
|
+
if (left.mainAheadCount !== right.mainAheadCount) return false;
|
|
131
|
+
if (!branchHistoryEqual(left.branchHistory, right.branchHistory)) return false;
|
|
132
|
+
if (!diffFilesEqual(left.files, right.files)) return false;
|
|
133
|
+
if (!diffFilesEqual(left.pullRequestFiles ?? [], right.pullRequestFiles ?? [])) return false;
|
|
134
|
+
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function diffFilesEqual(left: WorkspaceDiffFile[], right: WorkspaceDiffFile[]) {
|
|
139
|
+
if (left.length !== right.length) return false;
|
|
140
|
+
|
|
141
|
+
return left.every((file, index) => {
|
|
142
|
+
const other = right[index];
|
|
143
|
+
return (
|
|
144
|
+
Boolean(other) &&
|
|
145
|
+
file.path === other.path &&
|
|
146
|
+
file.changeType === other.changeType &&
|
|
147
|
+
file.isUntracked === other.isUntracked &&
|
|
148
|
+
file.additions === other.additions &&
|
|
149
|
+
file.deletions === other.deletions &&
|
|
150
|
+
file.patchDigest === other.patchDigest &&
|
|
151
|
+
file.mimeType === other.mimeType &&
|
|
152
|
+
file.size === other.size
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function stripTrailingSlash(value: string) {
|
|
158
|
+
return value.replace(/\/+$/u, '');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function normalizeRepoRelativePath(value: string) {
|
|
162
|
+
const normalizedInput = value.replace(/\\/gu, '/').trim();
|
|
163
|
+
const hadTrailingSlash = normalizedInput.endsWith('/');
|
|
164
|
+
|
|
165
|
+
const normalized = path.posix
|
|
166
|
+
.normalize(normalizedInput || '.')
|
|
167
|
+
.replace(/^(\.\/)+/u, '')
|
|
168
|
+
.replace(/^\/+/u, '');
|
|
169
|
+
|
|
170
|
+
if (!normalized || normalized === '.' || normalized === '..' || normalized.startsWith('../')) {
|
|
171
|
+
throw new Error('Path must stay inside the repository');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return hadTrailingSlash && !normalized.endsWith('/') ? `${normalized}/` : normalized;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function fileExists(filePath: string) {
|
|
178
|
+
try {
|
|
179
|
+
await stat(filePath);
|
|
180
|
+
return true;
|
|
181
|
+
} catch {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function runGit(args: string[], cwd: string) {
|
|
187
|
+
const process = Bun.spawn(['git', '-C', cwd, ...args], {
|
|
188
|
+
stdout: 'pipe',
|
|
189
|
+
stderr: 'pipe',
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
193
|
+
new Response(process.stdout).text(),
|
|
194
|
+
new Response(process.stderr).text(),
|
|
195
|
+
process.exited,
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
return { stdout, stderr, exitCode };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function runCommand(args: string[]) {
|
|
202
|
+
const process = Bun.spawn(args, {
|
|
203
|
+
stdout: 'pipe',
|
|
204
|
+
stderr: 'pipe',
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
208
|
+
new Response(process.stdout).text(),
|
|
209
|
+
new Response(process.stderr).text(),
|
|
210
|
+
process.exited,
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
return { stdout, stderr, exitCode };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function formatGitFailure(result: Awaited<ReturnType<typeof runGit>>) {
|
|
217
|
+
return [result.stderr.trim(), result.stdout.trim()].filter(Boolean).join('\n');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function summarizeGitFailure(detail: string, fallback: string) {
|
|
221
|
+
return (
|
|
222
|
+
detail
|
|
223
|
+
.split(/\r?\n/u)
|
|
224
|
+
.map((line) => line.trim())
|
|
225
|
+
.find((line) => line.length > 0) ?? fallback
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function createBranchActionFailure(
|
|
230
|
+
title: string,
|
|
231
|
+
detail: string,
|
|
232
|
+
fallbackMessage: string,
|
|
233
|
+
snapshotChanged = false,
|
|
234
|
+
): BranchActionFailure {
|
|
235
|
+
return {
|
|
236
|
+
ok: false,
|
|
237
|
+
title,
|
|
238
|
+
message: summarizeGitFailure(detail, fallbackMessage),
|
|
239
|
+
detail,
|
|
240
|
+
snapshotChanged,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const SAFE_INITIAL_GITIGNORE_ENTRIES = [
|
|
245
|
+
'.env',
|
|
246
|
+
'.env.*',
|
|
247
|
+
'!.env.example',
|
|
248
|
+
'*.pem',
|
|
249
|
+
'*.key',
|
|
250
|
+
'*.p12',
|
|
251
|
+
'*.pfx',
|
|
252
|
+
'node_modules/',
|
|
253
|
+
'.DS_Store',
|
|
254
|
+
'.miko/',
|
|
255
|
+
'.miko-dev/',
|
|
256
|
+
] as const;
|
|
257
|
+
|
|
258
|
+
async function ensureSafeInitialGitignore(repoRoot: string) {
|
|
259
|
+
const gitignorePath = path.join(repoRoot, '.gitignore');
|
|
260
|
+
const currentContents = await Bun.file(gitignorePath)
|
|
261
|
+
.text()
|
|
262
|
+
.catch(() => null);
|
|
263
|
+
let nextContents = currentContents;
|
|
264
|
+
|
|
265
|
+
for (const entry of SAFE_INITIAL_GITIGNORE_ENTRIES) {
|
|
266
|
+
nextContents = appendGitIgnoreEntry(nextContents, entry);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (nextContents !== null && nextContents !== currentContents) {
|
|
270
|
+
await Bun.write(gitignorePath, nextContents);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function ensureInitialCommit(repoRoot: string): Promise<BranchActionFailure | null> {
|
|
275
|
+
const repo = await resolveRepo(repoRoot);
|
|
276
|
+
if (repo?.baseCommit) return null;
|
|
277
|
+
|
|
278
|
+
await ensureSafeInitialGitignore(repoRoot);
|
|
279
|
+
|
|
280
|
+
const addResult = await runGit(['add', '-A'], repoRoot);
|
|
281
|
+
if (addResult.exitCode !== 0) {
|
|
282
|
+
return createBranchActionFailure(
|
|
283
|
+
'Initial commit failed',
|
|
284
|
+
formatGitFailure(addResult),
|
|
285
|
+
'Git could not stage files for the initial commit.',
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const commitResult = await runGit(
|
|
290
|
+
[
|
|
291
|
+
'-c',
|
|
292
|
+
'user.name=Miko',
|
|
293
|
+
'-c',
|
|
294
|
+
'user.email=miko@example.com',
|
|
295
|
+
'commit',
|
|
296
|
+
'--allow-empty',
|
|
297
|
+
'-m',
|
|
298
|
+
'Initial commit',
|
|
299
|
+
],
|
|
300
|
+
repoRoot,
|
|
301
|
+
);
|
|
302
|
+
if (commitResult.exitCode !== 0) {
|
|
303
|
+
return createBranchActionFailure(
|
|
304
|
+
'Initial commit failed',
|
|
305
|
+
formatGitFailure(commitResult),
|
|
306
|
+
'Git could not create the initial commit.',
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export async function resolveRepo(
|
|
314
|
+
workspacePath: string,
|
|
315
|
+
): Promise<{ repoRoot: string; baseCommit: string | null } | null> {
|
|
316
|
+
const topLevel = await runGit(['rev-parse', '--show-toplevel'], workspacePath);
|
|
317
|
+
if (topLevel.exitCode !== 0) return null;
|
|
318
|
+
|
|
319
|
+
const repoRoot = topLevel.stdout.trim();
|
|
320
|
+
const head = await runGit(['rev-parse', 'HEAD'], repoRoot);
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
repoRoot,
|
|
324
|
+
baseCommit: head.exitCode === 0 ? head.stdout.trim() : null,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function getBranchName(repoRoot: string) {
|
|
329
|
+
const branch = await runGit(['branch', '--show-current'], repoRoot);
|
|
330
|
+
const trimmed = branch.stdout.trim();
|
|
331
|
+
if (trimmed) return trimmed;
|
|
332
|
+
|
|
333
|
+
const symbolic = await runGit(['symbolic-ref', '--short', 'HEAD'], repoRoot);
|
|
334
|
+
return symbolic.exitCode === 0 ? symbolic.stdout.trim() : undefined;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function getOriginRemoteUrl(repoRoot: string) {
|
|
338
|
+
const remote = await runGit(['remote', 'get-url', 'origin'], repoRoot);
|
|
339
|
+
return remote.exitCode === 0 ? remote.stdout.trim() : null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function extractGitHubRepoSlug(remoteUrl: string | null | undefined) {
|
|
343
|
+
if (!remoteUrl) return null;
|
|
344
|
+
|
|
345
|
+
const patterns = [
|
|
346
|
+
/^git@github\.com:([^/]+\/[^/]+?)(?:\.git)?$/u,
|
|
347
|
+
/^ssh:\/\/git@github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/u,
|
|
348
|
+
/^https:\/\/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/u,
|
|
349
|
+
];
|
|
350
|
+
|
|
351
|
+
for (const pattern of patterns) {
|
|
352
|
+
const match = remoteUrl.match(pattern);
|
|
353
|
+
if (match?.[1]) return match[1];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function splitRepoSlug(repoSlug: string | null | undefined) {
|
|
360
|
+
const [owner, repo] = repoSlug?.split('/') ?? [];
|
|
361
|
+
return owner && repo ? { owner, repo } : null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function getLocalBranchNames(repoRoot: string) {
|
|
365
|
+
const result = await runGit(
|
|
366
|
+
['for-each-ref', '--format=%(refname:short)', 'refs/heads'],
|
|
367
|
+
repoRoot,
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
if (result.exitCode !== 0) return [];
|
|
371
|
+
|
|
372
|
+
return result.stdout
|
|
373
|
+
.split(/\r?\n/u)
|
|
374
|
+
.map((line) => line.trim())
|
|
375
|
+
.filter(Boolean);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export async function resolveDefaultBranchName(repoRoot: string) {
|
|
379
|
+
const originHead = await runGit(
|
|
380
|
+
['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'],
|
|
381
|
+
repoRoot,
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
if (originHead.exitCode === 0) {
|
|
385
|
+
return originHead.stdout.trim().replace(/^origin\//u, '');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const branches = await getLocalBranchNames(repoRoot);
|
|
389
|
+
if (branches.includes('main')) return 'main';
|
|
390
|
+
if (branches.includes('master')) return 'master';
|
|
391
|
+
return branches[0];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function hasUpstreamBranch(repoRoot: string) {
|
|
395
|
+
const upstream = await runGit(
|
|
396
|
+
['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'],
|
|
397
|
+
repoRoot,
|
|
398
|
+
);
|
|
399
|
+
return upstream.exitCode === 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export async function getUpstreamStatusCounts(repoRoot: string) {
|
|
403
|
+
const result = await runGit(
|
|
404
|
+
['rev-list', '--left-right', '--count', 'HEAD...@{upstream}'],
|
|
405
|
+
repoRoot,
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
if (result.exitCode !== 0) {
|
|
409
|
+
return { aheadCount: undefined, behindCount: undefined };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const [aheadText = '', behindText = ''] = result.stdout.trim().split(/\s+/u);
|
|
413
|
+
const aheadCount = Number.parseInt(aheadText, 10);
|
|
414
|
+
const behindCount = Number.parseInt(behindText, 10);
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
aheadCount: Number.isFinite(aheadCount) ? aheadCount : undefined,
|
|
418
|
+
behindCount: Number.isFinite(behindCount) ? behindCount : undefined,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function getLastFetchedAt(repoRoot: string) {
|
|
423
|
+
const gitPath = await runGit(['rev-parse', '--git-path', 'FETCH_HEAD'], repoRoot);
|
|
424
|
+
if (gitPath.exitCode !== 0) return undefined;
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const fetchHeadPath = gitPath.stdout.trim();
|
|
428
|
+
const info = await stat(
|
|
429
|
+
path.isAbsolute(fetchHeadPath) ? fetchHeadPath : path.join(repoRoot, fetchHeadPath),
|
|
430
|
+
);
|
|
431
|
+
return info.mtime.toISOString();
|
|
432
|
+
} catch {
|
|
433
|
+
return undefined;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function refExists(repoRoot: string, ref: string) {
|
|
438
|
+
const result = await runGit(['rev-parse', '--verify', '--quiet', ref], repoRoot);
|
|
439
|
+
return result.exitCode === 0;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export async function hasPushedCommits(args: {
|
|
443
|
+
repoRoot: string;
|
|
444
|
+
branchName?: string;
|
|
445
|
+
defaultBranchName?: string;
|
|
446
|
+
}) {
|
|
447
|
+
if (!args.branchName) return false;
|
|
448
|
+
|
|
449
|
+
const remoteBranchRef = `refs/remotes/origin/${args.branchName}`;
|
|
450
|
+
if (!(await refExists(args.repoRoot, remoteBranchRef))) return false;
|
|
451
|
+
|
|
452
|
+
const baseBranch = args.defaultBranchName || 'main';
|
|
453
|
+
const remoteBaseRef = `refs/remotes/origin/${baseBranch}`;
|
|
454
|
+
if (!(await refExists(args.repoRoot, remoteBaseRef))) return false;
|
|
455
|
+
|
|
456
|
+
const result = await runGit(
|
|
457
|
+
['rev-list', '--count', `${remoteBaseRef}..${remoteBranchRef}`],
|
|
458
|
+
args.repoRoot,
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
if (result.exitCode !== 0) return false;
|
|
462
|
+
|
|
463
|
+
const count = Number.parseInt(result.stdout.trim(), 10);
|
|
464
|
+
return Number.isFinite(count) && count > 0;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function getBranchPublishState(args: {
|
|
468
|
+
repoRoot: string;
|
|
469
|
+
branchName?: string;
|
|
470
|
+
hasUpstream: boolean;
|
|
471
|
+
}): Promise<WorkspaceGitSnapshot['branchPublishState']> {
|
|
472
|
+
if (!args.branchName) return 'unknown';
|
|
473
|
+
if (args.hasUpstream) return 'published';
|
|
474
|
+
|
|
475
|
+
const remoteBranchRef = `refs/remotes/origin/${args.branchName}`;
|
|
476
|
+
return (await refExists(args.repoRoot, remoteBranchRef)) ? 'published' : 'local_only';
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export async function getMainAheadCount(args: { repoRoot: string; defaultBranchName?: string }) {
|
|
480
|
+
const baseBranch = args.defaultBranchName || 'main';
|
|
481
|
+
const remoteBaseRef = `refs/remotes/origin/${baseBranch}`;
|
|
482
|
+
if (!(await refExists(args.repoRoot, remoteBaseRef))) return undefined;
|
|
483
|
+
|
|
484
|
+
const result = await runGit(['rev-list', '--count', `HEAD..${remoteBaseRef}`], args.repoRoot);
|
|
485
|
+
if (result.exitCode !== 0) return undefined;
|
|
486
|
+
|
|
487
|
+
const count = Number.parseInt(result.stdout.trim(), 10);
|
|
488
|
+
return Number.isFinite(count) ? count : undefined;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function resolveBranchComparisonRef(args: {
|
|
492
|
+
repoRoot: string;
|
|
493
|
+
branchName?: string;
|
|
494
|
+
defaultBranchName?: string;
|
|
495
|
+
}) {
|
|
496
|
+
if (!args.defaultBranchName || args.branchName === args.defaultBranchName) return null;
|
|
497
|
+
|
|
498
|
+
const candidates = [
|
|
499
|
+
`refs/remotes/origin/${args.defaultBranchName}`,
|
|
500
|
+
`refs/heads/${args.defaultBranchName}`,
|
|
501
|
+
args.defaultBranchName,
|
|
502
|
+
];
|
|
503
|
+
|
|
504
|
+
for (const candidate of candidates) {
|
|
505
|
+
if (await refExists(args.repoRoot, candidate)) return candidate;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function parseNameStatusCode(status: string): WorkspaceDiffFile['changeType'] {
|
|
512
|
+
if (status.startsWith('A')) return 'added';
|
|
513
|
+
if (status.startsWith('D')) return 'deleted';
|
|
514
|
+
if (status.startsWith('R')) return 'renamed';
|
|
515
|
+
return 'modified';
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export function parseNameStatusEntries(output: string) {
|
|
519
|
+
const parts = output.split('\0').filter(Boolean);
|
|
520
|
+
const entries: DirtyPathEntry[] = [];
|
|
521
|
+
|
|
522
|
+
for (let index = 0; index < parts.length; ) {
|
|
523
|
+
const status = parts[index++];
|
|
524
|
+
if (!status) continue;
|
|
525
|
+
|
|
526
|
+
if (status.startsWith('R')) {
|
|
527
|
+
const previousPath = parts[index++];
|
|
528
|
+
const nextPath = parts[index++];
|
|
529
|
+
if (!previousPath || !nextPath) continue;
|
|
530
|
+
entries.push({
|
|
531
|
+
path: normalizeRepoRelativePath(nextPath),
|
|
532
|
+
previousPath: normalizeRepoRelativePath(previousPath),
|
|
533
|
+
changeType: 'renamed',
|
|
534
|
+
isUntracked: false,
|
|
535
|
+
});
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const filePath = parts[index++];
|
|
540
|
+
if (!filePath) continue;
|
|
541
|
+
entries.push({
|
|
542
|
+
path: normalizeRepoRelativePath(filePath),
|
|
543
|
+
changeType: parseNameStatusCode(status),
|
|
544
|
+
isUntracked: false,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return entries;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export async function listBranchDiffPaths(repoRoot: string, comparisonRef: string) {
|
|
552
|
+
const result = await runGit(
|
|
553
|
+
['diff', '--name-status', '--find-renames', '-z', `${comparisonRef}...HEAD`],
|
|
554
|
+
repoRoot,
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
if (result.exitCode !== 0) return [];
|
|
558
|
+
return parseNameStatusEntries(result.stdout);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export function parseStatusLine(line: string): DirtyPathEntry | null {
|
|
562
|
+
if (!line.trim()) return null;
|
|
563
|
+
|
|
564
|
+
const status = line.slice(0, 2);
|
|
565
|
+
const rawPath = line.slice(3);
|
|
566
|
+
const renameIndex = rawPath.indexOf(' -> ');
|
|
567
|
+
const isUntracked = status === '??';
|
|
568
|
+
|
|
569
|
+
if (renameIndex >= 0) {
|
|
570
|
+
const previousPath = rawPath.slice(0, renameIndex).trim();
|
|
571
|
+
const nextPath = rawPath.slice(renameIndex + 4).trim();
|
|
572
|
+
return {
|
|
573
|
+
path: nextPath,
|
|
574
|
+
previousPath,
|
|
575
|
+
changeType: 'renamed',
|
|
576
|
+
isUntracked: false,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
let changeType: WorkspaceDiffFile['changeType'] = 'modified';
|
|
581
|
+
if (isUntracked || status.includes('A')) {
|
|
582
|
+
changeType = 'added';
|
|
583
|
+
} else if (status.includes('D')) {
|
|
584
|
+
changeType = 'deleted';
|
|
585
|
+
} else if (status.includes('R')) {
|
|
586
|
+
changeType = 'renamed';
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
path: rawPath.trim(),
|
|
591
|
+
changeType,
|
|
592
|
+
isUntracked,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export async function listDirtyPaths(repoRoot: string) {
|
|
597
|
+
// TODO: switch to `git status --porcelain=v1 -z` and parse NUL-delimited records
|
|
598
|
+
// before this powers broad external use. The current line parser is acceptable for
|
|
599
|
+
// normal code paths, but quoted paths/newlines can produce incorrect diff rows.
|
|
600
|
+
const status = await runGit(['status', '--porcelain=v1', '--untracked-files=all'], repoRoot);
|
|
601
|
+
if (status.exitCode !== 0) {
|
|
602
|
+
throw new Error(formatGitFailure(status) || 'Failed to list git changes');
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return status.stdout
|
|
606
|
+
.split(/\r?\n/u)
|
|
607
|
+
.map((line) => parseStatusLine(line))
|
|
608
|
+
.filter((entry): entry is DirtyPathEntry => Boolean(entry))
|
|
609
|
+
.map((entry) => ({
|
|
610
|
+
...entry,
|
|
611
|
+
path: normalizeRepoRelativePath(entry.path),
|
|
612
|
+
previousPath: entry.previousPath ? normalizeRepoRelativePath(entry.previousPath) : undefined,
|
|
613
|
+
}));
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export async function findDirtyPath(repoRoot: string, relativePath: string) {
|
|
617
|
+
const normalizedPath = stripTrailingSlash(relativePath);
|
|
618
|
+
const dirtyPaths = await listDirtyPaths(repoRoot);
|
|
619
|
+
return dirtyPaths.find((entry) => stripTrailingSlash(entry.path) === normalizedPath) ?? null;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function hashPatch(patch: string) {
|
|
623
|
+
return createHash('sha256').update(patch).digest('hex');
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function normalizeNoIndexPatchHeaders(patch: string, relativePath: string) {
|
|
627
|
+
const normalizedPath = stripTrailingSlash(relativePath);
|
|
628
|
+
const absolutePathPattern = /[^\s]+/u;
|
|
629
|
+
return patch
|
|
630
|
+
.replace(
|
|
631
|
+
new RegExp(
|
|
632
|
+
`^diff --git a/${absolutePathPattern.source} b/${absolutePathPattern.source}$`,
|
|
633
|
+
'mu',
|
|
634
|
+
),
|
|
635
|
+
`diff --git a/${normalizedPath} b/${normalizedPath}`,
|
|
636
|
+
)
|
|
637
|
+
.replace(/^(\+\+\+) b\/.*$/mu, `+++ b/${normalizedPath}`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function hashFileContents(relativePath: string, contents: string) {
|
|
641
|
+
return createHash('sha256').update(`${relativePath}\0${contents}`).digest('hex');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function hashFileMetadata(relativePath: string, size: number, mtimeMs: number) {
|
|
645
|
+
return createHash('sha256').update(`${relativePath}\0${size}\0${mtimeMs}`).digest('hex');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function isPreviewableTextMimeType(mimeType: string) {
|
|
649
|
+
const normalized = mimeType.toLowerCase();
|
|
650
|
+
return (
|
|
651
|
+
normalized.startsWith('text/') ||
|
|
652
|
+
normalized === 'application/json' ||
|
|
653
|
+
normalized.startsWith('application/json;')
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function isDefaultBinaryMimeType(mimeType: string) {
|
|
658
|
+
return mimeType.toLowerCase() === DEFAULT_BINARY_MIME_TYPE;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function isPreviewableImageMimeType(mimeType: string) {
|
|
662
|
+
const normalized = mimeType.toLowerCase();
|
|
663
|
+
return normalized.startsWith('image/') && normalized !== 'image/svg+xml';
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function hasSuspiciousControlCharacters(value: string) {
|
|
667
|
+
for (const character of value) {
|
|
668
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
669
|
+
const isAllowedControlCharacter = codePoint === 9 || codePoint === 10 || codePoint === 13;
|
|
670
|
+
if (codePoint < 32 && !isAllowedControlCharacter) return true;
|
|
671
|
+
}
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
class RepositoryPathEscapeError extends Error {
|
|
676
|
+
constructor() {
|
|
677
|
+
super('Path must stay inside the repository');
|
|
678
|
+
this.name = 'RepositoryPathEscapeError';
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async function resolveWorkspaceFilePath(repoRoot: string, relativePath: string) {
|
|
683
|
+
const absolutePath = path.join(repoRoot, relativePath);
|
|
684
|
+
const [repoRootRealPath, targetRealPath] = await Promise.all([
|
|
685
|
+
realpath(repoRoot),
|
|
686
|
+
realpath(absolutePath),
|
|
687
|
+
]);
|
|
688
|
+
|
|
689
|
+
if (
|
|
690
|
+
targetRealPath !== repoRootRealPath &&
|
|
691
|
+
!targetRealPath.startsWith(`${repoRootRealPath}${path.sep}`)
|
|
692
|
+
) {
|
|
693
|
+
throw new RepositoryPathEscapeError();
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return targetRealPath;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
async function previewFileAtPath(args: {
|
|
700
|
+
filePath: string;
|
|
701
|
+
displayPath: string;
|
|
702
|
+
contentUrl?: (metadataDigest: string) => string;
|
|
703
|
+
}): Promise<WorkspaceFileContentsResult> {
|
|
704
|
+
const info = await stat(args.filePath);
|
|
705
|
+
if (!info.isFile()) throw new Error(`Path is not a file: ${args.displayPath}`);
|
|
706
|
+
if (info.size > MAX_WORKSPACE_FILE_CONTENT_BYTES) {
|
|
707
|
+
throw new Error(`File is too large to preview: ${args.displayPath}`);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
let mimeType = inferWorkspaceFileContentType(args.filePath);
|
|
711
|
+
const metadataDigest = hashFileMetadata(args.displayPath, info.size, info.mtimeMs);
|
|
712
|
+
|
|
713
|
+
if (args.contentUrl && isPreviewableImageMimeType(mimeType)) {
|
|
714
|
+
return {
|
|
715
|
+
kind: 'image',
|
|
716
|
+
path: args.displayPath,
|
|
717
|
+
name: path.basename(args.displayPath),
|
|
718
|
+
contentUrl: args.contentUrl(metadataDigest),
|
|
719
|
+
mimeType,
|
|
720
|
+
size: info.size,
|
|
721
|
+
cacheKey: `${args.displayPath}:${metadataDigest}`,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
let contents: string | null = null;
|
|
726
|
+
if (isPreviewableTextMimeType(mimeType) || isDefaultBinaryMimeType(mimeType)) {
|
|
727
|
+
contents = await readPreviewableTextFile(args.filePath, args.displayPath).catch(() => null);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (contents === null) {
|
|
731
|
+
return {
|
|
732
|
+
kind: 'binary',
|
|
733
|
+
path: args.displayPath,
|
|
734
|
+
name: path.basename(args.displayPath),
|
|
735
|
+
mimeType,
|
|
736
|
+
size: info.size,
|
|
737
|
+
cacheKey: `${args.displayPath}:${metadataDigest}`,
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (isDefaultBinaryMimeType(mimeType)) {
|
|
742
|
+
mimeType = TEXT_PLAIN_CONTENT_TYPE;
|
|
743
|
+
}
|
|
744
|
+
const contentDigest = hashFileContents(args.displayPath, contents);
|
|
745
|
+
|
|
746
|
+
return {
|
|
747
|
+
kind: 'text',
|
|
748
|
+
path: args.displayPath,
|
|
749
|
+
name: path.basename(args.displayPath),
|
|
750
|
+
contents,
|
|
751
|
+
mimeType,
|
|
752
|
+
size: info.size,
|
|
753
|
+
encoding: 'utf-8',
|
|
754
|
+
cacheKey: `${args.displayPath}:${contentDigest}`,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
async function readPreviewableTextFile(filePath: string, relativePath: string) {
|
|
759
|
+
const file = Bun.file(filePath);
|
|
760
|
+
const buffer = await file.arrayBuffer();
|
|
761
|
+
if (new Uint8Array(buffer).includes(0)) {
|
|
762
|
+
throw new Error(`File is not previewable as text: ${relativePath}`);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
let contents: string;
|
|
766
|
+
try {
|
|
767
|
+
contents = new TextDecoder('utf-8', { fatal: true }).decode(buffer);
|
|
768
|
+
} catch {
|
|
769
|
+
throw new Error(`File is not previewable as text: ${relativePath}`);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (hasSuspiciousControlCharacters(contents)) {
|
|
773
|
+
throw new Error(`File is not previewable as text: ${relativePath}`);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
return contents;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
export async function readPatchForEntry(
|
|
780
|
+
repoRoot: string,
|
|
781
|
+
baseCommit: string | null,
|
|
782
|
+
entry: DirtyPathEntry,
|
|
783
|
+
): Promise<string> {
|
|
784
|
+
const targetPath = stripTrailingSlash(entry.path);
|
|
785
|
+
const absolutePath = path.join(repoRoot, targetPath);
|
|
786
|
+
|
|
787
|
+
if (entry.isUntracked || (!baseCommit && entry.changeType === 'added')) {
|
|
788
|
+
const result = await runGit(
|
|
789
|
+
['diff', '--no-index', '--no-color', '--', '/dev/null', absolutePath],
|
|
790
|
+
repoRoot,
|
|
791
|
+
);
|
|
792
|
+
return normalizeNoIndexPatchHeaders(result.stdout, targetPath);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const diffArgs = ['diff', '--no-ext-diff', '--no-color', '--find-renames'];
|
|
796
|
+
if (baseCommit) diffArgs.push(baseCommit);
|
|
797
|
+
|
|
798
|
+
diffArgs.push('--', targetPath);
|
|
799
|
+
if (entry.previousPath && entry.previousPath !== entry.path) {
|
|
800
|
+
diffArgs.push(stripTrailingSlash(entry.previousPath));
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const result = await runGit(diffArgs, repoRoot);
|
|
804
|
+
return result.stdout;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
export async function readPatchForBranchEntry(
|
|
808
|
+
repoRoot: string,
|
|
809
|
+
comparisonRef: string,
|
|
810
|
+
entry: DirtyPathEntry,
|
|
811
|
+
): Promise<string> {
|
|
812
|
+
const targetPath = stripTrailingSlash(entry.path);
|
|
813
|
+
const diffArgs = [
|
|
814
|
+
'diff',
|
|
815
|
+
'--no-ext-diff',
|
|
816
|
+
'--no-color',
|
|
817
|
+
'--find-renames',
|
|
818
|
+
`${comparisonRef}...HEAD`,
|
|
819
|
+
'--',
|
|
820
|
+
targetPath,
|
|
821
|
+
];
|
|
822
|
+
|
|
823
|
+
if (entry.previousPath && entry.previousPath !== entry.path) {
|
|
824
|
+
diffArgs.push(stripTrailingSlash(entry.previousPath));
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const result = await runGit(diffArgs, repoRoot);
|
|
828
|
+
return result.stdout;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function countPatchChanges(patch: string) {
|
|
832
|
+
let additions = 0;
|
|
833
|
+
let deletions = 0;
|
|
834
|
+
|
|
835
|
+
for (const line of patch.split('\n')) {
|
|
836
|
+
if (line.startsWith('+++') || line.startsWith('---')) continue;
|
|
837
|
+
if (line.startsWith('+')) additions += 1;
|
|
838
|
+
if (line.startsWith('-')) deletions += 1;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
return { additions, deletions };
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
async function diffFileFromEntry(
|
|
845
|
+
repoRoot: string,
|
|
846
|
+
entry: DirtyPathEntry,
|
|
847
|
+
patch: string,
|
|
848
|
+
): Promise<WorkspaceDiffFile> {
|
|
849
|
+
const { additions, deletions } = countPatchChanges(patch);
|
|
850
|
+
const absolutePath = path.join(repoRoot, stripTrailingSlash(entry.path));
|
|
851
|
+
const exists = await fileExists(absolutePath);
|
|
852
|
+
const size = exists ? (await stat(absolutePath)).size : undefined;
|
|
853
|
+
|
|
854
|
+
return {
|
|
855
|
+
path: entry.path,
|
|
856
|
+
changeType: entry.changeType,
|
|
857
|
+
isUntracked: entry.isUntracked,
|
|
858
|
+
additions,
|
|
859
|
+
deletions,
|
|
860
|
+
patchDigest: hashPatch(patch),
|
|
861
|
+
mimeType: exists ? inferWorkspaceFileContentType(entry.path) : undefined,
|
|
862
|
+
size,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
export async function computeCurrentFiles(repoRoot: string, baseCommit: string | null) {
|
|
867
|
+
const dirtyPaths = await listDirtyPaths(repoRoot);
|
|
868
|
+
const files = await Promise.all(
|
|
869
|
+
dirtyPaths.map(async (entry) => {
|
|
870
|
+
const patch = await readPatchForEntry(repoRoot, baseCommit, entry);
|
|
871
|
+
return diffFileFromEntry(repoRoot, entry, patch);
|
|
872
|
+
}),
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
return files.sort((left, right) => left.path.localeCompare(right.path));
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
export async function computeBranchFiles(repoRoot: string, comparisonRef: string | null) {
|
|
879
|
+
if (!comparisonRef) return [];
|
|
880
|
+
|
|
881
|
+
const entries = await listBranchDiffPaths(repoRoot, comparisonRef);
|
|
882
|
+
const files = await Promise.all(
|
|
883
|
+
entries.map(async (entry) => {
|
|
884
|
+
const patch = await readPatchForBranchEntry(repoRoot, comparisonRef, entry);
|
|
885
|
+
return diffFileFromEntry(repoRoot, entry, patch);
|
|
886
|
+
}),
|
|
887
|
+
);
|
|
888
|
+
|
|
889
|
+
return files.sort((left, right) => left.path.localeCompare(right.path));
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
export async function getBranchHistory(args: {
|
|
893
|
+
repoRoot: string;
|
|
894
|
+
ref: string;
|
|
895
|
+
limit: number;
|
|
896
|
+
}): Promise<WorkspaceBranchHistorySnapshot> {
|
|
897
|
+
const remoteUrl = await getOriginRemoteUrl(args.repoRoot);
|
|
898
|
+
const repoSlug = extractGitHubRepoSlug(remoteUrl);
|
|
899
|
+
const result = await runGit(
|
|
900
|
+
[
|
|
901
|
+
'log',
|
|
902
|
+
args.ref,
|
|
903
|
+
`--max-count=${args.limit}`,
|
|
904
|
+
'--date=iso-strict',
|
|
905
|
+
'--decorate=full',
|
|
906
|
+
'--format=%H%x00%s%x00%b%x00%an%x00%aI%x00%D%x00',
|
|
907
|
+
],
|
|
908
|
+
args.repoRoot,
|
|
909
|
+
);
|
|
910
|
+
|
|
911
|
+
if (result.exitCode !== 0) return { entries: [] };
|
|
912
|
+
|
|
913
|
+
const fields = result.stdout.split('\0');
|
|
914
|
+
const entries: WorkspaceBranchHistoryEntry[] = [];
|
|
915
|
+
for (let index = 0; index + 5 < fields.length; index += 6) {
|
|
916
|
+
const [
|
|
917
|
+
rawSha = '',
|
|
918
|
+
summary = '',
|
|
919
|
+
description = '',
|
|
920
|
+
authorName = '',
|
|
921
|
+
authoredAt = '',
|
|
922
|
+
refs = '',
|
|
923
|
+
] = fields.slice(index, index + 6);
|
|
924
|
+
const sha = rawSha.trim();
|
|
925
|
+
if (!sha.trim()) continue;
|
|
926
|
+
|
|
927
|
+
const tags = refs
|
|
928
|
+
.split(',')
|
|
929
|
+
.map((ref) => ref.trim())
|
|
930
|
+
.flatMap((ref) => {
|
|
931
|
+
if (!ref.startsWith('tag: ')) return [];
|
|
932
|
+
return [ref.slice('tag: '.length).replace(/^refs\/tags\//u, '')];
|
|
933
|
+
})
|
|
934
|
+
.filter(Boolean);
|
|
935
|
+
|
|
936
|
+
entries.push({
|
|
937
|
+
sha,
|
|
938
|
+
summary,
|
|
939
|
+
description: description.trim(),
|
|
940
|
+
authorName: authorName || undefined,
|
|
941
|
+
authoredAt,
|
|
942
|
+
tags,
|
|
943
|
+
githubUrl: repoSlug ? `https://github.com/${repoSlug}/commit/${sha}` : undefined,
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
return { entries };
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
export async function discardRenamedPath(repoRoot: string, entry: DirtyPathEntry) {
|
|
950
|
+
const currentPath = stripTrailingSlash(entry.path);
|
|
951
|
+
const previousPath = stripTrailingSlash(entry.previousPath ?? '');
|
|
952
|
+
const restoreResult = await runGit(
|
|
953
|
+
['restore', '--staged', '--worktree', '--source=HEAD', '--', previousPath],
|
|
954
|
+
repoRoot,
|
|
955
|
+
);
|
|
956
|
+
|
|
957
|
+
if (restoreResult.exitCode !== 0) {
|
|
958
|
+
throw new Error(formatGitFailure(restoreResult) || 'Failed to restore renamed file');
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
await rm(path.join(repoRoot, currentPath), { recursive: true, force: true });
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
async function discardAddedPath(repoRoot: string, hasCommit: boolean, relativePath: string) {
|
|
965
|
+
const result = hasCommit
|
|
966
|
+
? await runGit(['reset', 'HEAD', '--', relativePath], repoRoot)
|
|
967
|
+
: await runGit(['rm', '--cached', '--ignore-unmatch', '--', relativePath], repoRoot);
|
|
968
|
+
|
|
969
|
+
if (result.exitCode !== 0) {
|
|
970
|
+
throw new Error(formatGitFailure(result) || 'Failed to unstage added file');
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
export function sanitizeRepoName(value: string) {
|
|
975
|
+
return value
|
|
976
|
+
.trim()
|
|
977
|
+
.toLowerCase()
|
|
978
|
+
.replace(/[^a-z0-9._-]+/gu, '-')
|
|
979
|
+
.replace(/^-+|-+$/gu, '');
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
interface GhAuthInfo {
|
|
983
|
+
ghInstalled: boolean;
|
|
984
|
+
authenticated: boolean;
|
|
985
|
+
activeAccountLogin?: string;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
async function getGhAuthInfo(): Promise<GhAuthInfo> {
|
|
989
|
+
if (!Bun.which('gh')) return { ghInstalled: false, authenticated: false };
|
|
990
|
+
|
|
991
|
+
const result = await runCommand(['gh', 'api', 'user']);
|
|
992
|
+
if (result.exitCode !== 0) return { ghInstalled: true, authenticated: false };
|
|
993
|
+
|
|
994
|
+
try {
|
|
995
|
+
const parsed = JSON.parse(result.stdout) as { login?: string };
|
|
996
|
+
return { ghInstalled: true, authenticated: true, activeAccountLogin: parsed.login };
|
|
997
|
+
} catch {
|
|
998
|
+
return { ghInstalled: true, authenticated: true };
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
async function getGitHubOwners() {
|
|
1003
|
+
const userResult = await runCommand(['gh', 'api', 'user']);
|
|
1004
|
+
const orgsResult = await runCommand(['gh', 'api', 'user/orgs']);
|
|
1005
|
+
const owners: string[] = [];
|
|
1006
|
+
|
|
1007
|
+
try {
|
|
1008
|
+
const user = JSON.parse(userResult.stdout) as { login?: string };
|
|
1009
|
+
if (user.login) owners.push(user.login);
|
|
1010
|
+
} catch {}
|
|
1011
|
+
|
|
1012
|
+
try {
|
|
1013
|
+
const orgs = JSON.parse(orgsResult.stdout) as Array<{ login?: string }>;
|
|
1014
|
+
for (const org of orgs) {
|
|
1015
|
+
if (org.login) owners.push(org.login);
|
|
1016
|
+
}
|
|
1017
|
+
} catch {}
|
|
1018
|
+
|
|
1019
|
+
return [...new Set(owners)];
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
export function appendGitIgnoreEntry(currentContents: string | null, entry: string) {
|
|
1023
|
+
const normalizedEntry = normalizeRepoRelativePath(entry);
|
|
1024
|
+
const normalizedEntryWithoutSlash = stripTrailingSlash(normalizedEntry);
|
|
1025
|
+
const current = currentContents ?? '';
|
|
1026
|
+
const currentLines = current
|
|
1027
|
+
.split(/\r?\n/u)
|
|
1028
|
+
.map((line) => line.trim())
|
|
1029
|
+
.filter(Boolean);
|
|
1030
|
+
|
|
1031
|
+
if (
|
|
1032
|
+
currentLines.some((line) => {
|
|
1033
|
+
if (line === normalizedEntry) return true;
|
|
1034
|
+
if (line.startsWith('#') || line.startsWith('!')) return false;
|
|
1035
|
+
if (stripTrailingSlash(line) === normalizedEntryWithoutSlash) return true;
|
|
1036
|
+
if (line.startsWith('*.') && normalizedEntry.endsWith(line.slice(1))) return true;
|
|
1037
|
+
return false;
|
|
1038
|
+
})
|
|
1039
|
+
) {
|
|
1040
|
+
return current.endsWith('\n') ? current : `${current}\n`;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const prefix = current.length === 0 || current.endsWith('\n') ? current : `${current}\n`;
|
|
1044
|
+
return `${prefix}${normalizedEntry}\n`;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
export class DiffStore {
|
|
1048
|
+
private readonly states = new Map<string, StoredWorkspaceGitState>();
|
|
1049
|
+
|
|
1050
|
+
// biome-ignore lint/complexity/noUselessConstructor: constructor kept for server wiring compatibility.
|
|
1051
|
+
constructor(_: string) {}
|
|
1052
|
+
|
|
1053
|
+
async initialize() {}
|
|
1054
|
+
|
|
1055
|
+
async initializeGit(args: {
|
|
1056
|
+
localPath: string;
|
|
1057
|
+
}): Promise<BranchActionSuccess | BranchActionFailure> {
|
|
1058
|
+
const { localPath } = args;
|
|
1059
|
+
const existingRepo = await resolveRepo(localPath);
|
|
1060
|
+
|
|
1061
|
+
if (existingRepo) {
|
|
1062
|
+
const initialCommitFailure = await ensureInitialCommit(existingRepo.repoRoot);
|
|
1063
|
+
if (initialCommitFailure) return initialCommitFailure;
|
|
1064
|
+
|
|
1065
|
+
return {
|
|
1066
|
+
ok: true,
|
|
1067
|
+
branchName: await getBranchName(existingRepo.repoRoot),
|
|
1068
|
+
snapshotChanged: false,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
let initResult = await runGit(['init', '-b', 'main'], localPath);
|
|
1073
|
+
if (initResult.exitCode !== 0) {
|
|
1074
|
+
initResult = await runGit(['init'], localPath);
|
|
1075
|
+
}
|
|
1076
|
+
if (initResult.exitCode !== 0) {
|
|
1077
|
+
return createBranchActionFailure(
|
|
1078
|
+
'Initialize git failed',
|
|
1079
|
+
formatGitFailure(initResult),
|
|
1080
|
+
'Git could not initialize this folder.',
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const repo = await resolveRepo(localPath);
|
|
1085
|
+
if (repo) {
|
|
1086
|
+
const initialCommitFailure = await ensureInitialCommit(repo.repoRoot);
|
|
1087
|
+
if (initialCommitFailure) return initialCommitFailure;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
return {
|
|
1091
|
+
ok: true,
|
|
1092
|
+
branchName: repo ? await getBranchName(repo.repoRoot) : undefined,
|
|
1093
|
+
snapshotChanged: false,
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
async getGitHubPublishInfo(args: { localPath: string }): Promise<GithubPublishInfo> {
|
|
1098
|
+
const { localPath } = args;
|
|
1099
|
+
const authInfo = await getGhAuthInfo();
|
|
1100
|
+
const suggestedRepoName = sanitizeRepoName(path.basename(localPath)) || 'my-repo';
|
|
1101
|
+
|
|
1102
|
+
if (!authInfo.ghInstalled || !authInfo.authenticated) {
|
|
1103
|
+
return {
|
|
1104
|
+
ghInstalled: authInfo.ghInstalled,
|
|
1105
|
+
authenticated: authInfo.authenticated,
|
|
1106
|
+
activeAccountLogin: authInfo.activeAccountLogin,
|
|
1107
|
+
owners: authInfo.activeAccountLogin ? [authInfo.activeAccountLogin] : [],
|
|
1108
|
+
suggestedRepoName,
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
return {
|
|
1113
|
+
ghInstalled: true,
|
|
1114
|
+
authenticated: true,
|
|
1115
|
+
activeAccountLogin: authInfo.activeAccountLogin,
|
|
1116
|
+
owners: await getGitHubOwners(),
|
|
1117
|
+
suggestedRepoName,
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
async checkGitHubRepoAvailability(args: {
|
|
1122
|
+
owner: string;
|
|
1123
|
+
name: string;
|
|
1124
|
+
}): Promise<GitHubRepoAvailabilityResult> {
|
|
1125
|
+
const authInfo = await getGhAuthInfo();
|
|
1126
|
+
if (!authInfo.ghInstalled) return { available: false, message: 'GitHub CLI is not installed.' };
|
|
1127
|
+
if (!authInfo.authenticated)
|
|
1128
|
+
return { available: false, message: 'GitHub CLI is not authenticated.' };
|
|
1129
|
+
|
|
1130
|
+
const owner = args.owner.trim();
|
|
1131
|
+
const name = sanitizeRepoName(args.name);
|
|
1132
|
+
if (!owner || !name)
|
|
1133
|
+
return { available: false, message: 'Enter an owner and repository name.' };
|
|
1134
|
+
|
|
1135
|
+
const result = await runCommand(['gh', 'api', `repos/${owner}/${name}`]);
|
|
1136
|
+
if (result.exitCode === 0)
|
|
1137
|
+
return { available: false, message: `${owner}/${name} already exists.` };
|
|
1138
|
+
|
|
1139
|
+
const detail = `${result.stderr}\n${result.stdout}`.toLowerCase();
|
|
1140
|
+
if (detail.includes('404'))
|
|
1141
|
+
return { available: true, message: `${owner}/${name} is available.` };
|
|
1142
|
+
|
|
1143
|
+
return { available: false, message: 'Could not verify repository availability.' };
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
async publishToGitHub(args: {
|
|
1147
|
+
localPath: string;
|
|
1148
|
+
owner: string;
|
|
1149
|
+
name: string;
|
|
1150
|
+
visibility: 'public' | 'private';
|
|
1151
|
+
description?: string;
|
|
1152
|
+
}): Promise<BranchActionSuccess | BranchActionFailure> {
|
|
1153
|
+
const { localPath } = args;
|
|
1154
|
+
const repo = await resolveRepo(localPath);
|
|
1155
|
+
if (!repo) {
|
|
1156
|
+
return {
|
|
1157
|
+
ok: false,
|
|
1158
|
+
title: 'Publish failed',
|
|
1159
|
+
message: 'Initialize git before publishing to GitHub.',
|
|
1160
|
+
snapshotChanged: false,
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const authInfo = await getGhAuthInfo();
|
|
1165
|
+
if (!authInfo.ghInstalled) {
|
|
1166
|
+
return {
|
|
1167
|
+
ok: false,
|
|
1168
|
+
title: 'GitHub CLI not installed',
|
|
1169
|
+
message: 'Install GitHub CLI (`gh`) to publish this repository.',
|
|
1170
|
+
snapshotChanged: false,
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
if (!authInfo.authenticated) {
|
|
1174
|
+
return {
|
|
1175
|
+
ok: false,
|
|
1176
|
+
title: 'GitHub CLI not signed in',
|
|
1177
|
+
message: 'Run `gh auth login` and try again.',
|
|
1178
|
+
snapshotChanged: false,
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const owner = args.owner.trim();
|
|
1183
|
+
const repoName = sanitizeRepoName(args.name);
|
|
1184
|
+
if (!owner || !repoName) {
|
|
1185
|
+
return {
|
|
1186
|
+
ok: false,
|
|
1187
|
+
title: 'Publish failed',
|
|
1188
|
+
message: 'Owner and repository name are required.',
|
|
1189
|
+
snapshotChanged: false,
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
const availability = await this.checkGitHubRepoAvailability({ owner, name: repoName });
|
|
1194
|
+
if (!availability.available) {
|
|
1195
|
+
return {
|
|
1196
|
+
ok: false,
|
|
1197
|
+
title: 'Publish failed',
|
|
1198
|
+
message: availability.message,
|
|
1199
|
+
snapshotChanged: false,
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const createArgs = [
|
|
1204
|
+
'gh',
|
|
1205
|
+
'repo',
|
|
1206
|
+
'create',
|
|
1207
|
+
`${owner}/${repoName}`,
|
|
1208
|
+
args.visibility === 'private' ? '--private' : '--public',
|
|
1209
|
+
'--source',
|
|
1210
|
+
localPath,
|
|
1211
|
+
'--remote',
|
|
1212
|
+
'origin',
|
|
1213
|
+
];
|
|
1214
|
+
|
|
1215
|
+
const initialCommitFailure = await ensureInitialCommit(repo.repoRoot);
|
|
1216
|
+
if (initialCommitFailure) return initialCommitFailure;
|
|
1217
|
+
|
|
1218
|
+
const branchName = await getBranchName(repo.repoRoot);
|
|
1219
|
+
if (branchName !== 'main' && branchName !== 'master') {
|
|
1220
|
+
return {
|
|
1221
|
+
ok: false,
|
|
1222
|
+
title: 'Publish failed',
|
|
1223
|
+
message: 'Switch to the main branch before publishing this repository.',
|
|
1224
|
+
detail: `Current branch is ${branchName ?? 'unknown'}.`,
|
|
1225
|
+
snapshotChanged: false,
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
createArgs.push('--push');
|
|
1230
|
+
if (args.description?.trim()) createArgs.push('--description', args.description.trim());
|
|
1231
|
+
|
|
1232
|
+
const createResult = await runCommand(createArgs);
|
|
1233
|
+
if (createResult.exitCode !== 0) {
|
|
1234
|
+
const detail = [createResult.stderr.trim(), createResult.stdout.trim()]
|
|
1235
|
+
.filter(Boolean)
|
|
1236
|
+
.join('\n');
|
|
1237
|
+
return {
|
|
1238
|
+
ok: false,
|
|
1239
|
+
title: 'Publish failed',
|
|
1240
|
+
message: summarizeGitFailure(detail, 'GitHub CLI could not publish this repository.'),
|
|
1241
|
+
detail,
|
|
1242
|
+
snapshotChanged: false,
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
return { ok: true, branchName: await getBranchName(repo.repoRoot), snapshotChanged: false };
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
getWorkspaceGitSnapshot(workspaceId: string): WorkspaceGitSnapshot {
|
|
1250
|
+
const state = this.states.get(workspaceId) ?? createEmptyState();
|
|
1251
|
+
return {
|
|
1252
|
+
status: state.status,
|
|
1253
|
+
branchName: state.branchName,
|
|
1254
|
+
defaultBranchName: state.defaultBranchName,
|
|
1255
|
+
hasOriginRemote: state.hasOriginRemote,
|
|
1256
|
+
originRepoSlug: state.originRepoSlug,
|
|
1257
|
+
hasUpstream: state.hasUpstream,
|
|
1258
|
+
aheadCount: state.aheadCount,
|
|
1259
|
+
behindCount: state.behindCount,
|
|
1260
|
+
lastFetchedAt: state.lastFetchedAt,
|
|
1261
|
+
files: [...state.files],
|
|
1262
|
+
pullRequestFiles: [...(state.pullRequestFiles ?? [])],
|
|
1263
|
+
hasPushedCommits: state.hasPushedCommits,
|
|
1264
|
+
branchPublishState: state.branchPublishState,
|
|
1265
|
+
mainAheadCount: state.mainAheadCount,
|
|
1266
|
+
branchHistory: {
|
|
1267
|
+
entries: state.branchHistory.entries.map((entry) => ({
|
|
1268
|
+
...entry,
|
|
1269
|
+
tags: [...entry.tags],
|
|
1270
|
+
})),
|
|
1271
|
+
},
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
async inspectGitHubBackedRepo(localPath: string): Promise<GitHubBackedRepoInspection> {
|
|
1276
|
+
const repo = await resolveRepo(localPath);
|
|
1277
|
+
if (!repo) {
|
|
1278
|
+
return { ok: false, message: 'Directory must be a git repository.' };
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
const originRemoteUrl = await getOriginRemoteUrl(repo.repoRoot);
|
|
1282
|
+
const originRepoSlug = extractGitHubRepoSlug(originRemoteUrl);
|
|
1283
|
+
const split = splitRepoSlug(originRepoSlug);
|
|
1284
|
+
|
|
1285
|
+
if (!split) {
|
|
1286
|
+
return {
|
|
1287
|
+
ok: false,
|
|
1288
|
+
repoRoot: repo.repoRoot,
|
|
1289
|
+
message: 'Directory must have a GitHub origin remote.',
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
return {
|
|
1294
|
+
ok: true,
|
|
1295
|
+
repoRoot: repo.repoRoot,
|
|
1296
|
+
branchName: await getBranchName(repo.repoRoot),
|
|
1297
|
+
defaultBranchName: await resolveDefaultBranchName(repo.repoRoot),
|
|
1298
|
+
githubOwner: split.owner,
|
|
1299
|
+
githubRepo: split.repo,
|
|
1300
|
+
originRepoSlug: `${split.owner}/${split.repo}`,
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
async readPatch(args: {
|
|
1305
|
+
workspacePath: string;
|
|
1306
|
+
path: string;
|
|
1307
|
+
}): Promise<WorkspaceDiffPatchResult> {
|
|
1308
|
+
const relativePath = normalizeRepoRelativePath(args.path);
|
|
1309
|
+
const repo = await resolveRepo(args.workspacePath);
|
|
1310
|
+
if (!repo) throw new Error('Workspace is not in a git repository');
|
|
1311
|
+
|
|
1312
|
+
const entry = await findDirtyPath(repo.repoRoot, relativePath);
|
|
1313
|
+
if (entry) {
|
|
1314
|
+
const patch = await readPatchForEntry(repo.repoRoot, repo.baseCommit, entry);
|
|
1315
|
+
return { path: entry.path, patch, patchDigest: hashPatch(patch) };
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
const [branchName, defaultBranchName] = await Promise.all([
|
|
1319
|
+
getBranchName(repo.repoRoot),
|
|
1320
|
+
resolveDefaultBranchName(repo.repoRoot),
|
|
1321
|
+
]);
|
|
1322
|
+
const comparisonRef = await resolveBranchComparisonRef({
|
|
1323
|
+
repoRoot: repo.repoRoot,
|
|
1324
|
+
branchName,
|
|
1325
|
+
defaultBranchName,
|
|
1326
|
+
});
|
|
1327
|
+
const branchEntry = comparisonRef
|
|
1328
|
+
? (await listBranchDiffPaths(repo.repoRoot, comparisonRef)).find(
|
|
1329
|
+
(candidate) => stripTrailingSlash(candidate.path) === stripTrailingSlash(relativePath),
|
|
1330
|
+
)
|
|
1331
|
+
: null;
|
|
1332
|
+
if (!branchEntry || !comparisonRef)
|
|
1333
|
+
throw new Error(`File is no longer changed: ${relativePath}`);
|
|
1334
|
+
|
|
1335
|
+
const patch = await readPatchForBranchEntry(repo.repoRoot, comparisonRef, branchEntry);
|
|
1336
|
+
return { path: branchEntry.path, patch, patchDigest: hashPatch(patch) };
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
async readFileContents(args: {
|
|
1340
|
+
workspaceId: string;
|
|
1341
|
+
workspacePath: string;
|
|
1342
|
+
path: string;
|
|
1343
|
+
}): Promise<WorkspaceFileContentsResult> {
|
|
1344
|
+
const relativePath = stripTrailingSlash(normalizeRepoRelativePath(args.path));
|
|
1345
|
+
const repo = await resolveRepo(args.workspacePath);
|
|
1346
|
+
if (!repo) throw new Error('Workspace is not in a git repository');
|
|
1347
|
+
|
|
1348
|
+
const filePath = await resolveWorkspaceFilePath(repo.repoRoot, relativePath).catch((error) => {
|
|
1349
|
+
if (error instanceof RepositoryPathEscapeError) throw error;
|
|
1350
|
+
throw new Error(`File does not exist: ${relativePath}`);
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
return previewFileAtPath({
|
|
1354
|
+
filePath,
|
|
1355
|
+
displayPath: relativePath,
|
|
1356
|
+
contentUrl: (metadataDigest) =>
|
|
1357
|
+
`/api/workspaces/${encodeURIComponent(args.workspaceId)}/files/${encodeURIComponent(relativePath)}/content?v=${encodeURIComponent(metadataDigest)}`,
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
async readExternalFileContents(args: { path: string }): Promise<WorkspaceFileContentsResult> {
|
|
1362
|
+
const requestedPath = args.path;
|
|
1363
|
+
if (!path.isAbsolute(requestedPath)) throw new Error('External file path must be absolute.');
|
|
1364
|
+
|
|
1365
|
+
let filePath: string;
|
|
1366
|
+
try {
|
|
1367
|
+
filePath = await realpath(requestedPath);
|
|
1368
|
+
} catch {
|
|
1369
|
+
throw new Error(`File does not exist: ${requestedPath}`);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
return previewFileAtPath({
|
|
1373
|
+
filePath,
|
|
1374
|
+
displayPath: filePath,
|
|
1375
|
+
contentUrl: (metadataDigest) => {
|
|
1376
|
+
const token = registerExternalFileAccess(filePath);
|
|
1377
|
+
return `/api/external-files/content?token=${encodeURIComponent(token)}&v=${encodeURIComponent(metadataDigest)}`;
|
|
1378
|
+
},
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
async refreshWorkspaceGitSnapshot(workspaceId: string, workspacePath: string) {
|
|
1383
|
+
const repo = await resolveRepo(workspacePath);
|
|
1384
|
+
if (!repo) {
|
|
1385
|
+
const nextState = {
|
|
1386
|
+
status: 'no_repo',
|
|
1387
|
+
branchName: undefined,
|
|
1388
|
+
defaultBranchName: undefined,
|
|
1389
|
+
hasOriginRemote: undefined,
|
|
1390
|
+
originRepoSlug: undefined,
|
|
1391
|
+
hasUpstream: undefined,
|
|
1392
|
+
aheadCount: undefined,
|
|
1393
|
+
behindCount: undefined,
|
|
1394
|
+
lastFetchedAt: undefined,
|
|
1395
|
+
files: [],
|
|
1396
|
+
pullRequestFiles: [],
|
|
1397
|
+
hasPushedCommits: undefined,
|
|
1398
|
+
branchPublishState: 'unknown',
|
|
1399
|
+
mainAheadCount: undefined,
|
|
1400
|
+
branchHistory: { entries: [] },
|
|
1401
|
+
} satisfies StoredWorkspaceGitState;
|
|
1402
|
+
|
|
1403
|
+
const changed = !snapshotsEqual(this.states.get(workspaceId), nextState);
|
|
1404
|
+
this.states.set(workspaceId, nextState);
|
|
1405
|
+
return changed;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
const [files, branchName, defaultBranchName, originRemoteUrl, hasUpstream, lastFetchedAt] =
|
|
1409
|
+
await Promise.all([
|
|
1410
|
+
computeCurrentFiles(repo.repoRoot, repo.baseCommit),
|
|
1411
|
+
getBranchName(repo.repoRoot),
|
|
1412
|
+
resolveDefaultBranchName(repo.repoRoot),
|
|
1413
|
+
getOriginRemoteUrl(repo.repoRoot),
|
|
1414
|
+
hasUpstreamBranch(repo.repoRoot),
|
|
1415
|
+
getLastFetchedAt(repo.repoRoot),
|
|
1416
|
+
]);
|
|
1417
|
+
const comparisonRef = await resolveBranchComparisonRef({
|
|
1418
|
+
repoRoot: repo.repoRoot,
|
|
1419
|
+
branchName,
|
|
1420
|
+
defaultBranchName,
|
|
1421
|
+
});
|
|
1422
|
+
const pullRequestFiles = await computeBranchFiles(repo.repoRoot, comparisonRef);
|
|
1423
|
+
|
|
1424
|
+
const originRepoSlug = extractGitHubRepoSlug(originRemoteUrl) ?? undefined;
|
|
1425
|
+
const { aheadCount, behindCount } = hasUpstream
|
|
1426
|
+
? await getUpstreamStatusCounts(repo.repoRoot)
|
|
1427
|
+
: { aheadCount: undefined, behindCount: undefined };
|
|
1428
|
+
const pushedCommits = await hasPushedCommits({
|
|
1429
|
+
repoRoot: repo.repoRoot,
|
|
1430
|
+
branchName,
|
|
1431
|
+
defaultBranchName,
|
|
1432
|
+
});
|
|
1433
|
+
const branchPublishState = await getBranchPublishState({
|
|
1434
|
+
repoRoot: repo.repoRoot,
|
|
1435
|
+
branchName,
|
|
1436
|
+
hasUpstream,
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
const mainAheadCount = await getMainAheadCount({ repoRoot: repo.repoRoot, defaultBranchName });
|
|
1440
|
+
const branchHistory = repo.baseCommit
|
|
1441
|
+
? await getBranchHistory({ repoRoot: repo.repoRoot, ref: branchName ?? 'HEAD', limit: 20 })
|
|
1442
|
+
: { entries: [] };
|
|
1443
|
+
|
|
1444
|
+
const nextState = {
|
|
1445
|
+
status: 'ready',
|
|
1446
|
+
branchName,
|
|
1447
|
+
defaultBranchName,
|
|
1448
|
+
hasOriginRemote: originRemoteUrl !== null,
|
|
1449
|
+
originRepoSlug,
|
|
1450
|
+
hasUpstream,
|
|
1451
|
+
aheadCount,
|
|
1452
|
+
behindCount,
|
|
1453
|
+
lastFetchedAt,
|
|
1454
|
+
files,
|
|
1455
|
+
pullRequestFiles,
|
|
1456
|
+
hasPushedCommits: pushedCommits,
|
|
1457
|
+
branchPublishState,
|
|
1458
|
+
mainAheadCount,
|
|
1459
|
+
branchHistory,
|
|
1460
|
+
} satisfies StoredWorkspaceGitState;
|
|
1461
|
+
|
|
1462
|
+
const changed = !snapshotsEqual(this.states.get(workspaceId), nextState);
|
|
1463
|
+
this.states.set(workspaceId, nextState);
|
|
1464
|
+
return changed;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
async fetchWorkspaceGit(args: {
|
|
1468
|
+
workspaceId: string;
|
|
1469
|
+
workspacePath: string;
|
|
1470
|
+
}): Promise<BranchActionSuccess | BranchActionFailure> {
|
|
1471
|
+
const repo = await resolveRepo(args.workspacePath);
|
|
1472
|
+
if (!repo) {
|
|
1473
|
+
throw new Error('Workspace is not in a git repository');
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
const fetchResult = await runGit(['fetch', '--all', '--prune'], repo.repoRoot);
|
|
1477
|
+
if (fetchResult.exitCode !== 0) {
|
|
1478
|
+
return createBranchActionFailure(
|
|
1479
|
+
'Fetch failed',
|
|
1480
|
+
formatGitFailure(fetchResult),
|
|
1481
|
+
'Git could not fetch the latest remote changes.',
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
const snapshotChanged = await this.refreshWorkspaceGitSnapshot(
|
|
1486
|
+
args.workspaceId,
|
|
1487
|
+
args.workspacePath,
|
|
1488
|
+
);
|
|
1489
|
+
|
|
1490
|
+
return {
|
|
1491
|
+
ok: true,
|
|
1492
|
+
branchName: await getBranchName(repo.repoRoot),
|
|
1493
|
+
snapshotChanged,
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
async discardFile(args: { workspaceId: string; workspacePath: string; path: string }) {
|
|
1498
|
+
const relativePath = normalizeRepoRelativePath(args.path);
|
|
1499
|
+
const repo = await resolveRepo(args.workspacePath);
|
|
1500
|
+
if (!repo) throw new Error('Workspace is not in a git repository');
|
|
1501
|
+
|
|
1502
|
+
const entry = await findDirtyPath(repo.repoRoot, relativePath);
|
|
1503
|
+
if (!entry) throw new Error(`File is no longer changed: ${relativePath}`);
|
|
1504
|
+
|
|
1505
|
+
if (entry.isUntracked) {
|
|
1506
|
+
await rm(path.join(repo.repoRoot, stripTrailingSlash(entry.path)), {
|
|
1507
|
+
recursive: true,
|
|
1508
|
+
force: true,
|
|
1509
|
+
});
|
|
1510
|
+
} else if (entry.changeType === 'added') {
|
|
1511
|
+
await discardAddedPath(
|
|
1512
|
+
repo.repoRoot,
|
|
1513
|
+
repo.baseCommit !== null,
|
|
1514
|
+
stripTrailingSlash(entry.path),
|
|
1515
|
+
);
|
|
1516
|
+
await rm(path.join(repo.repoRoot, stripTrailingSlash(entry.path)), {
|
|
1517
|
+
recursive: true,
|
|
1518
|
+
force: true,
|
|
1519
|
+
});
|
|
1520
|
+
} else if (entry.changeType === 'renamed') {
|
|
1521
|
+
if (!repo.baseCommit) {
|
|
1522
|
+
throw new Error('Cannot discard a rename before the repository has an initial commit');
|
|
1523
|
+
}
|
|
1524
|
+
await discardRenamedPath(repo.repoRoot, entry);
|
|
1525
|
+
} else {
|
|
1526
|
+
if (!repo.baseCommit) {
|
|
1527
|
+
throw new Error(
|
|
1528
|
+
'Cannot discard tracked changes before the repository has an initial commit',
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
const restoreResult = await runGit(
|
|
1533
|
+
[
|
|
1534
|
+
'restore',
|
|
1535
|
+
'--staged',
|
|
1536
|
+
'--worktree',
|
|
1537
|
+
'--source=HEAD',
|
|
1538
|
+
'--',
|
|
1539
|
+
stripTrailingSlash(entry.path),
|
|
1540
|
+
],
|
|
1541
|
+
repo.repoRoot,
|
|
1542
|
+
);
|
|
1543
|
+
|
|
1544
|
+
if (restoreResult.exitCode !== 0) {
|
|
1545
|
+
throw new Error(formatGitFailure(restoreResult) || 'Failed to discard file changes');
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
return {
|
|
1550
|
+
snapshotChanged: await this.refreshWorkspaceGitSnapshot(args.workspaceId, args.workspacePath),
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
async ignoreFile(args: { workspaceId: string; workspacePath: string; path: string }) {
|
|
1555
|
+
const ignoreEntry = normalizeRepoRelativePath(args.path);
|
|
1556
|
+
const repo = await resolveRepo(args.workspacePath);
|
|
1557
|
+
if (!repo) throw new Error('Workspace is not in a git repository');
|
|
1558
|
+
|
|
1559
|
+
const dirtyPaths = await listDirtyPaths(repo.repoRoot);
|
|
1560
|
+
const exactEntry = dirtyPaths.find((candidate) => candidate.path === ignoreEntry);
|
|
1561
|
+
if (exactEntry && !exactEntry.isUntracked) {
|
|
1562
|
+
throw new Error('Only untracked files can be ignored from the diff viewer');
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
const ignoreDescendantPrefix = ignoreEntry.endsWith('/') ? ignoreEntry : `${ignoreEntry}/`;
|
|
1566
|
+
const entry = dirtyPaths.find(
|
|
1567
|
+
(candidate) =>
|
|
1568
|
+
candidate.isUntracked &&
|
|
1569
|
+
(candidate.path === ignoreEntry || candidate.path.startsWith(ignoreDescendantPrefix)),
|
|
1570
|
+
);
|
|
1571
|
+
|
|
1572
|
+
if (!entry) throw new Error(`File is no longer changed: ${ignoreEntry}`);
|
|
1573
|
+
|
|
1574
|
+
const gitignorePath = path.join(repo.repoRoot, '.gitignore');
|
|
1575
|
+
const currentContents = await Bun.file(gitignorePath)
|
|
1576
|
+
.text()
|
|
1577
|
+
.catch(() => null);
|
|
1578
|
+
const nextContents = appendGitIgnoreEntry(currentContents, ignoreEntry);
|
|
1579
|
+
if (nextContents !== currentContents) {
|
|
1580
|
+
await Bun.write(gitignorePath, nextContents);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
return {
|
|
1584
|
+
snapshotChanged: await this.refreshWorkspaceGitSnapshot(args.workspaceId, args.workspacePath),
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
}
|