@sanurb/ringi 0.2.0 → 0.3.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/dist/cli.mjs +2132 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/mcp.mjs +1057 -0
- package/dist/mcp.mjs.map +1 -0
- package/dist/runtime.mjs +3116 -0
- package/dist/runtime.mjs.map +1 -0
- package/package.json +15 -14
- package/server/nitro.json +17 -0
- package/server/public/assets/ClientOnly-QdfAxyFs.js +1 -0
- package/server/public/assets/_reviewId-CmXHvWLn.js +1 -0
- package/server/public/assets/_reviewId-DdOpDx4U.js +1 -0
- package/server/public/assets/abap-B1dkBSPn.js +1 -0
- package/server/public/assets/action-bar-DLRNvLjj.js +45 -0
- package/server/public/assets/actionscript-3-BT4ibYlP.js +1 -0
- package/server/public/assets/ada-CD92zeps.js +1 -0
- package/server/public/assets/andromeeda-DqSmgxi0.js +1 -0
- package/server/public/assets/angular-html-BDC0PfKr.js +1 -0
- package/server/public/assets/angular-ts-B9yoQMtj.js +1 -0
- package/server/public/assets/apache-D5suuoa_.js +1 -0
- package/server/public/assets/apex-BL-m4VHy.js +1 -0
- package/server/public/assets/apl-CldhY0Pn.js +1 -0
- package/server/public/assets/applescript-CLiBqvKT.js +1 -0
- package/server/public/assets/ara-LdDF8cmv.js +1 -0
- package/server/public/assets/asciidoc-2DZ9hC2N.js +1 -0
- package/server/public/assets/asm-0ZPGRSUy.js +1 -0
- package/server/public/assets/astro-DR6labZJ.js +1 -0
- package/server/public/assets/aurora-x-Da7Zfvbg.js +1 -0
- package/server/public/assets/awk-Bn0gn_B_.js +1 -0
- package/server/public/assets/ayu-dark-bqYKoqpM.js +1 -0
- package/server/public/assets/ayu-light-C45jTIzZ.js +1 -0
- package/server/public/assets/ayu-mirage-BV_FCTQi.js +1 -0
- package/server/public/assets/ballerina-D2nw_w8Q.js +1 -0
- package/server/public/assets/bat-ByUBN5gS.js +1 -0
- package/server/public/assets/beancount-BTb2W6Mp.js +1 -0
- package/server/public/assets/berry-5SO2uITG.js +1 -0
- package/server/public/assets/bibtex-CDBTNfUI.js +1 -0
- package/server/public/assets/bicep-fgxG_4rP.js +1 -0
- package/server/public/assets/bird2-BCwzDhwX.js +1 -0
- package/server/public/assets/blade-BBTRu2-g.js +1 -0
- package/server/public/assets/bsl-CG-fq4sc.js +1 -0
- package/server/public/assets/c-CEvNj7xl.js +1 -0
- package/server/public/assets/c3-Dlaci63_.js +1 -0
- package/server/public/assets/cadence-DHbRuEmm.js +1 -0
- package/server/public/assets/cairo-Ds0kTeYT.js +1 -0
- package/server/public/assets/catppuccin-frappe-DrL1fUuH.js +1 -0
- package/server/public/assets/catppuccin-latte-C0REgVjl.js +1 -0
- package/server/public/assets/catppuccin-macchiato-ChQpylWO.js +1 -0
- package/server/public/assets/catppuccin-mocha-Dd0JU1T0.js +1 -0
- package/server/public/assets/clarity-DMoTOm4G.js +1 -0
- package/server/public/assets/clojure-DBhE3PpS.js +1 -0
- package/server/public/assets/cmake-DwMc40Or.js +1 -0
- package/server/public/assets/cobol-BdNJSMRt.js +1 -0
- package/server/public/assets/codeowners-1lVr8wqV.js +1 -0
- package/server/public/assets/codeql-DWflolvo.js +1 -0
- package/server/public/assets/coffee-RA4xA24H.js +1 -0
- package/server/public/assets/common-lisp-CrVQ5xT-.js +1 -0
- package/server/public/assets/compiler-runtime-DZXZ41-q.js +1 -0
- package/server/public/assets/coq-CjfoyYSh.js +1 -0
- package/server/public/assets/cpp-BoW7e2Ow.js +1 -0
- package/server/public/assets/createServerFn-DTk395iP.js +9 -0
- package/server/public/assets/crystal-CYKRo3F9.js +1 -0
- package/server/public/assets/csharp-C7bIWP5y.js +1 -0
- package/server/public/assets/css-Ck2tii2d.js +1 -0
- package/server/public/assets/csv-DsAkDVtA.js +1 -0
- package/server/public/assets/cue-BWmQgbOB.js +1 -0
- package/server/public/assets/cypher-D-jVC50Q.js +1 -0
- package/server/public/assets/d-CaviyOrm.js +1 -0
- package/server/public/assets/dark-plus-DIrnwZt9.js +1 -0
- package/server/public/assets/dart-CZEi7JgC.js +1 -0
- package/server/public/assets/dax-BK-8zffy.js +1 -0
- package/server/public/assets/desktop-D3cjbL4D.js +1 -0
- package/server/public/assets/diff-sHAzLvlp.js +1 -0
- package/server/public/assets/docker--xs2Ng3w.js +1 -0
- package/server/public/assets/dotenv-Cm4nwcJ7.js +1 -0
- package/server/public/assets/dracula-CAUSusef.js +1 -0
- package/server/public/assets/dracula-soft-cjNkMFza.js +1 -0
- package/server/public/assets/dream-maker-fjmWTFCO.js +1 -0
- package/server/public/assets/edge-DxycC9wl.js +1 -0
- package/server/public/assets/elixir-B-50Er3p.js +1 -0
- package/server/public/assets/elm-B4-ygIVo.js +1 -0
- package/server/public/assets/emacs-lisp-CJzqStIa.js +1 -0
- package/server/public/assets/erb-DJvYE1L1.js +1 -0
- package/server/public/assets/erlang-C-m_88FN.js +1 -0
- package/server/public/assets/everforest-dark-DBpaSMx1.js +1 -0
- package/server/public/assets/everforest-light-CiGrXwia.js +1 -0
- package/server/public/assets/fennel-DRaXF7k8.js +1 -0
- package/server/public/assets/file-tree-CI3Xwwid.js +1907 -0
- package/server/public/assets/fish-Bn-Yh3Jj.js +1 -0
- package/server/public/assets/fluent-DF5F8Ks_.js +1 -0
- package/server/public/assets/fortran-fixed-form-Cx1lv7HN.js +1 -0
- package/server/public/assets/fortran-free-form-hCQHRqew.js +1 -0
- package/server/public/assets/fsharp-DC5k9sy2.js +1 -0
- package/server/public/assets/gdresource-D0EsKdgH.js +1 -0
- package/server/public/assets/gdscript-_C9_Hi_w.js +1 -0
- package/server/public/assets/gdshader-BW7b1X1Y.js +1 -0
- package/server/public/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
- package/server/public/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
- package/server/public/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
- package/server/public/assets/genie-Ch_6TCHd.js +1 -0
- package/server/public/assets/gherkin-CaNUsmTq.js +1 -0
- package/server/public/assets/git-commit-BcFsuO5E.js +1 -0
- package/server/public/assets/git-rebase-ChGA-z50.js +1 -0
- package/server/public/assets/github-dark-B9ygjgg6.js +1 -0
- package/server/public/assets/github-dark-default-Br2bgYSx.js +1 -0
- package/server/public/assets/github-dark-dimmed-CmtqpPJ-.js +1 -0
- package/server/public/assets/github-dark-high-contrast-fSfmrZcC.js +1 -0
- package/server/public/assets/github-light-BZjUqfZl.js +1 -0
- package/server/public/assets/github-light-default-lIytXXhR.js +1 -0
- package/server/public/assets/github-light-high-contrast-BRrjFb7n.js +1 -0
- package/server/public/assets/gleam-DALMDpNs.js +1 -0
- package/server/public/assets/glimmer-js-maLb6ysA.js +1 -0
- package/server/public/assets/glimmer-ts-DGNr-OBA.js +1 -0
- package/server/public/assets/glsl-CmplqyQ1.js +1 -0
- package/server/public/assets/gn-DGjqrYN9.js +1 -0
- package/server/public/assets/gnuplot-BYckvgQI.js +1 -0
- package/server/public/assets/go-JycvP538.js +1 -0
- package/server/public/assets/graphql-VhP7n4--.js +1 -0
- package/server/public/assets/groovy-D5qMRONT.js +1 -0
- package/server/public/assets/gruvbox-dark-hard-M1dj1e6V.js +1 -0
- package/server/public/assets/gruvbox-dark-medium-cqq_ncQu.js +1 -0
- package/server/public/assets/gruvbox-dark-soft-B4QwL2a9.js +1 -0
- package/server/public/assets/gruvbox-light-hard-DLayMKOQ.js +1 -0
- package/server/public/assets/gruvbox-light-medium-D52XgPKf.js +1 -0
- package/server/public/assets/gruvbox-light-soft-Dola3KdD.js +1 -0
- package/server/public/assets/hack-BVSQ2bxM.js +1 -0
- package/server/public/assets/haml-CwTtRHoj.js +1 -0
- package/server/public/assets/handlebars-CcO01SVo.js +1 -0
- package/server/public/assets/haskell-ys7wPPEd.js +1 -0
- package/server/public/assets/haxe-94kiChn7.js +1 -0
- package/server/public/assets/hcl-DmHt_-wq.js +1 -0
- package/server/public/assets/hjson-xMmoJ0Gx.js +1 -0
- package/server/public/assets/hlsl-b-Pskdze.js +1 -0
- package/server/public/assets/horizon-BKMqttiR.js +1 -0
- package/server/public/assets/horizon-bright-HNkBlnm5.js +1 -0
- package/server/public/assets/houston-BkBSBSOQ.js +1 -0
- package/server/public/assets/html-derivative-Cz-cKMi2.js +1 -0
- package/server/public/assets/html-zQbUS8Is.js +1 -0
- package/server/public/assets/http-CaGQ9BgA.js +1 -0
- package/server/public/assets/hurl-BBoki9bg.js +1 -0
- package/server/public/assets/hxml-iQTOTWpM.js +1 -0
- package/server/public/assets/hy-DKl1XhBq.js +1 -0
- package/server/public/assets/imba-DPxkOTAg.js +1 -0
- package/server/public/assets/ini-lkLGq_1x.js +1 -0
- package/server/public/assets/java-LAx6oszV.js +1 -0
- package/server/public/assets/javascript-COqx-gKX.js +1 -0
- package/server/public/assets/jinja-x-G_qSCP.js +1 -0
- package/server/public/assets/jison-7oSeVkKJ.js +1 -0
- package/server/public/assets/json-sTLOVXhc.js +1 -0
- package/server/public/assets/json5-Cy6ypJuJ.js +1 -0
- package/server/public/assets/jsonc-Cw2ugYAK.js +1 -0
- package/server/public/assets/jsonl-Dp5_qBVH.js +1 -0
- package/server/public/assets/jsonnet-BTbmg_-u.js +1 -0
- package/server/public/assets/jssm-CnT7nPea.js +1 -0
- package/server/public/assets/jsx-zXeIBQLI.js +1 -0
- package/server/public/assets/julia-E-6Xm9nd.js +1 -0
- package/server/public/assets/just-D9n74gZy.js +1 -0
- package/server/public/assets/kanagawa-dragon-CxsBnuhV.js +1 -0
- package/server/public/assets/kanagawa-lotus-vHdxDDOS.js +1 -0
- package/server/public/assets/kanagawa-wave-CIkfTKWk.js +1 -0
- package/server/public/assets/kdl-BwK60g80.js +1 -0
- package/server/public/assets/kotlin-Dbd9Vi-v.js +1 -0
- package/server/public/assets/kusto-BuTk9usc.js +1 -0
- package/server/public/assets/laserwave-C0wf_d3o.js +1 -0
- package/server/public/assets/latex-D0t4RtEU.js +1 -0
- package/server/public/assets/lean-CYAW8bRN.js +1 -0
- package/server/public/assets/less-D4uen21c.js +1 -0
- package/server/public/assets/light-plus-oqYyWKEE.js +1 -0
- package/server/public/assets/liquid-BzXN12F6.js +1 -0
- package/server/public/assets/llvm-Z1xJzteV.js +1 -0
- package/server/public/assets/log-BGUxlsk3.js +1 -0
- package/server/public/assets/logo-wVUhvQ1b.js +1 -0
- package/server/public/assets/lua-B0Cg8RP4.js +1 -0
- package/server/public/assets/luau-rPFZzCmq.js +1 -0
- package/server/public/assets/main-FvxVz-kD.js +15 -0
- package/server/public/assets/make-BmPf6m0P.js +1 -0
- package/server/public/assets/markdown-AseU6zcW.js +1 -0
- package/server/public/assets/marko-BlRPXWOe.js +1 -0
- package/server/public/assets/material-theme-6_W6rQhR.js +1 -0
- package/server/public/assets/material-theme-darker-VPEo3Sem.js +1 -0
- package/server/public/assets/material-theme-lighter-CUhzCcZ9.js +1 -0
- package/server/public/assets/material-theme-ocean-B2JdsaGb.js +1 -0
- package/server/public/assets/material-theme-palenight-DhY-sklA.js +1 -0
- package/server/public/assets/matlab-BOj_BDQv.js +1 -0
- package/server/public/assets/mdc-FiVDZSZ4.js +1 -0
- package/server/public/assets/mdx-Cm6cDkDI.js +1 -0
- package/server/public/assets/mermaid-DLO-R4hv.js +1 -0
- package/server/public/assets/min-dark-D34a_pX7.js +1 -0
- package/server/public/assets/min-light-Cdd4KORE.js +1 -0
- package/server/public/assets/mipsasm-DYpHF-GA.js +1 -0
- package/server/public/assets/mojo-DqYVFv_G.js +1 -0
- package/server/public/assets/monokai-CDR4sQ2n.js +1 -0
- package/server/public/assets/moonbit-DRKee9wk.js +1 -0
- package/server/public/assets/move-DbRk6Vn9.js +1 -0
- package/server/public/assets/narrat-IOfmaXfb.js +1 -0
- package/server/public/assets/new-DOyplRwM.js +1 -0
- package/server/public/assets/nextflow-D-Ec_bsY.js +1 -0
- package/server/public/assets/nextflow-groovy-EYl0c2BQ.js +1 -0
- package/server/public/assets/nginx-3JLAqmJa.js +1 -0
- package/server/public/assets/night-owl-light-Bedht9b4.js +1 -0
- package/server/public/assets/night-owl-yQJ3-I0I.js +1 -0
- package/server/public/assets/nim-DyjFVMzT.js +1 -0
- package/server/public/assets/nix-C2IovEl2.js +1 -0
- package/server/public/assets/nord-BjZ63GNL.js +1 -0
- package/server/public/assets/nushell-BflTrRB5.js +1 -0
- package/server/public/assets/objective-c-GRClK1S7.js +1 -0
- package/server/public/assets/objective-cpp-l3qYw-v5.js +1 -0
- package/server/public/assets/ocaml-BBDyhyMH.js +1 -0
- package/server/public/assets/odin-jCJ7Js99.js +1 -0
- package/server/public/assets/one-dark-pro-PIx2Diul.js +1 -0
- package/server/public/assets/one-light-BFMEz49S.js +1 -0
- package/server/public/assets/openscad-Drf0LgCX.js +1 -0
- package/server/public/assets/pascal-BT2XAUTl.js +1 -0
- package/server/public/assets/perl-Dr47G_2Q.js +1 -0
- package/server/public/assets/php-BhBDWTJe.js +1 -0
- package/server/public/assets/pierre-dark-CTXzTLfO.js +1 -0
- package/server/public/assets/pierre-light-C_5rlJRo.js +1 -0
- package/server/public/assets/pkl-ML-dWShO.js +1 -0
- package/server/public/assets/plastic-BFI-Z5Z2.js +1 -0
- package/server/public/assets/plsql-0vd5cLro.js +1 -0
- package/server/public/assets/po-CbZ_uqQA.js +1 -0
- package/server/public/assets/poimandres-Cayhd01L.js +1 -0
- package/server/public/assets/polar-C4hfV8Nc.js +1 -0
- package/server/public/assets/postcss-osFUbTLw.js +1 -0
- package/server/public/assets/powerquery-CTlGUQPj.js +1 -0
- package/server/public/assets/powershell-DyZsOmuq.js +1 -0
- package/server/public/assets/preload-helper-D7oT-Xwl.js +20 -0
- package/server/public/assets/prisma-SS92PO_I.js +1 -0
- package/server/public/assets/prolog-B1O1NNVC.js +1 -0
- package/server/public/assets/proto-BWu3eZTs.js +1 -0
- package/server/public/assets/pug-Dij_IK5w.js +1 -0
- package/server/public/assets/puppet-tvtRVdr6.js +1 -0
- package/server/public/assets/purescript-Dtbpb7D-.js +1 -0
- package/server/public/assets/python-Dlk0Acio.js +1 -0
- package/server/public/assets/qml-qUwk3nhh.js +1 -0
- package/server/public/assets/qmldir-B-iEOngH.js +1 -0
- package/server/public/assets/qss-Ba0p-aHw.js +1 -0
- package/server/public/assets/r-WmtNicKM.js +1 -0
- package/server/public/assets/racket-BXDsxf2U.js +1 -0
- package/server/public/assets/raku-Dw1WWFXK.js +1 -0
- package/server/public/assets/razor-DaqiVx3Q.js +1 -0
- package/server/public/assets/red-BC3Ds49b.js +1 -0
- package/server/public/assets/reg-DXFHGaM4.js +1 -0
- package/server/public/assets/regexp-CiSWN5Ne.js +1 -0
- package/server/public/assets/rel-Dc5_Ytx2.js +1 -0
- package/server/public/assets/reviews-CJvVXRLH.js +1 -0
- package/server/public/assets/reviews-CfbuF6ib.js +1 -0
- package/server/public/assets/riscv-ZgswiWij.js +1 -0
- package/server/public/assets/ron-YghabWAH.js +1 -0
- package/server/public/assets/rose-pine-ByWLnVr3.js +1 -0
- package/server/public/assets/rose-pine-dawn-DBmeySrz.js +1 -0
- package/server/public/assets/rose-pine-moon-B9J-N3nK.js +1 -0
- package/server/public/assets/rosmsg-DTKmAsVH.js +1 -0
- package/server/public/assets/routes-DNxq1Fba.js +1 -0
- package/server/public/assets/routes-Dp0ODZ55.js +2 -0
- package/server/public/assets/rst-CP6xOYlY.js +1 -0
- package/server/public/assets/ruby-BXYLc1CM.js +1 -0
- package/server/public/assets/rust-nfXwuE6F.js +1 -0
- package/server/public/assets/sas-CFdtZutF.js +1 -0
- package/server/public/assets/sass-CbRjkld3.js +1 -0
- package/server/public/assets/scala-BzD3eypx.js +1 -0
- package/server/public/assets/scheme-D4d1PV1y.js +1 -0
- package/server/public/assets/scss-D8KhdObH.js +1 -0
- package/server/public/assets/sdbl-Cf-Ydnvx.js +1 -0
- package/server/public/assets/shaderlab-DGohHMiF.js +1 -0
- package/server/public/assets/shellscript-BHdEbumI.js +1 -0
- package/server/public/assets/shellsession-Dh-bxrap.js +1 -0
- package/server/public/assets/slack-dark-MszIyPZ2.js +1 -0
- package/server/public/assets/slack-ochin-tQ3Q0gE9.js +1 -0
- package/server/public/assets/smalltalk-_uWoArwn.js +1 -0
- package/server/public/assets/snazzy-light-9sniMEk5.js +1 -0
- package/server/public/assets/solarized-dark-CdD0Hxzv.js +1 -0
- package/server/public/assets/solarized-light-C-nsEdqF.js +1 -0
- package/server/public/assets/solidity-D6uC-xwP.js +1 -0
- package/server/public/assets/soy-cDuODfbT.js +1 -0
- package/server/public/assets/sparql-BgU2QITA.js +1 -0
- package/server/public/assets/splunk-LQYHRu14.js +1 -0
- package/server/public/assets/sql-CKZpK620.js +1 -0
- package/server/public/assets/ssh-config-B7BUl8Rd.js +1 -0
- package/server/public/assets/stata-BLJTbKOO.js +1 -0
- package/server/public/assets/styles-UDowwF7S.css +2 -0
- package/server/public/assets/stylus-Byjxdx_q.js +1 -0
- package/server/public/assets/surrealql-C96KvYaj.js +1 -0
- package/server/public/assets/svelte-Qnbj2GWx.js +1 -0
- package/server/public/assets/swift-BexLlMrU.js +1 -0
- package/server/public/assets/synthwave-84-BxMBwQMS.js +1 -0
- package/server/public/assets/system-verilog-DVGwm0mw.js +1 -0
- package/server/public/assets/systemd-H2IT3-p5.js +1 -0
- package/server/public/assets/talonscript-mKZIGM8n.js +1 -0
- package/server/public/assets/tasl-B7he_Ugr.js +1 -0
- package/server/public/assets/tcl-5mT3RxHH.js +1 -0
- package/server/public/assets/templ-CQPDll3D.js +1 -0
- package/server/public/assets/terraform-BZP0GLsT.js +1 -0
- package/server/public/assets/test-D7JRfog1.js +1 -0
- package/server/public/assets/tex-97QNLoBJ.js +1 -0
- package/server/public/assets/tokyo-night-CTPVdZt9.js +1 -0
- package/server/public/assets/toml-CTFA98he.js +1 -0
- package/server/public/assets/ts-tags-B8zlXe2n.js +1 -0
- package/server/public/assets/tsv-BayJtYdY.js +1 -0
- package/server/public/assets/tsx-VqRU8NCz.js +1 -0
- package/server/public/assets/turtle-TVCBh_kY.js +1 -0
- package/server/public/assets/twig-FTTF8rVk.js +1 -0
- package/server/public/assets/typescript-CuX0hIVY.js +1 -0
- package/server/public/assets/typespec-BUvaJDLF.js +1 -0
- package/server/public/assets/typst-8NBaY7Ec.js +1 -0
- package/server/public/assets/useStore-M3H8PB1v.js +1 -0
- package/server/public/assets/utils-DElCu2hq.js +1 -0
- package/server/public/assets/v-VihyTigi.js +1 -0
- package/server/public/assets/vala-DyFAPyX6.js +1 -0
- package/server/public/assets/vb-Dg1Iqi4J.js +1 -0
- package/server/public/assets/verilog-D2Xc-vhD.js +1 -0
- package/server/public/assets/vesper-DJbtqYNr.js +1 -0
- package/server/public/assets/vhdl-CU3BVeE7.js +1 -0
- package/server/public/assets/viml-hG2shuOW.js +1 -0
- package/server/public/assets/vitesse-black-DbG2gsc0.js +1 -0
- package/server/public/assets/vitesse-dark-B6WV4xXH.js +1 -0
- package/server/public/assets/vitesse-light-DC1pdD02.js +1 -0
- package/server/public/assets/vue-DXwaEU0U.js +1 -0
- package/server/public/assets/vue-html-QD7AJ6JJ.js +1 -0
- package/server/public/assets/vue-vine-Bh2m1D2Z.js +1 -0
- package/server/public/assets/vyper-C1wojIuk.js +1 -0
- package/server/public/assets/wasm-C6Y0s02M.js +1 -0
- package/server/public/assets/wasm-qTvCOSHz.js +1 -0
- package/server/public/assets/wenyan-BG5vPQF0.js +1 -0
- package/server/public/assets/wgsl-DrVb-Cub.js +1 -0
- package/server/public/assets/wikitext-PRC4s8sH.js +1 -0
- package/server/public/assets/wit-ChW5qvg_.js +1 -0
- package/server/public/assets/wolfram-B8mKuZSQ.js +1 -0
- package/server/public/assets/xml-BK-rcb5a.js +1 -0
- package/server/public/assets/xsl-dt-d2R7p.js +1 -0
- package/server/public/assets/yaml-UiXU3hGj.js +1 -0
- package/server/public/assets/zenscript-C-jEPC9j.js +1 -0
- package/server/public/assets/zig-EbnRGjcz.js +1 -0
- package/server/public/favicon.ico +0 -0
- package/server/public/logo192.png +0 -0
- package/server/public/logo512.png +0 -0
- package/server/public/manifest.json +25 -0
- package/server/public/robots.txt +3 -0
- package/server/public/tanstack-circle-logo.png +0 -0
- package/server/public/tanstack-word-logo-white.svg +1 -0
- package/server/server/_chunks/ssr-renderer.mjs +15 -0
- package/server/server/_libs/@floating-ui/core+[...].mjs +698 -0
- package/server/server/_libs/@floating-ui/dom+[...].mjs +644 -0
- package/server/server/_libs/@floating-ui/react-dom+[...].mjs +839 -0
- package/server/server/_libs/@pierre/diffs+[...].mjs +18578 -0
- package/server/server/_libs/@radix-ui/react-arrow+[...].mjs +174 -0
- package/server/server/_libs/@radix-ui/react-collection+[...].mjs +162 -0
- package/server/server/_libs/@radix-ui/react-dialog+[...].mjs +1666 -0
- package/server/server/_libs/@radix-ui/react-popper+[...].mjs +289 -0
- package/server/server/_libs/@radix-ui/react-radio-group+[...].mjs +420 -0
- package/server/server/_libs/@radix-ui/react-select+[...].mjs +990 -0
- package/server/server/_libs/@tanstack/react-router+[...].mjs +14113 -0
- package/server/server/_libs/_.mjs +2 -0
- package/server/server/_libs/chokidar+readdirp.mjs +1599 -0
- package/server/server/_libs/class-variance-authority+clsx.mjs +69 -0
- package/server/server/_libs/effect+[...].mjs +34047 -0
- package/server/server/_libs/h3+rou3+srvx.mjs +1195 -0
- package/server/server/_libs/hookable.mjs +41 -0
- package/server/server/_libs/lucide-react.mjs +298 -0
- package/server/server/_libs/pierre__theme.mjs +2668 -0
- package/server/server/_libs/radix-ui__number.mjs +6 -0
- package/server/server/_libs/radix-ui__primitive.mjs +9 -0
- package/server/server/_libs/radix-ui__react-direction.mjs +11 -0
- package/server/server/_libs/shiki.mjs +16 -0
- package/server/server/_libs/shikijs__langs.mjs +1355 -0
- package/server/server/_libs/shikijs__themes.mjs +262 -0
- package/server/server/_libs/tailwind-merge.mjs +1962 -0
- package/server/server/_libs/tanstack__history.mjs +342 -0
- package/server/server/_libs/tanstack__router-core.mjs +6 -0
- package/server/server/_libs/ufo.mjs +64 -0
- package/server/server/_reviewId-AWnOGz5k.mjs +33 -0
- package/server/server/_reviewId-Com4yOlc.mjs +29 -0
- package/server/server/_reviewId-DAhmekJ2.mjs +277 -0
- package/server/server/_reviewId-p9mhYVwa.mjs +18 -0
- package/server/server/_runtime.mjs +35 -0
- package/server/server/_ssr/action-bar-C68xGnWW.mjs +592 -0
- package/server/server/_ssr/api-handler-CstW2n82.mjs +189 -0
- package/server/server/_ssr/client-runtime-BoPuAEoA.mjs +245 -0
- package/server/server/_ssr/createServerRpc--0mcGlWK.mjs +12 -0
- package/server/server/_ssr/createSsrRpc-AwdiLXmF.mjs +16 -0
- package/server/server/_ssr/domain-rpc-3Ds9DPr0.mjs +287 -0
- package/server/server/_ssr/file-tree-CQ5w2GHh.mjs +1951 -0
- package/server/server/_ssr/load-scoped-diff-NL2XAcdz.mjs +45 -0
- package/server/server/_ssr/new-BKl_G2Ks.mjs +37 -0
- package/server/server/_ssr/new-BREdMFAM.mjs +12 -0
- package/server/server/_ssr/new-DCz5eHkb.mjs +137 -0
- package/server/server/_ssr/reviews-BL5Nsgst.mjs +7 -0
- package/server/server/_ssr/reviews-BoaEgGKs.mjs +100 -0
- package/server/server/_ssr/reviews-C7_NIhY8.mjs +19 -0
- package/server/server/_ssr/reviews-Dd69YBDa.mjs +12 -0
- package/server/server/_ssr/router-DLxN8FOm.mjs +415 -0
- package/server/server/_ssr/routes-D25G8OuS.mjs +80 -0
- package/server/server/_ssr/routes-lz0AN75A.mjs +929 -0
- package/server/server/_ssr/runtime-D9IbnMlF.mjs +1401 -0
- package/server/server/_ssr/server-runtime-D99qpmma.mjs +12 -0
- package/server/server/_ssr/ssr.mjs +5318 -0
- package/server/server/_ssr/start-BIQfOZtj.mjs +4 -0
- package/server/server/_ssr/test-CQdMYlqa.mjs +6 -0
- package/server/server/_ssr/todo-m_uUvxca.mjs +88 -0
- package/server/server/_ssr/use-keyboard-shortcuts-D5b1Mxpq.mjs +25 -0
- package/server/server/_ssr/utils-BuOt9_LA.mjs +8 -0
- package/server/server/_tanstack-start-manifest_v-CnL10NRH.mjs +71 -0
- package/server/server/index.mjs +2615 -0
- package/server/server/node_modules/detect-libc/lib/detect-libc.js +313 -0
- package/server/server/node_modules/detect-libc/lib/elf.js +39 -0
- package/server/server/node_modules/detect-libc/lib/filesystem.js +51 -0
- package/server/server/node_modules/detect-libc/lib/process.js +24 -0
- package/server/server/node_modules/detect-libc/package.json +44 -0
- package/server/server/node_modules/msgpackr-extract/index.js +1 -0
- package/server/server/node_modules/msgpackr-extract/package.json +50 -0
- package/server/server/node_modules/node-gyp-build-optional-packages/index.js +6 -0
- package/server/server/node_modules/node-gyp-build-optional-packages/node-gyp-build.js +236 -0
- package/server/server/node_modules/node-gyp-build-optional-packages/package.json +32 -0
- package/server/server/node_modules/tslib/modules/index.js +70 -0
- package/server/server/node_modules/tslib/modules/package.json +3 -0
- package/server/server/node_modules/tslib/package.json +47 -0
- package/server/server/node_modules/tslib/tslib.js +484 -0
- package/server/server/package.json +12 -0
- package/dist/chunk-KMYSGMD3.js +0 -3526
- package/dist/chunk-KMYSGMD3.js.map +0 -1
- package/dist/cli.js +0 -1839
- package/dist/cli.js.map +0 -1
- package/dist/mcp.js +0 -1228
- package/dist/mcp.js.map +0 -1
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,2132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { a as ReviewService, c as ReviewFileRepo, d as parseDiff, f as CommentService, g as ReviewNotFound, i as TodoService, l as serializeHunks, m as TodoNotFound, n as GhService, o as GitService, r as ExportService, s as ReviewRepo, t as CoreLive, u as getDiffSummary } from "./runtime.mjs";
|
|
3
|
+
import { exec, execFileSync, fork } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import * as Schema from "effect/Schema";
|
|
7
|
+
import * as Result from "effect/Result";
|
|
8
|
+
import { writeFile } from "node:fs/promises";
|
|
9
|
+
import { ServiceMap } from "effect";
|
|
10
|
+
import * as Effect from "effect/Effect";
|
|
11
|
+
import * as Layer from "effect/Layer";
|
|
12
|
+
import * as Option from "effect/Option";
|
|
13
|
+
import * as ManagedRuntime from "effect/ManagedRuntime";
|
|
14
|
+
import * as ConfigProvider from "effect/ConfigProvider";
|
|
15
|
+
//#region ../../packages/core/src/services/pr-preflight.ts
|
|
16
|
+
var PreflightFailure = class extends Schema.TaggedErrorClass()("PreflightFailure", {
|
|
17
|
+
exitCode: Schema.Number,
|
|
18
|
+
message: Schema.String,
|
|
19
|
+
phase: Schema.String
|
|
20
|
+
}) {};
|
|
21
|
+
/**
|
|
22
|
+
* Extract repository name from a git remote URL.
|
|
23
|
+
*
|
|
24
|
+
* Handles:
|
|
25
|
+
* - SSH: `git@github.com:owner/repo.git`
|
|
26
|
+
* - HTTPS: `https://github.com/owner/repo.git`
|
|
27
|
+
* - HTTPS: `https://github.com/owner/repo`
|
|
28
|
+
*/
|
|
29
|
+
const extractRepoNameFromRemote = (remote) => {
|
|
30
|
+
return remote.match(/[/:]([^/]+?)(?:\.git)?$/)?.[1] ?? null;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Runs the strict fail-fast preflight sequence for a PR review:
|
|
34
|
+
*
|
|
35
|
+
* 1. Verify `gh` installed
|
|
36
|
+
* 2. Verify `gh` auth for target host
|
|
37
|
+
* 3. Verify local git repository
|
|
38
|
+
* 4. Check repository affinity (warn if mismatch, fail if no repo)
|
|
39
|
+
* 5. Fetch PR metadata and validate it has changed files
|
|
40
|
+
* 6. Fetch PR diff
|
|
41
|
+
*/
|
|
42
|
+
const runPreflight = Effect.fn("PrPreflight.run")(function* (target) {
|
|
43
|
+
const gh = yield* GhService;
|
|
44
|
+
const git = yield* GitService;
|
|
45
|
+
yield* gh.ensureInstalled.pipe(Effect.mapError((e) => new PreflightFailure({
|
|
46
|
+
exitCode: 1,
|
|
47
|
+
message: e.message,
|
|
48
|
+
phase: "gh_install"
|
|
49
|
+
})));
|
|
50
|
+
yield* gh.ensureAuthenticated(target.host).pipe(Effect.mapError((e) => new PreflightFailure({
|
|
51
|
+
exitCode: 5,
|
|
52
|
+
message: e.message,
|
|
53
|
+
phase: "gh_auth"
|
|
54
|
+
})));
|
|
55
|
+
const localRepoPath = yield* git.getRepositoryPath.pipe(Effect.mapError(() => new PreflightFailure({
|
|
56
|
+
exitCode: 4,
|
|
57
|
+
message: "Not inside a git repository. Navigate to a repo or use --repo.",
|
|
58
|
+
phase: "repo_discovery"
|
|
59
|
+
})));
|
|
60
|
+
const repoInfo = yield* git.getRepositoryInfo.pipe(Effect.mapError(() => new PreflightFailure({
|
|
61
|
+
exitCode: 1,
|
|
62
|
+
message: "Could not read repository info.",
|
|
63
|
+
phase: "repo_info"
|
|
64
|
+
})));
|
|
65
|
+
let affinityMatch = false;
|
|
66
|
+
let affinityWarning = null;
|
|
67
|
+
if (repoInfo.remote) if (extractRepoNameFromRemote(repoInfo.remote)?.toLowerCase() === target.repo.toLowerCase()) affinityMatch = true;
|
|
68
|
+
else affinityWarning = `PR is from ${target.nwoRef} but local remote points to ${repoInfo.remote}. Review will be stored in ${localRepoPath}/.ringi/`;
|
|
69
|
+
else affinityWarning = `Local repository has no remote configured. Review will be stored in ${localRepoPath}/.ringi/`;
|
|
70
|
+
const metadata = yield* gh.fetchPrMetadata(target).pipe(Effect.mapError((e) => new PreflightFailure({
|
|
71
|
+
exitCode: 1,
|
|
72
|
+
message: `PR not accessible: ${e.message}`,
|
|
73
|
+
phase: "pr_fetch"
|
|
74
|
+
})));
|
|
75
|
+
if (metadata.changedFiles === 0) return yield* new PreflightFailure({
|
|
76
|
+
exitCode: 1,
|
|
77
|
+
message: `PR #${target.prNumber} has no changed files.`,
|
|
78
|
+
phase: "pr_validation"
|
|
79
|
+
});
|
|
80
|
+
const diff = yield* gh.fetchPrDiff(target).pipe(Effect.mapError((e) => new PreflightFailure({
|
|
81
|
+
exitCode: 1,
|
|
82
|
+
message: `Failed to fetch PR diff: ${e.message}`,
|
|
83
|
+
phase: "diff_fetch"
|
|
84
|
+
})));
|
|
85
|
+
if (!diff.trim()) return yield* new PreflightFailure({
|
|
86
|
+
exitCode: 1,
|
|
87
|
+
message: `PR #${target.prNumber} returned an empty diff.`,
|
|
88
|
+
phase: "diff_fetch"
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
affinityMatch,
|
|
92
|
+
affinityWarning,
|
|
93
|
+
diff,
|
|
94
|
+
localRepoPath,
|
|
95
|
+
metadata,
|
|
96
|
+
target
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region ../../packages/core/src/services/pr-session.ts
|
|
101
|
+
var PrSessionError = class extends Schema.TaggedErrorClass()("PrSessionError", {
|
|
102
|
+
code: Schema.String,
|
|
103
|
+
message: Schema.String
|
|
104
|
+
}) {};
|
|
105
|
+
/** Canonical source_ref for a PR: `host/owner/repo#number`. */
|
|
106
|
+
const prSourceRef = (target) => `${target.host}/${target.owner}/${target.repo}#${target.prNumber}`;
|
|
107
|
+
const parseStoredManifest = (raw) => {
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(raw);
|
|
110
|
+
} catch {
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Creates a new PR review session or resumes an existing non-terminal one.
|
|
116
|
+
*
|
|
117
|
+
* Resume logic:
|
|
118
|
+
* - Looks for an existing review with `source_type = "pull_request"` and
|
|
119
|
+
* matching `source_ref`.
|
|
120
|
+
* - If found and non-terminal (`in_progress` or `changes_requested`):
|
|
121
|
+
* resumes, checking for upstream drift via head OID comparison.
|
|
122
|
+
* - If found but terminal (`approved`): creates a new session.
|
|
123
|
+
* - If not found: creates a new session.
|
|
124
|
+
*
|
|
125
|
+
* New sessions persist the full diff as hunks in `review_files`, making
|
|
126
|
+
* the review fully offline-resumable after initial fetch.
|
|
127
|
+
*/
|
|
128
|
+
const createOrResumePrSession = Effect.fn("PrSession.createOrResume")(function* (preflight) {
|
|
129
|
+
const gh = yield* GhService;
|
|
130
|
+
const repo = yield* ReviewRepo;
|
|
131
|
+
const fileRepo = yield* ReviewFileRepo;
|
|
132
|
+
const { target, metadata, diff } = preflight;
|
|
133
|
+
const sourceRef = prSourceRef(target);
|
|
134
|
+
const resumable = (yield* repo.findAll({
|
|
135
|
+
repositoryPath: preflight.localRepoPath,
|
|
136
|
+
sourceType: "pull_request",
|
|
137
|
+
pageSize: 100
|
|
138
|
+
})).data.find((r) => r.sourceRef === sourceRef && r.status !== "approved");
|
|
139
|
+
if (resumable) {
|
|
140
|
+
const currentHeadOid = yield* gh.fetchPrHeadOid(target).pipe(Effect.catch((e) => Effect.logDebug(`Could not fetch current head OID for drift check: ${e.message}`).pipe(Effect.as(metadata.headRefOid))));
|
|
141
|
+
const storedHeadOid = parseStoredManifest(resumable.snapshotData).headOidAtFetch ?? "";
|
|
142
|
+
const isStale = storedHeadOid !== "" && storedHeadOid !== currentHeadOid;
|
|
143
|
+
return {
|
|
144
|
+
isResumed: true,
|
|
145
|
+
isStale,
|
|
146
|
+
reviewId: resumable.id,
|
|
147
|
+
staleWarning: isStale ? `PR head has changed (${storedHeadOid.slice(0, 7)} → ${currentHeadOid.slice(0, 7)}). Review data reflects the previous version. Use --force-refresh to re-fetch.` : null
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const files = parseDiff(diff);
|
|
151
|
+
if (files.length === 0) return yield* new PrSessionError({
|
|
152
|
+
code: "NO_CHANGES",
|
|
153
|
+
message: `PR #${target.prNumber} diff parsed to zero files.`
|
|
154
|
+
});
|
|
155
|
+
const reviewId = crypto.randomUUID();
|
|
156
|
+
const snapshotData = JSON.stringify({
|
|
157
|
+
diffByteSize: Buffer.byteLength(diff, "utf8"),
|
|
158
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
159
|
+
headOidAtFetch: metadata.headRefOid,
|
|
160
|
+
metadata,
|
|
161
|
+
source: "pull_request",
|
|
162
|
+
target,
|
|
163
|
+
version: 1
|
|
164
|
+
});
|
|
165
|
+
const fileInputs = files.map((f) => ({
|
|
166
|
+
additions: f.additions,
|
|
167
|
+
deletions: f.deletions,
|
|
168
|
+
filePath: f.newPath,
|
|
169
|
+
hunksData: serializeHunks(f.hunks),
|
|
170
|
+
oldPath: f.oldPath !== f.newPath ? f.oldPath : null,
|
|
171
|
+
reviewId,
|
|
172
|
+
status: f.status
|
|
173
|
+
}));
|
|
174
|
+
yield* repo.create({
|
|
175
|
+
baseRef: metadata.baseRefOid,
|
|
176
|
+
id: reviewId,
|
|
177
|
+
repositoryPath: preflight.localRepoPath,
|
|
178
|
+
snapshotData,
|
|
179
|
+
sourceRef,
|
|
180
|
+
sourceType: "pull_request",
|
|
181
|
+
status: "in_progress"
|
|
182
|
+
});
|
|
183
|
+
yield* fileRepo.createBulk(fileInputs);
|
|
184
|
+
return {
|
|
185
|
+
isResumed: false,
|
|
186
|
+
isStale: false,
|
|
187
|
+
reviewId,
|
|
188
|
+
staleWarning: null
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
/**
|
|
192
|
+
* Re-fetches PR data for an existing session and updates stored diff/metadata.
|
|
193
|
+
* Existing annotations are preserved (re-anchoring is a v1.1 feature).
|
|
194
|
+
*/
|
|
195
|
+
const forceRefreshPrSession = Effect.fn("PrSession.forceRefresh")(function* (reviewId, target) {
|
|
196
|
+
const gh = yield* GhService;
|
|
197
|
+
const repo = yield* ReviewRepo;
|
|
198
|
+
const fileRepo = yield* ReviewFileRepo;
|
|
199
|
+
if (!(yield* repo.findById(reviewId))) return yield* new PrSessionError({
|
|
200
|
+
code: "NOT_FOUND",
|
|
201
|
+
message: `Review session ${reviewId} not found.`
|
|
202
|
+
});
|
|
203
|
+
const metadata = yield* gh.fetchPrMetadata(target);
|
|
204
|
+
const diff = yield* gh.fetchPrDiff(target);
|
|
205
|
+
const files = parseDiff(diff);
|
|
206
|
+
const snapshotData = JSON.stringify({
|
|
207
|
+
diffByteSize: Buffer.byteLength(diff, "utf8"),
|
|
208
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
209
|
+
headOidAtFetch: metadata.headRefOid,
|
|
210
|
+
metadata,
|
|
211
|
+
source: "pull_request",
|
|
212
|
+
target,
|
|
213
|
+
version: 1
|
|
214
|
+
});
|
|
215
|
+
yield* repo.updateSnapshotData(reviewId, snapshotData);
|
|
216
|
+
yield* fileRepo.deleteByReview(reviewId);
|
|
217
|
+
const fileInputs = files.map((f) => ({
|
|
218
|
+
additions: f.additions,
|
|
219
|
+
deletions: f.deletions,
|
|
220
|
+
filePath: f.newPath,
|
|
221
|
+
hunksData: serializeHunks(f.hunks),
|
|
222
|
+
oldPath: f.oldPath !== f.newPath ? f.oldPath : null,
|
|
223
|
+
reviewId,
|
|
224
|
+
status: f.status
|
|
225
|
+
}));
|
|
226
|
+
yield* fileRepo.createBulk(fileInputs);
|
|
227
|
+
return {
|
|
228
|
+
filesUpdated: files.length,
|
|
229
|
+
headOid: metadata.headRefOid,
|
|
230
|
+
reviewId,
|
|
231
|
+
summary: getDiffSummary(files)
|
|
232
|
+
};
|
|
233
|
+
});
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region ../../packages/core/src/services/pr-url.ts
|
|
236
|
+
var InvalidPrUrl = class extends Schema.TaggedErrorClass()("InvalidPrUrl", {
|
|
237
|
+
message: Schema.String,
|
|
238
|
+
url: Schema.String
|
|
239
|
+
}) {};
|
|
240
|
+
/**
|
|
241
|
+
* Quick heuristic: does this string look like a PR URL?
|
|
242
|
+
*
|
|
243
|
+
* Used by the CLI parser to distinguish `review <url>` from `review <verb>`.
|
|
244
|
+
* No ambiguity: no review verb starts with `http`.
|
|
245
|
+
*/
|
|
246
|
+
const looksLikePrUrl = (s) => /^https?:\/\/[^/]+\/[^/]+\/[^/]+\/pull\/\d+/.test(s);
|
|
247
|
+
/**
|
|
248
|
+
* Parses a GitHub PR URL into structured components.
|
|
249
|
+
*
|
|
250
|
+
* Supports:
|
|
251
|
+
* - `https://github.com/owner/repo/pull/42`
|
|
252
|
+
* - `https://github.com/owner/repo/pull/42/files`
|
|
253
|
+
* - `https://github.com/owner/repo/pull/42/commits`
|
|
254
|
+
* - `https://ghe.corp.com/owner/repo/pull/42`
|
|
255
|
+
* - `http://...` (for GHE behind VPN)
|
|
256
|
+
*
|
|
257
|
+
* Does NOT support:
|
|
258
|
+
* - SSH URLs, API URLs, short references like `owner/repo#42`
|
|
259
|
+
*/
|
|
260
|
+
const parsePrUrl = Effect.fn("parsePrUrl")(function* (raw) {
|
|
261
|
+
const url = yield* Effect.try({
|
|
262
|
+
catch: () => new InvalidPrUrl({
|
|
263
|
+
message: "Not a valid URL. Expected: https://github.com/<owner>/<repo>/pull/<number>",
|
|
264
|
+
url: raw
|
|
265
|
+
}),
|
|
266
|
+
try: () => new URL(raw)
|
|
267
|
+
});
|
|
268
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return yield* new InvalidPrUrl({
|
|
269
|
+
message: `Unsupported protocol: ${url.protocol}. Expected https:// or http://`,
|
|
270
|
+
url: raw
|
|
271
|
+
});
|
|
272
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
273
|
+
if (segments.length < 4 || segments[2] !== "pull") return yield* new InvalidPrUrl({
|
|
274
|
+
message: "URL path must match /<owner>/<repo>/pull/<number>",
|
|
275
|
+
url: raw
|
|
276
|
+
});
|
|
277
|
+
const owner = segments[0];
|
|
278
|
+
const repo = segments[1];
|
|
279
|
+
const prNumber = Number.parseInt(segments[3], 10);
|
|
280
|
+
if (!Number.isFinite(prNumber) || prNumber <= 0) return yield* new InvalidPrUrl({
|
|
281
|
+
message: `Invalid PR number: ${segments[3]}`,
|
|
282
|
+
url: raw
|
|
283
|
+
});
|
|
284
|
+
return {
|
|
285
|
+
host: url.host,
|
|
286
|
+
nwoRef: `${owner}/${repo}`,
|
|
287
|
+
owner,
|
|
288
|
+
prNumber,
|
|
289
|
+
repo,
|
|
290
|
+
url: `${url.protocol}//${url.host}/${owner}/${repo}/pull/${prNumber}`
|
|
291
|
+
};
|
|
292
|
+
});
|
|
293
|
+
//#endregion
|
|
294
|
+
//#region src/cli/config.ts
|
|
295
|
+
var CliConfig = class extends ServiceMap.Service()("@ringi/CliConfig") {};
|
|
296
|
+
/**
|
|
297
|
+
* Wraps a concrete {@link CliConfigShape} in a layer for the Effect runtime.
|
|
298
|
+
*/
|
|
299
|
+
const CliConfigLive = (config) => Layer.succeed(CliConfig, CliConfig.of(config));
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region src/cli/contracts.ts
|
|
302
|
+
const ExitCode = {
|
|
303
|
+
AuthFailure: 5,
|
|
304
|
+
ResourceNotFound: 3,
|
|
305
|
+
RuntimeFailure: 1,
|
|
306
|
+
StateUnavailable: 4,
|
|
307
|
+
Success: 0,
|
|
308
|
+
UsageError: 2
|
|
309
|
+
};
|
|
310
|
+
const success = (command, result, nextActions = []) => ({
|
|
311
|
+
command,
|
|
312
|
+
next_actions: nextActions,
|
|
313
|
+
ok: true,
|
|
314
|
+
result
|
|
315
|
+
});
|
|
316
|
+
const failure = (command, error, fix, nextActions = []) => ({
|
|
317
|
+
command,
|
|
318
|
+
error,
|
|
319
|
+
fix,
|
|
320
|
+
next_actions: nextActions,
|
|
321
|
+
ok: false
|
|
322
|
+
});
|
|
323
|
+
/**
|
|
324
|
+
* Carries an exit code and optional operator-facing details so callers can
|
|
325
|
+
* present a short message without losing the underlying reason.
|
|
326
|
+
*/
|
|
327
|
+
var CliFailure = class extends Schema.TaggedErrorClass()("CliFailure", {
|
|
328
|
+
details: Schema.String.pipe(Schema.optionalKey),
|
|
329
|
+
exitCode: Schema.Number,
|
|
330
|
+
message: Schema.String
|
|
331
|
+
}) {};
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region src/cli/commands.ts
|
|
334
|
+
const formatTable = (headers, rows) => {
|
|
335
|
+
const widths = headers.map((header, index) => {
|
|
336
|
+
const cellWidths = rows.map((row) => row[index]?.length ?? 0);
|
|
337
|
+
return Math.max(header.length, ...cellWidths);
|
|
338
|
+
});
|
|
339
|
+
const renderRow = (row) => row.map((cell, index) => cell.padEnd(widths.at(index) ?? 0)).join(" ").trimEnd();
|
|
340
|
+
return [
|
|
341
|
+
renderRow(headers),
|
|
342
|
+
renderRow(widths.map((width) => "-".repeat(width))),
|
|
343
|
+
...rows.map(renderRow)
|
|
344
|
+
].join("\n");
|
|
345
|
+
};
|
|
346
|
+
const renderReviewList = (reviews) => {
|
|
347
|
+
if (reviews.length === 0) return "No reviews found.";
|
|
348
|
+
return formatTable([
|
|
349
|
+
"ID",
|
|
350
|
+
"STATUS",
|
|
351
|
+
"SOURCE",
|
|
352
|
+
"FILES",
|
|
353
|
+
"CREATED"
|
|
354
|
+
], reviews.map((review) => [
|
|
355
|
+
review.id,
|
|
356
|
+
review.status,
|
|
357
|
+
review.sourceType,
|
|
358
|
+
String(review.fileCount),
|
|
359
|
+
review.createdAt
|
|
360
|
+
]));
|
|
361
|
+
};
|
|
362
|
+
const renderReviewShow = (input) => {
|
|
363
|
+
const { comments, review, todos } = input;
|
|
364
|
+
const lines = [
|
|
365
|
+
`Review ${review.id}`,
|
|
366
|
+
`Status: ${review.status}`,
|
|
367
|
+
`Source: ${review.sourceType}${review.sourceRef ? ` (${review.sourceRef})` : ""}`,
|
|
368
|
+
`Created: ${review.createdAt}`,
|
|
369
|
+
`Files: ${review.summary.totalFiles}`,
|
|
370
|
+
`Diff: +${review.summary.totalAdditions} / -${review.summary.totalDeletions}`
|
|
371
|
+
];
|
|
372
|
+
if (review.files.length > 0) {
|
|
373
|
+
lines.push("", "Files:");
|
|
374
|
+
for (const file of review.files) lines.push(`- ${file.status.toUpperCase()} ${file.filePath} (+${file.additions} -${file.deletions})`);
|
|
375
|
+
}
|
|
376
|
+
if (comments && comments.length > 0) {
|
|
377
|
+
lines.push("", "Comments:");
|
|
378
|
+
for (const comment of comments) {
|
|
379
|
+
const location = `${comment.filePath}:${comment.lineNumber ?? "-"}`;
|
|
380
|
+
const state = comment.resolved ? "resolved" : "open";
|
|
381
|
+
lines.push(`- [${state}] ${location} ${comment.content}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (todos && todos.length > 0) {
|
|
385
|
+
lines.push("", "Todos:");
|
|
386
|
+
for (const todo of todos) {
|
|
387
|
+
const marker = todo.completed ? "x" : " ";
|
|
388
|
+
lines.push(`- [${marker}] (${todo.position + 1}) ${todo.content}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return lines.join("\n");
|
|
392
|
+
};
|
|
393
|
+
const renderTodoList = (todos) => {
|
|
394
|
+
if (todos.length === 0) return "No todos found.";
|
|
395
|
+
return todos.map((todo) => `- [${todo.completed ? "x" : " "}] (${todo.position + 1}) ${todo.content}`).join("\n");
|
|
396
|
+
};
|
|
397
|
+
const renderSourceList = (input) => {
|
|
398
|
+
const lines = [
|
|
399
|
+
`Repository: ${input.repo.name}`,
|
|
400
|
+
`Path: ${input.repo.path}`,
|
|
401
|
+
`Current branch: ${input.repo.branch}`,
|
|
402
|
+
`Staged files: ${input.stagedFiles.length}`
|
|
403
|
+
];
|
|
404
|
+
if (input.stagedFiles.length > 0) {
|
|
405
|
+
lines.push("", "Staged:");
|
|
406
|
+
for (const file of input.stagedFiles) lines.push(`- ${file.status} ${file.path}`);
|
|
407
|
+
}
|
|
408
|
+
if (input.branches.length > 0) {
|
|
409
|
+
lines.push("", "Branches:");
|
|
410
|
+
for (const branch of input.branches.slice(0, 10)) lines.push(`- ${branch.current ? "*" : " "} ${branch.name}`);
|
|
411
|
+
}
|
|
412
|
+
if (input.commits.length > 0) {
|
|
413
|
+
lines.push("", "Recent commits:");
|
|
414
|
+
for (const commit of input.commits.slice(0, 5)) lines.push(`- ${commit.hash.slice(0, 8)} ${commit.message} (${commit.author})`);
|
|
415
|
+
}
|
|
416
|
+
return lines.join("\n");
|
|
417
|
+
};
|
|
418
|
+
/**
|
|
419
|
+
* Resolves the special "last" selector before show/export handlers ask the
|
|
420
|
+
* shared services for a concrete review id.
|
|
421
|
+
*/
|
|
422
|
+
const resolveReviewSelector = Effect.fn("CLI.resolveReviewSelector")(function* resolveReviewSelector(selector) {
|
|
423
|
+
if (selector !== "last") return selector;
|
|
424
|
+
const cliConfig = yield* CliConfig;
|
|
425
|
+
const [review] = (yield* (yield* ReviewService).list({
|
|
426
|
+
page: 1,
|
|
427
|
+
pageSize: 1,
|
|
428
|
+
repositoryPath: cliConfig.repoRoot
|
|
429
|
+
})).reviews;
|
|
430
|
+
if (!review) return yield* new CliFailure({
|
|
431
|
+
exitCode: ExitCode.ResourceNotFound,
|
|
432
|
+
message: "No review sessions exist for this repository yet."
|
|
433
|
+
});
|
|
434
|
+
return review.id;
|
|
435
|
+
});
|
|
436
|
+
/**
|
|
437
|
+
* Mutating CLI commands stay server-backed so they share the same write path as
|
|
438
|
+
* the other clients instead of growing a second local-only behavior surface.
|
|
439
|
+
*/
|
|
440
|
+
const requireServerMode = (label) => Effect.fail(new CliFailure({
|
|
441
|
+
details: "Start 'ringi serve' and retry the command.",
|
|
442
|
+
exitCode: ExitCode.StateUnavailable,
|
|
443
|
+
message: `${label} requires a running local Ringi server. Standalone local writes are intentionally unsupported.`
|
|
444
|
+
}));
|
|
445
|
+
const diffSourceStrategies = {
|
|
446
|
+
branch: (git, command) => git.getBranchDiff(command.branch ?? ""),
|
|
447
|
+
commits: (git, command) => git.getCommitDiff((command.commits ?? "").split(",").map((item) => item.trim()).filter(Boolean)),
|
|
448
|
+
staged: (git) => git.getStagedDiff
|
|
449
|
+
};
|
|
450
|
+
const runReviewList = Effect.fn("CLI.reviewList")(function* runReviewList(command) {
|
|
451
|
+
const reviewService = yield* ReviewService;
|
|
452
|
+
const cliConfig = yield* CliConfig;
|
|
453
|
+
const result = yield* reviewService.list({
|
|
454
|
+
page: command.page,
|
|
455
|
+
pageSize: command.limit,
|
|
456
|
+
repositoryPath: cliConfig.repoRoot,
|
|
457
|
+
sourceType: command.source,
|
|
458
|
+
status: command.status
|
|
459
|
+
});
|
|
460
|
+
const nextActions = [];
|
|
461
|
+
for (const review of result.reviews.slice(0, 3)) nextActions.push({
|
|
462
|
+
command: `ringi review show ${review.id} --comments --todos`,
|
|
463
|
+
description: `Inspect review ${review.id} (${review.status})`
|
|
464
|
+
});
|
|
465
|
+
if (result.reviews.length > 0) nextActions.push({
|
|
466
|
+
command: "ringi review show <id> [--comments] [--todos]",
|
|
467
|
+
description: "Show full review details",
|
|
468
|
+
params: { id: {
|
|
469
|
+
description: "Review ID or 'last'",
|
|
470
|
+
required: true
|
|
471
|
+
} }
|
|
472
|
+
});
|
|
473
|
+
nextActions.push({
|
|
474
|
+
command: "ringi review create [--source <source>]",
|
|
475
|
+
description: "Create a new review session",
|
|
476
|
+
params: { source: {
|
|
477
|
+
default: "staged",
|
|
478
|
+
enum: [
|
|
479
|
+
"staged",
|
|
480
|
+
"branch",
|
|
481
|
+
"commits"
|
|
482
|
+
]
|
|
483
|
+
} }
|
|
484
|
+
});
|
|
485
|
+
return {
|
|
486
|
+
data: result,
|
|
487
|
+
human: renderReviewList(result.reviews),
|
|
488
|
+
nextActions
|
|
489
|
+
};
|
|
490
|
+
});
|
|
491
|
+
const runReviewShow = Effect.fn("CLI.reviewShow")(function* runReviewShow(command) {
|
|
492
|
+
const reviewService = yield* ReviewService;
|
|
493
|
+
const todoService = yield* TodoService;
|
|
494
|
+
const commentService = yield* CommentService;
|
|
495
|
+
const reviewId = yield* resolveReviewSelector(command.id);
|
|
496
|
+
const review = yield* reviewService.getById(reviewId);
|
|
497
|
+
const data = {
|
|
498
|
+
comments: command.comments ? yield* commentService.getByReview(reviewId) : void 0,
|
|
499
|
+
review,
|
|
500
|
+
todos: command.todos ? (yield* todoService.list({ reviewId })).data : void 0
|
|
501
|
+
};
|
|
502
|
+
const nextActions = [
|
|
503
|
+
{
|
|
504
|
+
command: `ringi review export ${reviewId}`,
|
|
505
|
+
description: "Export this review as markdown"
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
command: `ringi review show ${reviewId} --comments --todos`,
|
|
509
|
+
description: "Show with comments and todos"
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
command: "ringi todo list [--review <review-id>] [--status <status>]",
|
|
513
|
+
description: "List todos for this review",
|
|
514
|
+
params: {
|
|
515
|
+
"review-id": { value: reviewId },
|
|
516
|
+
status: {
|
|
517
|
+
default: "pending",
|
|
518
|
+
enum: [
|
|
519
|
+
"pending",
|
|
520
|
+
"done",
|
|
521
|
+
"all"
|
|
522
|
+
]
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
command: "ringi review list",
|
|
528
|
+
description: "Back to review list"
|
|
529
|
+
}
|
|
530
|
+
];
|
|
531
|
+
return {
|
|
532
|
+
data,
|
|
533
|
+
human: renderReviewShow(data),
|
|
534
|
+
nextActions
|
|
535
|
+
};
|
|
536
|
+
});
|
|
537
|
+
const runReviewExport = Effect.fn("CLI.reviewExport")(function* runReviewExport(command) {
|
|
538
|
+
if (command.noResolved || command.noSnippets) yield* new CliFailure({
|
|
539
|
+
exitCode: ExitCode.UsageError,
|
|
540
|
+
message: "--no-resolved and --no-snippets are documented, but the shared export service does not support adapter-level filtering yet."
|
|
541
|
+
});
|
|
542
|
+
const exportService = yield* ExportService;
|
|
543
|
+
const cliConfig = yield* CliConfig;
|
|
544
|
+
const reviewId = yield* resolveReviewSelector(command.id);
|
|
545
|
+
const markdown = yield* exportService.exportReview(reviewId);
|
|
546
|
+
const outputPath = command.outputPath ? resolve(cliConfig.cwd, command.outputPath) : void 0;
|
|
547
|
+
if (outputPath) yield* Effect.tryPromise({
|
|
548
|
+
catch: (error) => new CliFailure({
|
|
549
|
+
exitCode: ExitCode.RuntimeFailure,
|
|
550
|
+
message: `Failed to write export to ${outputPath}: ${String(error)}`
|
|
551
|
+
}),
|
|
552
|
+
try: () => writeFile(outputPath, markdown, "utf8")
|
|
553
|
+
});
|
|
554
|
+
const shouldPrintMarkdown = command.stdout || !outputPath;
|
|
555
|
+
const data = {
|
|
556
|
+
markdown,
|
|
557
|
+
outputPath: outputPath ?? null,
|
|
558
|
+
reviewId
|
|
559
|
+
};
|
|
560
|
+
const nextActions = [{
|
|
561
|
+
command: `ringi review show ${reviewId}`,
|
|
562
|
+
description: "View the exported review"
|
|
563
|
+
}, {
|
|
564
|
+
command: "ringi review list",
|
|
565
|
+
description: "Back to review list"
|
|
566
|
+
}];
|
|
567
|
+
return {
|
|
568
|
+
data,
|
|
569
|
+
human: shouldPrintMarkdown ? markdown : `Exported review ${reviewId} to ${outputPath}.`,
|
|
570
|
+
nextActions
|
|
571
|
+
};
|
|
572
|
+
});
|
|
573
|
+
const runSourceList = Effect.fn("CLI.sourceList")(function* runSourceList() {
|
|
574
|
+
const gitService = yield* GitService;
|
|
575
|
+
const repo = yield* gitService.getRepositoryInfo;
|
|
576
|
+
const stagedFiles = yield* gitService.getStagedFiles;
|
|
577
|
+
const data = {
|
|
578
|
+
branches: yield* gitService.getBranches,
|
|
579
|
+
commits: (yield* gitService.getCommits({
|
|
580
|
+
limit: 10,
|
|
581
|
+
offset: 0
|
|
582
|
+
})).commits,
|
|
583
|
+
repo,
|
|
584
|
+
stagedFiles
|
|
585
|
+
};
|
|
586
|
+
return {
|
|
587
|
+
data,
|
|
588
|
+
human: renderSourceList(data),
|
|
589
|
+
nextActions: [
|
|
590
|
+
{
|
|
591
|
+
command: "ringi source diff <source> [--stat]",
|
|
592
|
+
description: "View diff for a source",
|
|
593
|
+
params: { source: { enum: [
|
|
594
|
+
"staged",
|
|
595
|
+
"branch",
|
|
596
|
+
"commits"
|
|
597
|
+
] } }
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
command: "ringi review create [--source <source>]",
|
|
601
|
+
description: "Create a review from a source",
|
|
602
|
+
params: { source: {
|
|
603
|
+
default: "staged",
|
|
604
|
+
enum: [
|
|
605
|
+
"staged",
|
|
606
|
+
"branch",
|
|
607
|
+
"commits"
|
|
608
|
+
]
|
|
609
|
+
} }
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
command: "ringi review list",
|
|
613
|
+
description: "List existing reviews"
|
|
614
|
+
}
|
|
615
|
+
]
|
|
616
|
+
};
|
|
617
|
+
});
|
|
618
|
+
const runSourceDiff = Effect.fn("CLI.sourceDiff")(function* runSourceDiff(command) {
|
|
619
|
+
const gitService = yield* GitService;
|
|
620
|
+
const strategy = diffSourceStrategies[command.source];
|
|
621
|
+
if (!strategy) return yield* new CliFailure({
|
|
622
|
+
exitCode: ExitCode.UsageError,
|
|
623
|
+
message: "Unsupported review source."
|
|
624
|
+
});
|
|
625
|
+
const diffText = yield* strategy(gitService, command);
|
|
626
|
+
if (!diffText.trim()) yield* new CliFailure({
|
|
627
|
+
exitCode: ExitCode.RuntimeFailure,
|
|
628
|
+
message: "No diff available for the requested source."
|
|
629
|
+
});
|
|
630
|
+
const files = parseDiff(diffText);
|
|
631
|
+
const data = {
|
|
632
|
+
diff: diffText,
|
|
633
|
+
source: command.source,
|
|
634
|
+
summary: getDiffSummary(files)
|
|
635
|
+
};
|
|
636
|
+
const nextActions = [{
|
|
637
|
+
command: `ringi review create --source ${command.source}`,
|
|
638
|
+
description: `Create a review from this ${command.source} diff`
|
|
639
|
+
}, {
|
|
640
|
+
command: "ringi source list",
|
|
641
|
+
description: "List repository sources"
|
|
642
|
+
}];
|
|
643
|
+
return {
|
|
644
|
+
data,
|
|
645
|
+
human: command.stat ? [
|
|
646
|
+
`Source: ${command.source}`,
|
|
647
|
+
`Files: ${data.summary.totalFiles}`,
|
|
648
|
+
`Additions: ${data.summary.totalAdditions}`,
|
|
649
|
+
`Deletions: ${data.summary.totalDeletions}`
|
|
650
|
+
].join("\n") : diffText,
|
|
651
|
+
nextActions
|
|
652
|
+
};
|
|
653
|
+
});
|
|
654
|
+
const runReviewStatus = Effect.fn("CLI.reviewStatus")(function* runReviewStatus(command) {
|
|
655
|
+
const reviewService = yield* ReviewService;
|
|
656
|
+
const todoService = yield* TodoService;
|
|
657
|
+
const commentService = yield* CommentService;
|
|
658
|
+
const gitService = yield* GitService;
|
|
659
|
+
const cliConfig = yield* CliConfig;
|
|
660
|
+
const repo = yield* gitService.getRepositoryInfo;
|
|
661
|
+
const stagedFiles = yield* gitService.getStagedFiles;
|
|
662
|
+
let reviewId;
|
|
663
|
+
if (command.reviewId) reviewId = yield* resolveReviewSelector(command.reviewId);
|
|
664
|
+
const reviews = yield* reviewService.list({
|
|
665
|
+
page: 1,
|
|
666
|
+
pageSize: 1,
|
|
667
|
+
repositoryPath: cliConfig.repoRoot,
|
|
668
|
+
sourceType: command.source
|
|
669
|
+
});
|
|
670
|
+
const latestReview = reviewId ? yield* reviewService.getById(reviewId) : reviews.reviews[0];
|
|
671
|
+
let commentStats;
|
|
672
|
+
let todoStats;
|
|
673
|
+
if (latestReview) {
|
|
674
|
+
commentStats = yield* commentService.getStats(latestReview.id);
|
|
675
|
+
todoStats = yield* todoService.getStats();
|
|
676
|
+
}
|
|
677
|
+
const data = {
|
|
678
|
+
commentStats: commentStats ?? null,
|
|
679
|
+
repository: {
|
|
680
|
+
branch: repo.branch,
|
|
681
|
+
name: repo.name,
|
|
682
|
+
path: repo.path,
|
|
683
|
+
stagedFileCount: stagedFiles.length
|
|
684
|
+
},
|
|
685
|
+
review: latestReview ? {
|
|
686
|
+
createdAt: latestReview.createdAt,
|
|
687
|
+
id: latestReview.id,
|
|
688
|
+
sourceType: latestReview.sourceType,
|
|
689
|
+
status: latestReview.status
|
|
690
|
+
} : null,
|
|
691
|
+
todoStats: todoStats ?? null
|
|
692
|
+
};
|
|
693
|
+
const lines = [
|
|
694
|
+
`Repository: ${repo.name}`,
|
|
695
|
+
`Branch: ${repo.branch}`,
|
|
696
|
+
`Staged files: ${stagedFiles.length}`
|
|
697
|
+
];
|
|
698
|
+
if (latestReview) {
|
|
699
|
+
lines.push("", `Review: ${latestReview.id}`, `Status: ${latestReview.status}`, `Source: ${latestReview.sourceType}`);
|
|
700
|
+
if (commentStats) lines.push(`Comments: ${commentStats.unresolved ?? 0} unresolved / ${commentStats.total} total`);
|
|
701
|
+
if (todoStats) lines.push(`Todos: ${todoStats.pending} pending / ${todoStats.total} total`);
|
|
702
|
+
} else lines.push("", "No review sessions found.");
|
|
703
|
+
const nextActions = [];
|
|
704
|
+
if (latestReview) nextActions.push({
|
|
705
|
+
command: `ringi review show ${latestReview.id} --comments --todos`,
|
|
706
|
+
description: "Inspect the latest review"
|
|
707
|
+
}, {
|
|
708
|
+
command: `ringi review export ${latestReview.id}`,
|
|
709
|
+
description: "Export the latest review"
|
|
710
|
+
});
|
|
711
|
+
nextActions.push({
|
|
712
|
+
command: "ringi review create [--source <source>]",
|
|
713
|
+
description: "Create a new review session",
|
|
714
|
+
params: { source: {
|
|
715
|
+
default: "staged",
|
|
716
|
+
enum: [
|
|
717
|
+
"staged",
|
|
718
|
+
"branch",
|
|
719
|
+
"commits"
|
|
720
|
+
]
|
|
721
|
+
} }
|
|
722
|
+
});
|
|
723
|
+
return {
|
|
724
|
+
data,
|
|
725
|
+
human: lines.join("\n"),
|
|
726
|
+
nextActions
|
|
727
|
+
};
|
|
728
|
+
});
|
|
729
|
+
const runTodoList = Effect.fn("CLI.todoList")(function* runTodoList(command) {
|
|
730
|
+
const result = yield* (yield* TodoService).list({
|
|
731
|
+
completed: command.status === "all" ? void 0 : command.status === "done",
|
|
732
|
+
limit: command.limit,
|
|
733
|
+
offset: command.offset,
|
|
734
|
+
reviewId: command.reviewId
|
|
735
|
+
});
|
|
736
|
+
const nextActions = [];
|
|
737
|
+
if (command.reviewId) nextActions.push({
|
|
738
|
+
command: `ringi review show ${command.reviewId}`,
|
|
739
|
+
description: "View the associated review"
|
|
740
|
+
});
|
|
741
|
+
nextActions.push({
|
|
742
|
+
command: "ringi todo add --text <text> [--review <review-id>]",
|
|
743
|
+
description: "Add a new todo",
|
|
744
|
+
params: { text: {
|
|
745
|
+
description: "Todo text",
|
|
746
|
+
required: true
|
|
747
|
+
} }
|
|
748
|
+
}, {
|
|
749
|
+
command: "ringi review list",
|
|
750
|
+
description: "List reviews"
|
|
751
|
+
});
|
|
752
|
+
return {
|
|
753
|
+
data: result,
|
|
754
|
+
human: renderTodoList(result.data),
|
|
755
|
+
nextActions
|
|
756
|
+
};
|
|
757
|
+
});
|
|
758
|
+
const runReviewPr = Effect.fn("CLI.reviewPr")(function* runReviewPr(command) {
|
|
759
|
+
const target = yield* parsePrUrl(command.prUrl).pipe(Effect.mapError((e) => new CliFailure({
|
|
760
|
+
exitCode: ExitCode.UsageError,
|
|
761
|
+
message: e.message
|
|
762
|
+
})));
|
|
763
|
+
const preflight = yield* runPreflight(target).pipe(Effect.mapError((e) => new CliFailure({
|
|
764
|
+
exitCode: e.exitCode,
|
|
765
|
+
message: e.message
|
|
766
|
+
})));
|
|
767
|
+
if (preflight.affinityWarning) yield* Effect.logWarning(preflight.affinityWarning);
|
|
768
|
+
let session;
|
|
769
|
+
if (command.forceRefresh) {
|
|
770
|
+
const reviewService = yield* ReviewService;
|
|
771
|
+
const sourceRef = prSourceRef(target);
|
|
772
|
+
const cliConfig = yield* CliConfig;
|
|
773
|
+
const resumable = (yield* reviewService.list({
|
|
774
|
+
repositoryPath: cliConfig.repoRoot,
|
|
775
|
+
sourceType: "pull_request",
|
|
776
|
+
pageSize: 100
|
|
777
|
+
})).reviews.find((r) => r.sourceRef === sourceRef && r.status !== "approved");
|
|
778
|
+
if (resumable) {
|
|
779
|
+
yield* forceRefreshPrSession(resumable.id, target).pipe(Effect.mapError((e) => new CliFailure({
|
|
780
|
+
exitCode: ExitCode.RuntimeFailure,
|
|
781
|
+
message: e.message
|
|
782
|
+
})));
|
|
783
|
+
session = {
|
|
784
|
+
isResumed: true,
|
|
785
|
+
isStale: false,
|
|
786
|
+
reviewId: resumable.id,
|
|
787
|
+
staleWarning: null
|
|
788
|
+
};
|
|
789
|
+
} else session = yield* createOrResumePrSession(preflight).pipe(Effect.mapError((e) => new CliFailure({
|
|
790
|
+
exitCode: ExitCode.RuntimeFailure,
|
|
791
|
+
message: e.message
|
|
792
|
+
})));
|
|
793
|
+
} else session = yield* createOrResumePrSession(preflight).pipe(Effect.mapError((e) => new CliFailure({
|
|
794
|
+
exitCode: ExitCode.RuntimeFailure,
|
|
795
|
+
message: e.message
|
|
796
|
+
})));
|
|
797
|
+
if (session.staleWarning) yield* Effect.logWarning(session.staleWarning);
|
|
798
|
+
const serverUrl = `http://localhost:${command.port}`;
|
|
799
|
+
const reviewUrl = `${serverUrl}/review/${session.reviewId}`;
|
|
800
|
+
const data = {
|
|
801
|
+
isResumed: session.isResumed,
|
|
802
|
+
isStale: session.isStale,
|
|
803
|
+
prNumber: target.prNumber,
|
|
804
|
+
prUrl: target.url,
|
|
805
|
+
reviewId: session.reviewId,
|
|
806
|
+
reviewUrl
|
|
807
|
+
};
|
|
808
|
+
const statusLabel = session.isResumed ? command.forceRefresh ? "(refreshed)" : "(resumed)" : "(new)";
|
|
809
|
+
const humanLines = [
|
|
810
|
+
`PR #${target.prNumber}: ${preflight.metadata.title}`,
|
|
811
|
+
`Review: ${session.reviewId} ${statusLabel}`,
|
|
812
|
+
`Author: ${preflight.metadata.author.login}`,
|
|
813
|
+
`Branch: ${preflight.metadata.headRefName} → ${preflight.metadata.baseRefName}`,
|
|
814
|
+
`Files: ${preflight.metadata.changedFiles} (+${preflight.metadata.additions} -${preflight.metadata.deletions})`,
|
|
815
|
+
"",
|
|
816
|
+
`Server: ${serverUrl}`,
|
|
817
|
+
`Review: ${reviewUrl}`
|
|
818
|
+
];
|
|
819
|
+
if (preflight.metadata.isDraft) humanLines.splice(1, 0, "⚠ Draft PR");
|
|
820
|
+
if (preflight.metadata.state === "CLOSED" || preflight.metadata.state === "MERGED") humanLines.splice(1, 0, `⚠ This PR is ${preflight.metadata.state}`);
|
|
821
|
+
const nextActions = [{
|
|
822
|
+
command: `ringi review show ${session.reviewId} --comments --todos`,
|
|
823
|
+
description: "Inspect review details"
|
|
824
|
+
}, {
|
|
825
|
+
command: `ringi review export ${session.reviewId}`,
|
|
826
|
+
description: "Export review as markdown"
|
|
827
|
+
}];
|
|
828
|
+
if (session.isStale) nextActions.unshift({
|
|
829
|
+
command: `ringi review ${command.prUrl} --force-refresh`,
|
|
830
|
+
description: "Re-fetch PR data with latest changes"
|
|
831
|
+
});
|
|
832
|
+
return {
|
|
833
|
+
data,
|
|
834
|
+
human: humanLines.join("\n"),
|
|
835
|
+
nextActions
|
|
836
|
+
};
|
|
837
|
+
});
|
|
838
|
+
/**
|
|
839
|
+
* Data-driven command registry. Each command kind maps to its handler.
|
|
840
|
+
* Adding a new command means adding one entry — no switch duplication.
|
|
841
|
+
*/
|
|
842
|
+
const COMMAND_HANDLERS = {
|
|
843
|
+
"data-migrate": () => requireServerMode("ringi data migrate"),
|
|
844
|
+
"data-reset": () => requireServerMode("ringi data reset"),
|
|
845
|
+
doctor: () => Effect.succeed({
|
|
846
|
+
data: {
|
|
847
|
+
checks: [],
|
|
848
|
+
ok: true
|
|
849
|
+
},
|
|
850
|
+
human: "ringi doctor: not yet implemented.",
|
|
851
|
+
nextActions: []
|
|
852
|
+
}),
|
|
853
|
+
events: () => requireServerMode("ringi events"),
|
|
854
|
+
mcp: () => Effect.fail(new CliFailure({
|
|
855
|
+
exitCode: ExitCode.UsageError,
|
|
856
|
+
message: "ringi mcp is a runtime command. Use it directly, not through the command dispatcher."
|
|
857
|
+
})),
|
|
858
|
+
"review-create": () => requireServerMode("ringi review create"),
|
|
859
|
+
"review-export": (c) => runReviewExport(c),
|
|
860
|
+
"review-list": (c) => runReviewList(c),
|
|
861
|
+
"review-pr": (c) => runReviewPr(c),
|
|
862
|
+
"review-resolve": () => requireServerMode("ringi review resolve"),
|
|
863
|
+
"review-show": (c) => runReviewShow(c),
|
|
864
|
+
"review-status": (c) => runReviewStatus(c),
|
|
865
|
+
serve: () => Effect.fail(new CliFailure({
|
|
866
|
+
exitCode: ExitCode.UsageError,
|
|
867
|
+
message: "ringi serve is a runtime command. Use it directly, not through the command dispatcher."
|
|
868
|
+
})),
|
|
869
|
+
"source-diff": (c) => runSourceDiff(c),
|
|
870
|
+
"source-list": () => runSourceList(),
|
|
871
|
+
"todo-add": () => requireServerMode("ringi todo add"),
|
|
872
|
+
"todo-clear": () => requireServerMode("ringi todo clear"),
|
|
873
|
+
"todo-done": () => requireServerMode("ringi todo done"),
|
|
874
|
+
"todo-list": (c) => runTodoList(c),
|
|
875
|
+
"todo-move": () => requireServerMode("ringi todo move"),
|
|
876
|
+
"todo-remove": () => requireServerMode("ringi todo remove"),
|
|
877
|
+
"todo-undone": () => requireServerMode("ringi todo undone")
|
|
878
|
+
};
|
|
879
|
+
/** Human-readable command label for the JSON envelope `command` field. */
|
|
880
|
+
const COMMAND_LABELS = {
|
|
881
|
+
"data-migrate": "ringi data migrate",
|
|
882
|
+
"data-reset": "ringi data reset",
|
|
883
|
+
doctor: "ringi doctor",
|
|
884
|
+
events: "ringi events",
|
|
885
|
+
mcp: "ringi mcp",
|
|
886
|
+
"review-create": "ringi review create",
|
|
887
|
+
"review-export": "ringi review export",
|
|
888
|
+
"review-list": "ringi review list",
|
|
889
|
+
"review-pr": "ringi review <pr-url>",
|
|
890
|
+
"review-resolve": "ringi review resolve",
|
|
891
|
+
"review-show": "ringi review show",
|
|
892
|
+
"review-status": "ringi review status",
|
|
893
|
+
serve: "ringi serve",
|
|
894
|
+
"source-diff": "ringi source diff",
|
|
895
|
+
"source-list": "ringi source list",
|
|
896
|
+
"todo-add": "ringi todo add",
|
|
897
|
+
"todo-clear": "ringi todo clear",
|
|
898
|
+
"todo-done": "ringi todo done",
|
|
899
|
+
"todo-list": "ringi todo list",
|
|
900
|
+
"todo-move": "ringi todo move",
|
|
901
|
+
"todo-remove": "ringi todo remove",
|
|
902
|
+
"todo-undone": "ringi todo undone"
|
|
903
|
+
};
|
|
904
|
+
const commandLabel = (command) => COMMAND_LABELS[command.kind] ?? `ringi ${command.kind}`;
|
|
905
|
+
const runCommand = (command) => {
|
|
906
|
+
const handler = COMMAND_HANDLERS[command.kind];
|
|
907
|
+
if (!handler) return Effect.fail(new CliFailure({
|
|
908
|
+
exitCode: ExitCode.UsageError,
|
|
909
|
+
message: `No executable handler exists for ${command.kind}.`
|
|
910
|
+
}));
|
|
911
|
+
return handler(command);
|
|
912
|
+
};
|
|
913
|
+
//#endregion
|
|
914
|
+
//#region src/cli/parser.ts
|
|
915
|
+
const REVIEW_SOURCES = new Set([
|
|
916
|
+
"branch",
|
|
917
|
+
"commits",
|
|
918
|
+
"pull_request",
|
|
919
|
+
"staged"
|
|
920
|
+
]);
|
|
921
|
+
const REVIEW_STATUSES = new Set([
|
|
922
|
+
"approved",
|
|
923
|
+
"changes_requested",
|
|
924
|
+
"in_progress"
|
|
925
|
+
]);
|
|
926
|
+
const TODO_STATUSES = new Set([
|
|
927
|
+
"all",
|
|
928
|
+
"done",
|
|
929
|
+
"pending"
|
|
930
|
+
]);
|
|
931
|
+
const usageError = (message) => new CliFailure({
|
|
932
|
+
exitCode: ExitCode.UsageError,
|
|
933
|
+
message
|
|
934
|
+
});
|
|
935
|
+
/**
|
|
936
|
+
* Consumes the next token as a flag value, advancing the cursor by 2.
|
|
937
|
+
* Rejects another flag in the value slot so typos fail fast.
|
|
938
|
+
*/
|
|
939
|
+
const requireValue = (state, flag) => {
|
|
940
|
+
const value = state.tokens[state.index + 1];
|
|
941
|
+
if (!value || value.startsWith("-")) return Option.some(usageError(`Missing value for ${flag}.`));
|
|
942
|
+
state.index += 2;
|
|
943
|
+
return Option.none();
|
|
944
|
+
};
|
|
945
|
+
/** Peek at the value that {@link requireValue} would consume. */
|
|
946
|
+
const peekValue = (state) => state.tokens[state.index + 1] ?? "";
|
|
947
|
+
const decodePositiveInt = (raw, flag) => {
|
|
948
|
+
const value = Number.parseInt(raw, 10);
|
|
949
|
+
if (!Number.isInteger(value) || value < 0) return Result.fail(usageError(`${flag} must be a non-negative integer.`));
|
|
950
|
+
return Result.succeed(value);
|
|
951
|
+
};
|
|
952
|
+
const decodeEnum = (raw, valid, label) => {
|
|
953
|
+
if (!valid.has(raw)) return Result.fail(usageError(`Invalid ${label}: ${raw}.`));
|
|
954
|
+
return Result.succeed(raw);
|
|
955
|
+
};
|
|
956
|
+
/** Boolean flag: sets a key to `true`, advances cursor by 1. */
|
|
957
|
+
const boolFlag = (key) => (state, acc) => {
|
|
958
|
+
acc[key] = true;
|
|
959
|
+
state.index += 1;
|
|
960
|
+
return Option.none();
|
|
961
|
+
};
|
|
962
|
+
/** String flag: consumes next token, assigns to key. */
|
|
963
|
+
const stringFlag = (key) => (state, acc) => {
|
|
964
|
+
const flag = state.tokens[state.index] ?? "";
|
|
965
|
+
const value = peekValue(state);
|
|
966
|
+
const error = requireValue(state, flag);
|
|
967
|
+
if (Option.isSome(error)) return error;
|
|
968
|
+
acc[key] = value;
|
|
969
|
+
return Option.none();
|
|
970
|
+
};
|
|
971
|
+
/** Positive integer flag with an optional minimum (exclusive). */
|
|
972
|
+
const positiveIntFlag = (key, opts) => (state, acc) => {
|
|
973
|
+
const flag = state.tokens[state.index] ?? "";
|
|
974
|
+
const raw = peekValue(state);
|
|
975
|
+
const error = requireValue(state, flag);
|
|
976
|
+
if (Option.isSome(error)) return error;
|
|
977
|
+
const decoded = decodePositiveInt(raw, flag);
|
|
978
|
+
if (Result.isFailure(decoded)) return Option.some(decoded.failure);
|
|
979
|
+
if (opts?.min !== void 0 && decoded.success <= opts.min) return Option.some(usageError(`${flag} must be greater than ${opts.min}.`));
|
|
980
|
+
acc[key] = decoded.success;
|
|
981
|
+
return Option.none();
|
|
982
|
+
};
|
|
983
|
+
/** Enum flag: consumes next token, validates membership, assigns to key. */
|
|
984
|
+
const enumFlag = (key, valid, label) => (state, acc) => {
|
|
985
|
+
const flag = state.tokens[state.index] ?? "";
|
|
986
|
+
const raw = peekValue(state);
|
|
987
|
+
const error = requireValue(state, flag);
|
|
988
|
+
if (Option.isSome(error)) return error;
|
|
989
|
+
const decoded = decodeEnum(raw, valid, label);
|
|
990
|
+
if (Result.isFailure(decoded)) return Option.some(decoded.failure);
|
|
991
|
+
acc[key] = decoded.success;
|
|
992
|
+
return Option.none();
|
|
993
|
+
};
|
|
994
|
+
const createDefaultOptions = () => ({
|
|
995
|
+
color: true,
|
|
996
|
+
dbPath: void 0,
|
|
997
|
+
help: false,
|
|
998
|
+
json: false,
|
|
999
|
+
quiet: false,
|
|
1000
|
+
repo: void 0,
|
|
1001
|
+
verbose: false,
|
|
1002
|
+
version: false
|
|
1003
|
+
});
|
|
1004
|
+
/**
|
|
1005
|
+
* Global flags are accepted before or after subcommands because wrappers often
|
|
1006
|
+
* prepend them without preserving the CLI's preferred ordering.
|
|
1007
|
+
*/
|
|
1008
|
+
const GLOBAL_FLAG_HANDLERS = {
|
|
1009
|
+
"--db-path": stringFlag("dbPath"),
|
|
1010
|
+
"--help": boolFlag("help"),
|
|
1011
|
+
"--json": boolFlag("json"),
|
|
1012
|
+
"--no-color": (state, acc) => {
|
|
1013
|
+
acc.color = false;
|
|
1014
|
+
state.index += 1;
|
|
1015
|
+
return Option.none();
|
|
1016
|
+
},
|
|
1017
|
+
"--quiet": boolFlag("quiet"),
|
|
1018
|
+
"--repo": stringFlag("repo"),
|
|
1019
|
+
"--verbose": boolFlag("verbose"),
|
|
1020
|
+
"--version": boolFlag("version")
|
|
1021
|
+
};
|
|
1022
|
+
const maybeParseGlobalFlag = (state) => {
|
|
1023
|
+
const token = state.tokens[state.index];
|
|
1024
|
+
if (!token) return false;
|
|
1025
|
+
const handler = GLOBAL_FLAG_HANDLERS[token];
|
|
1026
|
+
if (!handler) return false;
|
|
1027
|
+
handler(state, state.options);
|
|
1028
|
+
return true;
|
|
1029
|
+
};
|
|
1030
|
+
/**
|
|
1031
|
+
* Consumes all remaining tokens in {@link state} by dispatching to the matching
|
|
1032
|
+
* handler in {@link handlers}. Global flags are tried first. Unknown flags
|
|
1033
|
+
* produce a usage error naming the {@link commandLabel}.
|
|
1034
|
+
*/
|
|
1035
|
+
const runFlagLoop = (state, acc, handlers, commandLabel) => {
|
|
1036
|
+
while (state.index < state.tokens.length) {
|
|
1037
|
+
if (maybeParseGlobalFlag(state)) continue;
|
|
1038
|
+
const token = state.tokens[state.index] ?? "";
|
|
1039
|
+
const handler = handlers[token];
|
|
1040
|
+
if (!handler) return Option.some(usageError(`Unknown flag for ${commandLabel}: ${token}.`));
|
|
1041
|
+
const error = handler(state, acc);
|
|
1042
|
+
if (Option.isSome(error)) return error;
|
|
1043
|
+
}
|
|
1044
|
+
return Option.none();
|
|
1045
|
+
};
|
|
1046
|
+
const REVIEW_LIST_FLAGS = {
|
|
1047
|
+
"--limit": positiveIntFlag("limit", { min: 0 }),
|
|
1048
|
+
"--page": positiveIntFlag("page", { min: 0 }),
|
|
1049
|
+
"--source": enumFlag("source", REVIEW_SOURCES, "review source"),
|
|
1050
|
+
"--status": enumFlag("status", REVIEW_STATUSES, "review status")
|
|
1051
|
+
};
|
|
1052
|
+
const parseReviewList = (state) => {
|
|
1053
|
+
const acc = {
|
|
1054
|
+
limit: 20,
|
|
1055
|
+
page: 1,
|
|
1056
|
+
source: void 0,
|
|
1057
|
+
status: void 0
|
|
1058
|
+
};
|
|
1059
|
+
const error = runFlagLoop(state, acc, REVIEW_LIST_FLAGS, "review list");
|
|
1060
|
+
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1061
|
+
return Result.succeed({
|
|
1062
|
+
kind: "review-list",
|
|
1063
|
+
...acc
|
|
1064
|
+
});
|
|
1065
|
+
};
|
|
1066
|
+
const REVIEW_SHOW_FLAGS = {
|
|
1067
|
+
"--comments": boolFlag("comments"),
|
|
1068
|
+
"--todos": boolFlag("todos")
|
|
1069
|
+
};
|
|
1070
|
+
const parseReviewShow = (state) => {
|
|
1071
|
+
const id = state.tokens[state.index];
|
|
1072
|
+
if (!id) return Result.fail(usageError("review show requires <id|last>."));
|
|
1073
|
+
state.index += 1;
|
|
1074
|
+
const acc = {
|
|
1075
|
+
comments: false,
|
|
1076
|
+
todos: false
|
|
1077
|
+
};
|
|
1078
|
+
const error = runFlagLoop(state, acc, REVIEW_SHOW_FLAGS, "review show");
|
|
1079
|
+
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1080
|
+
return Result.succeed({
|
|
1081
|
+
id,
|
|
1082
|
+
kind: "review-show",
|
|
1083
|
+
...acc
|
|
1084
|
+
});
|
|
1085
|
+
};
|
|
1086
|
+
const REVIEW_EXPORT_FLAGS = {
|
|
1087
|
+
"--no-resolved": boolFlag("noResolved"),
|
|
1088
|
+
"--no-snippets": boolFlag("noSnippets"),
|
|
1089
|
+
"--output": stringFlag("outputPath"),
|
|
1090
|
+
"--stdout": boolFlag("stdout")
|
|
1091
|
+
};
|
|
1092
|
+
const parseReviewExport = (state) => {
|
|
1093
|
+
const id = state.tokens[state.index];
|
|
1094
|
+
if (!id) return Result.fail(usageError("review export requires <id|last>."));
|
|
1095
|
+
state.index += 1;
|
|
1096
|
+
const acc = {
|
|
1097
|
+
noResolved: false,
|
|
1098
|
+
noSnippets: false,
|
|
1099
|
+
outputPath: void 0,
|
|
1100
|
+
stdout: false
|
|
1101
|
+
};
|
|
1102
|
+
const error = runFlagLoop(state, acc, REVIEW_EXPORT_FLAGS, "review export");
|
|
1103
|
+
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1104
|
+
return Result.succeed({
|
|
1105
|
+
id,
|
|
1106
|
+
kind: "review-export",
|
|
1107
|
+
...acc
|
|
1108
|
+
});
|
|
1109
|
+
};
|
|
1110
|
+
const REVIEW_CREATE_FLAGS = {
|
|
1111
|
+
"--branch": stringFlag("branch"),
|
|
1112
|
+
"--commits": stringFlag("commits"),
|
|
1113
|
+
"--source": enumFlag("source", REVIEW_SOURCES, "review source"),
|
|
1114
|
+
"--title": stringFlag("title")
|
|
1115
|
+
};
|
|
1116
|
+
const validateReviewCreate = (acc) => {
|
|
1117
|
+
if (acc.source === "branch" && !acc.branch) return Option.some(usageError("review create --source branch requires --branch."));
|
|
1118
|
+
if (acc.source === "commits" && !acc.commits) return Option.some(usageError("review create --source commits requires --commits."));
|
|
1119
|
+
if (acc.source === "staged" && (acc.branch || acc.commits)) return Option.some(usageError("review create --source staged does not accept --branch or --commits."));
|
|
1120
|
+
return Option.none();
|
|
1121
|
+
};
|
|
1122
|
+
const parseReviewCreate = (state) => {
|
|
1123
|
+
const acc = {
|
|
1124
|
+
branch: void 0,
|
|
1125
|
+
commits: void 0,
|
|
1126
|
+
source: "staged",
|
|
1127
|
+
title: void 0
|
|
1128
|
+
};
|
|
1129
|
+
const error = runFlagLoop(state, acc, REVIEW_CREATE_FLAGS, "review create");
|
|
1130
|
+
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1131
|
+
const validationError = validateReviewCreate(acc);
|
|
1132
|
+
if (Option.isSome(validationError)) return Result.fail(validationError.value);
|
|
1133
|
+
return Result.succeed({
|
|
1134
|
+
kind: "review-create",
|
|
1135
|
+
...acc
|
|
1136
|
+
});
|
|
1137
|
+
};
|
|
1138
|
+
const SOURCE_DIFF_FLAGS = {
|
|
1139
|
+
"--branch": stringFlag("branch"),
|
|
1140
|
+
"--commits": stringFlag("commits"),
|
|
1141
|
+
"--stat": boolFlag("stat")
|
|
1142
|
+
};
|
|
1143
|
+
const validateSourceDiff = (source, acc) => {
|
|
1144
|
+
if (source === "branch" && !acc.branch) return Option.some(usageError("source diff branch requires --branch."));
|
|
1145
|
+
if (source === "commits" && !acc.commits) return Option.some(usageError("source diff commits requires --commits."));
|
|
1146
|
+
return Option.none();
|
|
1147
|
+
};
|
|
1148
|
+
const parseSourceDiff = (state) => {
|
|
1149
|
+
const source = state.tokens[state.index];
|
|
1150
|
+
if (!source || !REVIEW_SOURCES.has(source)) return Result.fail(usageError("source diff requires <staged|branch|commits>."));
|
|
1151
|
+
state.index += 1;
|
|
1152
|
+
const acc = {
|
|
1153
|
+
branch: void 0,
|
|
1154
|
+
commits: void 0,
|
|
1155
|
+
stat: false
|
|
1156
|
+
};
|
|
1157
|
+
const error = runFlagLoop(state, acc, SOURCE_DIFF_FLAGS, "source diff");
|
|
1158
|
+
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1159
|
+
const validationError = validateSourceDiff(source, acc);
|
|
1160
|
+
if (Option.isSome(validationError)) return Result.fail(validationError.value);
|
|
1161
|
+
return Result.succeed({
|
|
1162
|
+
kind: "source-diff",
|
|
1163
|
+
source,
|
|
1164
|
+
...acc
|
|
1165
|
+
});
|
|
1166
|
+
};
|
|
1167
|
+
const TODO_LIST_FLAGS = {
|
|
1168
|
+
"--limit": positiveIntFlag("limit"),
|
|
1169
|
+
"--offset": positiveIntFlag("offset"),
|
|
1170
|
+
"--review": stringFlag("reviewId"),
|
|
1171
|
+
"--status": enumFlag("status", TODO_STATUSES, "todo status")
|
|
1172
|
+
};
|
|
1173
|
+
const parseTodoList = (state) => {
|
|
1174
|
+
const acc = {
|
|
1175
|
+
limit: void 0,
|
|
1176
|
+
offset: 0,
|
|
1177
|
+
reviewId: void 0,
|
|
1178
|
+
status: "pending"
|
|
1179
|
+
};
|
|
1180
|
+
const error = runFlagLoop(state, acc, TODO_LIST_FLAGS, "todo list");
|
|
1181
|
+
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1182
|
+
return Result.succeed({
|
|
1183
|
+
kind: "todo-list",
|
|
1184
|
+
...acc
|
|
1185
|
+
});
|
|
1186
|
+
};
|
|
1187
|
+
/**
|
|
1188
|
+
* Factory for commands that take exactly one positional `<id>` and no
|
|
1189
|
+
* command-specific flags (only globals). Avoids duplication for done/undone.
|
|
1190
|
+
*/
|
|
1191
|
+
const positionalIdParser = (kind, label) => (state) => {
|
|
1192
|
+
const id = state.tokens[state.index];
|
|
1193
|
+
if (!id) return Result.fail(usageError(`${label} requires <id>.`));
|
|
1194
|
+
state.index += 1;
|
|
1195
|
+
while (state.index < state.tokens.length) if (!maybeParseGlobalFlag(state)) return Result.fail(usageError(`Unknown flag for ${label}: ${state.tokens[state.index]}.`));
|
|
1196
|
+
return Result.succeed({
|
|
1197
|
+
id,
|
|
1198
|
+
kind
|
|
1199
|
+
});
|
|
1200
|
+
};
|
|
1201
|
+
const parseTodoDone = positionalIdParser("todo-done", "todo done");
|
|
1202
|
+
const parseTodoUndone = positionalIdParser("todo-undone", "todo undone");
|
|
1203
|
+
const TODO_MOVE_FLAGS = { "--position": positiveIntFlag("position") };
|
|
1204
|
+
const parseTodoMove = (state) => {
|
|
1205
|
+
const id = state.tokens[state.index];
|
|
1206
|
+
if (!id) return Result.fail(usageError("todo move requires <id>."));
|
|
1207
|
+
state.index += 1;
|
|
1208
|
+
const acc = { position: void 0 };
|
|
1209
|
+
const error = runFlagLoop(state, acc, TODO_MOVE_FLAGS, "todo move");
|
|
1210
|
+
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1211
|
+
if (acc.position === void 0) return Result.fail(usageError("todo move requires --position."));
|
|
1212
|
+
return Result.succeed({
|
|
1213
|
+
id,
|
|
1214
|
+
kind: "todo-move",
|
|
1215
|
+
position: acc.position
|
|
1216
|
+
});
|
|
1217
|
+
};
|
|
1218
|
+
const TODO_REMOVE_FLAGS = { "--yes": boolFlag("yes") };
|
|
1219
|
+
const parseTodoRemove = (state) => {
|
|
1220
|
+
const id = state.tokens[state.index];
|
|
1221
|
+
if (!id) return Result.fail(usageError("todo remove requires <id>."));
|
|
1222
|
+
state.index += 1;
|
|
1223
|
+
const acc = { yes: false };
|
|
1224
|
+
const error = runFlagLoop(state, acc, TODO_REMOVE_FLAGS, "todo remove");
|
|
1225
|
+
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1226
|
+
return Result.succeed({
|
|
1227
|
+
id,
|
|
1228
|
+
kind: "todo-remove",
|
|
1229
|
+
...acc
|
|
1230
|
+
});
|
|
1231
|
+
};
|
|
1232
|
+
const TODO_CLEAR_FLAGS = {
|
|
1233
|
+
"--all": boolFlag("all"),
|
|
1234
|
+
"--done-only": boolFlag("doneOnly"),
|
|
1235
|
+
"--review": stringFlag("reviewId"),
|
|
1236
|
+
"--yes": boolFlag("yes")
|
|
1237
|
+
};
|
|
1238
|
+
const parseTodoClear = (state) => {
|
|
1239
|
+
const acc = {
|
|
1240
|
+
all: false,
|
|
1241
|
+
doneOnly: true,
|
|
1242
|
+
reviewId: void 0,
|
|
1243
|
+
yes: false
|
|
1244
|
+
};
|
|
1245
|
+
const error = runFlagLoop(state, acc, TODO_CLEAR_FLAGS, "todo clear");
|
|
1246
|
+
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1247
|
+
return Result.succeed({
|
|
1248
|
+
kind: "todo-clear",
|
|
1249
|
+
...acc
|
|
1250
|
+
});
|
|
1251
|
+
};
|
|
1252
|
+
const REVIEW_STATUS_FLAGS = {
|
|
1253
|
+
"--review": stringFlag("reviewId"),
|
|
1254
|
+
"--source": enumFlag("source", REVIEW_SOURCES, "review source")
|
|
1255
|
+
};
|
|
1256
|
+
const parseReviewStatus = (state) => {
|
|
1257
|
+
const acc = {
|
|
1258
|
+
reviewId: void 0,
|
|
1259
|
+
source: void 0
|
|
1260
|
+
};
|
|
1261
|
+
const error = runFlagLoop(state, acc, REVIEW_STATUS_FLAGS, "review status");
|
|
1262
|
+
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1263
|
+
return Result.succeed({
|
|
1264
|
+
kind: "review-status",
|
|
1265
|
+
...acc
|
|
1266
|
+
});
|
|
1267
|
+
};
|
|
1268
|
+
const REVIEW_RESOLVE_FLAGS = {
|
|
1269
|
+
"--all-comments": boolFlag("allComments"),
|
|
1270
|
+
"--yes": boolFlag("yes")
|
|
1271
|
+
};
|
|
1272
|
+
const parseReviewResolve = (state) => {
|
|
1273
|
+
const id = state.tokens[state.index];
|
|
1274
|
+
if (!id) return Result.fail(usageError("review resolve requires <id|last>."));
|
|
1275
|
+
state.index += 1;
|
|
1276
|
+
const acc = {
|
|
1277
|
+
allComments: true,
|
|
1278
|
+
yes: false
|
|
1279
|
+
};
|
|
1280
|
+
const error = runFlagLoop(state, acc, REVIEW_RESOLVE_FLAGS, "review resolve");
|
|
1281
|
+
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1282
|
+
return Result.succeed({
|
|
1283
|
+
id,
|
|
1284
|
+
kind: "review-resolve",
|
|
1285
|
+
...acc
|
|
1286
|
+
});
|
|
1287
|
+
};
|
|
1288
|
+
const TODO_ADD_FLAGS = {
|
|
1289
|
+
"--position": positiveIntFlag("position"),
|
|
1290
|
+
"--review": stringFlag("reviewId"),
|
|
1291
|
+
"--text": stringFlag("text")
|
|
1292
|
+
};
|
|
1293
|
+
const parseTodoAdd = (state) => {
|
|
1294
|
+
const acc = {
|
|
1295
|
+
position: void 0,
|
|
1296
|
+
reviewId: void 0,
|
|
1297
|
+
text: ""
|
|
1298
|
+
};
|
|
1299
|
+
const error = runFlagLoop(state, acc, TODO_ADD_FLAGS, "todo add");
|
|
1300
|
+
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1301
|
+
if (!acc.text.trim()) return Result.fail(usageError("todo add requires --text."));
|
|
1302
|
+
return Result.succeed({
|
|
1303
|
+
kind: "todo-add",
|
|
1304
|
+
...acc
|
|
1305
|
+
});
|
|
1306
|
+
};
|
|
1307
|
+
const SERVE_FLAGS = {
|
|
1308
|
+
"--auth": boolFlag("auth"),
|
|
1309
|
+
"--cert": stringFlag("cert"),
|
|
1310
|
+
"--host": stringFlag("host"),
|
|
1311
|
+
"--https": boolFlag("https"),
|
|
1312
|
+
"--key": stringFlag("key"),
|
|
1313
|
+
"--no-open": boolFlag("noOpen"),
|
|
1314
|
+
"--password": stringFlag("password"),
|
|
1315
|
+
"--port": positiveIntFlag("port", { min: 0 }),
|
|
1316
|
+
"--username": stringFlag("username")
|
|
1317
|
+
};
|
|
1318
|
+
const parseServe = (state) => {
|
|
1319
|
+
const acc = {
|
|
1320
|
+
auth: false,
|
|
1321
|
+
cert: void 0,
|
|
1322
|
+
host: "127.0.0.1",
|
|
1323
|
+
https: false,
|
|
1324
|
+
key: void 0,
|
|
1325
|
+
noOpen: false,
|
|
1326
|
+
password: void 0,
|
|
1327
|
+
port: 3e3,
|
|
1328
|
+
username: void 0
|
|
1329
|
+
};
|
|
1330
|
+
const error = runFlagLoop(state, acc, SERVE_FLAGS, "serve");
|
|
1331
|
+
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1332
|
+
return Result.succeed({
|
|
1333
|
+
kind: "serve",
|
|
1334
|
+
...acc
|
|
1335
|
+
});
|
|
1336
|
+
};
|
|
1337
|
+
const MCP_LOG_LEVELS = new Set([
|
|
1338
|
+
"debug",
|
|
1339
|
+
"error",
|
|
1340
|
+
"info",
|
|
1341
|
+
"silent"
|
|
1342
|
+
]);
|
|
1343
|
+
const MCP_FLAGS = {
|
|
1344
|
+
"--log-level": enumFlag("logLevel", MCP_LOG_LEVELS, "log level"),
|
|
1345
|
+
"--readonly": boolFlag("readonly")
|
|
1346
|
+
};
|
|
1347
|
+
const parseMcp = (state) => {
|
|
1348
|
+
const acc = {
|
|
1349
|
+
logLevel: "error",
|
|
1350
|
+
readonly: false
|
|
1351
|
+
};
|
|
1352
|
+
const error = runFlagLoop(state, acc, MCP_FLAGS, "mcp");
|
|
1353
|
+
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1354
|
+
return Result.succeed({
|
|
1355
|
+
kind: "mcp",
|
|
1356
|
+
...acc
|
|
1357
|
+
});
|
|
1358
|
+
};
|
|
1359
|
+
const EVENT_TYPES = new Set([
|
|
1360
|
+
"comments",
|
|
1361
|
+
"files",
|
|
1362
|
+
"reviews",
|
|
1363
|
+
"todos"
|
|
1364
|
+
]);
|
|
1365
|
+
const EVENTS_FLAGS = {
|
|
1366
|
+
"--since": positiveIntFlag("since"),
|
|
1367
|
+
"--type": enumFlag("type", EVENT_TYPES, "event type")
|
|
1368
|
+
};
|
|
1369
|
+
const parseEvents = (state) => {
|
|
1370
|
+
const acc = {
|
|
1371
|
+
since: void 0,
|
|
1372
|
+
type: void 0
|
|
1373
|
+
};
|
|
1374
|
+
const error = runFlagLoop(state, acc, EVENTS_FLAGS, "events");
|
|
1375
|
+
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1376
|
+
return Result.succeed({
|
|
1377
|
+
kind: "events",
|
|
1378
|
+
...acc
|
|
1379
|
+
});
|
|
1380
|
+
};
|
|
1381
|
+
const DATA_RESET_FLAGS = {
|
|
1382
|
+
"--keep-exports": boolFlag("keepExports"),
|
|
1383
|
+
"--yes": boolFlag("yes")
|
|
1384
|
+
};
|
|
1385
|
+
const parseDataReset = (state) => {
|
|
1386
|
+
const acc = {
|
|
1387
|
+
keepExports: false,
|
|
1388
|
+
yes: false
|
|
1389
|
+
};
|
|
1390
|
+
const error = runFlagLoop(state, acc, DATA_RESET_FLAGS, "data reset");
|
|
1391
|
+
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1392
|
+
return Result.succeed({
|
|
1393
|
+
kind: "data-reset",
|
|
1394
|
+
...acc
|
|
1395
|
+
});
|
|
1396
|
+
};
|
|
1397
|
+
const ensureNoExtraArgs = (state, label) => {
|
|
1398
|
+
if (state.index < state.tokens.length) return Option.some(usageError(`Unexpected argument for ${label}: ${state.tokens[state.index]}.`));
|
|
1399
|
+
return Option.none();
|
|
1400
|
+
};
|
|
1401
|
+
/** Review verb parsers keyed by verb name. */
|
|
1402
|
+
const REVIEW_VERB_PARSERS = {
|
|
1403
|
+
create: parseReviewCreate,
|
|
1404
|
+
export: parseReviewExport,
|
|
1405
|
+
list: parseReviewList,
|
|
1406
|
+
resolve: parseReviewResolve,
|
|
1407
|
+
show: parseReviewShow,
|
|
1408
|
+
status: parseReviewStatus
|
|
1409
|
+
};
|
|
1410
|
+
/** Todo verb parsers keyed by verb name. */
|
|
1411
|
+
const TODO_VERB_PARSERS = {
|
|
1412
|
+
add: parseTodoAdd,
|
|
1413
|
+
clear: parseTodoClear,
|
|
1414
|
+
done: parseTodoDone,
|
|
1415
|
+
list: parseTodoList,
|
|
1416
|
+
move: parseTodoMove,
|
|
1417
|
+
remove: parseTodoRemove,
|
|
1418
|
+
undone: parseTodoUndone
|
|
1419
|
+
};
|
|
1420
|
+
const REVIEW_PR_FLAGS = {
|
|
1421
|
+
"--force-refresh": boolFlag("forceRefresh"),
|
|
1422
|
+
"--no-open": boolFlag("noOpen"),
|
|
1423
|
+
"--port": positiveIntFlag("port", { min: 0 })
|
|
1424
|
+
};
|
|
1425
|
+
const parseReviewPr = (state, prUrl) => {
|
|
1426
|
+
const acc = {
|
|
1427
|
+
forceRefresh: false,
|
|
1428
|
+
noOpen: false,
|
|
1429
|
+
port: 3e3
|
|
1430
|
+
};
|
|
1431
|
+
const error = runFlagLoop(state, acc, REVIEW_PR_FLAGS, "review <pr-url>");
|
|
1432
|
+
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1433
|
+
return Result.succeed({
|
|
1434
|
+
kind: "review-pr",
|
|
1435
|
+
prUrl,
|
|
1436
|
+
...acc
|
|
1437
|
+
});
|
|
1438
|
+
};
|
|
1439
|
+
/** Subcommand family parsers keyed by family name. */
|
|
1440
|
+
/** Data verb parsers keyed by verb name. */
|
|
1441
|
+
const DATA_VERB_PARSERS = {
|
|
1442
|
+
migrate: (state) => {
|
|
1443
|
+
const error = ensureNoExtraArgs(state, "data migrate");
|
|
1444
|
+
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1445
|
+
return Result.succeed({ kind: "data-migrate" });
|
|
1446
|
+
},
|
|
1447
|
+
reset: parseDataReset
|
|
1448
|
+
};
|
|
1449
|
+
/** Subcommand family parsers keyed by family name. */
|
|
1450
|
+
const FAMILY_PARSERS = {
|
|
1451
|
+
data: (state) => {
|
|
1452
|
+
const verb = state.tokens[state.index];
|
|
1453
|
+
if (!verb) return Result.succeed({
|
|
1454
|
+
kind: "help",
|
|
1455
|
+
topic: ["data"]
|
|
1456
|
+
});
|
|
1457
|
+
state.index += 1;
|
|
1458
|
+
const parser = DATA_VERB_PARSERS[verb];
|
|
1459
|
+
if (!parser) return Result.fail(usageError(`Unknown data command: ${verb}.`));
|
|
1460
|
+
return parser(state);
|
|
1461
|
+
},
|
|
1462
|
+
doctor: (state) => {
|
|
1463
|
+
const error = ensureNoExtraArgs(state, "doctor");
|
|
1464
|
+
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1465
|
+
return Result.succeed({ kind: "doctor" });
|
|
1466
|
+
},
|
|
1467
|
+
events: parseEvents,
|
|
1468
|
+
export: parseReviewExport,
|
|
1469
|
+
mcp: parseMcp,
|
|
1470
|
+
review: (state) => {
|
|
1471
|
+
const verb = state.tokens[state.index];
|
|
1472
|
+
if (!verb) return Result.succeed({
|
|
1473
|
+
kind: "help",
|
|
1474
|
+
topic: ["review"]
|
|
1475
|
+
});
|
|
1476
|
+
if (looksLikePrUrl(verb)) {
|
|
1477
|
+
state.index += 1;
|
|
1478
|
+
return parseReviewPr(state, verb);
|
|
1479
|
+
}
|
|
1480
|
+
state.index += 1;
|
|
1481
|
+
const parser = REVIEW_VERB_PARSERS[verb];
|
|
1482
|
+
if (!parser) return Result.fail(usageError(`Unknown review command: ${verb}.`));
|
|
1483
|
+
return parser(state);
|
|
1484
|
+
},
|
|
1485
|
+
serve: parseServe,
|
|
1486
|
+
source: (state) => {
|
|
1487
|
+
const verb = state.tokens[state.index];
|
|
1488
|
+
if (!verb) return Result.succeed({
|
|
1489
|
+
kind: "help",
|
|
1490
|
+
topic: ["source"]
|
|
1491
|
+
});
|
|
1492
|
+
state.index += 1;
|
|
1493
|
+
if (verb === "list") {
|
|
1494
|
+
const error = ensureNoExtraArgs(state, "source list");
|
|
1495
|
+
if (Option.isSome(error)) return Result.fail(error.value);
|
|
1496
|
+
return Result.succeed({ kind: "source-list" });
|
|
1497
|
+
}
|
|
1498
|
+
if (verb === "diff") return parseSourceDiff(state);
|
|
1499
|
+
return Result.fail(usageError(`Unknown source command: ${verb}.`));
|
|
1500
|
+
},
|
|
1501
|
+
todo: (state) => {
|
|
1502
|
+
const verb = state.tokens[state.index];
|
|
1503
|
+
if (!verb) return Result.succeed({
|
|
1504
|
+
kind: "help",
|
|
1505
|
+
topic: ["todo"]
|
|
1506
|
+
});
|
|
1507
|
+
state.index += 1;
|
|
1508
|
+
const parser = TODO_VERB_PARSERS[verb];
|
|
1509
|
+
if (!parser) return Result.fail(usageError(`Unknown todo command: ${verb}.`));
|
|
1510
|
+
return parser(state);
|
|
1511
|
+
}
|
|
1512
|
+
};
|
|
1513
|
+
/**
|
|
1514
|
+
* Strips any leading globals with {@link maybeParseGlobalFlag} before dispatching
|
|
1515
|
+
* into a command family so `--help` and `--version` behave consistently.
|
|
1516
|
+
*/
|
|
1517
|
+
const parseWithState = (state) => {
|
|
1518
|
+
while (state.index < state.tokens.length && maybeParseGlobalFlag(state));
|
|
1519
|
+
if (state.options.version) return Result.succeed({ kind: "version" });
|
|
1520
|
+
if (state.index >= state.tokens.length) return Result.succeed({
|
|
1521
|
+
kind: "help",
|
|
1522
|
+
topic: []
|
|
1523
|
+
});
|
|
1524
|
+
const first = state.tokens[state.index];
|
|
1525
|
+
if (!first) return Result.succeed({
|
|
1526
|
+
kind: "help",
|
|
1527
|
+
topic: []
|
|
1528
|
+
});
|
|
1529
|
+
if (first === "help") {
|
|
1530
|
+
const topic = state.tokens.slice(state.index + 1);
|
|
1531
|
+
state.index = state.tokens.length;
|
|
1532
|
+
return Result.succeed({
|
|
1533
|
+
kind: "help",
|
|
1534
|
+
topic
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
if (state.options.help) return Result.succeed({
|
|
1538
|
+
kind: "help",
|
|
1539
|
+
topic: state.tokens.slice(state.index)
|
|
1540
|
+
});
|
|
1541
|
+
state.index += 1;
|
|
1542
|
+
const familyParser = FAMILY_PARSERS[first];
|
|
1543
|
+
if (!familyParser) return Result.fail(usageError(`Unknown command: ${first}.`));
|
|
1544
|
+
return familyParser(state);
|
|
1545
|
+
};
|
|
1546
|
+
const parseCliArgs = (argv) => {
|
|
1547
|
+
const options = createDefaultOptions();
|
|
1548
|
+
const result = parseWithState({
|
|
1549
|
+
index: 0,
|
|
1550
|
+
options,
|
|
1551
|
+
tokens: argv
|
|
1552
|
+
});
|
|
1553
|
+
if (Result.isFailure(result)) return Result.fail(result.failure);
|
|
1554
|
+
return Result.succeed({
|
|
1555
|
+
command: result.success,
|
|
1556
|
+
options
|
|
1557
|
+
});
|
|
1558
|
+
};
|
|
1559
|
+
//#endregion
|
|
1560
|
+
//#region src/cli/runtime.ts
|
|
1561
|
+
const resolveRepositoryRoot = (repoOverride) => {
|
|
1562
|
+
const cwd = repoOverride ? resolve(repoOverride) : process.cwd();
|
|
1563
|
+
try {
|
|
1564
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
1565
|
+
cwd,
|
|
1566
|
+
encoding: "utf8"
|
|
1567
|
+
}).trim();
|
|
1568
|
+
} catch {
|
|
1569
|
+
return new CliFailure({
|
|
1570
|
+
exitCode: ExitCode.StateUnavailable,
|
|
1571
|
+
message: repoOverride ? `Path ${cwd} is not a Git repository. Use --repo <path> with a valid repository root.` : `Could not resolve a Git repository from ${cwd}. Use --repo <path> with a valid repository root.`
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
};
|
|
1575
|
+
const resolveDbPath = (repoRoot, dbPathOverride) => dbPathOverride ? resolve(dbPathOverride) : resolve(repoRoot, ".ringi/reviews.db");
|
|
1576
|
+
const commandNeedsRepository = (command) => command.kind !== "help" && command.kind !== "version" && command.kind !== "mcp" && command.kind !== "serve";
|
|
1577
|
+
/**
|
|
1578
|
+
* Commands that need the database to already exist. `review-pr` is NOT here
|
|
1579
|
+
* because it auto-initializes `.ringi/` (like `serve` does).
|
|
1580
|
+
*/
|
|
1581
|
+
const commandNeedsDatabase = (command) => command.kind === "review-list" || command.kind === "review-show" || command.kind === "review-export" || command.kind === "review-status" || command.kind === "todo-list" || command.kind === "doctor";
|
|
1582
|
+
const commandUsesCoreRuntime = (command) => command.kind === "review-list" || command.kind === "review-show" || command.kind === "review-export" || command.kind === "review-pr" || command.kind === "review-status" || command.kind === "todo-list" || command.kind === "review-create" || command.kind === "todo-add" || command.kind === "doctor";
|
|
1583
|
+
const resolveCliConfig = (args) => {
|
|
1584
|
+
const repoRootResult = resolveRepositoryRoot(args.repo);
|
|
1585
|
+
if (repoRootResult instanceof CliFailure) return repoRootResult;
|
|
1586
|
+
return {
|
|
1587
|
+
color: args.color,
|
|
1588
|
+
cwd: process.cwd(),
|
|
1589
|
+
dbPath: resolveDbPath(repoRootResult, args.dbPath),
|
|
1590
|
+
outputMode: "human",
|
|
1591
|
+
quiet: args.quiet,
|
|
1592
|
+
repoRoot: repoRootResult,
|
|
1593
|
+
verbose: args.verbose
|
|
1594
|
+
};
|
|
1595
|
+
};
|
|
1596
|
+
const ensureLocalStateAvailable = (config) => {
|
|
1597
|
+
if (!existsSync(config.dbPath)) return new CliFailure({
|
|
1598
|
+
exitCode: ExitCode.StateUnavailable,
|
|
1599
|
+
message: `Local state is missing at ${config.dbPath}. Run 'ringi data migrate' or start 'ringi serve' once to initialize local state.`
|
|
1600
|
+
});
|
|
1601
|
+
};
|
|
1602
|
+
const makeConfigLayer = (config) => ConfigProvider.layer(ConfigProvider.fromUnknown({
|
|
1603
|
+
DB_PATH: config.dbPath,
|
|
1604
|
+
REPOSITORY_PATH: config.repoRoot
|
|
1605
|
+
}));
|
|
1606
|
+
const createCoreCliRuntime = (config) => ManagedRuntime.make(Layer.mergeAll(CoreLive, CliConfigLive(config)).pipe(Layer.provideMerge(makeConfigLayer(config))));
|
|
1607
|
+
const createGitCliRuntime = (config) => ManagedRuntime.make(Layer.mergeAll(GitService.Default, CliConfigLive(config)).pipe(Layer.provideMerge(makeConfigLayer(config))));
|
|
1608
|
+
const createCliRuntimeResources = (command, args) => {
|
|
1609
|
+
if (!commandNeedsRepository(command)) return null;
|
|
1610
|
+
const configResult = resolveCliConfig(args);
|
|
1611
|
+
if (configResult instanceof CliFailure) return configResult;
|
|
1612
|
+
if (commandNeedsDatabase(command)) {
|
|
1613
|
+
const stateError = ensureLocalStateAvailable(configResult);
|
|
1614
|
+
if (stateError) return stateError;
|
|
1615
|
+
}
|
|
1616
|
+
return {
|
|
1617
|
+
config: configResult,
|
|
1618
|
+
runtime: commandUsesCoreRuntime(command) ? createCoreCliRuntime(configResult) : createGitCliRuntime(configResult)
|
|
1619
|
+
};
|
|
1620
|
+
};
|
|
1621
|
+
//#endregion
|
|
1622
|
+
//#region src/cli/main.ts
|
|
1623
|
+
const CLI_VERSION = "0.3.0";
|
|
1624
|
+
const COMMAND_TREE = {
|
|
1625
|
+
commands: [
|
|
1626
|
+
{
|
|
1627
|
+
description: "List review sessions",
|
|
1628
|
+
name: "review list",
|
|
1629
|
+
usage: "ringi review list [--status <status>] [--source <type>] [--limit <n>] [--page <n>]"
|
|
1630
|
+
},
|
|
1631
|
+
{
|
|
1632
|
+
description: "Show review details",
|
|
1633
|
+
name: "review show",
|
|
1634
|
+
usage: "ringi review show <id|last> [--comments] [--todos]"
|
|
1635
|
+
},
|
|
1636
|
+
{
|
|
1637
|
+
description: "Create a review session",
|
|
1638
|
+
name: "review create",
|
|
1639
|
+
usage: "ringi review create [--source <staged|branch|commits>] [--branch <name>] [--commits <range>]"
|
|
1640
|
+
},
|
|
1641
|
+
{
|
|
1642
|
+
description: "Export review as markdown",
|
|
1643
|
+
name: "review export",
|
|
1644
|
+
usage: "ringi review export <id|last> [--output <path>] [--stdout]"
|
|
1645
|
+
},
|
|
1646
|
+
{
|
|
1647
|
+
description: "Resolve a review session",
|
|
1648
|
+
name: "review resolve",
|
|
1649
|
+
usage: "ringi review resolve <id|last> [--all-comments] [--yes]"
|
|
1650
|
+
},
|
|
1651
|
+
{
|
|
1652
|
+
description: "Show repository and review status",
|
|
1653
|
+
name: "review status",
|
|
1654
|
+
usage: "ringi review status [--review <id|last>] [--source <type>]"
|
|
1655
|
+
},
|
|
1656
|
+
{
|
|
1657
|
+
description: "List repository sources",
|
|
1658
|
+
name: "source list",
|
|
1659
|
+
usage: "ringi source list"
|
|
1660
|
+
},
|
|
1661
|
+
{
|
|
1662
|
+
description: "Show diff for a source",
|
|
1663
|
+
name: "source diff",
|
|
1664
|
+
usage: "ringi source diff <staged|branch|commits> [--branch <name>] [--commits <range>] [--stat]"
|
|
1665
|
+
},
|
|
1666
|
+
{
|
|
1667
|
+
description: "List todo items",
|
|
1668
|
+
name: "todo list",
|
|
1669
|
+
usage: "ringi todo list [--review <id>] [--status <pending|done|all>] [--limit <n>] [--offset <n>]"
|
|
1670
|
+
},
|
|
1671
|
+
{
|
|
1672
|
+
description: "Add a todo item",
|
|
1673
|
+
name: "todo add",
|
|
1674
|
+
usage: "ringi todo add --text <text> [--review <id>] [--position <n>]"
|
|
1675
|
+
},
|
|
1676
|
+
{
|
|
1677
|
+
description: "Mark a todo as done",
|
|
1678
|
+
name: "todo done",
|
|
1679
|
+
usage: "ringi todo done <id>"
|
|
1680
|
+
},
|
|
1681
|
+
{
|
|
1682
|
+
description: "Reopen a completed todo",
|
|
1683
|
+
name: "todo undone",
|
|
1684
|
+
usage: "ringi todo undone <id>"
|
|
1685
|
+
},
|
|
1686
|
+
{
|
|
1687
|
+
description: "Move a todo to a position",
|
|
1688
|
+
name: "todo move",
|
|
1689
|
+
usage: "ringi todo move <id> --position <n>"
|
|
1690
|
+
},
|
|
1691
|
+
{
|
|
1692
|
+
description: "Remove a todo",
|
|
1693
|
+
name: "todo remove",
|
|
1694
|
+
usage: "ringi todo remove <id> [--yes]"
|
|
1695
|
+
},
|
|
1696
|
+
{
|
|
1697
|
+
description: "Clear completed todos",
|
|
1698
|
+
name: "todo clear",
|
|
1699
|
+
usage: "ringi todo clear [--review <id>] [--done-only] [--all] [--yes]"
|
|
1700
|
+
},
|
|
1701
|
+
{
|
|
1702
|
+
description: "Start the local Ringi server",
|
|
1703
|
+
name: "serve",
|
|
1704
|
+
usage: "ringi serve [--host <host>] [--port <port>] [--https] [--auth] [--no-open]"
|
|
1705
|
+
},
|
|
1706
|
+
{
|
|
1707
|
+
description: "Start the MCP stdio server",
|
|
1708
|
+
name: "mcp",
|
|
1709
|
+
usage: "ringi mcp [--readonly] [--log-level <level>]"
|
|
1710
|
+
},
|
|
1711
|
+
{
|
|
1712
|
+
description: "Run local diagnostics",
|
|
1713
|
+
name: "doctor",
|
|
1714
|
+
usage: "ringi doctor"
|
|
1715
|
+
},
|
|
1716
|
+
{
|
|
1717
|
+
description: "Tail server events",
|
|
1718
|
+
name: "events",
|
|
1719
|
+
usage: "ringi events [--type <reviews|comments|todos|files>]"
|
|
1720
|
+
},
|
|
1721
|
+
{
|
|
1722
|
+
description: "Run database migrations",
|
|
1723
|
+
name: "data migrate",
|
|
1724
|
+
usage: "ringi data migrate"
|
|
1725
|
+
},
|
|
1726
|
+
{
|
|
1727
|
+
description: "Reset local data",
|
|
1728
|
+
name: "data reset",
|
|
1729
|
+
usage: "ringi data reset [--yes] [--keep-exports]"
|
|
1730
|
+
}
|
|
1731
|
+
],
|
|
1732
|
+
description: "ringi — local-first code review CLI",
|
|
1733
|
+
version: CLI_VERSION
|
|
1734
|
+
};
|
|
1735
|
+
const ROOT_NEXT_ACTIONS = [
|
|
1736
|
+
{
|
|
1737
|
+
command: "ringi review list [--status <status>] [--source <type>]",
|
|
1738
|
+
description: "List review sessions",
|
|
1739
|
+
params: {
|
|
1740
|
+
source: { enum: [
|
|
1741
|
+
"staged",
|
|
1742
|
+
"branch",
|
|
1743
|
+
"commits"
|
|
1744
|
+
] },
|
|
1745
|
+
status: { enum: [
|
|
1746
|
+
"in_progress",
|
|
1747
|
+
"approved",
|
|
1748
|
+
"changes_requested"
|
|
1749
|
+
] }
|
|
1750
|
+
}
|
|
1751
|
+
},
|
|
1752
|
+
{
|
|
1753
|
+
command: "ringi source list",
|
|
1754
|
+
description: "List repository sources"
|
|
1755
|
+
},
|
|
1756
|
+
{
|
|
1757
|
+
command: "ringi review create [--source <source>]",
|
|
1758
|
+
description: "Create a new review session",
|
|
1759
|
+
params: { source: {
|
|
1760
|
+
default: "staged",
|
|
1761
|
+
enum: [
|
|
1762
|
+
"staged",
|
|
1763
|
+
"branch",
|
|
1764
|
+
"commits"
|
|
1765
|
+
]
|
|
1766
|
+
} }
|
|
1767
|
+
},
|
|
1768
|
+
{
|
|
1769
|
+
command: "ringi todo list [--status <status>]",
|
|
1770
|
+
description: "List todos",
|
|
1771
|
+
params: { status: {
|
|
1772
|
+
default: "pending",
|
|
1773
|
+
enum: [
|
|
1774
|
+
"pending",
|
|
1775
|
+
"done",
|
|
1776
|
+
"all"
|
|
1777
|
+
]
|
|
1778
|
+
} }
|
|
1779
|
+
},
|
|
1780
|
+
{
|
|
1781
|
+
command: "ringi review status",
|
|
1782
|
+
description: "Show repository and review status"
|
|
1783
|
+
}
|
|
1784
|
+
];
|
|
1785
|
+
const ROOT_HELP = `ringi — local-first review CLI
|
|
1786
|
+
|
|
1787
|
+
Usage:
|
|
1788
|
+
ringi [global options] <command>
|
|
1789
|
+
|
|
1790
|
+
Global options:
|
|
1791
|
+
--json Emit structured JSON envelope to stdout
|
|
1792
|
+
--repo <path> Use a specific Git repository root
|
|
1793
|
+
--db-path <path> Override the SQLite database path
|
|
1794
|
+
--quiet Suppress human-readable success output
|
|
1795
|
+
--verbose Include stack traces on failures
|
|
1796
|
+
--no-color Disable ANSI color output
|
|
1797
|
+
--help Show help
|
|
1798
|
+
--version Show version
|
|
1799
|
+
|
|
1800
|
+
Commands:
|
|
1801
|
+
review list [--status <status>] [--source <type>] [--limit <n>] [--page <n>]
|
|
1802
|
+
review show <id|last> [--comments] [--todos]
|
|
1803
|
+
review create [--source <staged|branch|commits>] [--branch <name>] [--commits <range>]
|
|
1804
|
+
review export <id|last> [--output <path>] [--stdout]
|
|
1805
|
+
review resolve <id|last> [--all-comments] [--yes]
|
|
1806
|
+
review status [--review <id|last>] [--source <type>]
|
|
1807
|
+
source list
|
|
1808
|
+
source diff <staged|branch|commits> [--branch <name>] [--commits <range>] [--stat]
|
|
1809
|
+
todo list [--review <id>] [--status <pending|done|all>] [--limit <n>] [--offset <n>]
|
|
1810
|
+
todo add --text <text> [--review <id>]
|
|
1811
|
+
todo done <id>
|
|
1812
|
+
todo undone <id>
|
|
1813
|
+
todo move <id> --position <n>
|
|
1814
|
+
todo remove <id> [--yes]
|
|
1815
|
+
todo clear [--review <id>] [--done-only] [--all] [--yes]
|
|
1816
|
+
export <id|last> [--output <path>] [--stdout]
|
|
1817
|
+
`;
|
|
1818
|
+
const HELP_TOPICS = {
|
|
1819
|
+
data: `ringi data
|
|
1820
|
+
|
|
1821
|
+
Usage:
|
|
1822
|
+
ringi data migrate
|
|
1823
|
+
ringi data reset [--yes] [--keep-exports]
|
|
1824
|
+
`,
|
|
1825
|
+
review: `ringi review
|
|
1826
|
+
|
|
1827
|
+
Usage:
|
|
1828
|
+
ringi review list [--status <status>] [--source <type>] [--limit <n>] [--page <n>]
|
|
1829
|
+
ringi review show <id|last> [--comments] [--todos]
|
|
1830
|
+
ringi review create [--source <staged|branch|commits>] [--branch <name>] [--commits <range>]
|
|
1831
|
+
ringi review export <id|last> [--output <path>] [--stdout]
|
|
1832
|
+
ringi review resolve <id|last> [--all-comments] [--yes]
|
|
1833
|
+
ringi review status [--review <id|last>] [--source <type>]
|
|
1834
|
+
`,
|
|
1835
|
+
source: `ringi source
|
|
1836
|
+
|
|
1837
|
+
Usage:
|
|
1838
|
+
ringi source list
|
|
1839
|
+
ringi source diff <staged|branch|commits> [--branch <name>] [--commits <range>] [--stat]
|
|
1840
|
+
`,
|
|
1841
|
+
todo: `ringi todo
|
|
1842
|
+
|
|
1843
|
+
Usage:
|
|
1844
|
+
ringi todo list [--review <id>] [--status <pending|done|all>] [--limit <n>] [--offset <n>]
|
|
1845
|
+
ringi todo add --text <text> [--review <id>] [--position <n>]
|
|
1846
|
+
ringi todo done <id>
|
|
1847
|
+
ringi todo undone <id>
|
|
1848
|
+
ringi todo move <id> --position <n>
|
|
1849
|
+
ringi todo remove <id> [--yes]
|
|
1850
|
+
ringi todo clear [--review <id>] [--done-only] [--all] [--yes]
|
|
1851
|
+
`
|
|
1852
|
+
};
|
|
1853
|
+
const renderHelp = (command) => {
|
|
1854
|
+
if (command.kind !== "help") return ROOT_HELP;
|
|
1855
|
+
const [topic] = command.topic;
|
|
1856
|
+
return (topic && HELP_TOPICS[topic]) ?? ROOT_HELP;
|
|
1857
|
+
};
|
|
1858
|
+
const writeJson = (payload) => {
|
|
1859
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
1860
|
+
};
|
|
1861
|
+
const writeHuman = (text) => {
|
|
1862
|
+
if (text && text.length > 0) process.stdout.write(`${text}\n`);
|
|
1863
|
+
};
|
|
1864
|
+
/** Maps exit codes to error categories and retryable status. */
|
|
1865
|
+
const EXIT_CODE_META = {
|
|
1866
|
+
[ExitCode.AuthFailure]: {
|
|
1867
|
+
category: "auth",
|
|
1868
|
+
code: "AUTH_FAILURE",
|
|
1869
|
+
retryable: false
|
|
1870
|
+
},
|
|
1871
|
+
[ExitCode.ResourceNotFound]: {
|
|
1872
|
+
category: "not_found",
|
|
1873
|
+
code: "RESOURCE_NOT_FOUND",
|
|
1874
|
+
retryable: false
|
|
1875
|
+
},
|
|
1876
|
+
[ExitCode.RuntimeFailure]: {
|
|
1877
|
+
category: "server",
|
|
1878
|
+
code: "RUNTIME_FAILURE",
|
|
1879
|
+
retryable: true
|
|
1880
|
+
},
|
|
1881
|
+
[ExitCode.StateUnavailable]: {
|
|
1882
|
+
category: "config",
|
|
1883
|
+
code: "STATE_UNAVAILABLE",
|
|
1884
|
+
retryable: false
|
|
1885
|
+
},
|
|
1886
|
+
[ExitCode.UsageError]: {
|
|
1887
|
+
category: "validation",
|
|
1888
|
+
code: "USAGE_ERROR",
|
|
1889
|
+
retryable: false
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
const mapFailure = (error) => {
|
|
1893
|
+
if (error instanceof CliFailure) {
|
|
1894
|
+
const meta = EXIT_CODE_META[error.exitCode] ?? {
|
|
1895
|
+
category: "server",
|
|
1896
|
+
code: "UNKNOWN",
|
|
1897
|
+
retryable: false
|
|
1898
|
+
};
|
|
1899
|
+
return {
|
|
1900
|
+
category: meta.category,
|
|
1901
|
+
code: meta.code,
|
|
1902
|
+
exitCode: error.exitCode,
|
|
1903
|
+
message: error.message,
|
|
1904
|
+
retryable: meta.retryable,
|
|
1905
|
+
verbose: error.details
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
if (error instanceof ReviewNotFound || error instanceof TodoNotFound) return {
|
|
1909
|
+
category: "not_found",
|
|
1910
|
+
code: "RESOURCE_NOT_FOUND",
|
|
1911
|
+
exitCode: ExitCode.ResourceNotFound,
|
|
1912
|
+
message: error.message,
|
|
1913
|
+
retryable: false,
|
|
1914
|
+
verbose: error.stack
|
|
1915
|
+
};
|
|
1916
|
+
if (error instanceof Error) return {
|
|
1917
|
+
category: "server",
|
|
1918
|
+
code: "RUNTIME_FAILURE",
|
|
1919
|
+
exitCode: ExitCode.RuntimeFailure,
|
|
1920
|
+
message: error.message,
|
|
1921
|
+
retryable: true,
|
|
1922
|
+
verbose: error.stack
|
|
1923
|
+
};
|
|
1924
|
+
return {
|
|
1925
|
+
category: "server",
|
|
1926
|
+
code: "UNKNOWN_FAILURE",
|
|
1927
|
+
exitCode: ExitCode.RuntimeFailure,
|
|
1928
|
+
message: "Unknown CLI failure.",
|
|
1929
|
+
retryable: false
|
|
1930
|
+
};
|
|
1931
|
+
};
|
|
1932
|
+
/** Actionable fix guidance based on error category. */
|
|
1933
|
+
const FIX_GUIDANCE = {
|
|
1934
|
+
auth: "Check authentication credentials or run 'ringi serve --auth' with valid credentials.",
|
|
1935
|
+
config: "Run 'ringi serve' once to initialize local state, or check --repo and --db-path flags.",
|
|
1936
|
+
conflict: "Resolve the conflict and retry the operation.",
|
|
1937
|
+
connection: "Ensure the Ringi server is running: ringi serve",
|
|
1938
|
+
not_found: "Verify the resource ID. Use 'ringi review list' or 'ringi todo list' to find valid IDs.",
|
|
1939
|
+
server: "Retry the command. If the error persists, check 'ringi serve' logs.",
|
|
1940
|
+
validation: "Check command usage with 'ringi --help'. Verify flag names and values."
|
|
1941
|
+
};
|
|
1942
|
+
/** Build recovery next_actions based on error category. */
|
|
1943
|
+
const errorNextActions = (commandStr, normalized) => {
|
|
1944
|
+
const actions = [];
|
|
1945
|
+
if (normalized.retryable) actions.push({
|
|
1946
|
+
command: commandStr,
|
|
1947
|
+
description: "Retry the failed command"
|
|
1948
|
+
});
|
|
1949
|
+
if (normalized.category === "config" || normalized.category === "connection") actions.push({
|
|
1950
|
+
command: "ringi serve",
|
|
1951
|
+
description: "Start the local Ringi server"
|
|
1952
|
+
});
|
|
1953
|
+
if (normalized.category === "not_found") actions.push({
|
|
1954
|
+
command: "ringi review list",
|
|
1955
|
+
description: "List available reviews"
|
|
1956
|
+
}, {
|
|
1957
|
+
command: "ringi todo list",
|
|
1958
|
+
description: "List available todos"
|
|
1959
|
+
});
|
|
1960
|
+
if (normalized.category === "validation") actions.push({
|
|
1961
|
+
command: `${commandStr.split(" ").slice(0, 3).join(" ")} --help`,
|
|
1962
|
+
description: "Show command usage"
|
|
1963
|
+
});
|
|
1964
|
+
return actions;
|
|
1965
|
+
};
|
|
1966
|
+
/** Build a full error envelope from a normalized failure. */
|
|
1967
|
+
const buildErrorEnvelope = (commandStr, normalized) => {
|
|
1968
|
+
return failure(commandStr, {
|
|
1969
|
+
category: normalized.category,
|
|
1970
|
+
code: normalized.code,
|
|
1971
|
+
message: normalized.message,
|
|
1972
|
+
retryable: normalized.retryable,
|
|
1973
|
+
type: `ringi://errors/${normalized.code}`
|
|
1974
|
+
}, normalized.verbose ?? FIX_GUIDANCE[normalized.category], errorNextActions(commandStr, normalized));
|
|
1975
|
+
};
|
|
1976
|
+
const installSignalHandlers = (dispose) => {
|
|
1977
|
+
const shutdown = async () => {
|
|
1978
|
+
await dispose();
|
|
1979
|
+
process.exit(ExitCode.RuntimeFailure);
|
|
1980
|
+
};
|
|
1981
|
+
process.once("SIGINT", shutdown);
|
|
1982
|
+
process.once("SIGTERM", shutdown);
|
|
1983
|
+
return () => {
|
|
1984
|
+
process.off("SIGINT", shutdown);
|
|
1985
|
+
process.off("SIGTERM", shutdown);
|
|
1986
|
+
};
|
|
1987
|
+
};
|
|
1988
|
+
/**
|
|
1989
|
+
* Resolves the built Nitro server entry point.
|
|
1990
|
+
*
|
|
1991
|
+
* Lookup order (first match wins):
|
|
1992
|
+
* 1. **Packaged location** — `<pkg-root>/server/server/index.mjs`
|
|
1993
|
+
* The published npm package includes `server/` at the package root, which
|
|
1994
|
+
* contains `server/index.mjs` (the Nitro entry) and `public/` (static
|
|
1995
|
+
* assets). From the bundled CLI at `dist/cli.mjs`, the package root is
|
|
1996
|
+
* `import.meta.dirname/..`.
|
|
1997
|
+
* 2. **Monorepo development** — `apps/web/.output/server/index.mjs`
|
|
1998
|
+
* During local development the web build output lives inside the web
|
|
1999
|
+
* workspace. We resolve it relative to the CLI package root.
|
|
2000
|
+
* 3. **CWD fallback** — `.output/server/index.mjs` relative to the current
|
|
2001
|
+
* working directory, for running the server in a standalone checkout.
|
|
2002
|
+
*/
|
|
2003
|
+
const resolveServerEntry = () => {
|
|
2004
|
+
const candidates = [];
|
|
2005
|
+
if (import.meta.dirname) {
|
|
2006
|
+
const pkgRoot = resolve(import.meta.dirname, "..");
|
|
2007
|
+
candidates.push(resolve(pkgRoot, "server", "server", "index.mjs"));
|
|
2008
|
+
candidates.push(resolve(pkgRoot, "..", "web", ".output", "server", "index.mjs"));
|
|
2009
|
+
}
|
|
2010
|
+
candidates.push(resolve(process.cwd(), ".output", "server", "index.mjs"));
|
|
2011
|
+
return candidates.find((candidate) => existsSync(candidate));
|
|
2012
|
+
};
|
|
2013
|
+
const runServe = (command) => {
|
|
2014
|
+
const serverEntry = resolveServerEntry();
|
|
2015
|
+
if (!serverEntry) {
|
|
2016
|
+
const hint = import.meta.dirname && !import.meta.dirname.includes("apps/cli/") ? "The installed package is missing its server assets. Try reinstalling: npm install -g @sanurb/ringi" : "Run 'pnpm build' at the monorepo root, then 'pnpm --filter @sanurb/ringi build:server' to copy the server assets.";
|
|
2017
|
+
process.stderr.write(`No built server found.\n${hint}\n`);
|
|
2018
|
+
process.exit(ExitCode.RuntimeFailure);
|
|
2019
|
+
}
|
|
2020
|
+
const env = {
|
|
2021
|
+
...process.env,
|
|
2022
|
+
NITRO_HOST: command.host,
|
|
2023
|
+
NITRO_PORT: String(command.port)
|
|
2024
|
+
};
|
|
2025
|
+
if (command.https && command.cert && command.key) {
|
|
2026
|
+
env.NITRO_SSL_CERT = command.cert;
|
|
2027
|
+
env.NITRO_SSL_KEY = command.key;
|
|
2028
|
+
}
|
|
2029
|
+
const url = `${command.https ? "https" : "http"}://${command.host === "0.0.0.0" ? "localhost" : command.host}:${command.port}`;
|
|
2030
|
+
process.stderr.write(`ringi server starting on ${url}\n`);
|
|
2031
|
+
const child = fork(serverEntry, [], {
|
|
2032
|
+
env,
|
|
2033
|
+
execArgv: [],
|
|
2034
|
+
stdio: "inherit"
|
|
2035
|
+
});
|
|
2036
|
+
if (!command.noOpen) setTimeout(() => {
|
|
2037
|
+
let openCmd = "xdg-open";
|
|
2038
|
+
if (process.platform === "darwin") openCmd = "open";
|
|
2039
|
+
else if (process.platform === "win32") openCmd = "start";
|
|
2040
|
+
exec(`${openCmd} ${url}`, () => {});
|
|
2041
|
+
}, 1500);
|
|
2042
|
+
const shutdown = () => {
|
|
2043
|
+
child.kill("SIGTERM");
|
|
2044
|
+
};
|
|
2045
|
+
process.once("SIGINT", shutdown);
|
|
2046
|
+
process.once("SIGTERM", shutdown);
|
|
2047
|
+
child.on("exit", (code) => {
|
|
2048
|
+
process.off("SIGINT", shutdown);
|
|
2049
|
+
process.off("SIGTERM", shutdown);
|
|
2050
|
+
process.exit(code ?? ExitCode.Success);
|
|
2051
|
+
});
|
|
2052
|
+
};
|
|
2053
|
+
/** Single path for all CLI error exits. */
|
|
2054
|
+
const failAndExit = (opts) => {
|
|
2055
|
+
const normalized = mapFailure(opts.error);
|
|
2056
|
+
if (opts.json) writeJson(buildErrorEnvelope(opts.cmdStr, normalized));
|
|
2057
|
+
process.stderr.write(`${normalized.message}\n`);
|
|
2058
|
+
if (opts.verbose && normalized.verbose) process.stderr.write(`${normalized.verbose}\n`);
|
|
2059
|
+
return process.exit(normalized.exitCode);
|
|
2060
|
+
};
|
|
2061
|
+
const main = async () => {
|
|
2062
|
+
const argv = process.argv.slice(2);
|
|
2063
|
+
const parseResult = parseCliArgs(argv);
|
|
2064
|
+
if (Result.isFailure(parseResult)) return failAndExit({
|
|
2065
|
+
cmdStr: "ringi",
|
|
2066
|
+
error: parseResult.failure,
|
|
2067
|
+
json: argv.includes("--json"),
|
|
2068
|
+
verbose: false
|
|
2069
|
+
});
|
|
2070
|
+
const { command, options } = parseResult.success;
|
|
2071
|
+
if (command.kind === "help") {
|
|
2072
|
+
if (options.json) writeJson(success("ringi", COMMAND_TREE, ROOT_NEXT_ACTIONS));
|
|
2073
|
+
else writeHuman(renderHelp(command));
|
|
2074
|
+
process.exit(ExitCode.Success);
|
|
2075
|
+
}
|
|
2076
|
+
if (command.kind === "version") {
|
|
2077
|
+
if (options.json) writeJson(success("ringi --version", { version: CLI_VERSION }));
|
|
2078
|
+
else writeHuman(CLI_VERSION);
|
|
2079
|
+
process.exit(ExitCode.Success);
|
|
2080
|
+
}
|
|
2081
|
+
if (command.kind === "serve") {
|
|
2082
|
+
runServe(command);
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
const runtimeResources = createCliRuntimeResources(command, {
|
|
2086
|
+
color: options.color,
|
|
2087
|
+
dbPath: options.dbPath,
|
|
2088
|
+
quiet: options.quiet,
|
|
2089
|
+
repo: options.repo,
|
|
2090
|
+
verbose: options.verbose
|
|
2091
|
+
});
|
|
2092
|
+
if (runtimeResources === null) process.exit(ExitCode.Success);
|
|
2093
|
+
const cmdStr = commandLabel(command);
|
|
2094
|
+
if (runtimeResources instanceof CliFailure) return failAndExit({
|
|
2095
|
+
cmdStr,
|
|
2096
|
+
error: runtimeResources,
|
|
2097
|
+
json: options.json,
|
|
2098
|
+
verbose: options.verbose
|
|
2099
|
+
});
|
|
2100
|
+
const removeSignalHandlers = installSignalHandlers(() => runtimeResources.runtime.dispose());
|
|
2101
|
+
try {
|
|
2102
|
+
const output = await runtimeResources.runtime.runPromise(runCommand(command));
|
|
2103
|
+
if (options.json) writeJson(success(cmdStr, output.data, output.nextActions ?? []));
|
|
2104
|
+
else if (!options.quiet) writeHuman(output.human);
|
|
2105
|
+
await runtimeResources.runtime.dispose();
|
|
2106
|
+
removeSignalHandlers();
|
|
2107
|
+
process.exit(ExitCode.Success);
|
|
2108
|
+
} catch (error) {
|
|
2109
|
+
await runtimeResources.runtime.dispose();
|
|
2110
|
+
removeSignalHandlers();
|
|
2111
|
+
failAndExit({
|
|
2112
|
+
cmdStr,
|
|
2113
|
+
error,
|
|
2114
|
+
json: options.json,
|
|
2115
|
+
verbose: options.verbose
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2118
|
+
};
|
|
2119
|
+
try {
|
|
2120
|
+
await main();
|
|
2121
|
+
} catch (error) {
|
|
2122
|
+
failAndExit({
|
|
2123
|
+
cmdStr: "ringi",
|
|
2124
|
+
error,
|
|
2125
|
+
json: process.argv.slice(2).includes("--json"),
|
|
2126
|
+
verbose: false
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
//#endregion
|
|
2130
|
+
export {};
|
|
2131
|
+
|
|
2132
|
+
//# sourceMappingURL=cli.mjs.map
|