@armanckeser/downstream 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +133 -2
- package/SKILL.md +150 -0
- package/bin/downstream.mjs +19 -0
- package/dist/cli/downstream.js +543 -0
- package/dist/domain/agent.js +3 -0
- package/dist/domain/approval.js +190 -0
- package/dist/domain/delivery.js +21 -0
- package/dist/domain/home.js +1 -0
- package/dist/domain/inbox.js +245 -0
- package/dist/domain/model.js +180 -0
- package/dist/domain/next-move.js +34 -0
- package/dist/domain/pr-detail.js +45 -0
- package/dist/server/actions.js +564 -0
- package/dist/server/agent-host.js +230 -0
- package/dist/server/analyze/git.js +161 -0
- package/dist/server/analyze/graph.js +403 -0
- package/dist/server/analyze/py-graph.js +141 -0
- package/dist/server/analyze/py.js +346 -0
- package/dist/server/analyze/ts-decls.js +299 -0
- package/dist/server/analyze/ts-project.js +212 -0
- package/dist/server/gh.js +51 -0
- package/dist/server/github.js +264 -0
- package/dist/server/home-store.js +127 -0
- package/dist/server/home.js +325 -0
- package/dist/server/install.js +12 -0
- package/dist/server/main.js +228 -0
- package/dist/server/prompts.js +32 -0
- package/dist/server/publish.js +136 -0
- package/dist/server/repos.js +164 -0
- package/dist/server/stale.js +50 -0
- package/dist/server/store.js +269 -0
- package/dist/server/workspace.js +251 -0
- package/package.json +63 -4
- package/web/dist/assets/abap-CLvhMVsD.js +1 -0
- package/web/dist/assets/actionscript-3-Cs6JJXT5.js +1 -0
- package/web/dist/assets/ada-X3Y2mlq2.js +1 -0
- package/web/dist/assets/ahk-DwADkxyd.js +1 -0
- package/web/dist/assets/ahk2-_6rzW9BS.js +1 -0
- package/web/dist/assets/andromeeda-vGVdxbeo.js +1 -0
- package/web/dist/assets/angular-html-iITFzleb.js +1 -0
- package/web/dist/assets/angular-ts-qNGaWUZr.js +1 -0
- package/web/dist/assets/apache-U0d_L8uA.js +1 -0
- package/web/dist/assets/apex-KFGFyKwG.js +1 -0
- package/web/dist/assets/apl-DAcxbtpZ.js +1 -0
- package/web/dist/assets/applescript-CCn79oCD.js +1 -0
- package/web/dist/assets/ara-4CJ0cIlV.js +1 -0
- package/web/dist/assets/asciidoc-SCjQUq34.js +1 -0
- package/web/dist/assets/asm-Cmm7eHzH.js +1 -0
- package/web/dist/assets/astro-CxPQu5s8.js +1 -0
- package/web/dist/assets/aurora-x-CDeNXAV0.js +1 -0
- package/web/dist/assets/awk-BWXHIvNe.js +1 -0
- package/web/dist/assets/ayu-dark-DluEY0Gj.js +1 -0
- package/web/dist/assets/ayu-light-C3h-C4tm.js +1 -0
- package/web/dist/assets/ayu-mirage-Bqwy1Gya.js +1 -0
- package/web/dist/assets/ballerina-B7ZEbQpA.js +1 -0
- package/web/dist/assets/bat-qdxAdBWQ.js +1 -0
- package/web/dist/assets/beancount-C2IN4Vk8.js +1 -0
- package/web/dist/assets/berry-DKpUyyne.js +1 -0
- package/web/dist/assets/bibtex-Ci_nEsc7.js +1 -0
- package/web/dist/assets/bicep-CUHmPFLl.js +1 -0
- package/web/dist/assets/bird2-CYIUgYqB.js +1 -0
- package/web/dist/assets/blade-DJrnOqL4.js +1 -0
- package/web/dist/assets/bsl-_VjlB9mv.js +1 -0
- package/web/dist/assets/c-DJLzn8qa.js +1 -0
- package/web/dist/assets/c3-D0apG41V.js +1 -0
- package/web/dist/assets/cadence-CQ2zXKGN.js +1 -0
- package/web/dist/assets/cairo-DLTphjLi.js +1 -0
- package/web/dist/assets/catppuccin-frappe-C9Q5zVZY.js +1 -0
- package/web/dist/assets/catppuccin-latte-CtsyrtPq.js +1 -0
- package/web/dist/assets/catppuccin-macchiato-R8nAf1vR.js +1 -0
- package/web/dist/assets/catppuccin-mocha-DjuaAJKi.js +1 -0
- package/web/dist/assets/chapel-BHYX99F3.js +1 -0
- package/web/dist/assets/clarity-CtfBBspg.js +1 -0
- package/web/dist/assets/clojure-DqKBuwfJ.js +1 -0
- package/web/dist/assets/cmake-Bj61d0ZC.js +1 -0
- package/web/dist/assets/cobol-C_LDam8n.js +1 -0
- package/web/dist/assets/codeowners-C8r90Shi.js +1 -0
- package/web/dist/assets/codeql-oeQT6MSM.js +1 -0
- package/web/dist/assets/coffee-CcuifkxZ.js +1 -0
- package/web/dist/assets/common-lisp-Cv5bFMCO.js +1 -0
- package/web/dist/assets/coq-CGaSfhwR.js +1 -0
- package/web/dist/assets/cpp-E7p80IG9.js +1 -0
- package/web/dist/assets/crystal-iYHODIir.js +1 -0
- package/web/dist/assets/csharp-dmMxLVLW.js +1 -0
- package/web/dist/assets/css-Bk6njqO4.js +1 -0
- package/web/dist/assets/csv-Dx-8-gkx.js +1 -0
- package/web/dist/assets/cue-CE9AQfxI.js +1 -0
- package/web/dist/assets/cypher-ClKdZ_lG.js +1 -0
- package/web/dist/assets/d-qD-0Kul2.js +1 -0
- package/web/dist/assets/dark-plus-Cs2F2srj.js +1 -0
- package/web/dist/assets/dart-CHZY9Xhz.js +1 -0
- package/web/dist/assets/dax-BkyTk9wS.js +1 -0
- package/web/dist/assets/desktop-Dlh5hvp9.js +1 -0
- package/web/dist/assets/diff-woXpYk--.js +1 -0
- package/web/dist/assets/docker-IyjqRm3v.js +1 -0
- package/web/dist/assets/dotenv-CuH5my32.js +1 -0
- package/web/dist/assets/dracula-BHWKrbxM.js +1 -0
- package/web/dist/assets/dracula-soft-5eyTD99u.js +1 -0
- package/web/dist/assets/dream-maker-DW3nJb8Q.js +1 -0
- package/web/dist/assets/edge-Ct7_SkpN.js +1 -0
- package/web/dist/assets/elixir-YTzSQL2h.js +1 -0
- package/web/dist/assets/elm-bNkwlHr8.js +1 -0
- package/web/dist/assets/emacs-lisp-DR2vtWaj.js +1 -0
- package/web/dist/assets/erb-uFBspDdG.js +1 -0
- package/web/dist/assets/erlang-Cphh6RMH.js +1 -0
- package/web/dist/assets/everforest-dark-sB-x3p7T.js +1 -0
- package/web/dist/assets/everforest-light-Df2xbC6M.js +1 -0
- package/web/dist/assets/fennel-DQxkIbk2.js +1 -0
- package/web/dist/assets/fish-BJitypiv.js +1 -0
- package/web/dist/assets/fluent-C03EYrpw.js +1 -0
- package/web/dist/assets/fortran-fixed-form-DEKoE2YW.js +1 -0
- package/web/dist/assets/fortran-free-form-CYNrtFtB.js +1 -0
- package/web/dist/assets/fsharp-D13ZGOAj.js +1 -0
- package/web/dist/assets/gdresource-xMC7jr7x.js +1 -0
- package/web/dist/assets/gdscript-DYvRD3hg.js +1 -0
- package/web/dist/assets/gdshader-CBce3t8t.js +1 -0
- package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/web/dist/assets/genie-CV2tkWYe.js +1 -0
- package/web/dist/assets/gherkin-DExj1W_8.js +1 -0
- package/web/dist/assets/git-commit-BSykSTBG.js +1 -0
- package/web/dist/assets/git-rebase-CSzaMu4k.js +1 -0
- package/web/dist/assets/github-dark-C-LZuMrd.js +1 -0
- package/web/dist/assets/github-dark-default-DXG-b-1a.js +1 -0
- package/web/dist/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
- package/web/dist/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
- package/web/dist/assets/github-light-EUqPIrTm.js +1 -0
- package/web/dist/assets/github-light-default-BXViO-2h.js +1 -0
- package/web/dist/assets/github-light-high-contrast-B68TUdTA.js +1 -0
- package/web/dist/assets/gleam-CSRkHgEL.js +1 -0
- package/web/dist/assets/glimmer-js-WrpQIag3.js +1 -0
- package/web/dist/assets/glimmer-ts-BkR7s81M.js +1 -0
- package/web/dist/assets/glsl-r6ahr8UN.js +1 -0
- package/web/dist/assets/gn-CAXYAgV-.js +1 -0
- package/web/dist/assets/gnuplot-7GGW24-e.js +1 -0
- package/web/dist/assets/go-rLFTqkRN.js +1 -0
- package/web/dist/assets/graphql-vcFzEvUM.js +1 -0
- package/web/dist/assets/groovy-CacY0gHj.js +1 -0
- package/web/dist/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
- package/web/dist/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
- package/web/dist/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
- package/web/dist/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
- package/web/dist/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
- package/web/dist/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
- package/web/dist/assets/hack-DV1TStQR.js +1 -0
- package/web/dist/assets/haml-BzEpEFnM.js +1 -0
- package/web/dist/assets/handlebars-Bw2ai2tn.js +1 -0
- package/web/dist/assets/haskell-D8IpX4py.js +1 -0
- package/web/dist/assets/haxe-C8oCnwJJ.js +1 -0
- package/web/dist/assets/hcl-Dh228itO.js +1 -0
- package/web/dist/assets/hjson-CxZEssPk.js +1 -0
- package/web/dist/assets/hlsl-Cvrh5tZx.js +1 -0
- package/web/dist/assets/horizon-CE9ld1lL.js +1 -0
- package/web/dist/assets/horizon-bright-DSNQnXHK.js +1 -0
- package/web/dist/assets/houston-CsvMBhTu.js +1 -0
- package/web/dist/assets/html-CVWJgEI4.js +1 -0
- package/web/dist/assets/html-derivative-kTFEcoss.js +1 -0
- package/web/dist/assets/http-DXC6PGgt.js +1 -0
- package/web/dist/assets/hurl-Bpd8P_ef.js +1 -0
- package/web/dist/assets/hxml-Dcm88srW.js +1 -0
- package/web/dist/assets/hy-CZbG8q4J.js +1 -0
- package/web/dist/assets/imba-DsUTQ-LC.js +1 -0
- package/web/dist/assets/index-DpYMsPxu.js +1673 -0
- package/web/dist/assets/index-Zh-Bd2u6.css +2 -0
- package/web/dist/assets/ini-B5eOa1yu.js +1 -0
- package/web/dist/assets/java-Bsmh9nXE.js +1 -0
- package/web/dist/assets/javascript-CeyaFN_P.js +1 -0
- package/web/dist/assets/jinja-BuqTDofy.js +1 -0
- package/web/dist/assets/jison-BYfmmaBZ.js +1 -0
- package/web/dist/assets/json-Bhl6cDM_.js +1 -0
- package/web/dist/assets/json5-BR5RXkoi.js +1 -0
- package/web/dist/assets/jsonc-CYpm1nAK.js +1 -0
- package/web/dist/assets/jsonl-CmCQp5Yx.js +1 -0
- package/web/dist/assets/jsonnet-CJTPZ8u_.js +1 -0
- package/web/dist/assets/jssm-DXw9l8Rf.js +1 -0
- package/web/dist/assets/jsx-BSUHjwDX.js +1 -0
- package/web/dist/assets/julia-CWrNF0wf.js +1 -0
- package/web/dist/assets/just-wLw7gWsD.js +1 -0
- package/web/dist/assets/kanagawa-dragon-Bozd7klA.js +1 -0
- package/web/dist/assets/kanagawa-lotus-BmuOG9ZW.js +1 -0
- package/web/dist/assets/kanagawa-wave-BML5mF2J.js +1 -0
- package/web/dist/assets/kdl-CsD5j6eV.js +1 -0
- package/web/dist/assets/kotlin-DhhofPvG.js +1 -0
- package/web/dist/assets/kusto-C7mF5XQf.js +1 -0
- package/web/dist/assets/laserwave-C_8bwKvT.js +1 -0
- package/web/dist/assets/latex-DyWaeH0P.js +1 -0
- package/web/dist/assets/lean-CewbzKMR.js +1 -0
- package/web/dist/assets/less-DVTAwKKz.js +1 -0
- package/web/dist/assets/light-plus-DVQuIRkW.js +1 -0
- package/web/dist/assets/liquid-DeCc5Rg2.js +1 -0
- package/web/dist/assets/llvm-DqHOMS-s.js +1 -0
- package/web/dist/assets/log-BNLmms1o.js +1 -0
- package/web/dist/assets/logo-Cluzi2Zq.js +1 -0
- package/web/dist/assets/lua-Ba-NltBp.js +1 -0
- package/web/dist/assets/luau-VGWVLoqX.js +1 -0
- package/web/dist/assets/make-Dixweg8N.js +1 -0
- package/web/dist/assets/markdown-BYOwaDjH.js +1 -0
- package/web/dist/assets/marko-DGb2HMVj.js +1 -0
- package/web/dist/assets/material-theme-Bm3Qr25_.js +1 -0
- package/web/dist/assets/material-theme-darker-2IIEA8gg.js +1 -0
- package/web/dist/assets/material-theme-lighter-uhdI0v04.js +1 -0
- package/web/dist/assets/material-theme-ocean-CHQ94UKr.js +1 -0
- package/web/dist/assets/material-theme-palenight-B5W6OYN7.js +1 -0
- package/web/dist/assets/matlab-D7qyCx1q.js +1 -0
- package/web/dist/assets/mdc-CL7bhRQ-.js +1 -0
- package/web/dist/assets/mdx-DQZ5AkYe.js +1 -0
- package/web/dist/assets/mermaid-BxpfTIzt.js +1 -0
- package/web/dist/assets/min-dark-BSWPekZh.js +1 -0
- package/web/dist/assets/min-light-DDpmG2fV.js +1 -0
- package/web/dist/assets/mipsasm-BMqwQI7S.js +1 -0
- package/web/dist/assets/mojo-fg40_ntQ.js +1 -0
- package/web/dist/assets/monokai-BscAjiQZ.js +1 -0
- package/web/dist/assets/moonbit-D9W784E_.js +1 -0
- package/web/dist/assets/move-BYnGhZoM.js +1 -0
- package/web/dist/assets/narrat-_X_XdTYD.js +1 -0
- package/web/dist/assets/nextflow-CSgcYZcx.js +1 -0
- package/web/dist/assets/nextflow-groovy-D7IRt6Pp.js +1 -0
- package/web/dist/assets/nginx-CbyuJibN.js +1 -0
- package/web/dist/assets/night-owl-DhmEMT88.js +1 -0
- package/web/dist/assets/night-owl-light-eJ-hLW7d.js +1 -0
- package/web/dist/assets/nim-C7LemgDR.js +1 -0
- package/web/dist/assets/nix-IvuFDN5E.js +1 -0
- package/web/dist/assets/nord-Cb4Vim4T.js +1 -0
- package/web/dist/assets/nsis-CjNLYCLg.js +1 -0
- package/web/dist/assets/nushell--0fT3PI1.js +1 -0
- package/web/dist/assets/objective-c-D1A_Heim.js +1 -0
- package/web/dist/assets/objective-cpp-BsSzOQcm.js +1 -0
- package/web/dist/assets/ocaml-O90oeIOV.js +1 -0
- package/web/dist/assets/odin-B1RWQWA5.js +1 -0
- package/web/dist/assets/one-dark-pro-C6YyR8OD.js +1 -0
- package/web/dist/assets/one-light-D7Lr4KcI.js +1 -0
- package/web/dist/assets/openscad-BUDT5pXO.js +1 -0
- package/web/dist/assets/org-wrbLazmP.js +1 -0
- package/web/dist/assets/pascal-4ZHwLPI5.js +1 -0
- package/web/dist/assets/perl-DarHXyEu.js +1 -0
- package/web/dist/assets/php--54M0-pj.js +1 -0
- package/web/dist/assets/pierre-dark-CpLgRqie.js +1 -0
- package/web/dist/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
- package/web/dist/assets/pierre-dark-soft-kZQmAZld.js +1 -0
- package/web/dist/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
- package/web/dist/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
- package/web/dist/assets/pierre-light-CoaEpmwp.js +1 -0
- package/web/dist/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
- package/web/dist/assets/pierre-light-soft-lWLdNTOI.js +1 -0
- package/web/dist/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
- package/web/dist/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
- package/web/dist/assets/pkl-ot-7Btpt.js +1 -0
- package/web/dist/assets/plastic-DQwYfKfQ.js +1 -0
- package/web/dist/assets/plsql-DGHpHOYJ.js +1 -0
- package/web/dist/assets/po-BiJDBrnU.js +1 -0
- package/web/dist/assets/poimandres-DRFjx7u4.js +1 -0
- package/web/dist/assets/polar-C7UOKdEL.js +1 -0
- package/web/dist/assets/postcss-BXeXVLqQ.js +1 -0
- package/web/dist/assets/powerquery-DNMTfnFr.js +1 -0
- package/web/dist/assets/powershell-QtSnmXcu.js +1 -0
- package/web/dist/assets/prisma-DMgNhnYS.js +1 -0
- package/web/dist/assets/prolog-iXnhIJG7.js +1 -0
- package/web/dist/assets/proto-DB4EqR-F.js +1 -0
- package/web/dist/assets/pug-CFrC1rDC.js +1 -0
- package/web/dist/assets/puppet-CDv2pdJW.js +1 -0
- package/web/dist/assets/purescript-9MfHhQsQ.js +1 -0
- package/web/dist/assets/python-gzcpVVnB.js +1 -0
- package/web/dist/assets/qml-Go4B46qX.js +1 -0
- package/web/dist/assets/qmldir-DCQb3MpD.js +1 -0
- package/web/dist/assets/qss-Fe1Jh2GI.js +1 -0
- package/web/dist/assets/r-Bpu17F3f.js +1 -0
- package/web/dist/assets/racket-DcIDlBhZ.js +1 -0
- package/web/dist/assets/raku-B3gFvitq.js +1 -0
- package/web/dist/assets/razor-CeZTEIIg.js +1 -0
- package/web/dist/assets/rbs-C0MpOrj4.js +1 -0
- package/web/dist/assets/red-CYlAq2b0.js +1 -0
- package/web/dist/assets/reg-CRGYupPL.js +1 -0
- package/web/dist/assets/regexp-D4K6CiTG.js +1 -0
- package/web/dist/assets/rel-BtDbiS_P.js +1 -0
- package/web/dist/assets/riscv-Ckw8ddFX.js +1 -0
- package/web/dist/assets/ron-VUp2lXgN.js +1 -0
- package/web/dist/assets/rose-pine-aMsaMD1D.js +1 -0
- package/web/dist/assets/rose-pine-dawn-8PwUyIlP.js +1 -0
- package/web/dist/assets/rose-pine-moon-DO-qXg-I.js +1 -0
- package/web/dist/assets/rosmsg-CAekHB0j.js +1 -0
- package/web/dist/assets/rst-Dy2Bl1Fg.js +1 -0
- package/web/dist/assets/ruby-CNqr2Wuz.js +1 -0
- package/web/dist/assets/rust-DpiIf5JO.js +1 -0
- package/web/dist/assets/sas-B6MsvXFb.js +1 -0
- package/web/dist/assets/sass-DXrisJhu.js +1 -0
- package/web/dist/assets/scala-BiFt_7mu.js +1 -0
- package/web/dist/assets/scheme-DQCgrYNe.js +1 -0
- package/web/dist/assets/scss-CHuFWGi8.js +1 -0
- package/web/dist/assets/sdbl-bTVj8UrX.js +1 -0
- package/web/dist/assets/shaderlab-TOUzSsQk.js +1 -0
- package/web/dist/assets/shellscript-EEdMjEbs.js +1 -0
- package/web/dist/assets/shellsession-U_I5Q2cJ.js +1 -0
- package/web/dist/assets/slack-dark-DnToyrRv.js +1 -0
- package/web/dist/assets/slack-ochin-B2OO5cIa.js +1 -0
- package/web/dist/assets/smalltalk-BHpQb5nM.js +1 -0
- package/web/dist/assets/smithy-Dgaz7aur.js +1 -0
- package/web/dist/assets/snazzy-light-4G7pJPwS.js +1 -0
- package/web/dist/assets/solarized-dark-b97czJOk.js +1 -0
- package/web/dist/assets/solarized-light-DFPxpIYQ.js +1 -0
- package/web/dist/assets/solidity-Bj7E1mRn.js +1 -0
- package/web/dist/assets/soy-BnCBZkG3.js +1 -0
- package/web/dist/assets/sparql-D_iOobhT.js +1 -0
- package/web/dist/assets/splunk-BC2Px7Mm.js +1 -0
- package/web/dist/assets/sql-5dStcFSw.js +1 -0
- package/web/dist/assets/ssh-config-BgfXC-Er.js +1 -0
- package/web/dist/assets/stata-B5iSQdtS.js +1 -0
- package/web/dist/assets/stylus-B6D30XZt.js +1 -0
- package/web/dist/assets/surrealql-D4K5msa4.js +1 -0
- package/web/dist/assets/svelte-Dn-_7BgE.js +1 -0
- package/web/dist/assets/swift-CyEgAFGc.js +1 -0
- package/web/dist/assets/synthwave-84-nFMaYfgc.js +1 -0
- package/web/dist/assets/system-verilog-Dh5ihWbf.js +1 -0
- package/web/dist/assets/systemd-BxMlprV5.js +1 -0
- package/web/dist/assets/talonscript-CohzipZa.js +1 -0
- package/web/dist/assets/tasl-DMoTqEGO.js +1 -0
- package/web/dist/assets/tcl-CZd0xW_V.js +1 -0
- package/web/dist/assets/templ-BTe8yLW8.js +1 -0
- package/web/dist/assets/terraform-DswuEJGm.js +1 -0
- package/web/dist/assets/tex-BBG77enc.js +1 -0
- package/web/dist/assets/tokyo-night-oM2G3aXe.js +1 -0
- package/web/dist/assets/toml-CcmNWLt0.js +1 -0
- package/web/dist/assets/ts-tags-BzJXXnMD.js +1 -0
- package/web/dist/assets/tsv-sltzmVWM.js +1 -0
- package/web/dist/assets/tsx-Cj2inrAP.js +1 -0
- package/web/dist/assets/turtle-ByJddavk.js +1 -0
- package/web/dist/assets/twig-BH3HU2q4.js +1 -0
- package/web/dist/assets/typescript-DpDozqSz.js +1 -0
- package/web/dist/assets/typespec-BGhR2SqG.js +1 -0
- package/web/dist/assets/typst-GHzvrUad.js +1 -0
- package/web/dist/assets/v-DBj4K9d_.js +1 -0
- package/web/dist/assets/vala-zf12oZj6.js +1 -0
- package/web/dist/assets/vb-DOwzaHZ0.js +1 -0
- package/web/dist/assets/verilog-B2gwz-sJ.js +1 -0
- package/web/dist/assets/vesper-DdrHHSXu.js +1 -0
- package/web/dist/assets/vhdl-BroJfC0k.js +1 -0
- package/web/dist/assets/viml-DvXPmvsu.js +1 -0
- package/web/dist/assets/vitesse-black-fwtXNY1n.js +1 -0
- package/web/dist/assets/vitesse-dark-BZCL-v6S.js +1 -0
- package/web/dist/assets/vitesse-light-VbXTXTou.js +1 -0
- package/web/dist/assets/vue-CqkncxvB.js +1 -0
- package/web/dist/assets/vue-html-DrD64fqb.js +1 -0
- package/web/dist/assets/vue-vine-BJy37wv2.js +1 -0
- package/web/dist/assets/vyper-CgoNMtux.js +1 -0
- package/web/dist/assets/wasm-BnjxR4X6.js +1 -0
- package/web/dist/assets/wasm-ByWQv1Qj.js +1 -0
- package/web/dist/assets/wenyan-C8pVoKbM.js +1 -0
- package/web/dist/assets/wgsl-BsKzXJz4.js +1 -0
- package/web/dist/assets/wikitext-ClFFjSW2.js +1 -0
- package/web/dist/assets/wit-DdvCle-K.js +1 -0
- package/web/dist/assets/wolfram-DLL8P-h_.js +1 -0
- package/web/dist/assets/xml-CtXDvb3g.js +1 -0
- package/web/dist/assets/xsl-9J2XPEqv.js +1 -0
- package/web/dist/assets/yaml-CKsrpaXo.js +1 -0
- package/web/dist/assets/zenscript-BnlCZFoB.js +1 -0
- package/web/dist/assets/zig-CMLA9XwU.js +1 -0
- package/web/dist/favicon.svg +1 -0
- package/web/dist/index.html +15 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
// Python without a type checker: declarations from indentation, calls resolved
|
|
2
|
+
// through each file's imports. Good enough to draw the flow and answer
|
|
3
|
+
// "what is this?" on hover; the agent fills in what static reading can't see.
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
export const isPy = (file) => file.endsWith(".py") || file.endsWith(".pyi");
|
|
6
|
+
const ROUTE = /^\s*@\s*[\w.]+\.(get|post|put|patch|delete|head|options|route|api_route|websocket)\s*\(\s*(?:path\s*=\s*)?["']([^"']+)["']/;
|
|
7
|
+
const COMMAND = /^\s*@\s*[\w.]*\b(command|group|task|shared_task|receiver|on_event|listener|handler)\b/;
|
|
8
|
+
const NOT_CALLS = new Set([
|
|
9
|
+
"if", "elif", "while", "for", "return", "yield", "not", "and", "or", "in", "is", "lambda", "with", "assert", "del", "await", "print", "len", "range",
|
|
10
|
+
"str", "int", "float", "bool", "dict", "list", "set", "tuple", "frozenset", "bytes", "isinstance", "issubclass", "super", "getattr", "setattr", "hasattr",
|
|
11
|
+
"type", "object", "enumerate", "zip", "map", "filter", "sorted", "reversed", "min", "max", "sum", "any", "all", "abs", "round", "open", "repr", "iter", "next",
|
|
12
|
+
"id", "hash", "vars", "dir", "callable", "except", "raise", "def", "class", "import", "from", "as", "pass", "else", "try", "finally", "case", "match",
|
|
13
|
+
]);
|
|
14
|
+
/** Blank out strings and comments, keeping every character's position, so regexes see only code. */
|
|
15
|
+
export function stripPy(text) {
|
|
16
|
+
let out = "";
|
|
17
|
+
let i = 0;
|
|
18
|
+
while (i < text.length) {
|
|
19
|
+
const c = text[i];
|
|
20
|
+
if (c === "#") {
|
|
21
|
+
while (i < text.length && text[i] !== "\n")
|
|
22
|
+
(out += " "), i++;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (c === '"' || c === "'") {
|
|
26
|
+
// Prefixes like f"", rb'' were already emitted as identifiers; that's fine.
|
|
27
|
+
const triple = text.startsWith(c.repeat(3), i);
|
|
28
|
+
const quote = triple ? c.repeat(3) : c;
|
|
29
|
+
out += quote.replace(/./g, " ");
|
|
30
|
+
i += quote.length;
|
|
31
|
+
while (i < text.length && !text.startsWith(quote, i)) {
|
|
32
|
+
if (text[i] === "\\")
|
|
33
|
+
(out += " "), (i += 2);
|
|
34
|
+
else if (!triple && text[i] === "\n")
|
|
35
|
+
break;
|
|
36
|
+
else
|
|
37
|
+
(out += text[i] === "\n" ? "\n" : " "), i++;
|
|
38
|
+
}
|
|
39
|
+
if (text.startsWith(quote, i))
|
|
40
|
+
(out += quote.replace(/./g, " ")), (i += quote.length);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
out += c;
|
|
44
|
+
i++;
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
const indentOf = (line) => line.length - line.trimStart().length;
|
|
49
|
+
export function parsePy(file, text) {
|
|
50
|
+
const raw = text.split("\n");
|
|
51
|
+
const code = stripPy(text).split("\n");
|
|
52
|
+
const decls = [];
|
|
53
|
+
const stack = [];
|
|
54
|
+
for (let i = 0; i < code.length; i++) {
|
|
55
|
+
const line = code[i];
|
|
56
|
+
const m = line.match(/^(\s*)(async\s+def|def|class)\s+([A-Za-z_]\w*)/);
|
|
57
|
+
if (!m)
|
|
58
|
+
continue;
|
|
59
|
+
const indent = m[1].length;
|
|
60
|
+
while (stack.length && stack.at(-1).indent >= indent)
|
|
61
|
+
stack.pop();
|
|
62
|
+
const parent = stack.at(-1) ?? null;
|
|
63
|
+
// Header: until the colon that closes the signature.
|
|
64
|
+
let depth = 0;
|
|
65
|
+
let headerEnd = i;
|
|
66
|
+
for (let j = i; j < code.length; j++) {
|
|
67
|
+
for (const ch of code[j])
|
|
68
|
+
depth += ch === "(" || ch === "[" ? 1 : ch === ")" || ch === "]" ? -1 : 0;
|
|
69
|
+
headerEnd = j;
|
|
70
|
+
if (depth <= 0 && /:\s*$/.test(code[j].trimEnd()))
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
// Body: every following line indented deeper (blank and comment lines don't end it).
|
|
74
|
+
let end = headerEnd;
|
|
75
|
+
for (let j = headerEnd + 1; j < code.length; j++) {
|
|
76
|
+
if (!code[j].trim())
|
|
77
|
+
continue;
|
|
78
|
+
if (indentOf(code[j]) > indent)
|
|
79
|
+
end = j;
|
|
80
|
+
else
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
// Decorators sit directly above.
|
|
84
|
+
let start = i;
|
|
85
|
+
while (start > 0 && /^\s*@/.test(code[start - 1]) && indentOf(code[start - 1]) === indent)
|
|
86
|
+
start--;
|
|
87
|
+
const isClass = m[2] === "class";
|
|
88
|
+
const name = m[3];
|
|
89
|
+
const decorators = raw.slice(start, i).join("\n");
|
|
90
|
+
const routeMatch = raw.slice(start, i).map((d) => d.match(ROUTE)).find(Boolean);
|
|
91
|
+
const command = raw.slice(start, i).some((d) => COMMAND.test(d));
|
|
92
|
+
const kind = isClass ? "class" : routeMatch || command ? "route" : parent?.isClass ? "method" : "function";
|
|
93
|
+
const signature = (decorators ? decorators + "\n" : "") + raw.slice(i, headerEnd + 1).join("\n").trimEnd();
|
|
94
|
+
const decl = {
|
|
95
|
+
key: parent ? `${parent.key}.${name}` : name,
|
|
96
|
+
name,
|
|
97
|
+
container: parent ? parent.key : null,
|
|
98
|
+
kind,
|
|
99
|
+
range: { start: start + 1, end: end + 1 },
|
|
100
|
+
defLine: i + 1,
|
|
101
|
+
signature: routeMatch ? `${routeMatch[1].toUpperCase()} ${routeMatch[2]}\n${signature}` : signature,
|
|
102
|
+
doc: docstring(raw, headerEnd + 1),
|
|
103
|
+
exported: !name.startsWith("_"),
|
|
104
|
+
memberRanges: [],
|
|
105
|
+
indent,
|
|
106
|
+
isClass,
|
|
107
|
+
kept: false,
|
|
108
|
+
};
|
|
109
|
+
// Nested functions inside functions are part of their parent, unless the parent is a big container.
|
|
110
|
+
// Helpers defined inside a test belong to that test.
|
|
111
|
+
const keep = !parent || (parent.kept && (parent.isClass || (parent.range.end - parent.range.start >= 40 && !parent.name.startsWith("test"))));
|
|
112
|
+
decl.kept = keep;
|
|
113
|
+
if (keep) {
|
|
114
|
+
if (parent)
|
|
115
|
+
parent.memberRanges.push(decl.range);
|
|
116
|
+
decls.push(decl);
|
|
117
|
+
}
|
|
118
|
+
stack.push(decl);
|
|
119
|
+
}
|
|
120
|
+
const imports = new Map();
|
|
121
|
+
const joined = code.join("\n");
|
|
122
|
+
for (const m of joined.matchAll(/^[ \t]*from\s+(\.*[\w.]*)\s+import\s+(\([^)]*\)|[^\n]+)/gm)) {
|
|
123
|
+
for (const part of m[2].replace(/[()\\]/g, " ").split(",")) {
|
|
124
|
+
const [name, alias] = part.trim().split(/\s+as\s+/);
|
|
125
|
+
if (name && name !== "*")
|
|
126
|
+
imports.set((alias ?? name).trim(), { module: m[1], name: name.trim() });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
for (const m of joined.matchAll(/^[ \t]*import\s+([^\n]+)/gm)) {
|
|
130
|
+
for (const part of m[1].split(",")) {
|
|
131
|
+
const [mod, alias] = part.trim().split(/\s+as\s+/);
|
|
132
|
+
if (!mod)
|
|
133
|
+
continue;
|
|
134
|
+
if (alias)
|
|
135
|
+
imports.set(alias.trim(), { module: mod.trim(), name: null });
|
|
136
|
+
else
|
|
137
|
+
imports.set(mod.trim().split(".")[0], { module: mod.trim().split(".")[0], name: null });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const calls = [];
|
|
141
|
+
code.forEach((line, idx) => {
|
|
142
|
+
if (/^\s*(async\s+def|def|class)\s/.test(line))
|
|
143
|
+
return;
|
|
144
|
+
for (const m of line.matchAll(/(?<![\w.])([A-Za-z_][\w]*(?:\s*\.\s*[A-Za-z_]\w*)*)\s*\(/g)) {
|
|
145
|
+
const chain = m[1].replace(/\s+/g, "");
|
|
146
|
+
if (NOT_CALLS.has(chain))
|
|
147
|
+
continue;
|
|
148
|
+
calls.push({ chain, line: idx + 1, col: m.index });
|
|
149
|
+
}
|
|
150
|
+
// Call on a fresh instance: GrantService().create(1) reads as GrantService.create.
|
|
151
|
+
for (const m of line.matchAll(/(?<![\w.])([A-Za-z_][\w.]*)\s*\([^()]*\)\s*\.\s*([A-Za-z_]\w*)\s*\(/g)) {
|
|
152
|
+
calls.push({ chain: `${m[1]}.${m[2]}`, line: idx + 1, col: m.index + m[0].lastIndexOf(m[2]) });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
return { file, decls, imports, calls, code };
|
|
156
|
+
}
|
|
157
|
+
function docstring(raw, from) {
|
|
158
|
+
for (let j = from; j < raw.length && j < from + 3; j++) {
|
|
159
|
+
const t = raw[j].trim();
|
|
160
|
+
if (!t)
|
|
161
|
+
continue;
|
|
162
|
+
const q = t.startsWith('"""') ? '"""' : t.startsWith("'''") ? "'''" : null;
|
|
163
|
+
if (!q)
|
|
164
|
+
return "";
|
|
165
|
+
const rest = t.slice(3);
|
|
166
|
+
if (rest.includes(q))
|
|
167
|
+
return rest.slice(0, rest.indexOf(q)).trim();
|
|
168
|
+
const lines = [rest];
|
|
169
|
+
for (let k = j + 1; k < raw.length && k < j + 40; k++) {
|
|
170
|
+
const l = raw[k];
|
|
171
|
+
if (l.includes(q)) {
|
|
172
|
+
lines.push(l.slice(0, l.indexOf(q)));
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
lines.push(l);
|
|
176
|
+
}
|
|
177
|
+
return dedent(lines).trim();
|
|
178
|
+
}
|
|
179
|
+
return "";
|
|
180
|
+
}
|
|
181
|
+
function dedent(lines) {
|
|
182
|
+
const ind = Math.min(...lines.slice(1).filter((l) => l.trim()).map(indentOf), 1e9);
|
|
183
|
+
return [lines[0], ...lines.slice(1).map((l) => l.slice(ind === 1e9 ? 0 : ind))].join("\n");
|
|
184
|
+
}
|
|
185
|
+
export function enclosingPy(decls, line) {
|
|
186
|
+
let best = null;
|
|
187
|
+
for (const d of decls) {
|
|
188
|
+
if (line < d.range.start || line > d.range.end)
|
|
189
|
+
continue;
|
|
190
|
+
if (!best || d.range.end - d.range.start < best.range.end - best.range.start)
|
|
191
|
+
best = d;
|
|
192
|
+
}
|
|
193
|
+
return best;
|
|
194
|
+
}
|
|
195
|
+
/** Every Python file in the repo, parsed on demand, with module-path resolution. */
|
|
196
|
+
export class PyIndex {
|
|
197
|
+
parsed = new Map();
|
|
198
|
+
byModuleSuffix;
|
|
199
|
+
files;
|
|
200
|
+
read;
|
|
201
|
+
constructor(files, read) {
|
|
202
|
+
this.files = files;
|
|
203
|
+
this.read = read;
|
|
204
|
+
this.byModuleSuffix = new Map();
|
|
205
|
+
for (const f of files.filter(isPy)) {
|
|
206
|
+
const mod = f.replace(/\.pyi?$/, "").replace(/\/__init__$/, "").split("/");
|
|
207
|
+
// Index every suffix so `app.models` finds `src/app/models.py`.
|
|
208
|
+
for (let i = 0; i < mod.length; i++) {
|
|
209
|
+
const key = mod.slice(i).join(".");
|
|
210
|
+
(this.byModuleSuffix.get(key) ?? this.byModuleSuffix.set(key, []).get(key)).push(f);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
all() {
|
|
215
|
+
return this.files.filter(isPy);
|
|
216
|
+
}
|
|
217
|
+
get(file) {
|
|
218
|
+
if (!this.parsed.has(file)) {
|
|
219
|
+
const text = this.read(file);
|
|
220
|
+
this.parsed.set(file, text === null ? null : parsePy(file, text));
|
|
221
|
+
}
|
|
222
|
+
return this.parsed.get(file);
|
|
223
|
+
}
|
|
224
|
+
moduleFile(fromFile, module) {
|
|
225
|
+
if (module.startsWith(".")) {
|
|
226
|
+
const dots = module.match(/^\.+/)[0].length;
|
|
227
|
+
let dir = path.posix.dirname(fromFile);
|
|
228
|
+
for (let i = 1; i < dots; i++)
|
|
229
|
+
dir = path.posix.dirname(dir);
|
|
230
|
+
const rest = module.slice(dots).replace(/\./g, "/");
|
|
231
|
+
const base = rest ? `${dir}/${rest}` : dir;
|
|
232
|
+
for (const cand of [`${base}.py`, `${base}/__init__.py`, `${base}.pyi`])
|
|
233
|
+
if (this.files.includes(cand.replace(/^\.\//, "")))
|
|
234
|
+
return cand.replace(/^\.\//, "");
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
const hits = this.byModuleSuffix.get(module);
|
|
238
|
+
if (!hits?.length)
|
|
239
|
+
return null;
|
|
240
|
+
return [...hits].sort((a, b) => a.length - b.length)[0];
|
|
241
|
+
}
|
|
242
|
+
/** What a call chain in a file refers to, as (file, decl). */
|
|
243
|
+
resolve(file, chain, line) {
|
|
244
|
+
const pf = this.get(file);
|
|
245
|
+
if (!pf)
|
|
246
|
+
return null;
|
|
247
|
+
const parts = chain.split(".");
|
|
248
|
+
const find = (f, key) => {
|
|
249
|
+
const target = this.get(f);
|
|
250
|
+
const d = target?.decls.find((x) => x.key === key) ?? null;
|
|
251
|
+
// Calling a class runs its __init__ when it has one.
|
|
252
|
+
if (d?.kind === "class")
|
|
253
|
+
return target?.decls.find((x) => x.key === `${key}.__init__`) ?? d;
|
|
254
|
+
return d;
|
|
255
|
+
};
|
|
256
|
+
if ((parts[0] === "self" || parts[0] === "cls") && parts.length === 2) {
|
|
257
|
+
const here = enclosingPy(pf.decls, line);
|
|
258
|
+
const cls = here?.container ?? (here?.kind === "class" ? here.key : null);
|
|
259
|
+
if (cls) {
|
|
260
|
+
const d = find(file, `${cls}.${parts[1]}`);
|
|
261
|
+
if (d)
|
|
262
|
+
return { file, decl: d };
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
if (parts.length === 1) {
|
|
267
|
+
const local = find(file, parts[0]);
|
|
268
|
+
if (local && !local.container)
|
|
269
|
+
return { file, decl: local };
|
|
270
|
+
}
|
|
271
|
+
const imp = pf.imports.get(parts[0]);
|
|
272
|
+
if (imp) {
|
|
273
|
+
if (imp.name) {
|
|
274
|
+
// from mod import name → name.rest...
|
|
275
|
+
const target = this.moduleFile(file, imp.module);
|
|
276
|
+
if (target) {
|
|
277
|
+
const d = find(target, [imp.name, ...parts.slice(1)].join("."));
|
|
278
|
+
if (d)
|
|
279
|
+
return { file: target, decl: d };
|
|
280
|
+
}
|
|
281
|
+
// from pkg import submodule → submodule.func
|
|
282
|
+
const sub = this.moduleFile(file, `${imp.module}${imp.module.endsWith(".") ? "" : "."}${imp.name}`);
|
|
283
|
+
if (sub && parts.length > 1) {
|
|
284
|
+
const d = find(sub, parts.slice(1).join("."));
|
|
285
|
+
if (d)
|
|
286
|
+
return { file: sub, decl: d };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
// import a.b as x → x.func, or import a → a.b.func
|
|
291
|
+
for (let cut = parts.length - 1; cut >= 1; cut--) {
|
|
292
|
+
const modPath = imp.module.split(".").length > 1 && parts.length - cut === 1 ? imp.module : [imp.module, ...parts.slice(1, cut)].join(".");
|
|
293
|
+
const target = this.moduleFile(file, modPath);
|
|
294
|
+
if (target) {
|
|
295
|
+
const d = find(target, parts.slice(cut).join("."));
|
|
296
|
+
if (d)
|
|
297
|
+
return { file: target, decl: d };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// obj.method(): if exactly one method by that name exists anywhere, it's almost surely that one.
|
|
303
|
+
if (parts.length >= 2) {
|
|
304
|
+
const method = parts.at(-1);
|
|
305
|
+
if (method.startsWith("__"))
|
|
306
|
+
return null;
|
|
307
|
+
const hits = [];
|
|
308
|
+
for (const f of this.all()) {
|
|
309
|
+
for (const d of this.get(f)?.decls ?? [])
|
|
310
|
+
if (d.name === method && d.container)
|
|
311
|
+
hits.push({ file: f, decl: d });
|
|
312
|
+
if (hits.length > 1)
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
return hits[0] ?? null;
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
/** Callee names inside a range, for diffing calls across sides. */
|
|
320
|
+
static calleeNames(pf, range) {
|
|
321
|
+
const names = new Map();
|
|
322
|
+
for (const c of pf.calls) {
|
|
323
|
+
if (c.line < range.start || c.line > range.end)
|
|
324
|
+
continue;
|
|
325
|
+
const name = c.chain.split(".").at(-1);
|
|
326
|
+
if (!names.has(name))
|
|
327
|
+
names.set(name, c.line);
|
|
328
|
+
}
|
|
329
|
+
return names;
|
|
330
|
+
}
|
|
331
|
+
/** The identifier chain under a cursor, for hover and go-to-definition. */
|
|
332
|
+
chainAt(file, line, col) {
|
|
333
|
+
const pf = this.get(file);
|
|
334
|
+
const text = pf?.code[line - 1];
|
|
335
|
+
if (!text)
|
|
336
|
+
return null;
|
|
337
|
+
let s = col;
|
|
338
|
+
let e = col;
|
|
339
|
+
while (s > 0 && /[\w.]/.test(text[s - 1]))
|
|
340
|
+
s--;
|
|
341
|
+
while (e < text.length && /\w/.test(text[e]))
|
|
342
|
+
e++;
|
|
343
|
+
const chain = text.slice(s, e).replace(/^\.+|\.+$/g, "");
|
|
344
|
+
return chain || null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// Syntax-only declaration extraction. No type checker needed, so it runs on
|
|
2
|
+
// both sides of a change (the old side has no program to check against).
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
const TS_EXT = /\.(m|c)?(t|j)sx?$/;
|
|
5
|
+
export const isTsLike = (file) => TS_EXT.test(file) && !file.endsWith(".d.ts");
|
|
6
|
+
export function scriptKind(file) {
|
|
7
|
+
if (file.endsWith(".tsx"))
|
|
8
|
+
return ts.ScriptKind.TSX;
|
|
9
|
+
if (file.endsWith(".jsx"))
|
|
10
|
+
return ts.ScriptKind.JSX;
|
|
11
|
+
if (/\.(m|c)?js$/.test(file))
|
|
12
|
+
return ts.ScriptKind.JS;
|
|
13
|
+
return ts.ScriptKind.TS;
|
|
14
|
+
}
|
|
15
|
+
export function parse(file, text) {
|
|
16
|
+
return ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, scriptKind(file));
|
|
17
|
+
}
|
|
18
|
+
const HTTP = new Set(["get", "post", "put", "patch", "delete", "all", "options", "head"]);
|
|
19
|
+
/** Where a declaration really starts: its JSDoc belongs to it, and reviewers read docs too. */
|
|
20
|
+
function docStart(sf, node) {
|
|
21
|
+
const holder = ts.isVariableDeclaration(node) && ts.isVariableDeclarationList(node.parent) && node.parent.declarations.length === 1 ? node.parent.parent : node;
|
|
22
|
+
const docs = holder.jsDoc;
|
|
23
|
+
return docs?.length ? Math.min(docs[0].getStart(sf), node.getStart(sf)) : node.getStart(sf);
|
|
24
|
+
}
|
|
25
|
+
function lines(sf, node) {
|
|
26
|
+
const start = sf.getLineAndCharacterOfPosition(docStart(sf, node)).line + 1;
|
|
27
|
+
const end = sf.getLineAndCharacterOfPosition(node.getEnd()).line + 1;
|
|
28
|
+
return { start, end };
|
|
29
|
+
}
|
|
30
|
+
function isExported(node) {
|
|
31
|
+
const mods = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
|
|
32
|
+
return !!mods?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
33
|
+
}
|
|
34
|
+
const squash = (s) => s.replace(/\s+/g, " ").trim();
|
|
35
|
+
function headText(sf, from, body) {
|
|
36
|
+
const start = from.getStart(sf);
|
|
37
|
+
const end = body ? body.getStart(sf) : from.getEnd();
|
|
38
|
+
const text = squash(sf.text.slice(start, end)).replace(/\s*(=>|\{)?\s*$/, "");
|
|
39
|
+
return text.length > 240 ? text.slice(0, 237) + "…" : text;
|
|
40
|
+
}
|
|
41
|
+
function clip(text, maxLines = 14) {
|
|
42
|
+
const ls = text.split("\n");
|
|
43
|
+
return ls.length > maxLines ? ls.slice(0, maxLines).join("\n") + "\n …" : text;
|
|
44
|
+
}
|
|
45
|
+
function blockText(sf, node, maxLines = 14) {
|
|
46
|
+
const text = sf.text.slice(node.getStart(sf), node.getEnd());
|
|
47
|
+
const ls = text.split("\n");
|
|
48
|
+
return ls.length > maxLines ? ls.slice(0, maxLines).join("\n") + "\n …" : text;
|
|
49
|
+
}
|
|
50
|
+
function isFunctionLike(init) {
|
|
51
|
+
return !!init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init));
|
|
52
|
+
}
|
|
53
|
+
/** `memo(() => …)`, `forwardRef(function X() …)`, `defineAction({ run() … })`. */
|
|
54
|
+
function wrapsFunction(init) {
|
|
55
|
+
if (!init || !ts.isCallExpression(init))
|
|
56
|
+
return false;
|
|
57
|
+
return init.arguments.some((a) => isFunctionLike(a) || ts.isObjectLiteralExpression(a) || wrapsFunction(a));
|
|
58
|
+
}
|
|
59
|
+
export function extractDecls(sf) {
|
|
60
|
+
const out = [];
|
|
61
|
+
const jsx = sf.fileName.endsWith("x");
|
|
62
|
+
const fnKind = (name) => (jsx && /^[A-Z]/.test(name) ? "component" : "function");
|
|
63
|
+
const pushMembers = (container, members, exported) => {
|
|
64
|
+
const ranges = [];
|
|
65
|
+
for (const m of members) {
|
|
66
|
+
let name = null;
|
|
67
|
+
let body;
|
|
68
|
+
if (ts.isConstructorDeclaration(m)) {
|
|
69
|
+
name = "constructor";
|
|
70
|
+
body = m.body;
|
|
71
|
+
}
|
|
72
|
+
else if ((ts.isMethodDeclaration(m) || ts.isGetAccessor(m) || ts.isSetAccessor(m)) && m.name) {
|
|
73
|
+
name = m.name.getText(sf);
|
|
74
|
+
body = m.body;
|
|
75
|
+
}
|
|
76
|
+
else if ((ts.isPropertyDeclaration(m) || ts.isPropertyAssignment(m)) && isFunctionLike(m.initializer)) {
|
|
77
|
+
name = m.name.getText(sf);
|
|
78
|
+
body = m.initializer.body;
|
|
79
|
+
}
|
|
80
|
+
if (!name)
|
|
81
|
+
continue;
|
|
82
|
+
const range = lines(sf, m);
|
|
83
|
+
ranges.push(range);
|
|
84
|
+
out.push({
|
|
85
|
+
key: `${container}.${name}`,
|
|
86
|
+
name,
|
|
87
|
+
container,
|
|
88
|
+
kind: "method",
|
|
89
|
+
node: m,
|
|
90
|
+
nameNode: m.name ?? m.getFirstToken(sf) ?? null,
|
|
91
|
+
range,
|
|
92
|
+
exported,
|
|
93
|
+
signature: headText(sf, m, body),
|
|
94
|
+
memberRanges: [],
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return ranges;
|
|
98
|
+
};
|
|
99
|
+
for (const st of sf.statements) {
|
|
100
|
+
const exported = isExported(st);
|
|
101
|
+
if (ts.isFunctionDeclaration(st)) {
|
|
102
|
+
const name = st.name?.text ?? "default";
|
|
103
|
+
out.push({
|
|
104
|
+
key: name,
|
|
105
|
+
name,
|
|
106
|
+
container: null,
|
|
107
|
+
kind: fnKind(name),
|
|
108
|
+
node: st,
|
|
109
|
+
nameNode: st.name ?? null,
|
|
110
|
+
range: lines(sf, st),
|
|
111
|
+
exported,
|
|
112
|
+
signature: headText(sf, st, st.body),
|
|
113
|
+
memberRanges: [],
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else if (ts.isClassDeclaration(st)) {
|
|
117
|
+
const name = st.name?.text ?? "default";
|
|
118
|
+
const decl = {
|
|
119
|
+
key: name,
|
|
120
|
+
name,
|
|
121
|
+
container: null,
|
|
122
|
+
kind: "class",
|
|
123
|
+
node: st,
|
|
124
|
+
nameNode: st.name ?? null,
|
|
125
|
+
range: lines(sf, st),
|
|
126
|
+
exported,
|
|
127
|
+
signature: clip(sf.text.slice(st.getStart(sf), st.members.pos).replace(/\s*\{\s*$/, "")),
|
|
128
|
+
memberRanges: [],
|
|
129
|
+
};
|
|
130
|
+
out.push(decl);
|
|
131
|
+
decl.memberRanges = pushMembers(name, st.members, exported);
|
|
132
|
+
}
|
|
133
|
+
else if (ts.isInterfaceDeclaration(st) || ts.isTypeAliasDeclaration(st) || ts.isEnumDeclaration(st)) {
|
|
134
|
+
out.push({
|
|
135
|
+
key: st.name.text,
|
|
136
|
+
name: st.name.text,
|
|
137
|
+
container: null,
|
|
138
|
+
kind: ts.isInterfaceDeclaration(st) ? "interface" : "type",
|
|
139
|
+
node: st,
|
|
140
|
+
nameNode: st.name,
|
|
141
|
+
range: lines(sf, st),
|
|
142
|
+
exported,
|
|
143
|
+
signature: blockText(sf, st),
|
|
144
|
+
memberRanges: [],
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
else if (ts.isVariableStatement(st)) {
|
|
148
|
+
for (const d of st.declarationList.declarations) {
|
|
149
|
+
if (!ts.isIdentifier(d.name))
|
|
150
|
+
continue;
|
|
151
|
+
const name = d.name.text;
|
|
152
|
+
const init = d.initializer;
|
|
153
|
+
const kind = isFunctionLike(init)
|
|
154
|
+
? fnKind(name)
|
|
155
|
+
: init && ts.isClassExpression(init)
|
|
156
|
+
? "class"
|
|
157
|
+
: wrapsFunction(init) && jsx && /^[A-Z]/.test(name)
|
|
158
|
+
? "component"
|
|
159
|
+
: "const";
|
|
160
|
+
const range = lines(sf, st.declarationList.declarations.length === 1 ? st : d);
|
|
161
|
+
const decl = {
|
|
162
|
+
key: name,
|
|
163
|
+
name,
|
|
164
|
+
container: null,
|
|
165
|
+
kind,
|
|
166
|
+
node: d,
|
|
167
|
+
nameNode: d.name,
|
|
168
|
+
range,
|
|
169
|
+
exported,
|
|
170
|
+
signature: isFunctionLike(init)
|
|
171
|
+
? `${exported ? "export " : ""}const ${headText(sf, d, init.body)}`
|
|
172
|
+
: squash(blockText(sf, d, 1)).slice(0, 160),
|
|
173
|
+
memberRanges: [],
|
|
174
|
+
};
|
|
175
|
+
out.push(decl);
|
|
176
|
+
if (init && ts.isObjectLiteralExpression(init)) {
|
|
177
|
+
decl.memberRanges = pushMembers(name, init.properties.filter((p) => ts.isMethodDeclaration(p) || ts.isPropertyAssignment(p)), exported);
|
|
178
|
+
}
|
|
179
|
+
else if (init && ts.isClassExpression(init)) {
|
|
180
|
+
decl.memberRanges = pushMembers(name, init.members, exported);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else if (ts.isExportAssignment(st) && isFunctionLike(st.expression)) {
|
|
185
|
+
out.push({
|
|
186
|
+
key: "default",
|
|
187
|
+
name: "default",
|
|
188
|
+
container: null,
|
|
189
|
+
kind: fnKind("Default"),
|
|
190
|
+
node: st,
|
|
191
|
+
nameNode: null,
|
|
192
|
+
range: lines(sf, st),
|
|
193
|
+
exported: true,
|
|
194
|
+
signature: headText(sf, st, st.expression.body),
|
|
195
|
+
memberRanges: [],
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Big containers (Effect service layers, app builders, factories) define their real units as inner
|
|
200
|
+
// named functions: `const patchGrant = Effect.fn(...)(function* ...)`. Lift those out as members so a
|
|
201
|
+
// reviewer can open the unit that changed. Components are left whole; their handlers read best in place.
|
|
202
|
+
const NESTED_MIN_LINES = 40;
|
|
203
|
+
for (const d of [...out]) {
|
|
204
|
+
if (d.kind === "component" || d.range.end - d.range.start < NESTED_MIN_LINES)
|
|
205
|
+
continue;
|
|
206
|
+
const owned = (n) => d.memberRanges.some((r) => lines(sf, n).start >= r.start && lines(sf, n).end <= r.end);
|
|
207
|
+
const visit = (n) => {
|
|
208
|
+
let name = null;
|
|
209
|
+
let body;
|
|
210
|
+
if (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name) && (isFunctionLike(n.initializer) || wrapsFunction(n.initializer))) {
|
|
211
|
+
name = n.name.text;
|
|
212
|
+
body = isFunctionLike(n.initializer) ? n.initializer.body : undefined;
|
|
213
|
+
}
|
|
214
|
+
else if (ts.isFunctionDeclaration(n) && n.name) {
|
|
215
|
+
name = n.name.text;
|
|
216
|
+
body = n.body;
|
|
217
|
+
}
|
|
218
|
+
if (name && !owned(n)) {
|
|
219
|
+
const range = lines(sf, n);
|
|
220
|
+
if (range.end > range.start) {
|
|
221
|
+
d.memberRanges.push(range);
|
|
222
|
+
out.push({
|
|
223
|
+
key: `${d.name}.${name}`,
|
|
224
|
+
name,
|
|
225
|
+
container: d.name,
|
|
226
|
+
kind: "method",
|
|
227
|
+
node: n,
|
|
228
|
+
nameNode: n.name ?? null,
|
|
229
|
+
range,
|
|
230
|
+
exported: d.exported,
|
|
231
|
+
signature: body ? headText(sf, n, body) : squash(blockText(sf, n, 1)).slice(0, 200),
|
|
232
|
+
memberRanges: [],
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
ts.forEachChild(n, visit);
|
|
238
|
+
};
|
|
239
|
+
ts.forEachChild(d.node, visit);
|
|
240
|
+
}
|
|
241
|
+
// Route registrations anywhere in the file: app.get("/x", handler).
|
|
242
|
+
const visit = (node) => {
|
|
243
|
+
if (ts.isCallExpression(node) &&
|
|
244
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
245
|
+
HTTP.has(node.expression.name.text) &&
|
|
246
|
+
node.arguments.length >= 2 &&
|
|
247
|
+
ts.isStringLiteralLike(node.arguments[0]) &&
|
|
248
|
+
node.arguments[0].text.startsWith("/")) {
|
|
249
|
+
const method = node.expression.name.text.toUpperCase();
|
|
250
|
+
const route = node.arguments[0].text;
|
|
251
|
+
const name = `${method} ${route}`;
|
|
252
|
+
out.push({
|
|
253
|
+
key: `route:${name}`,
|
|
254
|
+
name,
|
|
255
|
+
container: null,
|
|
256
|
+
kind: "route",
|
|
257
|
+
node,
|
|
258
|
+
nameNode: node.expression.name,
|
|
259
|
+
range: lines(sf, node),
|
|
260
|
+
exported: true,
|
|
261
|
+
signature: `${node.expression.expression.getText(sf)}.${node.expression.name.text}("${route}", …)`,
|
|
262
|
+
memberRanges: [],
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
ts.forEachChild(node, visit);
|
|
266
|
+
};
|
|
267
|
+
visit(sf);
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
/** Innermost declaration containing a position. Routes and methods win over their enclosing function. */
|
|
271
|
+
export function enclosing(decls, sf, pos) {
|
|
272
|
+
let best = null;
|
|
273
|
+
for (const d of decls) {
|
|
274
|
+
if (pos < d.node.getStart(sf) || pos > d.node.getEnd())
|
|
275
|
+
continue;
|
|
276
|
+
if (!best || d.node.getEnd() - d.node.getStart(sf) < best.node.getEnd() - best.node.getStart(sf))
|
|
277
|
+
best = d;
|
|
278
|
+
}
|
|
279
|
+
return best;
|
|
280
|
+
}
|
|
281
|
+
/** Names called inside a node. Used to diff call sets across sides without a checker. */
|
|
282
|
+
export function calleeNames(sf, node) {
|
|
283
|
+
const names = new Map();
|
|
284
|
+
const visit = (n) => {
|
|
285
|
+
let target;
|
|
286
|
+
if (ts.isCallExpression(n) || ts.isNewExpression(n))
|
|
287
|
+
target = n.expression;
|
|
288
|
+
else if (ts.isJsxOpeningElement(n) || ts.isJsxSelfClosingElement(n))
|
|
289
|
+
target = n.tagName;
|
|
290
|
+
if (target) {
|
|
291
|
+
const name = ts.isPropertyAccessExpression(target) ? target.name.text : ts.isIdentifier(target) ? target.text : null;
|
|
292
|
+
if (name && !names.has(name))
|
|
293
|
+
names.set(name, sf.getLineAndCharacterOfPosition(n.getStart(sf)).line + 1);
|
|
294
|
+
}
|
|
295
|
+
ts.forEachChild(n, visit);
|
|
296
|
+
};
|
|
297
|
+
ts.forEachChild(node, visit);
|
|
298
|
+
return names;
|
|
299
|
+
}
|