farming-code 2.2.6
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 +287 -0
- package/README.zh_cn.md +329 -0
- package/SECURITY.md +29 -0
- package/THIRD_PARTY_NOTICES.md +106 -0
- package/backend/acp/client-services.js +312 -0
- package/backend/acp/patch-decisions.js +137 -0
- package/backend/acp/permission-security.js +108 -0
- package/backend/acp-runtime.js +1144 -0
- package/backend/acp-session-state.js +448 -0
- package/backend/acp-transcript.js +169 -0
- package/backend/agent-activity.js +186 -0
- package/backend/agent-env.js +250 -0
- package/backend/agent-json-stream.js +268 -0
- package/backend/agent-manager.js +4259 -0
- package/backend/agent-memory-report.js +583 -0
- package/backend/agent-order.js +162 -0
- package/backend/agent-provider-session.js +287 -0
- package/backend/agent-session-history.js +888 -0
- package/backend/app-server-api.js +917 -0
- package/backend/async-cache.js +94 -0
- package/backend/auth.js +178 -0
- package/backend/claude-settings.js +121 -0
- package/backend/cli-agents.js +484 -0
- package/backend/codex-app-server-home.js +39 -0
- package/backend/codex-app-server-runtime.js +901 -0
- package/backend/codex-context-window.js +189 -0
- package/backend/codex-models.js +229 -0
- package/backend/codex-rollout-follower.js +243 -0
- package/backend/codex-session-history.js +553 -0
- package/backend/codex-transcript-sanitizer.js +103 -0
- package/backend/codex-transcript.js +2341 -0
- package/backend/command-runner-child.js +67 -0
- package/backend/config-manager.js +815 -0
- package/backend/control-api.js +151 -0
- package/backend/data/chinese-poetic-words.json +8223 -0
- package/backend/data/poetic-word-sources/china.json +49209 -0
- package/backend/data/poetic-word-sources/india.json +9763 -0
- package/backend/data/poetic-word-sources/japan.json +8455 -0
- package/backend/executable-discovery.js +207 -0
- package/backend/farming-app-cli.js +790 -0
- package/backend/farming-cli.js +400 -0
- package/backend/farming-session-store.js +373 -0
- package/backend/haiku-token.js +772 -0
- package/backend/index-html.js +49 -0
- package/backend/input-parts.js +54 -0
- package/backend/input-routing.js +24 -0
- package/backend/json-cli-runtime.js +161 -0
- package/backend/local-session-engine.js +620 -0
- package/backend/main-agent-skills.js +292 -0
- package/backend/main-page-session.js +119 -0
- package/backend/native-pty-host-client.js +516 -0
- package/backend/native-pty-host-path.js +22 -0
- package/backend/native-pty-host.js +816 -0
- package/backend/native-session-engine.js +196 -0
- package/backend/network.js +23 -0
- package/backend/npm-update-helper.js +196 -0
- package/backend/packaged-node-pty.js +79 -0
- package/backend/provider-session-id.js +47 -0
- package/backend/qr-share-tickets.js +100 -0
- package/backend/review-diff-router.js +180 -0
- package/backend/review-diff-service.js +1270 -0
- package/backend/review-session-router.js +74 -0
- package/backend/review-session-service.js +432 -0
- package/backend/review-session-store.js +144 -0
- package/backend/review-state-router.js +116 -0
- package/backend/review-state-store.js +287 -0
- package/backend/run-history-store.js +70 -0
- package/backend/server.js +2446 -0
- package/backend/session-engine-bridge.js +110 -0
- package/backend/session-engine-router.js +66 -0
- package/backend/session-engine.js +39 -0
- package/backend/shell-busy-integration.js +472 -0
- package/backend/slash-command-discovery.js +296 -0
- package/backend/storage-layout.js +101 -0
- package/backend/system-monitor.js +77 -0
- package/backend/terminal-runtime-cleanup.js +80 -0
- package/backend/terminal-screen-state.js +240 -0
- package/backend/terminal-screen-worker-pool.js +165 -0
- package/backend/terminal-screen-worker-thread.js +161 -0
- package/backend/terminal-screen-worker.js +255 -0
- package/backend/terminal-status.js +356 -0
- package/backend/theme-manager.js +145 -0
- package/backend/update-service.js +1183 -0
- package/backend/usage-forecast.js +86 -0
- package/backend/usage-monitor.js +1357 -0
- package/backend/workspace-discovery.js +311 -0
- package/backend/workspace-file-router.js +431 -0
- package/backend/workspace-file-service.js +2765 -0
- package/bin/farming +5 -0
- package/config/farming.deploy.env.example +13 -0
- package/config/farming.install.env.example +17 -0
- package/dist/assets/App-BrP-ENHg.css +1 -0
- package/dist/assets/App-Dza5yEDe.js +124 -0
- package/dist/assets/FileEditorMarkdownPreview-DRDNQ8mX.css +1 -0
- package/dist/assets/FileEditorMarkdownPreview-elKWc8Im.js +404 -0
- package/dist/assets/FileEditorPane-BQLocXOp.js +5 -0
- package/dist/assets/IconGlyphs-RkMAdBXF.js +1 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/ProjectFilesSection-lS1azbgj.js +12 -0
- package/dist/assets/ReviewPage-DgnmgU2T.js +3 -0
- package/dist/assets/abap-Cc5D5h5i.js +1 -0
- package/dist/assets/abnfDiagram-VRR7QNED-BgawzDQT.js +1 -0
- package/dist/assets/apex-BKopwLmx.js +1 -0
- package/dist/assets/api-D1lyBYIQ.js +1 -0
- package/dist/assets/arc-CWoVlxJw.js +1 -0
- package/dist/assets/architecture-TIHT7OUA-CbJBFh3w.js +1 -0
- package/dist/assets/architectureDiagram-ZJ3FMSHR-DUShwPg0.js +36 -0
- package/dist/assets/array-HeX70jSN.js +1 -0
- package/dist/assets/azcli-DI-C34WA.js +1 -0
- package/dist/assets/bat-Cbp6u42z.js +1 -0
- package/dist/assets/bicep-4QA1jTCM.js +2 -0
- package/dist/assets/blockDiagram-677ZJIJ3-CeGZviXR.js +132 -0
- package/dist/assets/c4Diagram-LMCZKHZV-CLhYht6S.js +10 -0
- package/dist/assets/cameligo-Dda_adPs.js +1 -0
- package/dist/assets/channel-Uw90AO0b.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-d5WWh8jo.js +1 -0
- package/dist/assets/chunk-32BRIVSS-LFYpT-Cu.js +1 -0
- package/dist/assets/chunk-52WLFC77-BH6jDw9J.js +10 -0
- package/dist/assets/chunk-5FCAYU7R-C7AmIhYv.js +1 -0
- package/dist/assets/chunk-5HE753X5-C8cXaouP.js +1 -0
- package/dist/assets/chunk-5JV3BV7I-C1PtimcA.js +1 -0
- package/dist/assets/chunk-5TONJI2A-_81zUME8.js +2 -0
- package/dist/assets/chunk-5VM5RSS4-Dlw7AXWp.js +15 -0
- package/dist/assets/chunk-7BUUIJ7U-ZBRhjLOh.js +1 -0
- package/dist/assets/chunk-BIQX33UG-CWmzxeAw.js +1 -0
- package/dist/assets/chunk-C7G6YPKG-B_mIUlb7.js +1 -0
- package/dist/assets/chunk-CQNSW5MT-CMayolwI.js +1 -0
- package/dist/assets/chunk-CYSBUYHQ-lSPbrL3Z.js +1 -0
- package/dist/assets/chunk-DECur_0Z.js +1 -0
- package/dist/assets/chunk-EMLP6XTP-D9-jtCnM.js +1 -0
- package/dist/assets/chunk-EX3LRPZG-C63hVuAJ.js +231 -0
- package/dist/assets/chunk-FWX5IMBZ-CboX-WF_.js +2 -0
- package/dist/assets/chunk-HOUHSVGY-BOgSd6Un.js +1 -0
- package/dist/assets/chunk-ICXQ74PX-C0K-pgZt.js +2 -0
- package/dist/assets/chunk-JG7HCLWE-Cib606u_.js +2 -0
- package/dist/assets/chunk-JWPE2WC7-DSyBRaRG.js +1 -0
- package/dist/assets/chunk-KEIR6QF5-CNOHik0T.js +161 -0
- package/dist/assets/chunk-MOJQB5TN-CVMfwN_7.js +88 -0
- package/dist/assets/chunk-MOZMSUNE-D6JeZqmE.js +1 -0
- package/dist/assets/chunk-OGEWGWER-DLT3pxyw.js +1 -0
- package/dist/assets/chunk-OSBZ3O6U-C0W8KSeg.js +1 -0
- package/dist/assets/chunk-PUDLZKDR-Cd079yIQ.js +156 -0
- package/dist/assets/chunk-Q4XR5HBZ-D0jHa6SO.js +70 -0
- package/dist/assets/chunk-QBLGF6JB-p4omp7uO.js +1 -0
- package/dist/assets/chunk-R7FJI6CG-CZ_p9hSr.js +1 -0
- package/dist/assets/chunk-RYQCIY6F-CZA9pvFX.js +1 -0
- package/dist/assets/chunk-U6XO7XAA-CcXKsWxp.js +2 -0
- package/dist/assets/chunk-V7JOEXUC-CVDtR75H.js +206 -0
- package/dist/assets/chunk-VAUOI2AC-B-W2_9Ot.js +1 -0
- package/dist/assets/chunk-VR4S4FIN-FD3kWh32.js +1 -0
- package/dist/assets/chunk-WYO6CB5R-C2bGaS5r.js +127 -0
- package/dist/assets/chunk-XXDRQBXY-BBXwjhUx.js +1 -0
- package/dist/assets/chunk-Y2CYZVJY-Di0sYzi1.js +1 -0
- package/dist/assets/chunk-YOTPTUD7-BXHmEk89.js +1 -0
- package/dist/assets/chunk-ZGVPDNZ5-ymgts0zo.js +62 -0
- package/dist/assets/chunk-ZIRB5QZD-DncwlQQc.js +32 -0
- package/dist/assets/classDiagram-OUVF2IWQ-DKW_dtku.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-C8Fi7-yv.js +1 -0
- package/dist/assets/clojure-CvY8A83L.js +1 -0
- package/dist/assets/code-dark-Cjb1QGhu.css +1 -0
- package/dist/assets/code-mobile-3C2Rr-CN.css +1 -0
- package/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/dist/assets/coffee-4LX2Sv8A.js +1 -0
- package/dist/assets/core-ZlAPicox.js +3 -0
- package/dist/assets/cose-bilkent-JH36ORCC-BNIAz0nN.js +1 -0
- package/dist/assets/cpp-D4AzSV28.js +1 -0
- package/dist/assets/csharp-C4nWN4Yd.js +1 -0
- package/dist/assets/csp-CiEaaS5Z.js +1 -0
- package/dist/assets/css-DXxBRsbk.js +3 -0
- package/dist/assets/css.worker-DyDu5ynT.js +89 -0
- package/dist/assets/cssMode-DDWxTPm_.js +1 -0
- package/dist/assets/cynefin-VYW2F7L2-DgXGHlRt.js +1 -0
- package/dist/assets/cynefinDiagram-TSTJHNR4-CwpobXIl.js +62 -0
- package/dist/assets/cypher-DOMzqyFA.js +1 -0
- package/dist/assets/cytoscape.esm-Cl8iC3D7.js +321 -0
- package/dist/assets/dagre-DXlcHgr3.js +1 -0
- package/dist/assets/dagre-VKFMJZFB-Dkhlfjna.js +4 -0
- package/dist/assets/dart-BWS7MXt_.js +1 -0
- package/dist/assets/defaultLocale-GJwWH1Jr.js +1 -0
- package/dist/assets/diagram-FQU43EPY-hA8dicp6.js +3 -0
- package/dist/assets/diagram-G47NLZAW-BTY6JTzO.js +24 -0
- package/dist/assets/diagram-NH7WQ7WH-xkaHHX9M.js +24 -0
- package/dist/assets/diagram-OA4YK3LP-gk_fg9UU.js +30 -0
- package/dist/assets/diagram-WEI45ONY-BvC8xd6T.js +41 -0
- package/dist/assets/dist-ClAL-Tv5.js +1 -0
- package/dist/assets/dockerfile-Baf4U3xT.js +1 -0
- package/dist/assets/ebnfDiagram-CCIWWBDH-CR7RaNat.js +1 -0
- package/dist/assets/ecl-CT7udlkQ.js +1 -0
- package/dist/assets/editor-CtbFZlk0.css +1 -0
- package/dist/assets/editor.api2-B9h6iFPK.js +872 -0
- package/dist/assets/editor.worker-Dpt_xPrs.js +26 -0
- package/dist/assets/elixir-CIbAZw5l.js +1 -0
- package/dist/assets/erDiagram-Q63AITRT-_M-JFPbj.js +85 -0
- package/dist/assets/eventmodeling-45OFAUF4-DXYTQ2nt.js +1 -0
- package/dist/assets/file-icons-EFUGSSwf.js +1 -0
- package/dist/assets/flow9-Dqiw45WV.js +1 -0
- package/dist/assets/flowDiagram-23GEKE2U-DAiS5Y7G.js +1 -0
- package/dist/assets/freemarker2-QBsTERH-.js +3 -0
- package/dist/assets/fsharp-E8E2L9jU.js +1 -0
- package/dist/assets/ganttDiagram-NO4QXBWP-Bl8P1yf_.js +292 -0
- package/dist/assets/gitGraph-TEB2WS4Q-CzTBjEvV.js +1 -0
- package/dist/assets/gitGraphDiagram-IHSO6WYX-DoHa5ysz.js +106 -0
- package/dist/assets/go-CouVSN4D.js +1 -0
- package/dist/assets/graphlib-CbSvZcZU.js +1 -0
- package/dist/assets/graphql-a6h6h5-t.js +1 -0
- package/dist/assets/handlebars-CuoxPo5A.js +1 -0
- package/dist/assets/hcl-vso3e6N-.js +1 -0
- package/dist/assets/html-gX5PEu0Q.js +1 -0
- package/dist/assets/html.worker-IWNXhnIC.js +502 -0
- package/dist/assets/htmlMode-DIWUbUxd.js +1 -0
- package/dist/assets/index-BpHPeJf0.js +10 -0
- package/dist/assets/info-DKCQHKI2-DznP7j3s.js +1 -0
- package/dist/assets/infoDiagram-FWYZ7A6U-DSFcSuUs.js +2 -0
- package/dist/assets/ini-SwZZkotR.js +1 -0
- package/dist/assets/init-BPLPMQ3Y.js +1 -0
- package/dist/assets/ishikawaDiagram-FXEZZL3T-DdkqullI.js +70 -0
- package/dist/assets/java-CtRLC4_3.js +1 -0
- package/dist/assets/javascript-BYKGIUGs.js +1 -0
- package/dist/assets/journeyDiagram-5HDEW3XC-B4hRnQJr.js +139 -0
- package/dist/assets/json.worker-c4h5s80L.js +58 -0
- package/dist/assets/jsonMode-AFmQQP6u.js +7 -0
- package/dist/assets/jsx-runtime-BthL93pI.js +1 -0
- package/dist/assets/julia-CuJWFKQH.js +1 -0
- package/dist/assets/kanban-definition-HUTT4EX6-tm2FZB2E.js +89 -0
- package/dist/assets/katex-C8DPyI6s.js +257 -0
- package/dist/assets/kotlin-B_kBhsre.js +1 -0
- package/dist/assets/less-CtrHIfz3.js +2 -0
- package/dist/assets/lexon-yVt1pJLu.js +1 -0
- package/dist/assets/line-CmsjK4kn.js +1 -0
- package/dist/assets/linear-Dld3_ERu.js +1 -0
- package/dist/assets/liquid-DBIl-f0z.js +1 -0
- package/dist/assets/lspLanguageFeatures-DK6tqicV.js +4 -0
- package/dist/assets/lua-prOwgJ4U.js +1 -0
- package/dist/assets/m3-DhrdTQ84.js +1 -0
- package/dist/assets/main-DTJwH51M.css +1 -0
- package/dist/assets/map-LZWWoeWa.js +1 -0
- package/dist/assets/markdown-DoI5HwSM.js +1 -0
- package/dist/assets/mdx-DS-bVtZN.js +1 -0
- package/dist/assets/mermaid-parser.core-BQtepoLd.js +4 -0
- package/dist/assets/mermaid.core-C7UT_EEL.js +11 -0
- package/dist/assets/mindmap-definition-LN4V7U3C-DplfjGms.js +96 -0
- package/dist/assets/mips-Dye5jsBU.js +1 -0
- package/dist/assets/monaco.contribution-CyLosfZI.js +2 -0
- package/dist/assets/msdax-TkNOyIL8.js +1 -0
- package/dist/assets/mysql-CEc-RKZF.js +1 -0
- package/dist/assets/objective-c-CqqCsQz0.js +1 -0
- package/dist/assets/ordinal-BLrOss5K.js +1 -0
- package/dist/assets/packet-7NZHBO7P-BDEPOPha.js +1 -0
- package/dist/assets/pascal-DO_pj8yg.js +1 -0
- package/dist/assets/pascaligo-1cbdZdF3.js +1 -0
- package/dist/assets/path-YdFzr2W6.js +1 -0
- package/dist/assets/pegDiagram-2B236MQR-lu3SdkQs.js +1 -0
- package/dist/assets/perl-B_N3PYfC.js +1 -0
- package/dist/assets/pgsql-9iYk1FQ7.js +1 -0
- package/dist/assets/php-rO2sgoUM.js +1 -0
- package/dist/assets/pie-RZYD4A2V-DWIrwPJQ.js +1 -0
- package/dist/assets/pieDiagram-ENE6RG2P-3fHbmDf9.js +39 -0
- package/dist/assets/pla-Cbgv60kq.js +1 -0
- package/dist/assets/postiats-y6xrhy_M.js +1 -0
- package/dist/assets/powerquery-BaD8gWNE.js +1 -0
- package/dist/assets/powershell-BMhG4JVe.js +1 -0
- package/dist/assets/preload-helper-mJowh_hw.js +1 -0
- package/dist/assets/protobuf-CdOP5kN3.js +2 -0
- package/dist/assets/pug-Ck60hNF9.js +1 -0
- package/dist/assets/python-C93q3xXz.js +1 -0
- package/dist/assets/qoder-C9LmmOSf.svg +1 -0
- package/dist/assets/qoder-Cf9gl0Y5.svg +1 -0
- package/dist/assets/qoder-gHCinseV.svg +1 -0
- package/dist/assets/qrcode-DV-_rjU8.js +3 -0
- package/dist/assets/qsharp-BcaKFfP-.js +1 -0
- package/dist/assets/quadrantDiagram-ABIIQ3AL-a5qoWz-X.js +7 -0
- package/dist/assets/r-rFNes0iK.js +1 -0
- package/dist/assets/radar-I7S5WNFK-B7OaD8vp.js +1 -0
- package/dist/assets/railroad-3IZDKUUU-BXW2nuCA.js +1 -0
- package/dist/assets/railroad-abnf-AHOZXSZD-Buw9QG2r.js +1 -0
- package/dist/assets/railroad-ebnf-EBAXGLYW-BjSi4sWJ.js +1 -0
- package/dist/assets/railroad-peg-LSFZ7HO6-DN1wIVhS.js +1 -0
- package/dist/assets/railroadDiagram-RFXS5EU6-D2TOuXW9.js +1 -0
- package/dist/assets/razor-CBifNGXO.js +1 -0
- package/dist/assets/react-dom-gtU6X-Hj.js +1 -0
- package/dist/assets/redis-DkpQ3K6G.js +1 -0
- package/dist/assets/redshift-Di9N5TI3.js +1 -0
- package/dist/assets/requirementDiagram-TGXJPOKE-LQBWpUvd.js +84 -0
- package/dist/assets/restructuredtext-By5ZzLt6.js +1 -0
- package/dist/assets/review-BcnwQtJv.css +1 -0
- package/dist/assets/rough.esm-c4PR5shF.js +1 -0
- package/dist/assets/ruby-fF5zGiLp.js +1 -0
- package/dist/assets/rust-C5b3AYY6.js +1 -0
- package/dist/assets/sankeyDiagram-HTMAVEWB-B1ohYoyK.js +40 -0
- package/dist/assets/sb-CD07UNRs.js +1 -0
- package/dist/assets/scala-jtn1ip9G.js +1 -0
- package/dist/assets/scheme-C4vqMkn_.js +1 -0
- package/dist/assets/scss-I4Glxjbz.js +3 -0
- package/dist/assets/sequenceDiagram-DBY2YBRQ-C3vM-hMq.js +162 -0
- package/dist/assets/shell-DubUcuns.js +1 -0
- package/dist/assets/sizeCapture-X5ZJPWSS-daTB6OcF.js +1 -0
- package/dist/assets/solidity-CNy2KSl2.js +1 -0
- package/dist/assets/sophia-D9_URt84.js +1 -0
- package/dist/assets/sparql-DviXpm4C.js +1 -0
- package/dist/assets/sql-vDRbAZgK.js +1 -0
- package/dist/assets/src-WBW1QxJt.js +1 -0
- package/dist/assets/st-CnpnRqA7.js +1 -0
- package/dist/assets/stateDiagram-2N3HPSRC-BvFhOryI.js +1 -0
- package/dist/assets/stateDiagram-v2-6OUMAXLB-WMrz4PNo.js +1 -0
- package/dist/assets/swift-D_g8Ganz.js +1 -0
- package/dist/assets/swimlanes-5IMT3BWC-DKF_b-2s.js +2 -0
- package/dist/assets/swimlanesDiagram-G3AALYLV-BGswC9oC.js +8 -0
- package/dist/assets/systemverilog-OuZa5h1M.js +1 -0
- package/dist/assets/tcl-DBm9ucsV.js +1 -0
- package/dist/assets/timeline-definition-FHXFAJF6-vAs11ux_.js +120 -0
- package/dist/assets/tokens-BwenNn5Q.css +1 -0
- package/dist/assets/treeView-QDETBFTQ-BVfICaZQ.js +1 -0
- package/dist/assets/treemap-6X3UGDF4-oWHPqo17.js +1 -0
- package/dist/assets/ts.worker-BwM5Ha3L.js +67719 -0
- package/dist/assets/tsMode-DGZTlTj8.js +11 -0
- package/dist/assets/twig-BRzEqX5Y.js +1 -0
- package/dist/assets/typescript-KJKfYO7K.js +1 -0
- package/dist/assets/typespec-D4ZgGeX8.js +1 -0
- package/dist/assets/useWorkspaceMenuKeyboard-CneKAZUJ.js +1 -0
- package/dist/assets/vb-DcP4s6WS.js +1 -0
- package/dist/assets/vennDiagram-L72KCM5P-BybTX_9B.js +34 -0
- package/dist/assets/wardley-OPB4EBWU-Dg8t5y3s.js +1 -0
- package/dist/assets/wardleyDiagram-EHGQE667-CicUmMBR.js +78 -0
- package/dist/assets/wgsl-CnRJ_QUf.js +298 -0
- package/dist/assets/workers-BSQRIkqa.js +1 -0
- package/dist/assets/workspace-view-state-CTyDzk2D.js +1 -0
- package/dist/assets/xml-C8FAcN6j.js +1 -0
- package/dist/assets/xychartDiagram-FW5EYKEG-BhWDyCX-.js +7 -0
- package/dist/assets/yaml-BQ4NrRhG.js +1 -0
- package/dist/assets/zsh-CLpveKlF.svg +1 -0
- package/dist/assets/zsh-FxSpMPbz.svg +1 -0
- package/dist/farming-2/app-icon-v2-180.png +0 -0
- package/dist/farming-2/app-icon-v2-192.png +0 -0
- package/dist/farming-2/app-icon-v2-512.png +0 -0
- package/dist/farming-2/app-icon-v2-maskable-512.png +0 -0
- package/dist/farming-2/app-icon-v2-master.png +0 -0
- package/dist/farming-2/favicon-v2-16.png +0 -0
- package/dist/farming-2/favicon-v2-32.png +0 -0
- package/dist/farming-2/favicon-v2-48.png +0 -0
- package/dist/farming-2/favicon-v2-64.png +0 -0
- package/dist/farming-2/favicon-v2.ico +0 -0
- package/dist/farming-2/favicon.ico +0 -0
- package/dist/farming-2/github-social-preview.png +0 -0
- package/dist/farming-2/images/avatar-watercolor-v1-bee-garden.png +0 -0
- package/dist/farming-2/images/avatar-watercolor-v2.png +0 -0
- package/dist/farming-2/images/avatar-watercolor-v3.png +0 -0
- package/dist/farming-2/legacy-icons-v1/favicon.ico +1 -0
- package/dist/farming-2/legacy-icons-v1/logo-16.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-180.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-192.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-32.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-48.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-512.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-64.png +0 -0
- package/dist/farming-2/logo-16.png +0 -0
- package/dist/farming-2/logo-180.png +0 -0
- package/dist/farming-2/logo-192.png +0 -0
- package/dist/farming-2/logo-32.png +0 -0
- package/dist/farming-2/logo-48.png +0 -0
- package/dist/farming-2/logo-512.png +0 -0
- package/dist/farming-2/logo-64.png +0 -0
- package/dist/farming-2/site.webmanifest +27 -0
- package/dist/farming-2/style_capsules/watercolor-pastoral.json +94 -0
- package/dist/index.html +27 -0
- package/frontend/ghostty-loader.js +24 -0
- package/frontend/runtime-paths.js +28 -0
- package/frontend/session-bridge.js +82 -0
- package/frontend/session-modal-bridge.js +331 -0
- package/frontend/skin-bridge.js +25 -0
- package/frontend/skins/crt/app.js +6940 -0
- package/frontend/skins/crt/assets/branding/farming-crt-icon.svg +11 -0
- package/frontend/skins/crt/assets/branding/farming-crt-logo-v1.png +0 -0
- package/frontend/skins/crt/assets/effects/phosphor-noise.svg +11 -0
- package/frontend/skins/crt/assets/fonts/departure-mono/DepartureMonoNerdFontMono-Regular.otf +0 -0
- package/frontend/skins/crt/assets/fonts/departure-mono/LICENSE +93 -0
- package/frontend/skins/crt/effects.js +8 -0
- package/frontend/skins/crt/index.html +1712 -0
- package/frontend/skins/crt/styles/billing.css +1003 -0
- package/frontend/skins/crt/styles/effects.css +171 -0
- package/frontend/skins/crt/styles/history.css +215 -0
- package/frontend/skins/crt/styles/monochrome-green.css +509 -0
- package/frontend/skins/crt/styles/search.css +271 -0
- package/frontend/terminal-bridge.js +192 -0
- package/frontend/theme-loader.js +84 -0
- package/frontend/themes/terminal/theme.json +24 -0
- package/frontend/vendor/ghostty-web/LICENSE +21 -0
- package/frontend/vendor/ghostty-web/__vite-browser-external-2447137e.js +4 -0
- package/frontend/vendor/ghostty-web/ghostty-vt.wasm +0 -0
- package/frontend/vendor/ghostty-web/ghostty-web.js +2963 -0
- package/frontend/vendor/ghostty-web/ghostty-web.umd.cjs +13 -0
- package/frontend/vendor/ghostty-web/index.d.ts +1842 -0
- package/index.html +23 -0
- package/package.json +144 -0
- package/scripts/sync-ghostty-vendor.js +66 -0
|
@@ -0,0 +1,2765 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const fsp = require('fs/promises');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { diffChars } = require('diff');
|
|
6
|
+
const { execFile, spawn } = require('child_process');
|
|
7
|
+
const readline = require('readline');
|
|
8
|
+
const { pathToFileURL } = require('url');
|
|
9
|
+
|
|
10
|
+
const DEFAULT_MAX_FILE_SIZE = 1024 * 1024;
|
|
11
|
+
const DEFAULT_MAX_WRITE_SIZE = 2 * 1024 * 1024;
|
|
12
|
+
const DEFAULT_MAX_PREVIEW_FILE_SIZE = 8 * 1024 * 1024;
|
|
13
|
+
const DEFAULT_SEARCH_LIMIT = 100;
|
|
14
|
+
const DEFAULT_GIT_CHANGES_LIMIT = 500;
|
|
15
|
+
const DEFAULT_GIT_STATUS_CACHE_TTL_MS = 30000;
|
|
16
|
+
const DEFAULT_GIT_STATUS_INLINE_TIMEOUT_MS = 80;
|
|
17
|
+
const DEFAULT_SEARCH_TIMEOUT_MS = 3000;
|
|
18
|
+
const DEFAULT_BLAME_TIMEOUT_MS = 5000;
|
|
19
|
+
const DEFAULT_DIFF_TIMEOUT_MS = 5000;
|
|
20
|
+
const DEFAULT_DIFF_MAX_BUFFER = 1024 * 1024;
|
|
21
|
+
const DEFAULT_WATCH_DEPTH = 1;
|
|
22
|
+
const SEARCH_FILE_LIST_MAX_BUFFER = 16 * 1024 * 1024;
|
|
23
|
+
const BINARY_SNIFF_BYTES = 8192;
|
|
24
|
+
const PATH_SEARCH_MIN_CANDIDATES = 120;
|
|
25
|
+
const PATH_SEARCH_CANDIDATE_MULTIPLIER = 8;
|
|
26
|
+
const TIMEOUT = Symbol('timeout');
|
|
27
|
+
const IMAGE_PREVIEW_MEDIA_TYPES = new Map([
|
|
28
|
+
['.png', 'image/png'],
|
|
29
|
+
['.jpg', 'image/jpeg'],
|
|
30
|
+
['.jpeg', 'image/jpeg'],
|
|
31
|
+
['.gif', 'image/gif'],
|
|
32
|
+
['.webp', 'image/webp'],
|
|
33
|
+
['.bmp', 'image/bmp'],
|
|
34
|
+
['.ico', 'image/x-icon'],
|
|
35
|
+
['.avif', 'image/avif'],
|
|
36
|
+
['.svg', 'image/svg+xml'],
|
|
37
|
+
]);
|
|
38
|
+
const TEXT_IMAGE_PREVIEW_EXTENSIONS = new Set(['.svg']);
|
|
39
|
+
const IGNORED_NAMES = new Set([
|
|
40
|
+
'.git',
|
|
41
|
+
'.farming',
|
|
42
|
+
'node_modules',
|
|
43
|
+
'dist',
|
|
44
|
+
'build',
|
|
45
|
+
'coverage',
|
|
46
|
+
'.next',
|
|
47
|
+
'.vite',
|
|
48
|
+
]);
|
|
49
|
+
const SEARCH_IGNORED_NAMES = new Set([
|
|
50
|
+
...IGNORED_NAMES,
|
|
51
|
+
'.dolt',
|
|
52
|
+
'.doltcfg',
|
|
53
|
+
'.idea',
|
|
54
|
+
'.vscode',
|
|
55
|
+
'.tmp',
|
|
56
|
+
'.DS_Store',
|
|
57
|
+
'dist-release',
|
|
58
|
+
'reference',
|
|
59
|
+
'test-results',
|
|
60
|
+
'playwright-report',
|
|
61
|
+
'server.log',
|
|
62
|
+
'local-farming.log',
|
|
63
|
+
'.turbo',
|
|
64
|
+
'.cache',
|
|
65
|
+
'.pytest_cache',
|
|
66
|
+
'.mypy_cache',
|
|
67
|
+
'.ruff_cache',
|
|
68
|
+
'__pycache__',
|
|
69
|
+
]);
|
|
70
|
+
const EXPANDED_SEARCH_IGNORED_NAMES = new Set([
|
|
71
|
+
'.git',
|
|
72
|
+
'.DS_Store',
|
|
73
|
+
'server.log',
|
|
74
|
+
'local-farming.log',
|
|
75
|
+
'node_modules',
|
|
76
|
+
'dist',
|
|
77
|
+
'build',
|
|
78
|
+
'coverage',
|
|
79
|
+
'.next',
|
|
80
|
+
'.vite',
|
|
81
|
+
'.turbo',
|
|
82
|
+
'.cache',
|
|
83
|
+
'.pytest_cache',
|
|
84
|
+
'.mypy_cache',
|
|
85
|
+
'.ruff_cache',
|
|
86
|
+
'__pycache__',
|
|
87
|
+
'test-results',
|
|
88
|
+
'playwright-report',
|
|
89
|
+
]);
|
|
90
|
+
const HIDDEN_NAMES = new Set([
|
|
91
|
+
...IGNORED_NAMES,
|
|
92
|
+
'.dolt',
|
|
93
|
+
'.doltcfg',
|
|
94
|
+
'.idea',
|
|
95
|
+
'.vscode',
|
|
96
|
+
'.tmp',
|
|
97
|
+
'.DS_Store',
|
|
98
|
+
'dist-release',
|
|
99
|
+
'reference',
|
|
100
|
+
'test-results',
|
|
101
|
+
'playwright-report',
|
|
102
|
+
'server.log',
|
|
103
|
+
'local-farming.log',
|
|
104
|
+
'.turbo',
|
|
105
|
+
'.cache',
|
|
106
|
+
'.pytest_cache',
|
|
107
|
+
'.mypy_cache',
|
|
108
|
+
'.ruff_cache',
|
|
109
|
+
'__pycache__',
|
|
110
|
+
]);
|
|
111
|
+
const TREE_HIDDEN_NAMES = new Set([
|
|
112
|
+
'.git',
|
|
113
|
+
'.DS_Store',
|
|
114
|
+
'server.log',
|
|
115
|
+
'local-farming.log',
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
function gitDiffWhitespaceArgs(value) {
|
|
119
|
+
if (value === 'ALL' || value === 'IGNORE_ALL') return ['--ignore-all-space'];
|
|
120
|
+
if (value === 'TRAILING' || value === 'IGNORE_TRAILING') return ['--ignore-space-at-eol'];
|
|
121
|
+
if (value === 'LEADING_AND_TRAILING' || value === 'IGNORE_LEADING_AND_TRAILING') return ['--ignore-space-change'];
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function gitDiffContextArgs(value) {
|
|
126
|
+
const context = Number(value);
|
|
127
|
+
if (!Number.isInteger(context) || context < 0) return [];
|
|
128
|
+
return [`--unified=${Math.min(context, 10000)}`];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function resolveBundledRipgrepPath() {
|
|
132
|
+
const script = path.join(__dirname, '..', 'node_modules', 'ripgrep', 'lib', 'rg.mjs');
|
|
133
|
+
if (fs.existsSync(script)) return script;
|
|
134
|
+
const binaryName = process.platform === 'win32' ? 'rg.cmd' : 'rg';
|
|
135
|
+
const bin = path.join(__dirname, '..', 'node_modules', '.bin', binaryName);
|
|
136
|
+
return fs.existsSync(bin) ? bin : '';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resolveBundledRipgrepModulePath() {
|
|
140
|
+
try {
|
|
141
|
+
return require.resolve('ripgrep');
|
|
142
|
+
} catch {
|
|
143
|
+
const candidate = path.join(__dirname, '..', 'node_modules', 'ripgrep', 'lib', 'index.mjs');
|
|
144
|
+
return fs.existsSync(candidate) ? candidate : '';
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isCommandUnavailable(error) {
|
|
149
|
+
return error && (error.code === 'ENOENT' || error.code === 'EACCES');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
class WorkspaceFileError extends Error {
|
|
153
|
+
constructor(message, statusCode = 400, details = {}) {
|
|
154
|
+
super(message);
|
|
155
|
+
this.name = 'WorkspaceFileError';
|
|
156
|
+
this.statusCode = statusCode;
|
|
157
|
+
this.details = details;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function resolveCommandRunnerNodePath(options = {}) {
|
|
162
|
+
return options.nodePath || process.env.FARMING_NODE_BIN || process.execPath;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isPackagedRuntime() {
|
|
166
|
+
return Boolean(process.pkg) || process.env.FARMING_PACKAGED_RUNTIME === '1';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let chokidarPromise = null;
|
|
170
|
+
|
|
171
|
+
async function loadChokidar() {
|
|
172
|
+
if (!chokidarPromise) {
|
|
173
|
+
chokidarPromise = import('chokidar').then(module => module.default || module);
|
|
174
|
+
}
|
|
175
|
+
return chokidarPromise;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
class CommandRunner {
|
|
179
|
+
constructor(options = {}) {
|
|
180
|
+
this.helperPath = options.helperPath || path.join(__dirname, 'command-runner-child.js');
|
|
181
|
+
this.nodePath = resolveCommandRunnerNodePath(options);
|
|
182
|
+
this.nextId = 1;
|
|
183
|
+
this.pending = new Map();
|
|
184
|
+
this.child = null;
|
|
185
|
+
this.ready = false;
|
|
186
|
+
if (!options.disableHelper && !isPackagedRuntime()) {
|
|
187
|
+
this.start();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
start() {
|
|
192
|
+
try {
|
|
193
|
+
this.child = spawn(this.nodePath, [this.helperPath], {
|
|
194
|
+
cwd: __dirname,
|
|
195
|
+
env: process.env,
|
|
196
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
197
|
+
});
|
|
198
|
+
} catch {
|
|
199
|
+
this.child = null;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.ready = true;
|
|
204
|
+
const rl = readline.createInterface({
|
|
205
|
+
input: this.child.stdout,
|
|
206
|
+
crlfDelay: Infinity,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
rl.on('line', line => {
|
|
210
|
+
let response;
|
|
211
|
+
try {
|
|
212
|
+
response = JSON.parse(line);
|
|
213
|
+
} catch {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const pending = this.pending.get(response.id);
|
|
218
|
+
if (!pending) return;
|
|
219
|
+
this.pending.delete(response.id);
|
|
220
|
+
|
|
221
|
+
if (response.ok) {
|
|
222
|
+
pending.resolve({
|
|
223
|
+
stdout: response.stdout || '',
|
|
224
|
+
stderr: response.stderr || '',
|
|
225
|
+
});
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const error = new Error(response.error?.message || 'command failed');
|
|
230
|
+
error.code = response.error?.code;
|
|
231
|
+
error.signal = response.error?.signal;
|
|
232
|
+
error.stdout = response.error?.stdout || '';
|
|
233
|
+
error.stderr = response.error?.stderr || '';
|
|
234
|
+
pending.reject(error);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
this.child.stderr.on('data', data => {
|
|
238
|
+
const text = String(data || '').trim();
|
|
239
|
+
if (text) console.error('Workspace command helper:', text);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
this.child.on('error', () => {
|
|
243
|
+
this.ready = false;
|
|
244
|
+
this.child = null;
|
|
245
|
+
const pending = Array.from(this.pending.values());
|
|
246
|
+
this.pending.clear();
|
|
247
|
+
pending.forEach(({ reject }) => reject(new Error('workspace command helper failed')));
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
this.child.on('exit', () => {
|
|
251
|
+
this.ready = false;
|
|
252
|
+
this.child = null;
|
|
253
|
+
const pending = Array.from(this.pending.values());
|
|
254
|
+
this.pending.clear();
|
|
255
|
+
pending.forEach(({ reject }) => reject(new Error('workspace command helper exited')));
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
run(command, args, options = {}) {
|
|
260
|
+
if (!this.ready || !this.child || !this.child.stdin.writable) {
|
|
261
|
+
return execFileAsync(command, args, options);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const id = String(this.nextId++);
|
|
265
|
+
const request = {
|
|
266
|
+
id,
|
|
267
|
+
command,
|
|
268
|
+
args,
|
|
269
|
+
cwd: options.cwd,
|
|
270
|
+
env: options.env,
|
|
271
|
+
maxBuffer: options.maxBuffer,
|
|
272
|
+
timeout: options.timeout,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
return new Promise((resolve, reject) => {
|
|
276
|
+
this.pending.set(id, { resolve, reject });
|
|
277
|
+
this.child.stdin.write(`${JSON.stringify(request)}\n`, error => {
|
|
278
|
+
if (!error) return;
|
|
279
|
+
this.pending.delete(id);
|
|
280
|
+
reject(error);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
dispose() {
|
|
286
|
+
if (!this.child) return;
|
|
287
|
+
this.child.kill();
|
|
288
|
+
this.child = null;
|
|
289
|
+
this.ready = false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function isInside(root, target) {
|
|
294
|
+
const relative = path.relative(root, target);
|
|
295
|
+
return relative === '' || Boolean(relative) && !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function isInsideAnyRoot(roots, target) {
|
|
299
|
+
return (Array.isArray(roots) ? roots : []).some((root) => {
|
|
300
|
+
let realRoot = root;
|
|
301
|
+
try {
|
|
302
|
+
realRoot = fs.realpathSync(root);
|
|
303
|
+
} catch {
|
|
304
|
+
// Missing allowed roots cannot authorize a resolved target.
|
|
305
|
+
}
|
|
306
|
+
return isInside(realRoot, target);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function joinRelativePath(parentPath, name) {
|
|
311
|
+
return [String(parentPath || '').replace(/\/+$/, ''), name]
|
|
312
|
+
.filter(Boolean)
|
|
313
|
+
.join('/')
|
|
314
|
+
.replace(/\\/g, '/');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function normalizeUserPath(userPath = '') {
|
|
318
|
+
const value = String(userPath || '').trim();
|
|
319
|
+
if (!value || value === '.') return '';
|
|
320
|
+
if (path.isAbsolute(value)) {
|
|
321
|
+
throw new WorkspaceFileError('absolute paths are not allowed', 400);
|
|
322
|
+
}
|
|
323
|
+
const normalized = path.normalize(value);
|
|
324
|
+
if (normalized === '..' || normalized.startsWith(`..${path.sep}`)) {
|
|
325
|
+
throw new WorkspaceFileError('path must stay inside the workspace', 403);
|
|
326
|
+
}
|
|
327
|
+
return normalized === '.' ? '' : normalized;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function relativeFromRoot(root, target) {
|
|
331
|
+
const relative = path.relative(root, target);
|
|
332
|
+
return relative.split(path.sep).join('/');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function normalizeSearchResultPath(resultPath) {
|
|
336
|
+
return String(resultPath || '').replace(/^\.\//, '');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function searchIgnoredNames(includeIgnored = false) {
|
|
340
|
+
return includeIgnored ? EXPANDED_SEARCH_IGNORED_NAMES : SEARCH_IGNORED_NAMES;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function searchExcludeGlobArgs() {
|
|
344
|
+
const args = [];
|
|
345
|
+
SEARCH_IGNORED_NAMES.forEach((name) => {
|
|
346
|
+
args.push('--glob', `!${name}/**`);
|
|
347
|
+
args.push('--glob', `!**/${name}/**`);
|
|
348
|
+
});
|
|
349
|
+
return args;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function isSearchIgnoredRelativePath(relativePath, includeIgnored = false) {
|
|
353
|
+
return String(relativePath || '').split(/[\\/]/).some(part => searchIgnoredNames(includeIgnored).has(part));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function shouldPruneDirectoryNameSearch(relativePath) {
|
|
357
|
+
return String(relativePath || '').split(/[\\/]/).some(part => IGNORED_NAMES.has(part));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function gitStatusExcludePathspecArgs() {
|
|
361
|
+
const args = ['--', '.'];
|
|
362
|
+
HIDDEN_NAMES.forEach((name) => {
|
|
363
|
+
args.push(`:(exclude)${name}`);
|
|
364
|
+
args.push(`:(exclude)${name}/**`);
|
|
365
|
+
args.push(`:(exclude)**/${name}`);
|
|
366
|
+
args.push(`:(exclude)**/${name}/**`);
|
|
367
|
+
});
|
|
368
|
+
return args;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function gitSearchExcludePathspecArgs(includeIgnored = false, searchPath = '.') {
|
|
372
|
+
const args = ['--', searchPath];
|
|
373
|
+
searchIgnoredNames(includeIgnored).forEach((name) => {
|
|
374
|
+
args.push(`:(exclude)${name}`);
|
|
375
|
+
args.push(`:(exclude)${name}/**`);
|
|
376
|
+
args.push(`:(exclude)**/${name}`);
|
|
377
|
+
args.push(`:(exclude)**/${name}/**`);
|
|
378
|
+
});
|
|
379
|
+
return args;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function normalizeGitStatusPath(resultPath) {
|
|
383
|
+
return normalizeSearchResultPath(resultPath).replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function sha1(buffer) {
|
|
387
|
+
return crypto.createHash('sha1').update(buffer).digest('hex');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function previewForPath(filePath, options = {}) {
|
|
391
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
392
|
+
if (!options.includeTextImages && TEXT_IMAGE_PREVIEW_EXTENSIONS.has(extension)) return null;
|
|
393
|
+
const mediaType = IMAGE_PREVIEW_MEDIA_TYPES.get(extension);
|
|
394
|
+
return mediaType ? { kind: 'image', mediaType } : null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function metadataFileVersion(relativePath, stat) {
|
|
398
|
+
return sha1(Buffer.from(`${relativePath}:${stat.size}:${stat.mtimeMs}`, 'utf8'));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function readTextPrefix(target, maxBytes) {
|
|
402
|
+
const handle = await fsp.open(target, 'r');
|
|
403
|
+
try {
|
|
404
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
405
|
+
const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
|
|
406
|
+
return buffer.subarray(0, bytesRead).toString('utf8');
|
|
407
|
+
} finally {
|
|
408
|
+
await handle.close();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function isProbablyBinaryFile(target) {
|
|
413
|
+
const handle = await fsp.open(target, 'r');
|
|
414
|
+
try {
|
|
415
|
+
const buffer = Buffer.alloc(BINARY_SNIFF_BYTES);
|
|
416
|
+
const { bytesRead } = await handle.read(buffer, 0, BINARY_SNIFF_BYTES, 0);
|
|
417
|
+
if (bytesRead === 0) return false;
|
|
418
|
+
|
|
419
|
+
let suspiciousControlBytes = 0;
|
|
420
|
+
for (let index = 0; index < bytesRead; index += 1) {
|
|
421
|
+
const byte = buffer[index];
|
|
422
|
+
if (byte === 0) return true;
|
|
423
|
+
const isAllowedControl = byte === 7 || byte === 8 || byte === 9 || byte === 10 || byte === 12 || byte === 13 || byte === 27;
|
|
424
|
+
if (byte < 32 && !isAllowedControl) {
|
|
425
|
+
suspiciousControlBytes += 1;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return suspiciousControlBytes / bytesRead > 0.3;
|
|
430
|
+
} finally {
|
|
431
|
+
await handle.close();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function shouldIgnorePath(filePath) {
|
|
436
|
+
return filePath.split(path.sep).some(part => IGNORED_NAMES.has(part));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function shouldHidePath(filePath) {
|
|
440
|
+
return String(filePath || '').split(/[\\/]/).some(part => HIDDEN_NAMES.has(part));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function normalizeEntryName(name) {
|
|
444
|
+
const value = String(name || '').trim();
|
|
445
|
+
if (!value || value === '.' || value === '..') {
|
|
446
|
+
throw new WorkspaceFileError('name is required', 400);
|
|
447
|
+
}
|
|
448
|
+
if (value.includes('/') || value.includes('\\') || path.basename(value) !== value) {
|
|
449
|
+
throw new WorkspaceFileError('name must be a single path segment', 400);
|
|
450
|
+
}
|
|
451
|
+
if (IGNORED_NAMES.has(value)) {
|
|
452
|
+
throw new WorkspaceFileError('ignored paths cannot be changed', 403);
|
|
453
|
+
}
|
|
454
|
+
return value;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function parentDirectory(filePath) {
|
|
458
|
+
const normalized = String(filePath || '').replace(/\/+$/, '');
|
|
459
|
+
const index = normalized.lastIndexOf('/');
|
|
460
|
+
return index === -1 ? '' : normalized.slice(0, index);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function execFileAsync(command, args, options = {}) {
|
|
464
|
+
return new Promise((resolve, reject) => {
|
|
465
|
+
execFile(command, args, {
|
|
466
|
+
encoding: 'utf8',
|
|
467
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
468
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
469
|
+
...options,
|
|
470
|
+
}, (error, stdout, stderr) => {
|
|
471
|
+
if (error) {
|
|
472
|
+
error.stdout = stdout;
|
|
473
|
+
error.stderr = stderr;
|
|
474
|
+
reject(error);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
resolve({ stdout, stderr });
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function gitStatusKind(statusCode) {
|
|
483
|
+
if (!statusCode || statusCode.length < 2) return 'modified';
|
|
484
|
+
if (statusCode === '??') return 'untracked';
|
|
485
|
+
if (statusCode.includes('U') || statusCode === 'AA' || statusCode === 'DD') return 'conflicted';
|
|
486
|
+
if (statusCode.includes('D')) return 'deleted';
|
|
487
|
+
if (statusCode.includes('A')) return 'added';
|
|
488
|
+
if (statusCode.includes('R')) return 'renamed';
|
|
489
|
+
if (statusCode.includes('M')) return 'modified';
|
|
490
|
+
return 'modified';
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function gitStatusLabel(kind) {
|
|
494
|
+
if (kind === 'added') return 'A';
|
|
495
|
+
if (kind === 'deleted') return 'D';
|
|
496
|
+
if (kind === 'renamed') return 'R';
|
|
497
|
+
if (kind === 'untracked') return 'U';
|
|
498
|
+
if (kind === 'conflicted') return '!';
|
|
499
|
+
return 'M';
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function gitStatusReviewRank(kind) {
|
|
503
|
+
if (kind === 'conflicted') return 0;
|
|
504
|
+
if (kind === 'modified') return 1;
|
|
505
|
+
if (kind === 'added') return 2;
|
|
506
|
+
if (kind === 'deleted') return 3;
|
|
507
|
+
if (kind === 'renamed') return 4;
|
|
508
|
+
if (kind === 'untracked') return 5;
|
|
509
|
+
return 6;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function workspaceEntryTypeForGitChange(root, filePath) {
|
|
513
|
+
try {
|
|
514
|
+
const stat = await fsp.lstat(path.join(root, filePath.replace(/\/+$/, '')));
|
|
515
|
+
if (stat.isDirectory()) return 'directory';
|
|
516
|
+
if (stat.isFile()) return 'file';
|
|
517
|
+
if (stat.isSymbolicLink()) return 'symlink';
|
|
518
|
+
return 'other';
|
|
519
|
+
} catch {
|
|
520
|
+
return 'file';
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function gitStatusRank(kind) {
|
|
525
|
+
if (kind === 'conflicted') return 6;
|
|
526
|
+
if (kind === 'deleted') return 5;
|
|
527
|
+
if (kind === 'modified') return 4;
|
|
528
|
+
if (kind === 'renamed') return 3;
|
|
529
|
+
if (kind === 'added') return 2;
|
|
530
|
+
if (kind === 'untracked') return 1;
|
|
531
|
+
return 0;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function isPathWordBoundary(text, index) {
|
|
535
|
+
if (index === 0) return true;
|
|
536
|
+
const previous = text[index - 1] || '';
|
|
537
|
+
const current = text[index] || '';
|
|
538
|
+
if (/[-_\s./\\]/.test(previous)) return true;
|
|
539
|
+
return /[a-z0-9]/.test(previous) && /[A-Z]/.test(current);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function scoreBoundaryMatch(text, normalizedQuery) {
|
|
543
|
+
if (!text || !normalizedQuery || normalizedQuery.length > 6) return null;
|
|
544
|
+
let queryIndex = 0;
|
|
545
|
+
let score = 0;
|
|
546
|
+
|
|
547
|
+
for (let textIndex = 0; textIndex < text.length && queryIndex < normalizedQuery.length; textIndex += 1) {
|
|
548
|
+
if (!isPathWordBoundary(text, textIndex)) continue;
|
|
549
|
+
if (text[textIndex].toLowerCase() !== normalizedQuery[queryIndex]) continue;
|
|
550
|
+
score += textIndex;
|
|
551
|
+
queryIndex += 1;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return queryIndex === normalizedQuery.length ? score : null;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function scorePathMatch(filePath, query, options = {}) {
|
|
558
|
+
const pathText = normalizeSearchResultPath(filePath);
|
|
559
|
+
const normalizedPath = pathText.toLowerCase();
|
|
560
|
+
const normalizedQuery = String(query || '').trim().replace(/^\.\/+/, '').toLowerCase();
|
|
561
|
+
if (!normalizedPath || !normalizedQuery) return null;
|
|
562
|
+
|
|
563
|
+
const fileNameText = path.posix.basename(pathText);
|
|
564
|
+
const fileName = fileNameText.toLowerCase();
|
|
565
|
+
if (normalizedPath === normalizedQuery) return 0;
|
|
566
|
+
if (fileName === normalizedQuery) return 1;
|
|
567
|
+
|
|
568
|
+
const nameBoundaryScore = scoreBoundaryMatch(fileNameText, normalizedQuery);
|
|
569
|
+
if (nameBoundaryScore !== null) return 5 + nameBoundaryScore / 100;
|
|
570
|
+
|
|
571
|
+
const nameIndex = fileName.indexOf(normalizedQuery);
|
|
572
|
+
if (nameIndex !== -1) return 10 + nameIndex;
|
|
573
|
+
|
|
574
|
+
if (options.allowPathMatch === false) return null;
|
|
575
|
+
|
|
576
|
+
const pathIndex = normalizedPath.indexOf(normalizedQuery);
|
|
577
|
+
if (pathIndex !== -1) return 40 + pathIndex;
|
|
578
|
+
|
|
579
|
+
if (normalizedQuery.length > 6) return null;
|
|
580
|
+
|
|
581
|
+
let queryIndex = 0;
|
|
582
|
+
let score = 80;
|
|
583
|
+
for (let pathIndex = 0; pathIndex < normalizedPath.length && queryIndex < normalizedQuery.length; pathIndex += 1) {
|
|
584
|
+
if (normalizedPath[pathIndex] !== normalizedQuery[queryIndex]) continue;
|
|
585
|
+
score += pathIndex;
|
|
586
|
+
queryIndex += 1;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return queryIndex === normalizedQuery.length ? score : null;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function pathSearchEntryRank(entryType) {
|
|
593
|
+
return entryType === 'directory' ? 0 : 1;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function pathSearchDepth(filePath) {
|
|
597
|
+
return normalizeSearchResultPath(filePath).split('/').filter(Boolean).length;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function comparePathSearchMatches(a, b) {
|
|
601
|
+
return (
|
|
602
|
+
a.score - b.score ||
|
|
603
|
+
pathSearchDepth(a.path) - pathSearchDepth(b.path) ||
|
|
604
|
+
pathSearchEntryRank(a.entryType) - pathSearchEntryRank(b.entryType) ||
|
|
605
|
+
a.path.localeCompare(b.path)
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function pathSearchMatchForPath(filePath, query, entryType, allowPathMatch) {
|
|
610
|
+
const score = scorePathMatch(filePath, query, { allowPathMatch });
|
|
611
|
+
if (score === null) return null;
|
|
612
|
+
return {
|
|
613
|
+
path: filePath,
|
|
614
|
+
entryType,
|
|
615
|
+
score,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function directoryNameSearchScore(directoryPath, query, allowPathMatch) {
|
|
620
|
+
const pathText = normalizeSearchResultPath(directoryPath);
|
|
621
|
+
const normalizedPath = pathText.toLowerCase();
|
|
622
|
+
const normalizedQuery = String(query || '').trim().replace(/^\.\/+/, '').toLowerCase();
|
|
623
|
+
if (!normalizedPath || !normalizedQuery) return null;
|
|
624
|
+
|
|
625
|
+
const directoryName = path.posix.basename(pathText).toLowerCase();
|
|
626
|
+
if (normalizedPath === normalizedQuery) return 0;
|
|
627
|
+
if (directoryName === normalizedQuery) return 1;
|
|
628
|
+
if (directoryName.startsWith(normalizedQuery)) return 5;
|
|
629
|
+
if (normalizedQuery.length >= 4) {
|
|
630
|
+
const nameIndex = directoryName.indexOf(normalizedQuery);
|
|
631
|
+
if (nameIndex !== -1) return 10 + nameIndex;
|
|
632
|
+
}
|
|
633
|
+
if (!allowPathMatch) return null;
|
|
634
|
+
const pathIndex = normalizedPath.indexOf(normalizedQuery);
|
|
635
|
+
if (pathIndex !== -1) return 40 + pathIndex;
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function directoryNameSearchMatchForPath(directoryPath, query, allowPathMatch) {
|
|
640
|
+
const score = directoryNameSearchScore(directoryPath, query, allowPathMatch);
|
|
641
|
+
if (score === null) return null;
|
|
642
|
+
return {
|
|
643
|
+
path: directoryPath,
|
|
644
|
+
entryType: 'directory',
|
|
645
|
+
score,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function ancestorDirectoryPaths(filePath) {
|
|
650
|
+
const segments = normalizeSearchResultPath(filePath).split('/').filter(Boolean);
|
|
651
|
+
const ancestors = [];
|
|
652
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
653
|
+
ancestors.push(segments.slice(0, index).join('/'));
|
|
654
|
+
}
|
|
655
|
+
return ancestors;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function pathSearchMatchesForFile(filePath, query, includeIgnored = false) {
|
|
659
|
+
const allowPathMatch = isLikelyPathQuery(query);
|
|
660
|
+
const matches = [];
|
|
661
|
+
const fileMatch = pathSearchMatchForPath(filePath, query, 'file', allowPathMatch);
|
|
662
|
+
if (fileMatch) matches.push(fileMatch);
|
|
663
|
+
ancestorDirectoryPaths(filePath).forEach((directoryPath) => {
|
|
664
|
+
if (isSearchIgnoredRelativePath(directoryPath, includeIgnored)) return;
|
|
665
|
+
const directoryMatch = pathSearchMatchForPath(directoryPath, query, 'directory', allowPathMatch);
|
|
666
|
+
if (directoryMatch) matches.push(directoryMatch);
|
|
667
|
+
});
|
|
668
|
+
return matches;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function isLikelyPathQuery(query) {
|
|
672
|
+
return /[./\\]/.test(String(query || '').trim());
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function dedupePathMatches(matches, limit) {
|
|
676
|
+
const seen = new Set();
|
|
677
|
+
const deduped = [];
|
|
678
|
+
for (const match of matches) {
|
|
679
|
+
if (!match || seen.has(match.path)) continue;
|
|
680
|
+
seen.add(match.path);
|
|
681
|
+
deduped.push(match);
|
|
682
|
+
if (deduped.length >= limit) break;
|
|
683
|
+
}
|
|
684
|
+
return deduped;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function parseGitStatus(stdout) {
|
|
688
|
+
const records = String(stdout || '').split('\0').filter(Boolean);
|
|
689
|
+
const statusByPath = new Map();
|
|
690
|
+
|
|
691
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
692
|
+
const record = records[index];
|
|
693
|
+
if (record.length < 4) continue;
|
|
694
|
+
|
|
695
|
+
const statusCode = record.slice(0, 2);
|
|
696
|
+
const filePath = normalizeGitStatusPath(record.slice(3));
|
|
697
|
+
if (!filePath) continue;
|
|
698
|
+
|
|
699
|
+
const kind = gitStatusKind(statusCode);
|
|
700
|
+
const previousPath = statusCode.includes('R') || statusCode.includes('C')
|
|
701
|
+
? normalizeGitStatusPath(records[index + 1])
|
|
702
|
+
: '';
|
|
703
|
+
statusByPath.set(filePath, {
|
|
704
|
+
kind,
|
|
705
|
+
label: gitStatusLabel(kind),
|
|
706
|
+
...(previousPath ? { previousPath } : {}),
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
if (statusCode.includes('R') || statusCode.includes('C')) {
|
|
710
|
+
index += 1;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return statusByPath;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function parseGitBlamePorcelain(stdout) {
|
|
718
|
+
const lines = String(stdout || '').split('\n');
|
|
719
|
+
const blameLines = [];
|
|
720
|
+
let index = 0;
|
|
721
|
+
|
|
722
|
+
while (index < lines.length) {
|
|
723
|
+
const header = lines[index];
|
|
724
|
+
const match = /^(\^?[0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/.exec(header || '');
|
|
725
|
+
if (!match) {
|
|
726
|
+
index += 1;
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const entry = {
|
|
731
|
+
commit: match[1].replace(/^\^/, ''),
|
|
732
|
+
originalLineNumber: Number(match[2]),
|
|
733
|
+
lineNumber: Number(match[3]),
|
|
734
|
+
author: '',
|
|
735
|
+
authorMail: '',
|
|
736
|
+
authorTime: null,
|
|
737
|
+
authorTimeIso: '',
|
|
738
|
+
summary: '',
|
|
739
|
+
content: '',
|
|
740
|
+
};
|
|
741
|
+
index += 1;
|
|
742
|
+
|
|
743
|
+
while (index < lines.length) {
|
|
744
|
+
const line = lines[index];
|
|
745
|
+
index += 1;
|
|
746
|
+
if (line.startsWith('\t')) {
|
|
747
|
+
entry.content = line.slice(1);
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const separator = line.indexOf(' ');
|
|
752
|
+
const fieldName = separator === -1 ? line : line.slice(0, separator);
|
|
753
|
+
const value = separator === -1 ? '' : line.slice(separator + 1);
|
|
754
|
+
if (fieldName === 'author') entry.author = value;
|
|
755
|
+
if (fieldName === 'author-mail') entry.authorMail = value.replace(/^<|>$/g, '');
|
|
756
|
+
if (fieldName === 'author-time') {
|
|
757
|
+
const time = Number(value);
|
|
758
|
+
entry.authorTime = Number.isFinite(time) ? time : null;
|
|
759
|
+
entry.authorTimeIso = Number.isFinite(time) ? new Date(time * 1000).toISOString() : '';
|
|
760
|
+
}
|
|
761
|
+
if (fieldName === 'summary') entry.summary = value;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const uncommitted = /^0+$/.test(entry.commit);
|
|
765
|
+
blameLines.push({
|
|
766
|
+
...entry,
|
|
767
|
+
shortCommit: uncommitted ? 'uncommitted' : entry.commit.slice(0, 8),
|
|
768
|
+
uncommitted,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return blameLines;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function parseUnifiedDiffHunks(patch) {
|
|
776
|
+
const lines = String(patch || '').split('\n');
|
|
777
|
+
const hunks = [];
|
|
778
|
+
let hunk = null;
|
|
779
|
+
|
|
780
|
+
const finishHunk = () => {
|
|
781
|
+
if (hunk) {
|
|
782
|
+
hunks.push({
|
|
783
|
+
...hunk,
|
|
784
|
+
patch: hunk.patchLines.join('\n'),
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
hunk = null;
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
for (const line of lines) {
|
|
791
|
+
const match = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/.exec(line);
|
|
792
|
+
if (match) {
|
|
793
|
+
finishHunk();
|
|
794
|
+
hunk = {
|
|
795
|
+
header: line,
|
|
796
|
+
oldStart: Number(match[1]),
|
|
797
|
+
oldLines: Number(match[2] || 1),
|
|
798
|
+
newStart: Number(match[3]),
|
|
799
|
+
newLines: Number(match[4] || 1),
|
|
800
|
+
heading: match[5] ? match[5].trim() : '',
|
|
801
|
+
patchLines: [line],
|
|
802
|
+
};
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (hunk) {
|
|
807
|
+
hunk.patchLines.push(line);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
finishHunk();
|
|
812
|
+
return hunks;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function intralineRanges(leftText, rightText) {
|
|
816
|
+
const changes = diffChars(leftText, rightText, { timeout: 100 });
|
|
817
|
+
if (!changes) return {};
|
|
818
|
+
const left = [];
|
|
819
|
+
const right = [];
|
|
820
|
+
let leftOffset = 0;
|
|
821
|
+
let rightOffset = 0;
|
|
822
|
+
for (const change of changes) {
|
|
823
|
+
const length = Array.from(change.value).length;
|
|
824
|
+
if (change.removed) {
|
|
825
|
+
if (length > 0) left.push({ start: leftOffset, end: leftOffset + length });
|
|
826
|
+
leftOffset += length;
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
if (change.added) {
|
|
830
|
+
if (length > 0) right.push({ start: rightOffset, end: rightOffset + length });
|
|
831
|
+
rightOffset += length;
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
leftOffset += length;
|
|
835
|
+
rightOffset += length;
|
|
836
|
+
}
|
|
837
|
+
return {
|
|
838
|
+
...(left.length ? { left } : {}),
|
|
839
|
+
...(right.length ? { right } : {}),
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function parseUnifiedDiffRows(patch) {
|
|
844
|
+
return parseUnifiedDiffHunks(patch).map(hunk => {
|
|
845
|
+
let oldLine = hunk.oldStart;
|
|
846
|
+
let newLine = hunk.newStart;
|
|
847
|
+
const rows = [];
|
|
848
|
+
let previousCell = null;
|
|
849
|
+
for (const line of hunk.patchLines.slice(1)) {
|
|
850
|
+
if (line.startsWith('\')) {
|
|
851
|
+
if (previousCell) previousCell.missingNewlineAtEnd = true;
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
if (line.startsWith(' ')) {
|
|
855
|
+
const text = line.slice(1);
|
|
856
|
+
const row = {
|
|
857
|
+
kind: 'context',
|
|
858
|
+
left: { line: oldLine, text },
|
|
859
|
+
right: { line: newLine, text },
|
|
860
|
+
};
|
|
861
|
+
rows.push(row);
|
|
862
|
+
previousCell = row.right;
|
|
863
|
+
oldLine++;
|
|
864
|
+
newLine++;
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
if (line.startsWith('-')) {
|
|
868
|
+
const row = { kind: 'deleted', left: { line: oldLine, text: line.slice(1) } };
|
|
869
|
+
rows.push(row);
|
|
870
|
+
previousCell = row.left;
|
|
871
|
+
oldLine++;
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
if (line.startsWith('+')) {
|
|
875
|
+
const row = { kind: 'added', right: { line: newLine, text: line.slice(1) } };
|
|
876
|
+
rows.push(row);
|
|
877
|
+
previousCell = row.right;
|
|
878
|
+
newLine++;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
const pairedRows = [];
|
|
882
|
+
for (let index = 0; index < rows.length;) {
|
|
883
|
+
if (rows[index].kind !== 'deleted') {
|
|
884
|
+
pairedRows.push(rows[index]);
|
|
885
|
+
index += 1;
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
const deleted = [];
|
|
889
|
+
while (index < rows.length && rows[index].kind === 'deleted') deleted.push(rows[index++]);
|
|
890
|
+
const added = [];
|
|
891
|
+
while (index < rows.length && rows[index].kind === 'added') added.push(rows[index++]);
|
|
892
|
+
const paired = Math.min(deleted.length, added.length);
|
|
893
|
+
for (let pairIndex = 0; pairIndex < paired; pairIndex += 1) {
|
|
894
|
+
const left = deleted[pairIndex].left;
|
|
895
|
+
const right = added[pairIndex].right;
|
|
896
|
+
const intraline = intralineRanges(left.text, right.text);
|
|
897
|
+
pairedRows.push({
|
|
898
|
+
kind: 'changed',
|
|
899
|
+
left: { ...left, ...(intraline.left ? { intraline: intraline.left } : {}) },
|
|
900
|
+
right: { ...right, ...(intraline.right ? { intraline: intraline.right } : {}) },
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
pairedRows.push(...deleted.slice(paired));
|
|
904
|
+
pairedRows.push(...added.slice(paired));
|
|
905
|
+
}
|
|
906
|
+
return {
|
|
907
|
+
header: hunk.header,
|
|
908
|
+
oldStart: hunk.oldStart,
|
|
909
|
+
oldLines: hunk.oldLines,
|
|
910
|
+
newStart: hunk.newStart,
|
|
911
|
+
newLines: hunk.newLines,
|
|
912
|
+
rows: pairedRows,
|
|
913
|
+
};
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function lineInUnifiedDiffHunk(hunk, side, lineNumber) {
|
|
918
|
+
const start = side === 'old' ? hunk.oldStart : hunk.newStart;
|
|
919
|
+
const count = side === 'old' ? hunk.oldLines : hunk.newLines;
|
|
920
|
+
const end = Math.max(start, start + count - 1);
|
|
921
|
+
return lineNumber >= start && lineNumber <= end;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function selectUnifiedDiffHunk(patch, side, lineNumber) {
|
|
925
|
+
const hunks = parseUnifiedDiffHunks(patch);
|
|
926
|
+
return hunks.find(hunk => lineInUnifiedDiffHunk(hunk, side, lineNumber)) || null;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function createAddedFileLineChangesHunk(content, lineNumber, contextLines = 20) {
|
|
930
|
+
const lines = String(content || '').split('\n');
|
|
931
|
+
if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
|
|
932
|
+
if (lines.length === 0) return null;
|
|
933
|
+
|
|
934
|
+
const startIndex = Math.max(0, lineNumber - contextLines - 1);
|
|
935
|
+
const endIndex = Math.min(lines.length, lineNumber + contextLines);
|
|
936
|
+
const newStart = startIndex + 1;
|
|
937
|
+
const newLines = endIndex - startIndex;
|
|
938
|
+
const patchLines = [
|
|
939
|
+
`@@ -0,0 +${newStart},${newLines} @@`,
|
|
940
|
+
...lines.slice(startIndex, endIndex).map(line => `+${line}`),
|
|
941
|
+
];
|
|
942
|
+
|
|
943
|
+
return {
|
|
944
|
+
header: patchLines[0],
|
|
945
|
+
oldStart: 0,
|
|
946
|
+
oldLines: 0,
|
|
947
|
+
newStart,
|
|
948
|
+
newLines,
|
|
949
|
+
heading: '',
|
|
950
|
+
patch: patchLines.join('\n'),
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function strongestStatus(current, next) {
|
|
955
|
+
if (!current) return next;
|
|
956
|
+
return gitStatusRank(next.kind) > gitStatusRank(current.kind) ? next : current;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function buildDescendantGitStatusByDirectory(statusByPath) {
|
|
960
|
+
const statusByDirectory = new Map();
|
|
961
|
+
statusByPath.forEach((status, statusPath) => {
|
|
962
|
+
let directory = parentDirectory(statusPath);
|
|
963
|
+
while (directory) {
|
|
964
|
+
statusByDirectory.set(directory, strongestStatus(statusByDirectory.get(directory), status));
|
|
965
|
+
directory = parentDirectory(directory);
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
return statusByDirectory;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
class WorkspaceFileService {
|
|
972
|
+
constructor(options = {}) {
|
|
973
|
+
this.maxFileSize = options.maxFileSize || DEFAULT_MAX_FILE_SIZE;
|
|
974
|
+
this.maxWriteSize = options.maxWriteSize || DEFAULT_MAX_WRITE_SIZE;
|
|
975
|
+
this.maxPreviewFileSize = options.maxPreviewFileSize || DEFAULT_MAX_PREVIEW_FILE_SIZE;
|
|
976
|
+
this.searchLimit = options.searchLimit || DEFAULT_SEARCH_LIMIT;
|
|
977
|
+
this.searchTimeoutMs = options.searchTimeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS;
|
|
978
|
+
this.blameTimeoutMs = options.blameTimeoutMs ?? DEFAULT_BLAME_TIMEOUT_MS;
|
|
979
|
+
this.diffTimeoutMs = options.diffTimeoutMs ?? DEFAULT_DIFF_TIMEOUT_MS;
|
|
980
|
+
this.diffMaxBuffer = options.diffMaxBuffer ?? DEFAULT_DIFF_MAX_BUFFER;
|
|
981
|
+
const bundledRipgrepPath = resolveBundledRipgrepPath();
|
|
982
|
+
this.rgPath = options.rgPath || process.env.FARMING_RG_BIN || bundledRipgrepPath || 'rg';
|
|
983
|
+
this.rgFallbackPath = options.rgFallbackPath ?? (this.rgPath === 'rg' ? bundledRipgrepPath : 'rg');
|
|
984
|
+
this.bundledRipgrepModulePath = options.bundledRipgrepModulePath ?? resolveBundledRipgrepModulePath();
|
|
985
|
+
this.bundledRipgrepModule = null;
|
|
986
|
+
this.gitPath = options.gitPath || 'git';
|
|
987
|
+
this.gitStatusCacheTtlMs = options.gitStatusCacheTtlMs ?? DEFAULT_GIT_STATUS_CACHE_TTL_MS;
|
|
988
|
+
this.gitStatusInlineTimeoutMs = options.gitStatusInlineTimeoutMs ?? DEFAULT_GIT_STATUS_INLINE_TIMEOUT_MS;
|
|
989
|
+
this.gitStatusCache = new Map();
|
|
990
|
+
this.watchers = new Map();
|
|
991
|
+
this.watchOptions = options.watchOptions || {};
|
|
992
|
+
this.watchDepth = Number.isFinite(options.watchDepth) ? Math.max(0, options.watchDepth) : DEFAULT_WATCH_DEPTH;
|
|
993
|
+
this.commandRunner = options.commandRunner || new CommandRunner(options.commandRunnerOptions);
|
|
994
|
+
this.ownsCommandRunner = !options.commandRunner;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
execFile(command, args, options = {}) {
|
|
998
|
+
return this.commandRunner.run(command, args, {
|
|
999
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
1000
|
+
...options,
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
async execRipgrep(args, options = {}) {
|
|
1005
|
+
try {
|
|
1006
|
+
return await this.execFile(this.rgPath, args, options);
|
|
1007
|
+
} catch (error) {
|
|
1008
|
+
if (!isCommandUnavailable(error) || !this.rgFallbackPath || this.rgFallbackPath === this.rgPath) {
|
|
1009
|
+
if (isCommandUnavailable(error)) {
|
|
1010
|
+
return this.runBundledRipgrep(args, options);
|
|
1011
|
+
}
|
|
1012
|
+
throw error;
|
|
1013
|
+
}
|
|
1014
|
+
this.rgPath = this.rgFallbackPath;
|
|
1015
|
+
try {
|
|
1016
|
+
return await this.execFile(this.rgPath, args, options);
|
|
1017
|
+
} catch (fallbackError) {
|
|
1018
|
+
if (isCommandUnavailable(fallbackError)) {
|
|
1019
|
+
return this.runBundledRipgrep(args, options);
|
|
1020
|
+
}
|
|
1021
|
+
throw fallbackError;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
async loadBundledRipgrep() {
|
|
1027
|
+
if (!this.bundledRipgrepModulePath) {
|
|
1028
|
+
const error = new Error('bundled ripgrep is unavailable');
|
|
1029
|
+
error.code = 'ENOENT';
|
|
1030
|
+
throw error;
|
|
1031
|
+
}
|
|
1032
|
+
if (!this.bundledRipgrepModule) {
|
|
1033
|
+
this.bundledRipgrepModule = import(pathToFileURL(this.bundledRipgrepModulePath).href);
|
|
1034
|
+
}
|
|
1035
|
+
return this.bundledRipgrepModule;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
async runBundledRipgrep(args, options = {}) {
|
|
1039
|
+
const { ripgrep } = await this.loadBundledRipgrep();
|
|
1040
|
+
const cwd = options.cwd || process.cwd();
|
|
1041
|
+
const run = ripgrep(args, {
|
|
1042
|
+
buffer: true,
|
|
1043
|
+
env: options.env || process.env,
|
|
1044
|
+
preopens: { '.': cwd },
|
|
1045
|
+
});
|
|
1046
|
+
const result = await (options.timeout
|
|
1047
|
+
? Promise.race([
|
|
1048
|
+
run,
|
|
1049
|
+
new Promise((_, reject) => {
|
|
1050
|
+
setTimeout(() => {
|
|
1051
|
+
const error = new Error('bundled ripgrep timed out');
|
|
1052
|
+
error.code = 'ETIMEDOUT';
|
|
1053
|
+
reject(error);
|
|
1054
|
+
}, options.timeout);
|
|
1055
|
+
}),
|
|
1056
|
+
])
|
|
1057
|
+
: run);
|
|
1058
|
+
const stdout = result.stdout || '';
|
|
1059
|
+
const stderr = result.stderr || '';
|
|
1060
|
+
if (result.code && result.code !== 0) {
|
|
1061
|
+
const error = new Error(stderr || `ripgrep exited with code ${result.code}`);
|
|
1062
|
+
error.code = result.code;
|
|
1063
|
+
error.stdout = stdout;
|
|
1064
|
+
error.stderr = stderr;
|
|
1065
|
+
throw error;
|
|
1066
|
+
}
|
|
1067
|
+
return { stdout, stderr };
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
async listSearchFiles(root, searchPath, timeout) {
|
|
1071
|
+
try {
|
|
1072
|
+
return await this.execFile(this.gitPath, [
|
|
1073
|
+
'-C',
|
|
1074
|
+
root,
|
|
1075
|
+
'ls-files',
|
|
1076
|
+
'-co',
|
|
1077
|
+
'--exclude-standard',
|
|
1078
|
+
'--',
|
|
1079
|
+
searchPath,
|
|
1080
|
+
], { cwd: root, timeout, maxBuffer: SEARCH_FILE_LIST_MAX_BUFFER });
|
|
1081
|
+
} catch {
|
|
1082
|
+
return this.execRipgrep([
|
|
1083
|
+
'--files',
|
|
1084
|
+
'--hidden',
|
|
1085
|
+
...searchExcludeGlobArgs(),
|
|
1086
|
+
searchPath,
|
|
1087
|
+
], { cwd: root, timeout, maxBuffer: SEARCH_FILE_LIST_MAX_BUFFER });
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
async collectGitIgnoredPathMatchCandidates(root, searchPath, query, limit, timeout) {
|
|
1092
|
+
const candidateLimit = Math.max(PATH_SEARCH_MIN_CANDIDATES, limit * PATH_SEARCH_CANDIDATE_MULTIPLIER);
|
|
1093
|
+
const { stdout } = await this.execFile(this.gitPath, [
|
|
1094
|
+
'-C',
|
|
1095
|
+
root,
|
|
1096
|
+
'ls-files',
|
|
1097
|
+
'--others',
|
|
1098
|
+
'--ignored',
|
|
1099
|
+
'--exclude-standard',
|
|
1100
|
+
...gitSearchExcludePathspecArgs(true, searchPath),
|
|
1101
|
+
], { cwd: root, timeout, maxBuffer: SEARCH_FILE_LIST_MAX_BUFFER });
|
|
1102
|
+
const scoredMatches = [];
|
|
1103
|
+
const seenMatchPaths = new Set();
|
|
1104
|
+
let truncated = false;
|
|
1105
|
+
|
|
1106
|
+
for (const line of stdout.split('\n')) {
|
|
1107
|
+
const filePath = normalizeSearchResultPath(line);
|
|
1108
|
+
if (!filePath || isSearchIgnoredRelativePath(filePath, true)) continue;
|
|
1109
|
+
for (const match of pathSearchMatchesForFile(filePath, query, true)) {
|
|
1110
|
+
if (seenMatchPaths.has(match.path)) continue;
|
|
1111
|
+
seenMatchPaths.add(match.path);
|
|
1112
|
+
scoredMatches.push(match);
|
|
1113
|
+
if (scoredMatches.length >= candidateLimit) {
|
|
1114
|
+
truncated = true;
|
|
1115
|
+
break;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
if (truncated) break;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
scoredMatches.sort(comparePathSearchMatches);
|
|
1122
|
+
return {
|
|
1123
|
+
matches: scoredMatches.slice(0, limit).map(match => ({
|
|
1124
|
+
kind: 'path',
|
|
1125
|
+
entryType: match.entryType,
|
|
1126
|
+
path: match.path,
|
|
1127
|
+
lineNumber: 1,
|
|
1128
|
+
lines: '',
|
|
1129
|
+
ranges: [],
|
|
1130
|
+
})),
|
|
1131
|
+
truncated,
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
streamPathMatches(command, root, searchPath, query, limit, timeout, stopAtLimit = false) {
|
|
1136
|
+
const args = [
|
|
1137
|
+
'--files',
|
|
1138
|
+
'--hidden',
|
|
1139
|
+
...searchExcludeGlobArgs(),
|
|
1140
|
+
searchPath,
|
|
1141
|
+
];
|
|
1142
|
+
const candidateLimit = stopAtLimit
|
|
1143
|
+
? limit
|
|
1144
|
+
: Math.max(PATH_SEARCH_MIN_CANDIDATES, limit * PATH_SEARCH_CANDIDATE_MULTIPLIER);
|
|
1145
|
+
|
|
1146
|
+
return new Promise((resolve, reject) => {
|
|
1147
|
+
const child = spawn(command, args, {
|
|
1148
|
+
cwd: root,
|
|
1149
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1150
|
+
});
|
|
1151
|
+
let pending = '';
|
|
1152
|
+
let stderr = '';
|
|
1153
|
+
let settled = false;
|
|
1154
|
+
let truncated = false;
|
|
1155
|
+
const scoredMatches = [];
|
|
1156
|
+
const seenMatchPaths = new Set();
|
|
1157
|
+
|
|
1158
|
+
const settle = (error) => {
|
|
1159
|
+
if (settled) return;
|
|
1160
|
+
settled = true;
|
|
1161
|
+
clearTimeout(timer);
|
|
1162
|
+
if (error) {
|
|
1163
|
+
reject(error);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
scoredMatches.sort(comparePathSearchMatches);
|
|
1167
|
+
resolve({
|
|
1168
|
+
matches: scoredMatches.slice(0, limit).map(match => ({
|
|
1169
|
+
kind: 'path',
|
|
1170
|
+
entryType: match.entryType,
|
|
1171
|
+
path: match.path,
|
|
1172
|
+
lineNumber: 1,
|
|
1173
|
+
lines: '',
|
|
1174
|
+
ranges: [],
|
|
1175
|
+
})),
|
|
1176
|
+
truncated,
|
|
1177
|
+
});
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1180
|
+
const stopEarly = () => {
|
|
1181
|
+
truncated = true;
|
|
1182
|
+
child.kill();
|
|
1183
|
+
settle();
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1186
|
+
const processLine = (line) => {
|
|
1187
|
+
const filePath = normalizeSearchResultPath(line);
|
|
1188
|
+
if (!filePath || isSearchIgnoredRelativePath(filePath)) return;
|
|
1189
|
+
for (const match of pathSearchMatchesForFile(filePath, query)) {
|
|
1190
|
+
if (seenMatchPaths.has(match.path)) continue;
|
|
1191
|
+
seenMatchPaths.add(match.path);
|
|
1192
|
+
scoredMatches.push(match);
|
|
1193
|
+
if (scoredMatches.length >= candidateLimit) {
|
|
1194
|
+
stopEarly();
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
const timer = setTimeout(() => {
|
|
1201
|
+
truncated = true;
|
|
1202
|
+
child.kill();
|
|
1203
|
+
settle();
|
|
1204
|
+
}, timeout);
|
|
1205
|
+
|
|
1206
|
+
child.stdout.on('data', chunk => {
|
|
1207
|
+
if (settled) return;
|
|
1208
|
+
pending += String(chunk || '');
|
|
1209
|
+
const lines = pending.split('\n');
|
|
1210
|
+
pending = lines.pop() || '';
|
|
1211
|
+
for (const line of lines) {
|
|
1212
|
+
processLine(line);
|
|
1213
|
+
if (settled) return;
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
child.stderr.on('data', chunk => {
|
|
1218
|
+
stderr += String(chunk || '');
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
child.on('error', error => {
|
|
1222
|
+
settle(error);
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
child.on('close', (code, signal) => {
|
|
1226
|
+
if (settled) return;
|
|
1227
|
+
if (pending) processLine(pending);
|
|
1228
|
+
if (settled) return;
|
|
1229
|
+
if (code && code !== 0 && signal !== 'SIGTERM') {
|
|
1230
|
+
const error = new Error(stderr || 'path search failed');
|
|
1231
|
+
error.code = code;
|
|
1232
|
+
error.signal = signal;
|
|
1233
|
+
error.stderr = stderr;
|
|
1234
|
+
settle(error);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
settle();
|
|
1238
|
+
});
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
async collectPathMatchCandidates(root, searchPath, query, limit, timeout, stopAtLimit = false) {
|
|
1243
|
+
try {
|
|
1244
|
+
return await this.streamPathMatches(this.rgPath, root, searchPath, query, limit, timeout, stopAtLimit);
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
if (!isCommandUnavailable(error) || !this.rgFallbackPath || this.rgFallbackPath === this.rgPath) {
|
|
1247
|
+
if (isCommandUnavailable(error)) {
|
|
1248
|
+
return this.collectBundledPathMatchCandidates(root, searchPath, query, limit, timeout, stopAtLimit);
|
|
1249
|
+
}
|
|
1250
|
+
throw error;
|
|
1251
|
+
}
|
|
1252
|
+
this.rgPath = this.rgFallbackPath;
|
|
1253
|
+
try {
|
|
1254
|
+
return await this.streamPathMatches(this.rgPath, root, searchPath, query, limit, timeout, stopAtLimit);
|
|
1255
|
+
} catch (fallbackError) {
|
|
1256
|
+
if (isCommandUnavailable(fallbackError)) {
|
|
1257
|
+
return this.collectBundledPathMatchCandidates(root, searchPath, query, limit, timeout, stopAtLimit);
|
|
1258
|
+
}
|
|
1259
|
+
throw fallbackError;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
async collectDirectoryNameMatchCandidates(root, searchPath, query, limit, timeout) {
|
|
1265
|
+
const startedAt = Date.now();
|
|
1266
|
+
const allowPathMatch = isLikelyPathQuery(query);
|
|
1267
|
+
const candidateLimit = Math.max(PATH_SEARCH_MIN_CANDIDATES, limit * PATH_SEARCH_CANDIDATE_MULTIPLIER);
|
|
1268
|
+
const startRelativePath = searchPath === '.' ? '' : normalizeSearchResultPath(searchPath);
|
|
1269
|
+
const startDirectory = path.resolve(root, startRelativePath || '.');
|
|
1270
|
+
const queue = [{ target: startDirectory, relativePath: startRelativePath }];
|
|
1271
|
+
const scoredMatches = [];
|
|
1272
|
+
const seenMatchPaths = new Set();
|
|
1273
|
+
let visited = 0;
|
|
1274
|
+
let truncated = false;
|
|
1275
|
+
|
|
1276
|
+
while (queue.length > 0) {
|
|
1277
|
+
if (Date.now() - startedAt > timeout) {
|
|
1278
|
+
truncated = true;
|
|
1279
|
+
break;
|
|
1280
|
+
}
|
|
1281
|
+
if (visited >= candidateLimit * 4) {
|
|
1282
|
+
truncated = true;
|
|
1283
|
+
break;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
const current = queue.shift();
|
|
1287
|
+
if (!current || shouldPruneDirectoryNameSearch(current.relativePath)) continue;
|
|
1288
|
+
visited += 1;
|
|
1289
|
+
|
|
1290
|
+
let entries;
|
|
1291
|
+
try {
|
|
1292
|
+
entries = await fsp.readdir(current.target, { withFileTypes: true });
|
|
1293
|
+
} catch {
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
for (const entry of entries) {
|
|
1298
|
+
if (!entry.isDirectory()) continue;
|
|
1299
|
+
const childRelativePath = normalizeSearchResultPath(
|
|
1300
|
+
current.relativePath ? `${current.relativePath}/${entry.name}` : entry.name
|
|
1301
|
+
);
|
|
1302
|
+
if (!childRelativePath || shouldPruneDirectoryNameSearch(childRelativePath)) continue;
|
|
1303
|
+
const childTarget = path.join(current.target, entry.name);
|
|
1304
|
+
const match = directoryNameSearchMatchForPath(childRelativePath, query, allowPathMatch);
|
|
1305
|
+
if (match && !seenMatchPaths.has(match.path)) {
|
|
1306
|
+
seenMatchPaths.add(match.path);
|
|
1307
|
+
scoredMatches.push(match);
|
|
1308
|
+
if (scoredMatches.length >= candidateLimit) {
|
|
1309
|
+
truncated = true;
|
|
1310
|
+
break;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
queue.push({ target: childTarget, relativePath: childRelativePath });
|
|
1314
|
+
}
|
|
1315
|
+
if (truncated) break;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
scoredMatches.sort(comparePathSearchMatches);
|
|
1319
|
+
return {
|
|
1320
|
+
matches: scoredMatches.slice(0, limit).map(match => ({
|
|
1321
|
+
kind: 'path',
|
|
1322
|
+
entryType: 'directory',
|
|
1323
|
+
path: match.path,
|
|
1324
|
+
lineNumber: 1,
|
|
1325
|
+
lines: '',
|
|
1326
|
+
ranges: [],
|
|
1327
|
+
})),
|
|
1328
|
+
truncated,
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
async collectBundledPathMatchCandidates(root, searchPath, query, limit, timeout, stopAtLimit = false) {
|
|
1333
|
+
const candidateLimit = stopAtLimit
|
|
1334
|
+
? limit
|
|
1335
|
+
: Math.max(PATH_SEARCH_MIN_CANDIDATES, limit * PATH_SEARCH_CANDIDATE_MULTIPLIER);
|
|
1336
|
+
const { stdout } = await this.runBundledRipgrep([
|
|
1337
|
+
'--files',
|
|
1338
|
+
'--hidden',
|
|
1339
|
+
...searchExcludeGlobArgs(),
|
|
1340
|
+
searchPath,
|
|
1341
|
+
], { cwd: root, timeout, maxBuffer: SEARCH_FILE_LIST_MAX_BUFFER });
|
|
1342
|
+
const scoredMatches = [];
|
|
1343
|
+
const seenMatchPaths = new Set();
|
|
1344
|
+
let truncated = false;
|
|
1345
|
+
for (const line of stdout.split('\n')) {
|
|
1346
|
+
const filePath = normalizeSearchResultPath(line);
|
|
1347
|
+
if (!filePath || isSearchIgnoredRelativePath(filePath)) continue;
|
|
1348
|
+
for (const match of pathSearchMatchesForFile(filePath, query)) {
|
|
1349
|
+
if (seenMatchPaths.has(match.path)) continue;
|
|
1350
|
+
seenMatchPaths.add(match.path);
|
|
1351
|
+
scoredMatches.push(match);
|
|
1352
|
+
if (scoredMatches.length >= candidateLimit) {
|
|
1353
|
+
truncated = true;
|
|
1354
|
+
break;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
if (truncated) break;
|
|
1358
|
+
}
|
|
1359
|
+
scoredMatches.sort(comparePathSearchMatches);
|
|
1360
|
+
return {
|
|
1361
|
+
matches: scoredMatches.slice(0, limit).map(match => ({
|
|
1362
|
+
kind: 'path',
|
|
1363
|
+
entryType: match.entryType,
|
|
1364
|
+
path: match.path,
|
|
1365
|
+
lineNumber: 1,
|
|
1366
|
+
lines: '',
|
|
1367
|
+
ranges: [],
|
|
1368
|
+
})),
|
|
1369
|
+
truncated,
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
async directPathMatchCandidate(root, query, includeIgnored = false) {
|
|
1374
|
+
if (!isLikelyPathQuery(query)) return null;
|
|
1375
|
+
const candidatePath = normalizeSearchResultPath(String(query || '').trim().replace(/^\.\/+/, ''));
|
|
1376
|
+
if (!candidatePath || candidatePath.includes('\0') || isSearchIgnoredRelativePath(candidatePath, includeIgnored)) return null;
|
|
1377
|
+
try {
|
|
1378
|
+
const { target, relativePath } = await this.resolvePath(root, candidatePath);
|
|
1379
|
+
if (!relativePath || isSearchIgnoredRelativePath(relativePath, includeIgnored)) return null;
|
|
1380
|
+
const stat = await fsp.stat(target);
|
|
1381
|
+
if (!stat.isFile()) return null;
|
|
1382
|
+
return {
|
|
1383
|
+
kind: 'path',
|
|
1384
|
+
entryType: 'file',
|
|
1385
|
+
path: relativePath,
|
|
1386
|
+
lineNumber: 1,
|
|
1387
|
+
lines: '',
|
|
1388
|
+
ranges: [],
|
|
1389
|
+
};
|
|
1390
|
+
} catch {
|
|
1391
|
+
return null;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
async resolveRoot(workspaceRoot) {
|
|
1396
|
+
if (typeof workspaceRoot !== 'string' || !workspaceRoot.trim()) {
|
|
1397
|
+
throw new WorkspaceFileError('workspace root is required', 400);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
let stat;
|
|
1401
|
+
try {
|
|
1402
|
+
stat = await fsp.stat(workspaceRoot);
|
|
1403
|
+
} catch {
|
|
1404
|
+
throw new WorkspaceFileError('workspace not found', 404);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
if (!stat.isDirectory()) {
|
|
1408
|
+
throw new WorkspaceFileError('workspace must be a directory', 400);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
return fsp.realpath(workspaceRoot);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
async resolvePath(workspaceRoot, userPath = '', options = {}) {
|
|
1415
|
+
const root = await this.resolveRoot(workspaceRoot);
|
|
1416
|
+
const normalized = normalizeUserPath(userPath);
|
|
1417
|
+
const target = path.resolve(root, normalized);
|
|
1418
|
+
const requestedRelativePath = relativeFromRoot(root, target);
|
|
1419
|
+
|
|
1420
|
+
if (!isInside(root, target)) {
|
|
1421
|
+
throw new WorkspaceFileError('path must stay inside the workspace', 403);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
if (options.allowMissing) {
|
|
1425
|
+
const parent = await fsp.realpath(path.dirname(target)).catch(() => null);
|
|
1426
|
+
if (!parent || !isInside(root, parent)) {
|
|
1427
|
+
throw new WorkspaceFileError('parent path must stay inside the workspace', 403);
|
|
1428
|
+
}
|
|
1429
|
+
return {
|
|
1430
|
+
root,
|
|
1431
|
+
target,
|
|
1432
|
+
relativePath: requestedRelativePath,
|
|
1433
|
+
actualRelativePath: requestedRelativePath,
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
let realTarget;
|
|
1438
|
+
try {
|
|
1439
|
+
realTarget = await fsp.realpath(target);
|
|
1440
|
+
} catch {
|
|
1441
|
+
throw new WorkspaceFileError('path not found', 404);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
const external = !isInside(root, realTarget);
|
|
1445
|
+
if (external && !isInsideAnyRoot(options.allowedExternalRoots, realTarget)) {
|
|
1446
|
+
throw new WorkspaceFileError('symlink target is outside allowed workspaces', 403);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
const requestedStat = await fsp.lstat(target).catch(() => null);
|
|
1450
|
+
|
|
1451
|
+
return {
|
|
1452
|
+
root,
|
|
1453
|
+
target: realTarget,
|
|
1454
|
+
relativePath: requestedRelativePath,
|
|
1455
|
+
actualRelativePath: external ? '' : relativeFromRoot(root, realTarget),
|
|
1456
|
+
external,
|
|
1457
|
+
readOnly: external,
|
|
1458
|
+
symbolicLink: Boolean(requestedStat && requestedStat.isSymbolicLink()),
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
async resolveEntryPath(workspaceRoot, userPath) {
|
|
1463
|
+
const root = await this.resolveRoot(workspaceRoot);
|
|
1464
|
+
const normalized = normalizeUserPath(userPath);
|
|
1465
|
+
const target = path.resolve(root, normalized);
|
|
1466
|
+
const relativePath = relativeFromRoot(root, target);
|
|
1467
|
+
if (!relativePath) {
|
|
1468
|
+
throw new WorkspaceFileError('workspace root cannot be changed', 400);
|
|
1469
|
+
}
|
|
1470
|
+
if (!isInside(root, target)) {
|
|
1471
|
+
throw new WorkspaceFileError('path must stay inside the workspace', 403);
|
|
1472
|
+
}
|
|
1473
|
+
try {
|
|
1474
|
+
await fsp.lstat(target);
|
|
1475
|
+
} catch {
|
|
1476
|
+
throw new WorkspaceFileError('path not found', 404);
|
|
1477
|
+
}
|
|
1478
|
+
const realParent = await fsp.realpath(path.dirname(target)).catch(() => null);
|
|
1479
|
+
if (!realParent || !isInside(root, realParent)) {
|
|
1480
|
+
throw new WorkspaceFileError('symlink target is read-only', 403);
|
|
1481
|
+
}
|
|
1482
|
+
return { root, target, relativePath };
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
async listTree(workspaceRoot, userPath = '', options = {}) {
|
|
1486
|
+
const { root, target, relativePath, external: parentExternal = false } = await this.resolvePath(workspaceRoot, userPath, options);
|
|
1487
|
+
const stat = await fsp.stat(target);
|
|
1488
|
+
if (!stat.isDirectory()) {
|
|
1489
|
+
throw new WorkspaceFileError('path must be a directory', 400);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
const entries = await fsp.readdir(target, { withFileTypes: true });
|
|
1493
|
+
const visibleEntries = entries.filter(entry => !TREE_HIDDEN_NAMES.has(entry.name));
|
|
1494
|
+
const [{ value: gitStatusByPath, pending: gitStatusPending }, ignoredPaths] = await Promise.all([
|
|
1495
|
+
this.getGitStatusForTree(root),
|
|
1496
|
+
this.loadGitIgnoredPaths(root, visibleEntries.map(entry => joinRelativePath(relativePath, entry.name))),
|
|
1497
|
+
]);
|
|
1498
|
+
const descendantGitStatusByPath = buildDescendantGitStatusByDirectory(gitStatusByPath);
|
|
1499
|
+
const items = await Promise.all(visibleEntries
|
|
1500
|
+
.map(async (entry) => {
|
|
1501
|
+
const absolute = path.join(target, entry.name);
|
|
1502
|
+
let entryStat;
|
|
1503
|
+
try {
|
|
1504
|
+
entryStat = await fsp.lstat(absolute);
|
|
1505
|
+
} catch (error) {
|
|
1506
|
+
if (error && error.code === 'ENOENT') return null;
|
|
1507
|
+
throw error;
|
|
1508
|
+
}
|
|
1509
|
+
const itemPath = joinRelativePath(relativePath, entry.name);
|
|
1510
|
+
let type = entryStat.isDirectory() ? 'directory' : entryStat.isFile() ? 'file' : 'other';
|
|
1511
|
+
let symbolicLink = false;
|
|
1512
|
+
let external = parentExternal;
|
|
1513
|
+
let readOnly = parentExternal;
|
|
1514
|
+
let linkTarget = '';
|
|
1515
|
+
let linkError = '';
|
|
1516
|
+
let targetStat = entryStat;
|
|
1517
|
+
if (entryStat.isSymbolicLink()) {
|
|
1518
|
+
symbolicLink = true;
|
|
1519
|
+
try {
|
|
1520
|
+
linkTarget = await fsp.realpath(absolute);
|
|
1521
|
+
external = !isInside(root, linkTarget);
|
|
1522
|
+
if (external && !isInsideAnyRoot(options.allowedExternalRoots, linkTarget)) {
|
|
1523
|
+
type = 'symlink';
|
|
1524
|
+
linkError = 'outside-allowed-roots';
|
|
1525
|
+
} else {
|
|
1526
|
+
targetStat = await fsp.stat(linkTarget);
|
|
1527
|
+
type = targetStat.isDirectory() ? 'directory' : targetStat.isFile() ? 'file' : 'other';
|
|
1528
|
+
readOnly = external;
|
|
1529
|
+
}
|
|
1530
|
+
} catch (error) {
|
|
1531
|
+
type = 'symlink';
|
|
1532
|
+
linkError = error && error.code === 'ENOENT' ? 'broken' : 'unavailable';
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
const directGitStatus = gitStatusByPath.get(itemPath);
|
|
1536
|
+
let descendantGitStatus = null;
|
|
1537
|
+
if (type === 'directory' && !external) {
|
|
1538
|
+
descendantGitStatus = descendantGitStatusByPath.get(itemPath) || null;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
return {
|
|
1542
|
+
name: entry.name,
|
|
1543
|
+
path: itemPath,
|
|
1544
|
+
type,
|
|
1545
|
+
size: targetStat.size,
|
|
1546
|
+
mtimeMs: targetStat.mtimeMs,
|
|
1547
|
+
...(symbolicLink ? { symbolicLink: true } : {}),
|
|
1548
|
+
...(external ? { external: true } : {}),
|
|
1549
|
+
...(readOnly ? { readOnly: true } : {}),
|
|
1550
|
+
...(linkTarget ? { linkTarget } : {}),
|
|
1551
|
+
...(linkError ? { linkError } : {}),
|
|
1552
|
+
...(ignoredPaths.has(itemPath) ? { ignored: true } : {}),
|
|
1553
|
+
...(directGitStatus ? {
|
|
1554
|
+
gitStatus: directGitStatus.kind,
|
|
1555
|
+
gitStatusLabel: directGitStatus.label,
|
|
1556
|
+
} : {}),
|
|
1557
|
+
...(descendantGitStatus ? {
|
|
1558
|
+
descendantGitStatus: descendantGitStatus.kind,
|
|
1559
|
+
} : {}),
|
|
1560
|
+
};
|
|
1561
|
+
}));
|
|
1562
|
+
const visibleItems = items.filter(Boolean);
|
|
1563
|
+
|
|
1564
|
+
visibleItems.sort((a, b) => {
|
|
1565
|
+
if (a.type === 'directory' && b.type !== 'directory') return -1;
|
|
1566
|
+
if (a.type !== 'directory' && b.type === 'directory') return 1;
|
|
1567
|
+
return a.name.localeCompare(b.name);
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
return {
|
|
1571
|
+
path: relativePath,
|
|
1572
|
+
items: visibleItems,
|
|
1573
|
+
gitStatusPending,
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
invalidateGitStatus(root) {
|
|
1578
|
+
if (root) this.gitStatusCache.delete(root);
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
async loadGitStatusByPath(root, options = {}) {
|
|
1582
|
+
const untrackedFiles = options.untrackedFiles || 'normal';
|
|
1583
|
+
try {
|
|
1584
|
+
const { stdout } = await this.execFile(this.gitPath, [
|
|
1585
|
+
'status',
|
|
1586
|
+
'--porcelain=v1',
|
|
1587
|
+
'-z',
|
|
1588
|
+
`--untracked-files=${untrackedFiles}`,
|
|
1589
|
+
'--ignored=no',
|
|
1590
|
+
...gitStatusExcludePathspecArgs(),
|
|
1591
|
+
], { cwd: root });
|
|
1592
|
+
return parseGitStatus(stdout);
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
if (error.code === 'ENOENT') return new Map();
|
|
1595
|
+
if (error.stderr && /not a git repository/i.test(error.stderr)) return new Map();
|
|
1596
|
+
return new Map();
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
async loadGitIgnoredPaths(root, relativePaths) {
|
|
1601
|
+
const paths = Array.from(new Set(relativePaths
|
|
1602
|
+
.map(normalizeGitStatusPath)
|
|
1603
|
+
.filter(Boolean)));
|
|
1604
|
+
if (paths.length === 0) return new Set();
|
|
1605
|
+
|
|
1606
|
+
try {
|
|
1607
|
+
const { stdout } = await this.execFile(this.gitPath, [
|
|
1608
|
+
'check-ignore',
|
|
1609
|
+
'--',
|
|
1610
|
+
...paths,
|
|
1611
|
+
], { cwd: root });
|
|
1612
|
+
return new Set(stdout
|
|
1613
|
+
.split(/\r?\n/)
|
|
1614
|
+
.map(normalizeGitStatusPath)
|
|
1615
|
+
.filter(Boolean));
|
|
1616
|
+
} catch {
|
|
1617
|
+
return new Set();
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
async loadGitStatusForPath(root, relativePath) {
|
|
1622
|
+
const normalizedPath = normalizeGitStatusPath(relativePath);
|
|
1623
|
+
if (!normalizedPath) return null;
|
|
1624
|
+
|
|
1625
|
+
try {
|
|
1626
|
+
const { stdout } = await this.execFile(this.gitPath, [
|
|
1627
|
+
'status',
|
|
1628
|
+
'--porcelain=v1',
|
|
1629
|
+
'-z',
|
|
1630
|
+
'--untracked-files=normal',
|
|
1631
|
+
'--ignored=no',
|
|
1632
|
+
'--',
|
|
1633
|
+
normalizedPath,
|
|
1634
|
+
], { cwd: root });
|
|
1635
|
+
return parseGitStatus(stdout).get(normalizedPath) || null;
|
|
1636
|
+
} catch (error) {
|
|
1637
|
+
if (error.code === 'ENOENT') return null;
|
|
1638
|
+
if (error.stderr && /not a git repository/i.test(error.stderr)) return null;
|
|
1639
|
+
return null;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
async getGitStatusByPath(root) {
|
|
1644
|
+
if (this.gitStatusCacheTtlMs > 0) {
|
|
1645
|
+
const now = Date.now();
|
|
1646
|
+
const cached = this.gitStatusCache.get(root);
|
|
1647
|
+
if (cached?.value && cached.expiresAt > now) {
|
|
1648
|
+
return cached.value;
|
|
1649
|
+
}
|
|
1650
|
+
if (cached?.promise) {
|
|
1651
|
+
return cached.promise;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
const promise = this.loadGitStatusByPath(root).then((value) => {
|
|
1655
|
+
this.gitStatusCache.set(root, {
|
|
1656
|
+
value,
|
|
1657
|
+
expiresAt: Date.now() + this.gitStatusCacheTtlMs,
|
|
1658
|
+
});
|
|
1659
|
+
return value;
|
|
1660
|
+
}, (error) => {
|
|
1661
|
+
this.gitStatusCache.delete(root);
|
|
1662
|
+
throw error;
|
|
1663
|
+
});
|
|
1664
|
+
this.gitStatusCache.set(root, {
|
|
1665
|
+
promise,
|
|
1666
|
+
expiresAt: now + this.gitStatusCacheTtlMs,
|
|
1667
|
+
});
|
|
1668
|
+
return promise;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
return this.loadGitStatusByPath(root);
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
async getGitStatusForTree(root) {
|
|
1675
|
+
const cached = this.gitStatusCache.get(root);
|
|
1676
|
+
if (cached?.value) {
|
|
1677
|
+
return { value: cached.value, pending: false };
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
const promise = this.getGitStatusByPath(root);
|
|
1681
|
+
if (this.gitStatusInlineTimeoutMs <= 0) {
|
|
1682
|
+
return { value: await promise, pending: false };
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
const result = await Promise.race([
|
|
1686
|
+
promise,
|
|
1687
|
+
new Promise(resolve => setTimeout(() => resolve(TIMEOUT), this.gitStatusInlineTimeoutMs)),
|
|
1688
|
+
]);
|
|
1689
|
+
|
|
1690
|
+
if (result === TIMEOUT) {
|
|
1691
|
+
return { value: new Map(), pending: true };
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
return { value: result, pending: false };
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
async getGitStatusForPath(root, relativePath) {
|
|
1698
|
+
const normalizedPath = normalizeGitStatusPath(relativePath);
|
|
1699
|
+
if (!normalizedPath) return null;
|
|
1700
|
+
|
|
1701
|
+
const cached = this.gitStatusCache.get(root);
|
|
1702
|
+
if (cached?.value) {
|
|
1703
|
+
return cached.value.get(normalizedPath) || null;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
return this.loadGitStatusForPath(root, normalizedPath);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
async gitBranch(workspaceRoot) {
|
|
1710
|
+
const root = await this.resolveRoot(workspaceRoot);
|
|
1711
|
+
try {
|
|
1712
|
+
const { stdout } = await this.execFile(this.gitPath, ['-C', root, 'branch', '--show-current'], {
|
|
1713
|
+
timeout: 1500,
|
|
1714
|
+
maxBuffer: 64 * 1024,
|
|
1715
|
+
});
|
|
1716
|
+
return String(stdout || '').trim();
|
|
1717
|
+
} catch {
|
|
1718
|
+
return '';
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
async changes(workspaceRoot, options = {}) {
|
|
1723
|
+
const root = await this.resolveRoot(workspaceRoot);
|
|
1724
|
+
const limit = Math.max(1, Math.min(2000, Number(options.limit) || DEFAULT_GIT_CHANGES_LIMIT));
|
|
1725
|
+
const gitStatusByPath = await this.loadGitStatusByPath(root, { untrackedFiles: 'all' });
|
|
1726
|
+
const allItems = await Promise.all(Array.from(gitStatusByPath.entries())
|
|
1727
|
+
.map(async ([filePath, status]) => ({
|
|
1728
|
+
path: filePath,
|
|
1729
|
+
name: path.posix.basename(filePath),
|
|
1730
|
+
type: await workspaceEntryTypeForGitChange(root, filePath),
|
|
1731
|
+
gitStatus: status.kind,
|
|
1732
|
+
gitStatusLabel: status.label,
|
|
1733
|
+
...(status.previousPath ? { previousPath: status.previousPath } : {}),
|
|
1734
|
+
})))
|
|
1735
|
+
allItems
|
|
1736
|
+
.sort((left, right) => (
|
|
1737
|
+
gitStatusReviewRank(left.gitStatus) - gitStatusReviewRank(right.gitStatus)
|
|
1738
|
+
|| left.path.localeCompare(right.path)
|
|
1739
|
+
));
|
|
1740
|
+
|
|
1741
|
+
return {
|
|
1742
|
+
items: allItems.slice(0, limit),
|
|
1743
|
+
truncated: allItems.length > limit,
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
async readFile(workspaceRoot, userPath, options = {}) {
|
|
1748
|
+
const { target, relativePath, external, readOnly, symbolicLink } = await this.resolvePath(workspaceRoot, userPath, options);
|
|
1749
|
+
const stat = await fsp.stat(target);
|
|
1750
|
+
if (!stat.isFile()) {
|
|
1751
|
+
throw new WorkspaceFileError('path must be a file', 400);
|
|
1752
|
+
}
|
|
1753
|
+
const preview = previewForPath(relativePath);
|
|
1754
|
+
if (preview) {
|
|
1755
|
+
if (stat.size > this.maxPreviewFileSize) {
|
|
1756
|
+
throw new WorkspaceFileError('file is too large to preview', 413, { size: stat.size });
|
|
1757
|
+
}
|
|
1758
|
+
const buffer = await fsp.readFile(target);
|
|
1759
|
+
return {
|
|
1760
|
+
path: relativePath,
|
|
1761
|
+
content: '',
|
|
1762
|
+
size: stat.size,
|
|
1763
|
+
mtimeMs: stat.mtimeMs,
|
|
1764
|
+
sha1: sha1(buffer),
|
|
1765
|
+
binary: true,
|
|
1766
|
+
preview,
|
|
1767
|
+
...(symbolicLink ? { symbolicLink: true } : {}),
|
|
1768
|
+
...(external ? { external: true } : {}),
|
|
1769
|
+
...(readOnly ? { readOnly: true } : {}),
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
if (await isProbablyBinaryFile(target)) {
|
|
1773
|
+
return {
|
|
1774
|
+
path: relativePath,
|
|
1775
|
+
content: '',
|
|
1776
|
+
size: stat.size,
|
|
1777
|
+
mtimeMs: stat.mtimeMs,
|
|
1778
|
+
sha1: metadataFileVersion(relativePath, stat),
|
|
1779
|
+
binary: true,
|
|
1780
|
+
preview: {
|
|
1781
|
+
kind: 'binary',
|
|
1782
|
+
mediaType: 'application/octet-stream',
|
|
1783
|
+
},
|
|
1784
|
+
...(symbolicLink ? { symbolicLink: true } : {}),
|
|
1785
|
+
...(external ? { external: true } : {}),
|
|
1786
|
+
...(readOnly ? { readOnly: true } : {}),
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
if (stat.size > this.maxFileSize) {
|
|
1790
|
+
return {
|
|
1791
|
+
path: relativePath,
|
|
1792
|
+
content: await readTextPrefix(target, this.maxFileSize),
|
|
1793
|
+
size: stat.size,
|
|
1794
|
+
mtimeMs: stat.mtimeMs,
|
|
1795
|
+
sha1: metadataFileVersion(relativePath, stat),
|
|
1796
|
+
preview: {
|
|
1797
|
+
kind: 'large-text',
|
|
1798
|
+
mediaType: 'text/plain',
|
|
1799
|
+
truncated: true,
|
|
1800
|
+
},
|
|
1801
|
+
...(symbolicLink ? { symbolicLink: true } : {}),
|
|
1802
|
+
...(external ? { external: true } : {}),
|
|
1803
|
+
...(readOnly ? { readOnly: true } : {}),
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
const buffer = await fsp.readFile(target);
|
|
1808
|
+
return {
|
|
1809
|
+
path: relativePath,
|
|
1810
|
+
content: buffer.toString('utf8'),
|
|
1811
|
+
size: stat.size,
|
|
1812
|
+
mtimeMs: stat.mtimeMs,
|
|
1813
|
+
sha1: sha1(buffer),
|
|
1814
|
+
...(symbolicLink ? { symbolicLink: true } : {}),
|
|
1815
|
+
...(external ? { external: true } : {}),
|
|
1816
|
+
...(readOnly ? { readOnly: true } : {}),
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
async readPreviewFile(workspaceRoot, userPath, options = {}) {
|
|
1821
|
+
const { target, relativePath, external, readOnly, symbolicLink } = await this.resolvePath(workspaceRoot, userPath, options);
|
|
1822
|
+
const stat = await fsp.stat(target);
|
|
1823
|
+
if (!stat.isFile()) {
|
|
1824
|
+
throw new WorkspaceFileError('path must be a file', 400);
|
|
1825
|
+
}
|
|
1826
|
+
const preview = previewForPath(relativePath, { includeTextImages: true });
|
|
1827
|
+
if (!preview) {
|
|
1828
|
+
throw new WorkspaceFileError('file preview is not available', 415);
|
|
1829
|
+
}
|
|
1830
|
+
if (stat.size > this.maxPreviewFileSize) {
|
|
1831
|
+
throw new WorkspaceFileError('file is too large to preview', 413, { size: stat.size });
|
|
1832
|
+
}
|
|
1833
|
+
const buffer = await fsp.readFile(target);
|
|
1834
|
+
return {
|
|
1835
|
+
path: relativePath,
|
|
1836
|
+
buffer,
|
|
1837
|
+
size: stat.size,
|
|
1838
|
+
mtimeMs: stat.mtimeMs,
|
|
1839
|
+
sha1: sha1(buffer),
|
|
1840
|
+
preview,
|
|
1841
|
+
...(symbolicLink ? { symbolicLink: true } : {}),
|
|
1842
|
+
...(external ? { external: true } : {}),
|
|
1843
|
+
...(readOnly ? { readOnly: true } : {}),
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
async blameCapability(workspaceRoot, userPath, options = {}) {
|
|
1848
|
+
const { root, target, relativePath, actualRelativePath, external } = await this.resolvePath(workspaceRoot, userPath, options);
|
|
1849
|
+
const capability = (available, reason = '') => ({
|
|
1850
|
+
isGitRepo: reason !== 'not-git-repo' && reason !== 'git-unavailable',
|
|
1851
|
+
path: relativePath,
|
|
1852
|
+
available,
|
|
1853
|
+
...(reason ? { reason } : {}),
|
|
1854
|
+
});
|
|
1855
|
+
const stat = await fsp.stat(target);
|
|
1856
|
+
if (!stat.isFile()) {
|
|
1857
|
+
throw new WorkspaceFileError('path must be a file', 400);
|
|
1858
|
+
}
|
|
1859
|
+
if (external) {
|
|
1860
|
+
return capability(false, 'external');
|
|
1861
|
+
}
|
|
1862
|
+
if (stat.size > this.maxFileSize) {
|
|
1863
|
+
return capability(false, 'too-large');
|
|
1864
|
+
}
|
|
1865
|
+
if (await isProbablyBinaryFile(target)) {
|
|
1866
|
+
return capability(false, 'binary');
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
const gitPath = actualRelativePath || relativePath;
|
|
1870
|
+
const directGitStatus = await this.getGitStatusForPath(root, gitPath);
|
|
1871
|
+
if (directGitStatus && ['added', 'deleted', 'renamed', 'untracked', 'conflicted'].includes(directGitStatus.kind)) {
|
|
1872
|
+
return capability(false, directGitStatus.kind);
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
try {
|
|
1876
|
+
await this.execFile(this.gitPath, [
|
|
1877
|
+
'ls-files',
|
|
1878
|
+
'--error-unmatch',
|
|
1879
|
+
'--',
|
|
1880
|
+
gitPath,
|
|
1881
|
+
], { cwd: root });
|
|
1882
|
+
return capability(true);
|
|
1883
|
+
} catch (error) {
|
|
1884
|
+
if (error.code === 'ENOENT') return capability(false, 'git-unavailable');
|
|
1885
|
+
if (error.stderr && /not a git repository/i.test(error.stderr)) return capability(false, 'not-git-repo');
|
|
1886
|
+
return capability(false, 'untracked');
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
async writeFile(workspaceRoot, userPath, content, options = {}) {
|
|
1891
|
+
if (typeof content !== 'string') {
|
|
1892
|
+
throw new WorkspaceFileError('content must be a string', 400);
|
|
1893
|
+
}
|
|
1894
|
+
if (Buffer.byteLength(content, 'utf8') > this.maxWriteSize) {
|
|
1895
|
+
throw new WorkspaceFileError('file is too large to save', 413);
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
const { root, target, relativePath } = await this.resolvePath(workspaceRoot, userPath, { allowMissing: true });
|
|
1899
|
+
let writeTarget = target;
|
|
1900
|
+
const baseSha1 = typeof options.baseSha1 === 'string' ? options.baseSha1 : '';
|
|
1901
|
+
const overwrite = options.overwrite === true;
|
|
1902
|
+
let currentMode = 0o666;
|
|
1903
|
+
let currentSha1 = '';
|
|
1904
|
+
let exists = false;
|
|
1905
|
+
|
|
1906
|
+
try {
|
|
1907
|
+
const realTarget = await fsp.realpath(target);
|
|
1908
|
+
if (!isInside(root, realTarget)) {
|
|
1909
|
+
throw new WorkspaceFileError('symlinks outside the workspace are not allowed', 403);
|
|
1910
|
+
}
|
|
1911
|
+
writeTarget = realTarget;
|
|
1912
|
+
} catch (error) {
|
|
1913
|
+
if (error instanceof WorkspaceFileError) throw error;
|
|
1914
|
+
if (error.code !== 'ENOENT') throw error;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
try {
|
|
1918
|
+
const stat = await fsp.stat(writeTarget);
|
|
1919
|
+
if (!stat.isFile()) {
|
|
1920
|
+
throw new WorkspaceFileError('path must be a file', 400);
|
|
1921
|
+
}
|
|
1922
|
+
if (stat.size > this.maxFileSize) {
|
|
1923
|
+
throw new WorkspaceFileError('existing file is too large to overwrite safely', 413, { size: stat.size });
|
|
1924
|
+
}
|
|
1925
|
+
if (await isProbablyBinaryFile(writeTarget)) {
|
|
1926
|
+
throw new WorkspaceFileError('binary files cannot be overwritten as text', 415);
|
|
1927
|
+
}
|
|
1928
|
+
const currentBuffer = await fsp.readFile(writeTarget);
|
|
1929
|
+
currentSha1 = sha1(currentBuffer);
|
|
1930
|
+
currentMode = stat.mode;
|
|
1931
|
+
exists = true;
|
|
1932
|
+
} catch (error) {
|
|
1933
|
+
if (error instanceof WorkspaceFileError) throw error;
|
|
1934
|
+
if (error.code !== 'ENOENT') throw error;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
if (baseSha1 && currentSha1 && baseSha1 !== currentSha1 && !overwrite) {
|
|
1938
|
+
throw new WorkspaceFileError('file changed on disk', 409, {
|
|
1939
|
+
path: relativePath,
|
|
1940
|
+
currentSha1,
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
if (baseSha1 && !exists && !overwrite) {
|
|
1944
|
+
throw new WorkspaceFileError('file was deleted on disk', 409, { path: relativePath });
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
const tempPath = path.join(
|
|
1948
|
+
path.dirname(writeTarget),
|
|
1949
|
+
`.${path.basename(writeTarget)}.farming-${process.pid}-${Date.now()}.tmp`
|
|
1950
|
+
);
|
|
1951
|
+
await fsp.writeFile(tempPath, content, { mode: currentMode });
|
|
1952
|
+
await fsp.rename(tempPath, writeTarget);
|
|
1953
|
+
this.invalidateGitStatus(root);
|
|
1954
|
+
|
|
1955
|
+
return this.readFile(workspaceRoot, relativePath);
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
async moveEntry(workspaceRoot, sourcePath, targetDirectory = '') {
|
|
1959
|
+
const root = await this.resolveRoot(workspaceRoot);
|
|
1960
|
+
const normalizedSource = normalizeUserPath(sourcePath);
|
|
1961
|
+
const normalizedTargetDirectory = normalizeUserPath(targetDirectory);
|
|
1962
|
+
|
|
1963
|
+
if (!normalizedSource) {
|
|
1964
|
+
throw new WorkspaceFileError('source path is required', 400);
|
|
1965
|
+
}
|
|
1966
|
+
if (shouldIgnorePath(normalizedSource) || shouldIgnorePath(normalizedTargetDirectory)) {
|
|
1967
|
+
throw new WorkspaceFileError('ignored paths cannot be moved', 403);
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
const source = await this.resolveEntryPath(workspaceRoot, normalizedSource);
|
|
1971
|
+
const targetDirectoryPath = path.resolve(root, normalizedTargetDirectory);
|
|
1972
|
+
let targetDirectoryRealPath;
|
|
1973
|
+
try {
|
|
1974
|
+
targetDirectoryRealPath = await fsp.realpath(targetDirectoryPath);
|
|
1975
|
+
} catch {
|
|
1976
|
+
throw new WorkspaceFileError('target directory not found', 404);
|
|
1977
|
+
}
|
|
1978
|
+
if (!isInside(root, targetDirectoryRealPath)) {
|
|
1979
|
+
throw new WorkspaceFileError('target directory must stay inside the workspace', 403);
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
const targetDirectoryStat = await fsp.stat(targetDirectoryRealPath);
|
|
1983
|
+
if (!targetDirectoryStat.isDirectory()) {
|
|
1984
|
+
throw new WorkspaceFileError('target path must be a directory', 400);
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
const target = path.join(targetDirectoryRealPath, path.basename(source.target));
|
|
1988
|
+
const targetPath = relativeFromRoot(root, target);
|
|
1989
|
+
if (shouldIgnorePath(targetPath)) {
|
|
1990
|
+
throw new WorkspaceFileError('ignored paths cannot be moved', 403);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
if (target === source.target) {
|
|
1994
|
+
return {
|
|
1995
|
+
sourcePath: source.relativePath,
|
|
1996
|
+
targetPath,
|
|
1997
|
+
sourceDirectory: parentDirectory(source.relativePath),
|
|
1998
|
+
targetDirectory: normalizedTargetDirectory,
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
const sourceStat = await fsp.lstat(source.target);
|
|
2003
|
+
if (sourceStat.isDirectory() && isInside(source.target, target)) {
|
|
2004
|
+
throw new WorkspaceFileError('directory cannot be moved into itself', 400);
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
try {
|
|
2008
|
+
await fsp.lstat(target);
|
|
2009
|
+
throw new WorkspaceFileError('target path already exists', 409, { path: targetPath });
|
|
2010
|
+
} catch (error) {
|
|
2011
|
+
if (error instanceof WorkspaceFileError) throw error;
|
|
2012
|
+
if (error.code !== 'ENOENT') throw error;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
await fsp.rename(source.target, target);
|
|
2016
|
+
this.invalidateGitStatus(root);
|
|
2017
|
+
|
|
2018
|
+
return {
|
|
2019
|
+
sourcePath: source.relativePath,
|
|
2020
|
+
targetPath,
|
|
2021
|
+
sourceDirectory: parentDirectory(source.relativePath),
|
|
2022
|
+
targetDirectory: normalizedTargetDirectory,
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
async createEntry(workspaceRoot, parentPath = '', name, type = 'file', content = '') {
|
|
2027
|
+
const root = await this.resolveRoot(workspaceRoot);
|
|
2028
|
+
const normalizedParentPath = normalizeUserPath(parentPath);
|
|
2029
|
+
const entryName = normalizeEntryName(name);
|
|
2030
|
+
const entryType = type === 'directory' ? 'directory' : 'file';
|
|
2031
|
+
|
|
2032
|
+
if (shouldIgnorePath(normalizedParentPath)) {
|
|
2033
|
+
throw new WorkspaceFileError('ignored paths cannot be changed', 403);
|
|
2034
|
+
}
|
|
2035
|
+
if (entryType === 'file' && Buffer.byteLength(String(content || ''), 'utf8') > this.maxWriteSize) {
|
|
2036
|
+
throw new WorkspaceFileError('file is too large to save', 413);
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
const parentDirectoryPath = path.resolve(root, normalizedParentPath);
|
|
2040
|
+
let parentDirectoryRealPath;
|
|
2041
|
+
try {
|
|
2042
|
+
parentDirectoryRealPath = await fsp.realpath(parentDirectoryPath);
|
|
2043
|
+
} catch {
|
|
2044
|
+
throw new WorkspaceFileError('parent directory not found', 404);
|
|
2045
|
+
}
|
|
2046
|
+
if (!isInside(root, parentDirectoryRealPath)) {
|
|
2047
|
+
throw new WorkspaceFileError('parent directory must stay inside the workspace', 403);
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
const parentStat = await fsp.stat(parentDirectoryRealPath);
|
|
2051
|
+
if (!parentStat.isDirectory()) {
|
|
2052
|
+
throw new WorkspaceFileError('parent path must be a directory', 400);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
const target = path.join(parentDirectoryRealPath, entryName);
|
|
2056
|
+
const targetPath = relativeFromRoot(root, target);
|
|
2057
|
+
if (shouldIgnorePath(targetPath)) {
|
|
2058
|
+
throw new WorkspaceFileError('ignored paths cannot be changed', 403);
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
try {
|
|
2062
|
+
await fsp.lstat(target);
|
|
2063
|
+
throw new WorkspaceFileError('target path already exists', 409, { path: targetPath });
|
|
2064
|
+
} catch (error) {
|
|
2065
|
+
if (error instanceof WorkspaceFileError) throw error;
|
|
2066
|
+
if (error.code !== 'ENOENT') throw error;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
if (entryType === 'directory') {
|
|
2070
|
+
await fsp.mkdir(target);
|
|
2071
|
+
this.invalidateGitStatus(root);
|
|
2072
|
+
return {
|
|
2073
|
+
entry: {
|
|
2074
|
+
name: entryName,
|
|
2075
|
+
path: targetPath,
|
|
2076
|
+
type: 'directory',
|
|
2077
|
+
size: 0,
|
|
2078
|
+
mtimeMs: Date.now(),
|
|
2079
|
+
},
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
await fsp.writeFile(target, String(content || ''));
|
|
2084
|
+
this.invalidateGitStatus(root);
|
|
2085
|
+
return {
|
|
2086
|
+
entry: {
|
|
2087
|
+
name: entryName,
|
|
2088
|
+
path: targetPath,
|
|
2089
|
+
type: 'file',
|
|
2090
|
+
size: Buffer.byteLength(String(content || ''), 'utf8'),
|
|
2091
|
+
mtimeMs: Date.now(),
|
|
2092
|
+
},
|
|
2093
|
+
file: await this.readFile(workspaceRoot, targetPath),
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
async renameEntry(workspaceRoot, sourcePath, name) {
|
|
2098
|
+
const root = await this.resolveRoot(workspaceRoot);
|
|
2099
|
+
const normalizedSource = normalizeUserPath(sourcePath);
|
|
2100
|
+
const entryName = normalizeEntryName(name);
|
|
2101
|
+
|
|
2102
|
+
if (!normalizedSource) {
|
|
2103
|
+
throw new WorkspaceFileError('source path is required', 400);
|
|
2104
|
+
}
|
|
2105
|
+
if (shouldIgnorePath(normalizedSource)) {
|
|
2106
|
+
throw new WorkspaceFileError('ignored paths cannot be changed', 403);
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
const source = await this.resolveEntryPath(workspaceRoot, normalizedSource);
|
|
2110
|
+
const target = path.join(path.dirname(source.target), entryName);
|
|
2111
|
+
const targetPath = relativeFromRoot(root, target);
|
|
2112
|
+
if (shouldIgnorePath(targetPath)) {
|
|
2113
|
+
throw new WorkspaceFileError('ignored paths cannot be changed', 403);
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
if (target === source.target) {
|
|
2117
|
+
return {
|
|
2118
|
+
sourcePath: source.relativePath,
|
|
2119
|
+
targetPath,
|
|
2120
|
+
sourceDirectory: parentDirectory(source.relativePath),
|
|
2121
|
+
targetDirectory: parentDirectory(targetPath),
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
try {
|
|
2126
|
+
await fsp.lstat(target);
|
|
2127
|
+
throw new WorkspaceFileError('target path already exists', 409, { path: targetPath });
|
|
2128
|
+
} catch (error) {
|
|
2129
|
+
if (error instanceof WorkspaceFileError) throw error;
|
|
2130
|
+
if (error.code !== 'ENOENT') throw error;
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
await fsp.rename(source.target, target);
|
|
2134
|
+
this.invalidateGitStatus(root);
|
|
2135
|
+
|
|
2136
|
+
return {
|
|
2137
|
+
sourcePath: source.relativePath,
|
|
2138
|
+
targetPath,
|
|
2139
|
+
sourceDirectory: parentDirectory(source.relativePath),
|
|
2140
|
+
targetDirectory: parentDirectory(targetPath),
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
async deleteEntry(workspaceRoot, userPath) {
|
|
2145
|
+
const { root, target, relativePath } = await this.resolveEntryPath(workspaceRoot, userPath);
|
|
2146
|
+
if (!relativePath) {
|
|
2147
|
+
throw new WorkspaceFileError('workspace root cannot be deleted', 400);
|
|
2148
|
+
}
|
|
2149
|
+
if (shouldIgnorePath(relativePath)) {
|
|
2150
|
+
throw new WorkspaceFileError('ignored paths cannot be changed', 403);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
const stat = await fsp.lstat(target);
|
|
2154
|
+
const type = stat.isDirectory() ? 'directory' : stat.isFile() ? 'file' : stat.isSymbolicLink() ? 'symlink' : 'other';
|
|
2155
|
+
if (stat.isDirectory()) {
|
|
2156
|
+
await fsp.rm(target, { recursive: true, force: false });
|
|
2157
|
+
} else {
|
|
2158
|
+
await fsp.unlink(target);
|
|
2159
|
+
}
|
|
2160
|
+
this.invalidateGitStatus(root);
|
|
2161
|
+
|
|
2162
|
+
return {
|
|
2163
|
+
path: relativePath,
|
|
2164
|
+
parentDirectory: parentDirectory(relativePath),
|
|
2165
|
+
type,
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
async search(workspaceRoot, query, options = {}) {
|
|
2170
|
+
if (typeof query !== 'string' || !query.trim()) {
|
|
2171
|
+
throw new WorkspaceFileError('query is required', 400);
|
|
2172
|
+
}
|
|
2173
|
+
const { root, relativePath } = await this.resolvePath(workspaceRoot, options.path || '');
|
|
2174
|
+
const limit = Math.max(1, Math.min(500, Number(options.limit) || this.searchLimit));
|
|
2175
|
+
const searchPath = relativePath || '.';
|
|
2176
|
+
const timeout = Math.max(1000, Number(options.timeoutMs) || this.searchTimeoutMs);
|
|
2177
|
+
const includeIgnored = options.includeIgnored === true;
|
|
2178
|
+
const likelyPathQuery = isLikelyPathQuery(query);
|
|
2179
|
+
const result = (matches, truncated) => ({
|
|
2180
|
+
query,
|
|
2181
|
+
path: searchPath,
|
|
2182
|
+
matches,
|
|
2183
|
+
truncated,
|
|
2184
|
+
timeoutMs: timeout,
|
|
2185
|
+
});
|
|
2186
|
+
let pathMatchCandidates = [];
|
|
2187
|
+
let searchOutputTruncated = false;
|
|
2188
|
+
|
|
2189
|
+
if (includeIgnored) {
|
|
2190
|
+
try {
|
|
2191
|
+
const directPathMatch = likelyPathQuery ? await this.directPathMatchCandidate(root, query, true) : null;
|
|
2192
|
+
if (directPathMatch) {
|
|
2193
|
+
return result([directPathMatch], false);
|
|
2194
|
+
}
|
|
2195
|
+
const ignoredPathSearch = await this.collectGitIgnoredPathMatchCandidates(root, searchPath, query, limit, timeout);
|
|
2196
|
+
return result(ignoredPathSearch.matches, ignoredPathSearch.truncated);
|
|
2197
|
+
} catch (error) {
|
|
2198
|
+
if (error.code === 'ETIMEDOUT' || error.signal === 'SIGTERM' || error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
|
|
2199
|
+
return result([], true);
|
|
2200
|
+
}
|
|
2201
|
+
return result([], false);
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
try {
|
|
2206
|
+
const directPathMatch = likelyPathQuery ? await this.directPathMatchCandidate(root, query) : null;
|
|
2207
|
+
if (directPathMatch) {
|
|
2208
|
+
return result([directPathMatch], false);
|
|
2209
|
+
}
|
|
2210
|
+
const pathSearch = await this.collectPathMatchCandidates(root, searchPath, query, limit, timeout, likelyPathQuery);
|
|
2211
|
+
const directoryNameSearch = await this.collectDirectoryNameMatchCandidates(root, searchPath, query, limit, timeout);
|
|
2212
|
+
pathMatchCandidates = dedupePathMatches([
|
|
2213
|
+
...directoryNameSearch.matches,
|
|
2214
|
+
...pathSearch.matches,
|
|
2215
|
+
], limit);
|
|
2216
|
+
searchOutputTruncated = pathSearch.truncated || directoryNameSearch.truncated;
|
|
2217
|
+
} catch (error) {
|
|
2218
|
+
if (error.code === 'ENOENT') {
|
|
2219
|
+
throw new WorkspaceFileError('ripgrep is not installed', 501);
|
|
2220
|
+
}
|
|
2221
|
+
if (error.code === 'ETIMEDOUT' || error.signal === 'SIGTERM') {
|
|
2222
|
+
searchOutputTruncated = true;
|
|
2223
|
+
}
|
|
2224
|
+
pathMatchCandidates = [];
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
if (pathMatchCandidates.length >= limit || pathMatchCandidates.length > 0 && likelyPathQuery) {
|
|
2228
|
+
return result(pathMatchCandidates, searchOutputTruncated || pathMatchCandidates.length >= limit);
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
const args = [
|
|
2232
|
+
'--json',
|
|
2233
|
+
'--color',
|
|
2234
|
+
'never',
|
|
2235
|
+
'--line-number',
|
|
2236
|
+
'--column',
|
|
2237
|
+
'--max-count',
|
|
2238
|
+
String(Math.min(3, limit)),
|
|
2239
|
+
'--hidden',
|
|
2240
|
+
...searchExcludeGlobArgs(),
|
|
2241
|
+
'--',
|
|
2242
|
+
query,
|
|
2243
|
+
searchPath,
|
|
2244
|
+
];
|
|
2245
|
+
|
|
2246
|
+
let stdout;
|
|
2247
|
+
try {
|
|
2248
|
+
({ stdout } = await this.execRipgrep(args, { cwd: root, timeout }));
|
|
2249
|
+
} catch (error) {
|
|
2250
|
+
if (error.code === 1) {
|
|
2251
|
+
stdout = error.stdout || '';
|
|
2252
|
+
} else if (error.code === 'ENOENT') {
|
|
2253
|
+
throw new WorkspaceFileError('ripgrep is not installed', 501);
|
|
2254
|
+
} else if (error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
|
|
2255
|
+
stdout = error.stdout || '';
|
|
2256
|
+
searchOutputTruncated = true;
|
|
2257
|
+
} else if (error.code === 'ETIMEDOUT' || error.signal === 'SIGTERM') {
|
|
2258
|
+
stdout = error.stdout || '';
|
|
2259
|
+
searchOutputTruncated = true;
|
|
2260
|
+
} else {
|
|
2261
|
+
console.error('Workspace search command failed:', {
|
|
2262
|
+
code: error.code,
|
|
2263
|
+
signal: error.signal,
|
|
2264
|
+
message: error.message,
|
|
2265
|
+
stderr: error.stderr,
|
|
2266
|
+
});
|
|
2267
|
+
throw new WorkspaceFileError(error.stderr || 'search failed', 500);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
const matches = [];
|
|
2272
|
+
stdout.split('\n').filter(Boolean).forEach((line) => {
|
|
2273
|
+
let event;
|
|
2274
|
+
try {
|
|
2275
|
+
event = JSON.parse(line);
|
|
2276
|
+
} catch {
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
if (event.type !== 'match') return;
|
|
2280
|
+
const data = event.data || {};
|
|
2281
|
+
const submatches = Array.isArray(data.submatches) ? data.submatches : [];
|
|
2282
|
+
const resultPath = normalizeSearchResultPath(data.path && data.path.text ? data.path.text : '');
|
|
2283
|
+
if (!resultPath || isSearchIgnoredRelativePath(resultPath)) return;
|
|
2284
|
+
matches.push({
|
|
2285
|
+
kind: 'content',
|
|
2286
|
+
entryType: 'file',
|
|
2287
|
+
path: resultPath,
|
|
2288
|
+
lineNumber: data.line_number,
|
|
2289
|
+
lines: data.lines && data.lines.text ? data.lines.text.replace(/\n$/, '') : '',
|
|
2290
|
+
ranges: submatches.map(match => ({
|
|
2291
|
+
start: match.start,
|
|
2292
|
+
end: match.end,
|
|
2293
|
+
})),
|
|
2294
|
+
});
|
|
2295
|
+
});
|
|
2296
|
+
|
|
2297
|
+
const combinedMatches = [];
|
|
2298
|
+
pathMatchCandidates.forEach(match => {
|
|
2299
|
+
combinedMatches.push(match);
|
|
2300
|
+
});
|
|
2301
|
+
matches.forEach(match => {
|
|
2302
|
+
combinedMatches.push(match);
|
|
2303
|
+
});
|
|
2304
|
+
|
|
2305
|
+
return result(combinedMatches.slice(0, limit), searchOutputTruncated || combinedMatches.length > limit);
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
async diff(workspaceRoot, userPath = '', options = {}) {
|
|
2309
|
+
const root = await this.resolveRoot(workspaceRoot);
|
|
2310
|
+
const normalized = normalizeUserPath(userPath);
|
|
2311
|
+
let target = null;
|
|
2312
|
+
let relativePath = normalized;
|
|
2313
|
+
let gitRelativePath = normalized;
|
|
2314
|
+
let targetMissing = false;
|
|
2315
|
+
if (normalized) {
|
|
2316
|
+
try {
|
|
2317
|
+
const resolved = await this.resolvePath(workspaceRoot, normalized);
|
|
2318
|
+
target = resolved.target;
|
|
2319
|
+
relativePath = resolved.relativePath;
|
|
2320
|
+
gitRelativePath = resolved.actualRelativePath || resolved.relativePath;
|
|
2321
|
+
} catch (error) {
|
|
2322
|
+
if (error instanceof WorkspaceFileError && error.statusCode === 404) {
|
|
2323
|
+
targetMissing = true;
|
|
2324
|
+
} else {
|
|
2325
|
+
throw error;
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
const args = ['-C', root, 'diff', ...gitDiffWhitespaceArgs(options.ignoreWhitespace), ...gitDiffContextArgs(options.context), 'HEAD', '--'];
|
|
2330
|
+
if (normalized) args.push(gitRelativePath);
|
|
2331
|
+
|
|
2332
|
+
try {
|
|
2333
|
+
const { stdout } = await this.execFile(this.gitPath, args, {
|
|
2334
|
+
cwd: root,
|
|
2335
|
+
timeout: this.diffTimeoutMs,
|
|
2336
|
+
maxBuffer: this.diffMaxBuffer,
|
|
2337
|
+
});
|
|
2338
|
+
const result = {
|
|
2339
|
+
isGitRepo: true,
|
|
2340
|
+
path: relativePath,
|
|
2341
|
+
patch: stdout,
|
|
2342
|
+
};
|
|
2343
|
+
if (!normalized) {
|
|
2344
|
+
return result;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
if (targetMissing) {
|
|
2348
|
+
const status = await this.getGitStatusForPath(root, gitRelativePath);
|
|
2349
|
+
if (status?.kind !== 'deleted') {
|
|
2350
|
+
throw new WorkspaceFileError('path not found', 404);
|
|
2351
|
+
}
|
|
2352
|
+
const original = await this.execFile(this.gitPath, [
|
|
2353
|
+
'-C',
|
|
2354
|
+
root,
|
|
2355
|
+
'show',
|
|
2356
|
+
`HEAD:${gitRelativePath}`,
|
|
2357
|
+
], { cwd: root, encoding: 'buffer' });
|
|
2358
|
+
const originalContent = Buffer.isBuffer(original.stdout)
|
|
2359
|
+
? original.stdout.toString('utf8')
|
|
2360
|
+
: String(original.stdout || '');
|
|
2361
|
+
return {
|
|
2362
|
+
...result,
|
|
2363
|
+
originalContent,
|
|
2364
|
+
modifiedContent: '',
|
|
2365
|
+
deleted: true,
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
const stat = await fsp.stat(target);
|
|
2370
|
+
if (!stat.isFile()) {
|
|
2371
|
+
return result;
|
|
2372
|
+
}
|
|
2373
|
+
if (stat.size > this.maxFileSize || await isProbablyBinaryFile(target)) {
|
|
2374
|
+
return {
|
|
2375
|
+
...result,
|
|
2376
|
+
path: relativePath,
|
|
2377
|
+
binary: true,
|
|
2378
|
+
size: stat.size,
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
const modifiedBuffer = await fsp.readFile(target);
|
|
2383
|
+
const gitStatusByPath = await this.loadGitStatusByPath(root);
|
|
2384
|
+
if (this.gitStatusCacheTtlMs > 0) {
|
|
2385
|
+
this.gitStatusCache.set(root, {
|
|
2386
|
+
value: gitStatusByPath,
|
|
2387
|
+
expiresAt: Date.now() + this.gitStatusCacheTtlMs,
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
const status = gitStatusByPath.get(normalizeGitStatusPath(gitRelativePath)) || null;
|
|
2391
|
+
const originalGitPath = status?.kind === 'renamed' && status.previousPath
|
|
2392
|
+
? status.previousPath
|
|
2393
|
+
: gitRelativePath;
|
|
2394
|
+
let originalContent = '';
|
|
2395
|
+
let untracked = false;
|
|
2396
|
+
try {
|
|
2397
|
+
const original = await this.execFile(this.gitPath, [
|
|
2398
|
+
'-C',
|
|
2399
|
+
root,
|
|
2400
|
+
'show',
|
|
2401
|
+
`HEAD:${originalGitPath}`,
|
|
2402
|
+
], { cwd: root, encoding: 'buffer' });
|
|
2403
|
+
originalContent = Buffer.isBuffer(original.stdout)
|
|
2404
|
+
? original.stdout.toString('utf8')
|
|
2405
|
+
: String(original.stdout || '');
|
|
2406
|
+
} catch (error) {
|
|
2407
|
+
if (error.stderr && /(exists on disk, but not in|path .* exists on disk|does not exist in|not in HEAD|invalid object name)/i.test(String(error.stderr))) {
|
|
2408
|
+
untracked = true;
|
|
2409
|
+
} else {
|
|
2410
|
+
throw error;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
return {
|
|
2415
|
+
...result,
|
|
2416
|
+
path: relativePath,
|
|
2417
|
+
originalContent,
|
|
2418
|
+
modifiedContent: modifiedBuffer.toString('utf8'),
|
|
2419
|
+
untracked,
|
|
2420
|
+
};
|
|
2421
|
+
} catch (error) {
|
|
2422
|
+
if (error.code === 'ENOENT') {
|
|
2423
|
+
throw new WorkspaceFileError('git is not installed', 501);
|
|
2424
|
+
}
|
|
2425
|
+
if (error.code === 'ETIMEDOUT' || error.signal === 'SIGTERM') {
|
|
2426
|
+
throw new WorkspaceFileError('git diff timed out', 504);
|
|
2427
|
+
}
|
|
2428
|
+
if (error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
|
|
2429
|
+
return {
|
|
2430
|
+
isGitRepo: true,
|
|
2431
|
+
path: relativePath,
|
|
2432
|
+
patch: error.stdout || '',
|
|
2433
|
+
truncated: true,
|
|
2434
|
+
};
|
|
2435
|
+
}
|
|
2436
|
+
if (error.stderr && /not a git repository/i.test(error.stderr)) {
|
|
2437
|
+
return {
|
|
2438
|
+
isGitRepo: false,
|
|
2439
|
+
path: normalized,
|
|
2440
|
+
patch: '',
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
throw new WorkspaceFileError(error.stderr || 'git diff failed', 500);
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
async lineChanges(workspaceRoot, userPath, lineNumber, mode = 'working') {
|
|
2448
|
+
const requestedLineNumber = Number(lineNumber);
|
|
2449
|
+
if (!Number.isInteger(requestedLineNumber) || requestedLineNumber < 1) {
|
|
2450
|
+
throw new WorkspaceFileError('lineNumber must be a positive integer', 400);
|
|
2451
|
+
}
|
|
2452
|
+
if (mode !== 'working' && mode !== 'previous') {
|
|
2453
|
+
throw new WorkspaceFileError('mode must be working or previous', 400);
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
const { root, target, relativePath, actualRelativePath } = await this.resolvePath(workspaceRoot, userPath);
|
|
2457
|
+
const stat = await fsp.stat(target);
|
|
2458
|
+
if (!stat.isFile()) {
|
|
2459
|
+
throw new WorkspaceFileError('path must be a file', 400);
|
|
2460
|
+
}
|
|
2461
|
+
if (stat.size > this.maxFileSize) {
|
|
2462
|
+
throw new WorkspaceFileError('file is too large to inspect line changes', 413, { size: stat.size });
|
|
2463
|
+
}
|
|
2464
|
+
if (await isProbablyBinaryFile(target)) {
|
|
2465
|
+
throw new WorkspaceFileError('binary files cannot be inspected as text', 415);
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
const gitRelativePath = actualRelativePath || relativePath;
|
|
2469
|
+
const baseResult = {
|
|
2470
|
+
isGitRepo: true,
|
|
2471
|
+
path: relativePath,
|
|
2472
|
+
mode,
|
|
2473
|
+
lineNumber: requestedLineNumber,
|
|
2474
|
+
lookupLineNumber: requestedLineNumber,
|
|
2475
|
+
targetSide: mode === 'working' ? 'working' : 'revision',
|
|
2476
|
+
available: false,
|
|
2477
|
+
reason: '',
|
|
2478
|
+
patch: '',
|
|
2479
|
+
hunk: null,
|
|
2480
|
+
};
|
|
2481
|
+
|
|
2482
|
+
try {
|
|
2483
|
+
if (mode === 'working') {
|
|
2484
|
+
const directGitStatus = await this.getGitStatusForPath(root, gitRelativePath);
|
|
2485
|
+
if (directGitStatus?.kind === 'untracked') {
|
|
2486
|
+
const modifiedContent = await fsp.readFile(target, 'utf8');
|
|
2487
|
+
const hunk = createAddedFileLineChangesHunk(modifiedContent, requestedLineNumber);
|
|
2488
|
+
return {
|
|
2489
|
+
...baseResult,
|
|
2490
|
+
available: Boolean(hunk),
|
|
2491
|
+
reason: hunk ? '' : 'unchanged',
|
|
2492
|
+
patch: hunk ? hunk.patch : '',
|
|
2493
|
+
hunk,
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
const { stdout } = await this.execFile(this.gitPath, [
|
|
2498
|
+
'-C',
|
|
2499
|
+
root,
|
|
2500
|
+
'diff',
|
|
2501
|
+
'--unified=20',
|
|
2502
|
+
'HEAD',
|
|
2503
|
+
'--',
|
|
2504
|
+
gitRelativePath,
|
|
2505
|
+
], {
|
|
2506
|
+
cwd: root,
|
|
2507
|
+
timeout: this.diffTimeoutMs,
|
|
2508
|
+
maxBuffer: this.diffMaxBuffer,
|
|
2509
|
+
});
|
|
2510
|
+
const hunk = selectUnifiedDiffHunk(stdout, 'new', requestedLineNumber);
|
|
2511
|
+
return {
|
|
2512
|
+
...baseResult,
|
|
2513
|
+
available: Boolean(hunk),
|
|
2514
|
+
reason: hunk ? '' : 'unchanged',
|
|
2515
|
+
patch: hunk ? hunk.patch : '',
|
|
2516
|
+
hunk,
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
const blame = await this.blame(workspaceRoot, userPath);
|
|
2521
|
+
if (!blame.isGitRepo) {
|
|
2522
|
+
return {
|
|
2523
|
+
...baseResult,
|
|
2524
|
+
isGitRepo: false,
|
|
2525
|
+
reason: 'not-git-repo',
|
|
2526
|
+
};
|
|
2527
|
+
}
|
|
2528
|
+
const blameLine = blame.lines.find(line => line.lineNumber === requestedLineNumber);
|
|
2529
|
+
if (!blameLine) {
|
|
2530
|
+
return {
|
|
2531
|
+
...baseResult,
|
|
2532
|
+
reason: 'line-not-found',
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
if (blameLine.uncommitted) {
|
|
2536
|
+
return {
|
|
2537
|
+
...baseResult,
|
|
2538
|
+
reason: 'uncommitted',
|
|
2539
|
+
commit: {
|
|
2540
|
+
hash: blameLine.commit,
|
|
2541
|
+
shortHash: blameLine.shortCommit,
|
|
2542
|
+
author: blameLine.author,
|
|
2543
|
+
authorTimeIso: blameLine.authorTimeIso,
|
|
2544
|
+
summary: blameLine.summary,
|
|
2545
|
+
},
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
const { stdout } = await this.execFile(this.gitPath, [
|
|
2550
|
+
'-C',
|
|
2551
|
+
root,
|
|
2552
|
+
'show',
|
|
2553
|
+
'--format=',
|
|
2554
|
+
'--unified=20',
|
|
2555
|
+
blameLine.commit,
|
|
2556
|
+
'--',
|
|
2557
|
+
gitRelativePath,
|
|
2558
|
+
], {
|
|
2559
|
+
cwd: root,
|
|
2560
|
+
timeout: this.diffTimeoutMs,
|
|
2561
|
+
maxBuffer: this.diffMaxBuffer,
|
|
2562
|
+
});
|
|
2563
|
+
const hunk = selectUnifiedDiffHunk(stdout, 'new', blameLine.originalLineNumber);
|
|
2564
|
+
return {
|
|
2565
|
+
...baseResult,
|
|
2566
|
+
lookupLineNumber: blameLine.originalLineNumber,
|
|
2567
|
+
available: Boolean(hunk),
|
|
2568
|
+
reason: hunk ? '' : 'not-found',
|
|
2569
|
+
patch: hunk ? hunk.patch : '',
|
|
2570
|
+
hunk,
|
|
2571
|
+
commit: {
|
|
2572
|
+
hash: blameLine.commit,
|
|
2573
|
+
shortHash: blameLine.shortCommit,
|
|
2574
|
+
author: blameLine.author,
|
|
2575
|
+
authorTimeIso: blameLine.authorTimeIso,
|
|
2576
|
+
summary: blameLine.summary,
|
|
2577
|
+
},
|
|
2578
|
+
};
|
|
2579
|
+
} catch (error) {
|
|
2580
|
+
if (error instanceof WorkspaceFileError) {
|
|
2581
|
+
if (mode === 'previous' && error.statusCode === 409) {
|
|
2582
|
+
return {
|
|
2583
|
+
...baseResult,
|
|
2584
|
+
reason: 'untracked',
|
|
2585
|
+
};
|
|
2586
|
+
}
|
|
2587
|
+
throw error;
|
|
2588
|
+
}
|
|
2589
|
+
if (error.code === 'ENOENT') {
|
|
2590
|
+
throw new WorkspaceFileError('git is not installed', 501);
|
|
2591
|
+
}
|
|
2592
|
+
if (error.code === 'ETIMEDOUT' || error.signal === 'SIGTERM') {
|
|
2593
|
+
throw new WorkspaceFileError('git line changes timed out', 504);
|
|
2594
|
+
}
|
|
2595
|
+
if (error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
|
|
2596
|
+
return {
|
|
2597
|
+
...baseResult,
|
|
2598
|
+
available: false,
|
|
2599
|
+
reason: 'truncated',
|
|
2600
|
+
patch: error.stdout || '',
|
|
2601
|
+
truncated: true,
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
if (error.stderr && /not a git repository/i.test(error.stderr)) {
|
|
2605
|
+
return {
|
|
2606
|
+
...baseResult,
|
|
2607
|
+
isGitRepo: false,
|
|
2608
|
+
reason: 'not-git-repo',
|
|
2609
|
+
};
|
|
2610
|
+
}
|
|
2611
|
+
if (mode === 'previous' && error.stderr && /(no such path|does not exist in|not in HEAD|path .* exists on disk|invalid object name)/i.test(String(error.stderr))) {
|
|
2612
|
+
return {
|
|
2613
|
+
...baseResult,
|
|
2614
|
+
reason: 'not-found',
|
|
2615
|
+
};
|
|
2616
|
+
}
|
|
2617
|
+
throw new WorkspaceFileError(error.stderr || 'git line changes failed', 500);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
async blame(workspaceRoot, userPath) {
|
|
2622
|
+
const { root, target, relativePath, actualRelativePath } = await this.resolvePath(workspaceRoot, userPath);
|
|
2623
|
+
const stat = await fsp.stat(target);
|
|
2624
|
+
if (!stat.isFile()) {
|
|
2625
|
+
throw new WorkspaceFileError('path must be a file', 400);
|
|
2626
|
+
}
|
|
2627
|
+
if (stat.size > this.maxFileSize) {
|
|
2628
|
+
throw new WorkspaceFileError('file is too large to blame', 413, { size: stat.size });
|
|
2629
|
+
}
|
|
2630
|
+
if (await isProbablyBinaryFile(target)) {
|
|
2631
|
+
throw new WorkspaceFileError('binary files cannot be blamed as text', 415);
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
try {
|
|
2635
|
+
const { stdout } = await this.execFile(this.gitPath, [
|
|
2636
|
+
'blame',
|
|
2637
|
+
'--line-porcelain',
|
|
2638
|
+
'--',
|
|
2639
|
+
actualRelativePath || relativePath,
|
|
2640
|
+
], { cwd: root, timeout: this.blameTimeoutMs });
|
|
2641
|
+
return {
|
|
2642
|
+
isGitRepo: true,
|
|
2643
|
+
path: relativePath,
|
|
2644
|
+
lines: parseGitBlamePorcelain(stdout),
|
|
2645
|
+
};
|
|
2646
|
+
} catch (error) {
|
|
2647
|
+
if (error.code === 'ENOENT') {
|
|
2648
|
+
throw new WorkspaceFileError('git is not installed', 501);
|
|
2649
|
+
}
|
|
2650
|
+
if (error.code === 'ETIMEDOUT' || error.signal === 'SIGTERM') {
|
|
2651
|
+
throw new WorkspaceFileError('git blame timed out', 504);
|
|
2652
|
+
}
|
|
2653
|
+
if (error.stderr && /not a git repository/i.test(error.stderr)) {
|
|
2654
|
+
return {
|
|
2655
|
+
isGitRepo: false,
|
|
2656
|
+
path: relativePath,
|
|
2657
|
+
lines: [],
|
|
2658
|
+
};
|
|
2659
|
+
}
|
|
2660
|
+
if (error.stderr && /(no such path|no such ref|does not exist in|not in HEAD|no such file)/i.test(error.stderr)) {
|
|
2661
|
+
throw new WorkspaceFileError('file is not tracked by git', 409, { path: relativePath });
|
|
2662
|
+
}
|
|
2663
|
+
throw new WorkspaceFileError(error.stderr || 'git blame failed', 500);
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
async subscribe(workspaceRoot, callback) {
|
|
2668
|
+
const root = await this.resolveRoot(workspaceRoot);
|
|
2669
|
+
const watchRoot = path.resolve(workspaceRoot);
|
|
2670
|
+
let record = this.watchers.get(root);
|
|
2671
|
+
|
|
2672
|
+
if (!record) {
|
|
2673
|
+
const subscribers = new Set();
|
|
2674
|
+
|
|
2675
|
+
const emit = (eventType, absolutePath) => {
|
|
2676
|
+
const relative = relativeFromRoot(watchRoot, absolutePath);
|
|
2677
|
+
if (shouldHidePath(relative)) return;
|
|
2678
|
+
this.invalidateGitStatus(root);
|
|
2679
|
+
subscribers.forEach((subscriber) => {
|
|
2680
|
+
subscriber({
|
|
2681
|
+
type: eventType,
|
|
2682
|
+
path: relative,
|
|
2683
|
+
});
|
|
2684
|
+
});
|
|
2685
|
+
};
|
|
2686
|
+
|
|
2687
|
+
const emitError = (error) => {
|
|
2688
|
+
subscribers.forEach((subscriber) => {
|
|
2689
|
+
subscriber({
|
|
2690
|
+
type: 'error',
|
|
2691
|
+
message: error.message,
|
|
2692
|
+
});
|
|
2693
|
+
});
|
|
2694
|
+
};
|
|
2695
|
+
|
|
2696
|
+
const configuredIgnored = this.watchOptions.ignored;
|
|
2697
|
+
const ignored = (candidatePath) => {
|
|
2698
|
+
const relative = path.relative(watchRoot, candidatePath);
|
|
2699
|
+
if (shouldHidePath(relative)) return true;
|
|
2700
|
+
if (typeof configuredIgnored === 'function') return configuredIgnored(candidatePath);
|
|
2701
|
+
if (configuredIgnored instanceof RegExp) return configuredIgnored.test(candidatePath);
|
|
2702
|
+
if (Array.isArray(configuredIgnored)) {
|
|
2703
|
+
return configuredIgnored.some(pattern => (
|
|
2704
|
+
pattern instanceof RegExp ? pattern.test(candidatePath) : String(candidatePath).includes(String(pattern))
|
|
2705
|
+
));
|
|
2706
|
+
}
|
|
2707
|
+
return false;
|
|
2708
|
+
};
|
|
2709
|
+
const chokidar = await loadChokidar();
|
|
2710
|
+
const watcher = chokidar.watch(watchRoot, {
|
|
2711
|
+
ignoreInitial: true,
|
|
2712
|
+
depth: this.watchDepth,
|
|
2713
|
+
...this.watchOptions,
|
|
2714
|
+
ignored,
|
|
2715
|
+
});
|
|
2716
|
+
|
|
2717
|
+
['add', 'change', 'unlink', 'addDir', 'unlinkDir'].forEach((eventType) => {
|
|
2718
|
+
watcher.on(eventType, filePath => emit(eventType, filePath));
|
|
2719
|
+
});
|
|
2720
|
+
watcher.on('error', emitError);
|
|
2721
|
+
const ready = new Promise(resolve => watcher.once('ready', resolve));
|
|
2722
|
+
|
|
2723
|
+
record = { watcher, subscribers, ready };
|
|
2724
|
+
this.watchers.set(root, record);
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
await record.ready;
|
|
2728
|
+
record.subscribers.add(callback);
|
|
2729
|
+
return async () => {
|
|
2730
|
+
record.subscribers.delete(callback);
|
|
2731
|
+
if (record.subscribers.size === 0) {
|
|
2732
|
+
this.watchers.delete(root);
|
|
2733
|
+
const closeResult = record.watcher.close();
|
|
2734
|
+
if (closeResult && typeof closeResult.then === 'function') {
|
|
2735
|
+
await closeResult;
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
};
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
async dispose() {
|
|
2742
|
+
const watchers = Array.from(this.watchers.values());
|
|
2743
|
+
this.watchers.clear();
|
|
2744
|
+
this.gitStatusCache.clear();
|
|
2745
|
+
await Promise.all(watchers.map(record => record.watcher.close()));
|
|
2746
|
+
if (this.ownsCommandRunner) {
|
|
2747
|
+
this.commandRunner.dispose();
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
module.exports = {
|
|
2753
|
+
WorkspaceFileService,
|
|
2754
|
+
WorkspaceFileError,
|
|
2755
|
+
DEFAULT_MAX_FILE_SIZE,
|
|
2756
|
+
DEFAULT_MAX_WRITE_SIZE,
|
|
2757
|
+
DEFAULT_WATCH_DEPTH,
|
|
2758
|
+
isPackagedRuntime,
|
|
2759
|
+
parseGitBlamePorcelain,
|
|
2760
|
+
parseUnifiedDiffRows,
|
|
2761
|
+
createAddedFileLineChangesHunk,
|
|
2762
|
+
parseUnifiedDiffHunks,
|
|
2763
|
+
selectUnifiedDiffHunk,
|
|
2764
|
+
resolveCommandRunnerNodePath,
|
|
2765
|
+
};
|