@herbertgao/pi-extensions 2026.10.0 → 2026.10.2
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/README.md +16 -12
- package/THIRD_PARTY_NOTICES.md +3 -3
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-help.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/index.js +4 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail-frontmatter.cjs +43 -3
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail.mjs +142 -59
- package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +21 -21
- package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
- package/node_modules/@dietrichgebert/ponytail/AGENTS.md +21 -21
- package/node_modules/@dietrichgebert/ponytail/README.md +52 -242
- package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +3 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/copilot-hooks.json +4 -4
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +70 -16
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-config.js +8 -7
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +30 -30
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-map.js +103 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +46 -23
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +39 -7
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-subagent.js +20 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/qoder-hooks.json +3 -3
- package/node_modules/@dietrichgebert/ponytail/package.json +3 -3
- package/node_modules/@dietrichgebert/ponytail/pi-extension/index.js +47 -17
- package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -7
- package/node_modules/@dietrichgebert/ponytail/skills/plugin.json +5 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +33 -101
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +89 -27
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +13 -12
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +21 -18
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +12 -13
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +96 -52
- package/node_modules/@herbertgao/pi-agency/LICENSE +21 -0
- package/node_modules/@herbertgao/pi-agency/README.md +40 -0
- package/node_modules/@herbertgao/pi-agency/package.json +53 -0
- package/node_modules/@herbertgao/pi-agency/src/index.ts +441 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +12 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +30 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +15 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +27 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +4 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +2 -2
- package/node_modules/@narumitw/pi-caffeinate/package.json +4 -4
- package/node_modules/@tifan/pi-mermaid-open/package.json +5 -1
- package/node_modules/@tifan/pi-mermaid-open/src/index.ts +26 -9
- package/node_modules/@tifan/pi-recap/package.json +1 -1
- package/node_modules/@tifan/pi-recap/src/index.ts +104 -20
- package/node_modules/@tifan/pi-recap/src/models.ts +46 -4
- package/node_modules/pi-antigravity/README.md +23 -2
- package/node_modules/pi-antigravity/package.json +5 -3
- package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +3 -2
- package/node_modules/pi-antigravity/src/features/generate-image.ts +72 -0
- package/node_modules/pi-antigravity/src/features/google-search.ts +71 -0
- package/node_modules/pi-antigravity/src/image/image.ts +86 -47
- package/node_modules/pi-antigravity/src/image/native.ts +69 -0
- package/node_modules/pi-antigravity/src/index.ts +52 -137
- package/node_modules/pi-antigravity/src/search/search.ts +130 -31
- package/node_modules/pi-antigravity/src/stream/stream.ts +162 -113
- package/node_modules/pi-antigravity/src/types/enums.ts +1 -0
- package/node_modules/pi-antigravity/src/types/types.ts +1 -1
- package/node_modules/pi-antigravity/src/utils/security.ts +47 -1
- package/node_modules/pi-antigravity/src/utils/util.ts +38 -6
- package/node_modules/pi-lens/CHANGELOG.md +1064 -0
- package/node_modules/pi-lens/README.md +115 -25
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +6 -2
- package/node_modules/pi-lens/dist/chunk-22AVIGXS.js +560 -0
- package/node_modules/pi-lens/dist/chunk-27BBHSZC.js +106 -0
- package/node_modules/pi-lens/dist/chunk-2BEMIWEW.js +51 -0
- package/node_modules/pi-lens/dist/chunk-2EECBNC5.js +89 -0
- package/node_modules/pi-lens/dist/chunk-34U7PLGE.js +23 -0
- package/node_modules/pi-lens/dist/chunk-3I5QFT3N.js +181 -0
- package/node_modules/pi-lens/dist/chunk-3MED75HT.js +37 -0
- package/node_modules/pi-lens/dist/chunk-47D2BO4S.js +87 -0
- package/node_modules/pi-lens/dist/chunk-4RERDEXR.js +348 -0
- package/node_modules/pi-lens/dist/chunk-4W2PLC4J.js +27 -0
- package/node_modules/pi-lens/dist/chunk-52H5B5TC.js +202 -0
- package/node_modules/pi-lens/dist/chunk-54B42YRX.js +27 -0
- package/node_modules/pi-lens/dist/chunk-5RXBNSW2.js +266 -0
- package/node_modules/pi-lens/dist/chunk-5THXD6X5.js +201 -0
- package/node_modules/pi-lens/dist/chunk-5W3R6LFT.js +24 -0
- package/node_modules/pi-lens/dist/chunk-5ZEJHV35.js +341 -0
- package/node_modules/pi-lens/dist/chunk-6P62QVEA.js +118 -0
- package/node_modules/pi-lens/dist/chunk-6QIUX4EP.js +459 -0
- package/node_modules/pi-lens/dist/chunk-6VCD4VKZ.js +78 -0
- package/node_modules/pi-lens/dist/chunk-7F72PXY2.js +326 -0
- package/node_modules/pi-lens/dist/chunk-7FBF7NPR.js +998 -0
- package/node_modules/pi-lens/dist/chunk-7U2IDNXN.js +37 -0
- package/node_modules/pi-lens/dist/chunk-ABBOA7UD.js +157 -0
- package/node_modules/pi-lens/dist/chunk-AHDW3GPE.js +50 -0
- package/node_modules/pi-lens/dist/chunk-AJ3QXTRQ.js +756 -0
- package/node_modules/pi-lens/dist/chunk-AJVLWNFN.js +1610 -0
- package/node_modules/pi-lens/dist/chunk-AJXODZY6.js +112 -0
- package/node_modules/pi-lens/dist/chunk-ARFCXG7S.js +84 -0
- package/node_modules/pi-lens/dist/chunk-AS44CFQ4.js +708 -0
- package/node_modules/pi-lens/dist/chunk-AWE5TMPO.js +38 -0
- package/node_modules/pi-lens/dist/chunk-B3I2UUT4.js +4692 -0
- package/node_modules/pi-lens/dist/chunk-BRJKOBZP.js +331 -0
- package/node_modules/pi-lens/dist/chunk-BW5KFIQC.js +59 -0
- package/node_modules/pi-lens/dist/chunk-C2AUYXQH.js +149 -0
- package/node_modules/pi-lens/dist/chunk-CGGAIP7J.js +29 -0
- package/node_modules/pi-lens/dist/chunk-D6QS3U6M.js +30 -0
- package/node_modules/pi-lens/dist/chunk-DFVMIKMQ.js +349 -0
- package/node_modules/pi-lens/dist/chunk-DIULIWKU.js +169 -0
- package/node_modules/pi-lens/dist/chunk-DQ2NZ7C4.js +2317 -0
- package/node_modules/pi-lens/dist/chunk-E2WFBRHM.js +213 -0
- package/node_modules/pi-lens/dist/chunk-E4MAEDPM.js +50 -0
- package/node_modules/pi-lens/dist/chunk-EOKRQ3SA.js +30 -0
- package/node_modules/pi-lens/dist/chunk-F3AU2TMM.js +2626 -0
- package/node_modules/pi-lens/dist/chunk-F3P7VQ7H.js +170 -0
- package/node_modules/pi-lens/dist/chunk-FC4RP4I2.js +12 -0
- package/node_modules/pi-lens/dist/chunk-FJNKANOS.js +32 -0
- package/node_modules/pi-lens/dist/chunk-GUOGVTCQ.js +146 -0
- package/node_modules/pi-lens/dist/chunk-GVXA6CCE.js +18 -0
- package/node_modules/pi-lens/dist/chunk-HHYAELLJ.js +2334 -0
- package/node_modules/pi-lens/dist/chunk-HMFXILPV.js +83 -0
- package/node_modules/pi-lens/dist/chunk-HVUZXSWK.js +421 -0
- package/node_modules/pi-lens/dist/chunk-HWBZIGMJ.js +62 -0
- package/node_modules/pi-lens/dist/chunk-I3R7GKDR.js +17 -0
- package/node_modules/pi-lens/dist/chunk-I7SPGOWI.js +55 -0
- package/node_modules/pi-lens/dist/chunk-IIXOAGHR.js +38 -0
- package/node_modules/pi-lens/dist/chunk-IVP5KAB7.js +2250 -0
- package/node_modules/pi-lens/dist/chunk-IXEBJSUE.js +22 -0
- package/node_modules/pi-lens/dist/chunk-JOLITGCC.js +27 -0
- package/node_modules/pi-lens/dist/chunk-K6VYLB2U.js +145 -0
- package/node_modules/pi-lens/dist/chunk-LJDODBBZ.js +78 -0
- package/node_modules/pi-lens/dist/chunk-LNAKEO63.js +78 -0
- package/node_modules/pi-lens/dist/chunk-LSMJ6KBE.js +4139 -0
- package/node_modules/pi-lens/dist/chunk-LW6XKDZQ.js +5565 -0
- package/node_modules/pi-lens/dist/chunk-N3YQJI6O.js +899 -0
- package/node_modules/pi-lens/dist/chunk-NABLC7W3.js +148 -0
- package/node_modules/pi-lens/dist/chunk-NKOUFPQF.js +659 -0
- package/node_modules/pi-lens/dist/chunk-NL5GLMFI.js +211 -0
- package/node_modules/pi-lens/dist/chunk-NMABWBZN.js +327 -0
- package/node_modules/pi-lens/dist/chunk-NTHH2J74.js +27 -0
- package/node_modules/pi-lens/dist/chunk-NTUJEGDZ.js +59 -0
- package/node_modules/pi-lens/dist/chunk-NXL4FFQQ.js +47 -0
- package/node_modules/pi-lens/dist/chunk-NXPS2L6U.js +466 -0
- package/node_modules/pi-lens/dist/chunk-O5WILGMI.js +42 -0
- package/node_modules/pi-lens/dist/chunk-O6TQT6RI.js +48 -0
- package/node_modules/pi-lens/dist/chunk-OAXNDZJ7.js +323 -0
- package/node_modules/pi-lens/dist/chunk-ODEEOXPH.js +44 -0
- package/node_modules/pi-lens/dist/chunk-OQGFCZJ4.js +4533 -0
- package/node_modules/pi-lens/dist/chunk-ORTMY4Q4.js +282 -0
- package/node_modules/pi-lens/dist/chunk-OS32RNR2.js +442 -0
- package/node_modules/pi-lens/dist/chunk-PAK6IDME.js +189 -0
- package/node_modules/pi-lens/dist/chunk-PWFHBTSF.js +406 -0
- package/node_modules/pi-lens/dist/chunk-Q5U6FMDI.js +2352 -0
- package/node_modules/pi-lens/dist/chunk-QOZXJ4NA.js +3364 -0
- package/node_modules/pi-lens/dist/chunk-QYF5S6P6.js +45 -0
- package/node_modules/pi-lens/dist/chunk-RIISMHKN.js +36 -0
- package/node_modules/pi-lens/dist/chunk-RPD4ZSB7.js +25 -0
- package/node_modules/pi-lens/dist/chunk-RPD5K5NC.js +79 -0
- package/node_modules/pi-lens/dist/chunk-RUOG5D2C.js +262 -0
- package/node_modules/pi-lens/dist/chunk-T4ERT73Y.js +599 -0
- package/node_modules/pi-lens/dist/chunk-T6D4Q4AQ.js +54 -0
- package/node_modules/pi-lens/dist/chunk-TGPXLY2J.js +391 -0
- package/node_modules/pi-lens/dist/chunk-TLGFBM3F.js +12116 -0
- package/node_modules/pi-lens/dist/chunk-TO6WL3GM.js +88 -0
- package/node_modules/pi-lens/dist/chunk-TT22N74J.js +517 -0
- package/node_modules/pi-lens/dist/chunk-TVN5N6GY.js +21216 -0
- package/node_modules/pi-lens/dist/chunk-U3VGR4VU.js +308 -0
- package/node_modules/pi-lens/dist/chunk-UK3CMEAL.js +1250 -0
- package/node_modules/pi-lens/dist/chunk-UOPVJQ4N.js +103 -0
- package/node_modules/pi-lens/dist/chunk-UXVNL3D7.js +23 -0
- package/node_modules/pi-lens/dist/chunk-UY752QSN.js +45 -0
- package/node_modules/pi-lens/dist/chunk-V5PXFSZK.js +188 -0
- package/node_modules/pi-lens/dist/chunk-V7R425XG.js +398 -0
- package/node_modules/pi-lens/dist/chunk-VCSRSYLF.js +81 -0
- package/node_modules/pi-lens/dist/chunk-VD4JQCQC.js +162 -0
- package/node_modules/pi-lens/dist/chunk-VDBPVIKD.js +62 -0
- package/node_modules/pi-lens/dist/chunk-VHQLPIRJ.js +114 -0
- package/node_modules/pi-lens/dist/chunk-VN2AXLEX.js +5414 -0
- package/node_modules/pi-lens/dist/chunk-VNMT7C64.js +478 -0
- package/node_modules/pi-lens/dist/chunk-WONERJTP.js +309 -0
- package/node_modules/pi-lens/dist/chunk-XECLWQZN.js +85 -0
- package/node_modules/pi-lens/dist/chunk-XIMYZ3JA.js +27 -0
- package/node_modules/pi-lens/dist/chunk-XK4CIILI.js +144 -0
- package/node_modules/pi-lens/dist/chunk-Y4T27COV.js +562 -0
- package/node_modules/pi-lens/dist/chunk-YGVYKNSN.js +1860 -0
- package/node_modules/pi-lens/dist/chunk-YLF3PPT5.js +308 -0
- package/node_modules/pi-lens/dist/chunk-YSNFMJKN.js +14 -0
- package/node_modules/pi-lens/dist/chunk-Z3CCVDUB.js +76 -0
- package/node_modules/pi-lens/dist/chunk-Z54ZSOBZ.js +61 -0
- package/node_modules/pi-lens/dist/chunk-ZAWW3AF6.js +97 -0
- package/node_modules/pi-lens/dist/chunk-ZMOL6RQC.js +167 -0
- package/node_modules/pi-lens/dist/index.js +16456 -8335
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +186 -232
- package/node_modules/pi-lens/dist/mcp/cli.js +137 -98
- package/node_modules/pi-lens/dist/mcp/server.js +22589 -1573
- package/node_modules/pi-lens/dist/mcp/worker.js +97 -38
- package/node_modules/pi-lens/dist/probes/installer.js +135 -0
- package/node_modules/pi-lens/dist/workers/project-snapshot-persist-worker.js +22 -0
- package/node_modules/pi-lens/dist/workers/review-graph-persist-worker.js +15 -0
- package/node_modules/pi-lens/docs/agent-guide.md +13 -4
- package/node_modules/pi-lens/docs/agent-tools.md +42 -0
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +3 -2
- package/node_modules/pi-lens/docs/configuration.md +20 -1
- package/node_modules/pi-lens/docs/environment-variables.md +52 -2
- package/node_modules/pi-lens/docs/features.md +8 -4
- package/node_modules/pi-lens/docs/globalconfig.md +1 -0
- package/node_modules/pi-lens/docs/io-bridge-v2-migration.md +94 -0
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +36 -4
- package/node_modules/pi-lens/docs/lsp-idle-eviction.md +124 -0
- package/node_modules/pi-lens/docs/mcp.md +21 -5
- package/node_modules/pi-lens/docs/merge-queue-rollout.md +77 -0
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +156 -88
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +74 -26
- package/node_modules/pi-lens/docs/pi-lens-merge-policy.md +694 -0
- package/node_modules/pi-lens/docs/pi-lens-monitor.md +73 -7
- package/node_modules/pi-lens/docs/pi-lens-retro.md +6 -4
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +188 -48
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +246 -45
- package/node_modules/pi-lens/docs/pi-lens-warden.md +18 -7
- package/node_modules/pi-lens/docs/public-api-stability.md +55 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +84 -17
- package/node_modules/pi-lens/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +53 -49
- package/node_modules/pi-lens/docs/settings.md +1 -0
- package/node_modules/pi-lens/docs/usage.md +5 -1
- package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm +0 -0
- package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm.json +3 -3
- package/node_modules/pi-lens/package.json +18 -6
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +1275 -62
- package/node_modules/pi-lens/scripts/download-grammars.js +5 -0
- package/node_modules/pi-lens/scripts/grammars.lock.json +6 -1
- package/node_modules/pi-lens/scripts/install-selftest.mjs +33 -34
- package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +13 -4
- package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +18 -0
- package/node_modules/pi-typesafe/README.md +1 -1
- package/node_modules/pi-typesafe/dist/backends.d.ts +15 -5
- package/node_modules/pi-typesafe/dist/backends.js +18 -0
- package/node_modules/pi-typesafe/dist/client.d.ts +4 -3
- package/node_modules/pi-typesafe/dist/client.js +22 -7
- package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
- package/node_modules/pi-typesafe/dist/index.js +1 -1
- package/node_modules/pi-typesafe/dist/usage.d.ts +14 -4
- package/node_modules/pi-typesafe/dist/usage.js +147 -42
- package/node_modules/pi-typesafe/package.json +1 -1
- package/node_modules/pi-web-access/CHANGELOG.md +48 -0
- package/node_modules/pi-web-access/README.md +57 -11
- package/node_modules/pi-web-access/brave-rate-limit.ts +8 -2
- package/node_modules/pi-web-access/curator-page.ts +5 -1
- package/node_modules/pi-web-access/curator-server.ts +2 -0
- package/node_modules/pi-web-access/declared-web-links.ts +2 -2
- package/node_modules/pi-web-access/degoog.ts +323 -0
- package/node_modules/pi-web-access/dist/index.js +3483 -2745
- package/node_modules/pi-web-access/dist/mcp-cli.js +27443 -0
- package/node_modules/pi-web-access/exa.ts +5 -1
- package/node_modules/pi-web-access/extract.ts +41 -3
- package/node_modules/pi-web-access/firecrawl.ts +35 -4
- package/node_modules/pi-web-access/gemini-search.ts +34 -11
- package/node_modules/pi-web-access/gemini-web.ts +14 -9
- package/node_modules/pi-web-access/index.ts +153 -1202
- package/node_modules/pi-web-access/keenable.ts +221 -0
- package/node_modules/pi-web-access/mcp-cli.ts +40 -0
- package/node_modules/pi-web-access/mcp-server.ts +81 -0
- package/node_modules/pi-web-access/mcp-tool-definitions.ts +117 -0
- package/node_modules/pi-web-access/openai-search.ts +34 -1
- package/node_modules/pi-web-access/package.json +23 -5
- package/node_modules/pi-web-access/perplexity.ts +64 -56
- package/node_modules/pi-web-access/tool-arguments.ts +20 -0
- package/node_modules/pi-web-access/video-extract.ts +1 -1
- package/node_modules/pi-web-access/web-tool-contract.ts +86 -0
- package/node_modules/pi-web-access/web-tool-core.ts +1502 -0
- package/node_modules/pi-web-access/youtube-extract.ts +2 -5
- package/package.json +17 -12
- package/node_modules/@dietrichgebert/ponytail/README.es.md +0 -329
- package/node_modules/@dietrichgebert/ponytail/README.ko.md +0 -346
- package/node_modules/@dietrichgebert/ponytail/assets/benchmark-3model.svg +0 -21
- package/node_modules/@dietrichgebert/ponytail/assets/benchmark-agentic.svg +0 -62
- package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.svg +0 -115
- package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt-dark.svg +0 -27
- package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt.svg +0 -27
- package/node_modules/@dietrichgebert/ponytail/assets/logo.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +0 -4
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +0 -4
- package/node_modules/@dietrichgebert/ponytail/assets/social-preview.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-es.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-ko.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner.png +0 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -35
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +0 -1610
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +0 -593
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +0 -147
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +0 -325
- package/node_modules/pi-lens/dist/clients/analysed-root.js +0 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +0 -731
- package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +0 -146
- package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +0 -118
- package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -10
- package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +0 -154
- package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +0 -75
- package/node_modules/pi-lens/dist/clients/atomic-write.js +0 -189
- package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +0 -12
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +0 -1056
- package/node_modules/pi-lens/dist/clients/biome-client.js +0 -357
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -837
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +0 -129
- package/node_modules/pi-lens/dist/clients/bootstrap.js +0 -569
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +0 -190
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +0 -202
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +0 -295
- package/node_modules/pi-lens/dist/clients/build-identity.js +0 -95
- package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +0 -113
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +0 -105
- package/node_modules/pi-lens/dist/clients/bus-publish.js +0 -171
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +0 -190
- package/node_modules/pi-lens/dist/clients/cache-manager.js +0 -524
- package/node_modules/pi-lens/dist/clients/cache-observability.js +0 -1313
- package/node_modules/pi-lens/dist/clients/call-graph.js +0 -852
- package/node_modules/pi-lens/dist/clients/cargo-manifest.js +0 -422
- package/node_modules/pi-lens/dist/clients/cascade-budget.js +0 -196
- package/node_modules/pi-lens/dist/clients/cascade-format.js +0 -315
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -31
- package/node_modules/pi-lens/dist/clients/cascade-types.js +0 -9
- package/node_modules/pi-lens/dist/clients/child-unref.js +0 -124
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +0 -286
- package/node_modules/pi-lens/dist/clients/codebase-model.js +0 -173
- package/node_modules/pi-lens/dist/clients/collateral-test-role.js +0 -40
- package/node_modules/pi-lens/dist/clients/complexity-client.js +0 -462
- package/node_modules/pi-lens/dist/clients/config-core/deny.js +0 -221
- package/node_modules/pi-lens/dist/clients/config-core/index.js +0 -50
- package/node_modules/pi-lens/dist/clients/config-core/merge.js +0 -357
- package/node_modules/pi-lens/dist/clients/config-core/normalize.js +0 -340
- package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +0 -248
- package/node_modules/pi-lens/dist/clients/config-core/provenance.js +0 -164
- package/node_modules/pi-lens/dist/clients/config-core/records.js +0 -218
- package/node_modules/pi-lens/dist/clients/config-core/resolve.js +0 -125
- package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +0 -78
- package/node_modules/pi-lens/dist/clients/config-core/schema.js +0 -165
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +0 -285
- package/node_modules/pi-lens/dist/clients/config-locations.js +0 -388
- package/node_modules/pi-lens/dist/clients/config-resolve.js +0 -798
- package/node_modules/pi-lens/dist/clients/config-schema.js +0 -251
- package/node_modules/pi-lens/dist/clients/config-warn.js +0 -407
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +0 -26
- package/node_modules/pi-lens/dist/clients/cooperative-budget.js +0 -42
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +0 -510
- package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -30
- package/node_modules/pi-lens/dist/clients/deadline-utils.js +0 -280
- package/node_modules/pi-lens/dist/clients/debug-handles.js +0 -211
- package/node_modules/pi-lens/dist/clients/debug-heap.js +0 -174
- package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +0 -106
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +0 -426
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +0 -169
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +0 -885
- package/node_modules/pi-lens/dist/clients/deps/ast-grep-napi.js +0 -22
- package/node_modules/pi-lens/dist/clients/deps/js-yaml.js +0 -9
- package/node_modules/pi-lens/dist/clients/deps/minimatch.js +0 -4
- package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +0 -14
- package/node_modules/pi-lens/dist/clients/deps/typebox.js +0 -14
- package/node_modules/pi-lens/dist/clients/deps/vscode-jsonrpc.js +0 -1
- package/node_modules/pi-lens/dist/clients/deps/web-tree-sitter.js +0 -24
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +0 -630
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +0 -305
- package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +0 -73
- package/node_modules/pi-lens/dist/clients/diagnostic-tracker.js +0 -105
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +0 -230
- package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +0 -65
- package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +0 -343
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -35
- package/node_modules/pi-lens/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +0 -1269
- package/node_modules/pi-lens/dist/clients/dispatch/fact-provider-types.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/fact-rule-runner.js +0 -17
- package/node_modules/pi-lens/dist/clients/dispatch/fact-runner.js +0 -30
- package/node_modules/pi-lens/dist/clients/dispatch/fact-scheduler.js +0 -68
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +0 -377
- package/node_modules/pi-lens/dist/clients/dispatch/facts/comment-facts.js +0 -22
- package/node_modules/pi-lens/dist/clients/dispatch/facts/file-content.js +0 -19
- package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +0 -348
- package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +0 -204
- package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
- package/node_modules/pi-lens/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
- package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +0 -339
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +0 -417
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +0 -89
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +0 -2377
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -11
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +0 -87
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -278
- package/node_modules/pi-lens/dist/clients/dispatch/priorities.js +0 -20
- package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +0 -50
- package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +0 -69
- package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +0 -135
- package/node_modules/pi-lens/dist/clients/dispatch/rules/async-noise.js +0 -40
- package/node_modules/pi-lens/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
- package/node_modules/pi-lens/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
- package/node_modules/pi-lens/dist/clients/dispatch/rules/error-obscuring.js +0 -32
- package/node_modules/pi-lens/dist/clients/dispatch/rules/error-swallowing.js +0 -31
- package/node_modules/pi-lens/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +0 -72
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +0 -74
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
- package/node_modules/pi-lens/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
- package/node_modules/pi-lens/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
- package/node_modules/pi-lens/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
- package/node_modules/pi-lens/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
- package/node_modules/pi-lens/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
- package/node_modules/pi-lens/dist/clients/dispatch/runner-context.js +0 -52
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -111
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -296
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -219
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -82
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -416
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -183
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -176
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -164
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -195
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -137
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -29
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -69
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -167
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +0 -104
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -135
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -91
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -171
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -940
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -117
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +0 -108
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -99
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -110
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -379
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -167
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -102
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +0 -414
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -85
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -120
- package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -62
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +0 -169
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -99
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -88
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +0 -256
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -173
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -102
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -128
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -259
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -183
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -166
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -163
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -174
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -168
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -125
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -643
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -253
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -128
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -95
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -79
- package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +0 -79
- package/node_modules/pi-lens/dist/clients/dispatch/tool-profile.js +0 -30
- package/node_modules/pi-lens/dist/clients/dispatch/types.js +0 -21
- package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +0 -45
- package/node_modules/pi-lens/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
- package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -32
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +0 -124
- package/node_modules/pi-lens/dist/clients/durable-store.js +0 -97
- package/node_modules/pi-lens/dist/clients/effective-config.js +0 -415
- package/node_modules/pi-lens/dist/clients/env-utils.js +0 -83
- package/node_modules/pi-lens/dist/clients/error-class.js +0 -23
- package/node_modules/pi-lens/dist/clients/eval-timestamp.js +0 -17
- package/node_modules/pi-lens/dist/clients/event-loop-hold.js +0 -274
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +0 -250
- package/node_modules/pi-lens/dist/clients/extension-log.js +0 -504
- package/node_modules/pi-lens/dist/clients/extension-mode.js +0 -73
- package/node_modules/pi-lens/dist/clients/feature-hints.js +0 -103
- package/node_modules/pi-lens/dist/clients/file-kinds.js +0 -412
- package/node_modules/pi-lens/dist/clients/file-role.js +0 -134
- package/node_modules/pi-lens/dist/clients/file-time.js +0 -160
- package/node_modules/pi-lens/dist/clients/file-utils.js +0 -1032
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +0 -494
- package/node_modules/pi-lens/dist/clients/finding-identity.js +0 -88
- package/node_modules/pi-lens/dist/clients/fix-worklog.js +0 -103
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +0 -343
- package/node_modules/pi-lens/dist/clients/format-service.js +0 -222
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +0 -9
- package/node_modules/pi-lens/dist/clients/formatters.js +0 -2032
- package/node_modules/pi-lens/dist/clients/freshness-cadence.js +0 -17
- package/node_modules/pi-lens/dist/clients/freshness.js +0 -38
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +0 -394
- package/node_modules/pi-lens/dist/clients/generation-guard.js +0 -215
- package/node_modules/pi-lens/dist/clients/git-guard.js +0 -1136
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +0 -243
- package/node_modules/pi-lens/dist/clients/gitleaks-client.js +0 -509
- package/node_modules/pi-lens/dist/clients/go-client.js +0 -104
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +0 -605
- package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +0 -348
- package/node_modules/pi-lens/dist/clients/grammar-source.js +0 -465
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +0 -135
- package/node_modules/pi-lens/dist/clients/hashline-anchor.js +0 -424
- package/node_modules/pi-lens/dist/clients/hook-budgets.js +0 -76
- package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +0 -140
- package/node_modules/pi-lens/dist/clients/host-ports.js +0 -39
- package/node_modules/pi-lens/dist/clients/indent-retarget.js +0 -154
- package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +0 -186
- package/node_modules/pi-lens/dist/clients/install-diagnostics.js +0 -156
- package/node_modules/pi-lens/dist/clients/installer/index.js +0 -5416
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +0 -58
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +0 -783
- package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +0 -7
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +0 -1137
- package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +0 -145
- package/node_modules/pi-lens/dist/clients/instance-registry.js +0 -773
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +0 -378
- package/node_modules/pi-lens/dist/clients/json-cache-read.js +0 -65
- package/node_modules/pi-lens/dist/clients/knip-client.js +0 -788
- package/node_modules/pi-lens/dist/clients/language-policy.js +0 -252
- package/node_modules/pi-lens/dist/clients/language-profile.js +0 -361
- package/node_modules/pi-lens/dist/clients/language-registry.js +0 -603
- package/node_modules/pi-lens/dist/clients/latency-logger.js +0 -727
- package/node_modules/pi-lens/dist/clients/lazy-import.js +0 -21
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +0 -54
- package/node_modules/pi-lens/dist/clients/lens-config.js +0 -404
- package/node_modules/pi-lens/dist/clients/lens-engine.js +0 -358
- package/node_modules/pi-lens/dist/clients/lens-events.js +0 -125
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +0 -467
- package/node_modules/pi-lens/dist/clients/lens-map.js +0 -1156
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +0 -74
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -222
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +0 -178
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +0 -421
- package/node_modules/pi-lens/dist/clients/lsp/client.js +0 -4593
- package/node_modules/pi-lens/dist/clients/lsp/config.js +0 -691
- package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +0 -41
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +0 -225
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +0 -467
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +0 -1219
- package/node_modules/pi-lens/dist/clients/lsp/index.js +0 -8305
- package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +0 -216
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +0 -298
- package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -63
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +0 -746
- package/node_modules/pi-lens/dist/clients/lsp/lombok.js +0 -158
- package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +0 -5
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -251
- package/node_modules/pi-lens/dist/clients/lsp/position-encoding.js +0 -61
- package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +0 -83
- package/node_modules/pi-lens/dist/clients/lsp/server.js +0 -3290
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +0 -192
- package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +0 -17
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -56
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +0 -332
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +0 -85
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/index.js +0 -10
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +0 -392
- package/node_modules/pi-lens/dist/clients/lsp/watch-queue.js +0 -61
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +0 -184
- package/node_modules/pi-lens/dist/clients/lsp-budget.js +0 -186
- package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +0 -110
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +0 -9
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +0 -396
- package/node_modules/pi-lens/dist/clients/lsp-status.js +0 -59
- package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +0 -40
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +0 -420
- package/node_modules/pi-lens/dist/clients/mcp/host-shim.js +0 -31
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +0 -523
- package/node_modules/pi-lens/dist/clients/mcp/review.js +0 -197
- package/node_modules/pi-lens/dist/clients/mcp/session.js +0 -412
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +0 -208
- package/node_modules/pi-lens/dist/clients/message-end-attribution.js +0 -30
- package/node_modules/pi-lens/dist/clients/metrics-client.js +0 -107
- package/node_modules/pi-lens/dist/clients/metrics-history.js +0 -327
- package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +0 -263
- package/node_modules/pi-lens/dist/clients/model-provider.js +0 -53
- package/node_modules/pi-lens/dist/clients/module-report-lsp.js +0 -189
- package/node_modules/pi-lens/dist/clients/module-report.js +0 -2150
- package/node_modules/pi-lens/dist/clients/mutating-tool.js +0 -651
- package/node_modules/pi-lens/dist/clients/mutation-attribution.js +0 -368
- package/node_modules/pi-lens/dist/clients/mutation-bridge.js +0 -240
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -734
- package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +0 -101
- package/node_modules/pi-lens/dist/clients/observed-mutation.js +0 -1253
- package/node_modules/pi-lens/dist/clients/oldtext-autopatch.js +0 -128
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +0 -496
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +0 -333
- package/node_modules/pi-lens/dist/clients/opengrep-config.js +0 -76
- package/node_modules/pi-lens/dist/clients/package-manager.js +0 -607
- package/node_modules/pi-lens/dist/clients/package-root.js +0 -38
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +0 -439
- package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +0 -43
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +0 -108
- package/node_modules/pi-lens/dist/clients/path-utils.js +0 -1244
- package/node_modules/pi-lens/dist/clients/performance-report.js +0 -331
- package/node_modules/pi-lens/dist/clients/persist-debounce.js +0 -70
- package/node_modules/pi-lens/dist/clients/persistent-reverify.js +0 -271
- package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +0 -119
- package/node_modules/pi-lens/dist/clients/pipeline.js +0 -1480
- package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +0 -37
- package/node_modules/pi-lens/dist/clients/print-mode.js +0 -21
- package/node_modules/pi-lens/dist/clients/probe-home-state.js +0 -228
- package/node_modules/pi-lens/dist/clients/process-bridge.js +0 -66
- package/node_modules/pi-lens/dist/clients/process-singletons.js +0 -210
- package/node_modules/pi-lens/dist/clients/process-snapshot.js +0 -68
- package/node_modules/pi-lens/dist/clients/project-changes.js +0 -181
- package/node_modules/pi-lens/dist/clients/project-conventions.js +0 -171
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +0 -137
- package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +0 -109
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +0 -520
- package/node_modules/pi-lens/dist/clients/project-diagnostics/types.js +0 -1
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +0 -807
- package/node_modules/pi-lens/dist/clients/project-report.js +0 -701
- package/node_modules/pi-lens/dist/clients/project-scale.js +0 -237
- package/node_modules/pi-lens/dist/clients/project-scan-policy.js +0 -11
- package/node_modules/pi-lens/dist/clients/project-snapshot-fingerprint.js +0 -48
- package/node_modules/pi-lens/dist/clients/project-snapshot-persist-worker.js +0 -20
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +0 -1653
- package/node_modules/pi-lens/dist/clients/project-trust.js +0 -181
- package/node_modules/pi-lens/dist/clients/python-environment.js +0 -217
- package/node_modules/pi-lens/dist/clients/python-provenance.js +0 -639
- package/node_modules/pi-lens/dist/clients/quiet-window-config.js +0 -47
- package/node_modules/pi-lens/dist/clients/quiet-window.js +0 -236
- package/node_modules/pi-lens/dist/clients/read-bridge.js +0 -139
- package/node_modules/pi-lens/dist/clients/read-expansion.js +0 -266
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +0 -251
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +0 -1306
- package/node_modules/pi-lens/dist/clients/read-guard.js +0 -1561
- package/node_modules/pi-lens/dist/clients/recent-touches.js +0 -240
- package/node_modules/pi-lens/dist/clients/redact/secrets.js +0 -449
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +0 -628
- package/node_modules/pi-lens/dist/clients/reverse-deps.js +0 -270
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +0 -5126
- package/node_modules/pi-lens/dist/clients/review-graph/format.js +0 -33
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +0 -163
- package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +0 -481
- package/node_modules/pi-lens/dist/clients/review-graph/persist-worker.js +0 -10
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +0 -244
- package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +0 -21
- package/node_modules/pi-lens/dist/clients/review-graph/service.js +0 -96
- package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +0 -82
- package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +0 -103
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +0 -299
- package/node_modules/pi-lens/dist/clients/review-graph/types.js +0 -1
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +0 -449
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +0 -81
- package/node_modules/pi-lens/dist/clients/ruff-client.js +0 -179
- package/node_modules/pi-lens/dist/clients/rules-scanner.js +0 -131
- package/node_modules/pi-lens/dist/clients/run-duration.js +0 -55
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +0 -746
- package/node_modules/pi-lens/dist/clients/runtime-config.js +0 -77
- package/node_modules/pi-lens/dist/clients/runtime-context.js +0 -211
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +0 -1324
- package/node_modules/pi-lens/dist/clients/runtime-session.js +0 -2485
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +0 -1295
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +0 -2049
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +0 -3767
- package/node_modules/pi-lens/dist/clients/rust-client.js +0 -97
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +0 -2357
- package/node_modules/pi-lens/dist/clients/sanitize.js +0 -286
- package/node_modules/pi-lens/dist/clients/scan-utils.js +0 -25
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +0 -165
- package/node_modules/pi-lens/dist/clients/search-read-registration.js +0 -94
- package/node_modules/pi-lens/dist/clients/secret-findings.js +0 -114
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +0 -352
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +0 -297
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +0 -484
- package/node_modules/pi-lens/dist/clients/session-start-observability.js +0 -94
- package/node_modules/pi-lens/dist/clients/session-state-store.js +0 -101
- package/node_modules/pi-lens/dist/clients/session-summary.js +0 -15
- package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +0 -27
- package/node_modules/pi-lens/dist/clients/sg-runner.js +0 -876
- package/node_modules/pi-lens/dist/clients/sgconfig.js +0 -347
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +0 -338
- package/node_modules/pi-lens/dist/clients/single-flight.js +0 -158
- package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +0 -108
- package/node_modules/pi-lens/dist/clients/skills-resolver.js +0 -141
- package/node_modules/pi-lens/dist/clients/slow-fs.js +0 -138
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +0 -231
- package/node_modules/pi-lens/dist/clients/source-filter.js +0 -596
- package/node_modules/pi-lens/dist/clients/source-walker.js +0 -211
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +0 -120
- package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +0 -110
- package/node_modules/pi-lens/dist/clients/stale-marker.js +0 -17
- package/node_modules/pi-lens/dist/clients/startup-scan.js +0 -400
- package/node_modules/pi-lens/dist/clients/startup-timing.js +0 -130
- package/node_modules/pi-lens/dist/clients/string-utils.js +0 -33
- package/node_modules/pi-lens/dist/clients/subagent-mode.js +0 -111
- package/node_modules/pi-lens/dist/clients/symbol-containment.js +0 -24
- package/node_modules/pi-lens/dist/clients/symbol-types.js +0 -5
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +0 -2507
- package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +0 -287
- package/node_modules/pi-lens/dist/clients/todo-scanner.js +0 -229
- package/node_modules/pi-lens/dist/clients/tool-agreement.js +0 -423
- package/node_modules/pi-lens/dist/clients/tool-config.js +0 -249
- package/node_modules/pi-lens/dist/clients/tool-cwd.js +0 -270
- package/node_modules/pi-lens/dist/clients/tool-definition.js +0 -47
- package/node_modules/pi-lens/dist/clients/tool-event.js +0 -105
- package/node_modules/pi-lens/dist/clients/tool-policy.js +0 -2755
- package/node_modules/pi-lens/dist/clients/tool-probe.js +0 -53
- package/node_modules/pi-lens/dist/clients/tool-render.js +0 -156
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +0 -118
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +0 -364
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +0 -4195
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +0 -100
- package/node_modules/pi-lens/dist/clients/tree-sitter-navigator.js +0 -269
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +0 -569
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +0 -158
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
- package/node_modules/pi-lens/dist/clients/trivy-client.js +0 -467
- package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -50
- package/node_modules/pi-lens/dist/clients/turn-context.js +0 -52
- package/node_modules/pi-lens/dist/clients/turn-end/lane.js +0 -51
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +0 -112
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +0 -204
- package/node_modules/pi-lens/dist/clients/turn-summary-render.js +0 -72
- package/node_modules/pi-lens/dist/clients/turn-summary.js +0 -132
- package/node_modules/pi-lens/dist/clients/typos-config.js +0 -29
- package/node_modules/pi-lens/dist/clients/user-notify.js +0 -51
- package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +0 -57
- package/node_modules/pi-lens/dist/clients/warm-attach.js +0 -305
- package/node_modules/pi-lens/dist/clients/widget-state.js +0 -1808
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -49
- package/node_modules/pi-lens/dist/clients/word-index-store.js +0 -459
- package/node_modules/pi-lens/dist/clients/word-index.js +0 -2080
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +0 -443
- package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +0 -65
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +0 -340
- package/node_modules/pi-lens/dist/i18n.js +0 -69
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +0 -124
- package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +0 -583
- package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +0 -129
- package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
- package/node_modules/pi-lens/dist/tools/activate-tools.js +0 -87
- package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +0 -137
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +0 -244
- package/node_modules/pi-lens/dist/tools/ast-grep-search.js +0 -762
- package/node_modules/pi-lens/dist/tools/effective-config.js +0 -90
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +0 -375
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +0 -2571
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +0 -1745
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +0 -1367
- package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +0 -294
- package/node_modules/pi-lens/dist/tools/module-report.js +0 -382
- package/node_modules/pi-lens/dist/tools/project-report.js +0 -86
- package/node_modules/pi-lens/dist/tools/render-compact.js +0 -374
- package/node_modules/pi-lens/dist/tools/scan-progress.js +0 -53
- package/node_modules/pi-lens/dist/tools/shared.js +0 -29
- package/node_modules/pi-lens/dist/tools/symbol-search.js +0 -155
|
@@ -1,3290 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LSP Server Definitions for pi-lens
|
|
3
|
-
*
|
|
4
|
-
* Defines 40+ language servers with:
|
|
5
|
-
* - Root detection (monorepo support)
|
|
6
|
-
* - Auto-installation strategies
|
|
7
|
-
* - Platform-specific handling
|
|
8
|
-
*/
|
|
9
|
-
import { existsSync, mkdirSync, statSync } from "node:fs";
|
|
10
|
-
import { access, readFile, readdir, stat } from "node:fs/promises";
|
|
11
|
-
import os from "node:os";
|
|
12
|
-
import path from "node:path";
|
|
13
|
-
import { getGlobalPiLensDir, getProjectIgnoreGlobs, isPathIgnoredByProject, } from "../file-utils.js";
|
|
14
|
-
import { augmentPythonEnvironment, detectPythonEnvironment, detectPythonVenv, pythonEnvironmentToolCandidates, } from "../python-environment.js";
|
|
15
|
-
export { detectPythonVenv };
|
|
16
|
-
import { STAGE_TMP_PATTERN } from "../atomic-write-staging.js";
|
|
17
|
-
import { DOTNET_CSHARP_ROOT_MARKERS, DOTNET_FSHARP_ROOT_MARKERS, KIND_EXTENSIONS, } from "../file-kinds.js";
|
|
18
|
-
import { extensionsForLanguage } from "../language-registry.js";
|
|
19
|
-
import { CARGO_WORKSPACE_MEMBER_DIALECT, direntsHaveMarkerGlobMatch, isAtOrAboveHomeDir, isFullyQualified, isWindowsPath, matchesWorkspaceMemberPattern, normalizeEphemeralMapKey, pathsEqual, toPosix, } from "../path-utils.js";
|
|
20
|
-
import { ensureTool, findManagedToolBinary, getToolEnvironment, getToolPath, } from "../installer/index.js";
|
|
21
|
-
import * as installer from "../installer/index.js";
|
|
22
|
-
import { classifyProbeFailure, describeInstallAttempt, logAvailabilityDecision, } from "../dispatch/runners/utils/availability-policy.js";
|
|
23
|
-
import { resolveOpengrepConfig } from "../opengrep-config.js";
|
|
24
|
-
import { isZizmorAuditTarget, resolveZizmorGitHubToken, } from "../zizmor-config.js";
|
|
25
|
-
import { logLatency } from "../latency-logger.js";
|
|
26
|
-
import { logSessionStart } from "../sessionstart-logger.js";
|
|
27
|
-
import { findLocalSgconfig, resolveBaselineSgconfig } from "../sgconfig.js";
|
|
28
|
-
import { findLocalTyposConfig } from "../typos-config.js";
|
|
29
|
-
import { resolvePackagePath } from "../package-root.js";
|
|
30
|
-
import { resolveToolCwd } from "../tool-cwd.js";
|
|
31
|
-
import { dirMtimeRecordsAsync, dirMtimesStillFreshAsync, unknownDirMtimeRecords, } from "../workspace-topology.js";
|
|
32
|
-
import { bounded } from "../deadline-utils.js";
|
|
33
|
-
import { FRESHNESS_CADENCE_MS } from "../freshness-cadence.js";
|
|
34
|
-
import { HOOK_WALL_BUDGET_MS } from "../hook-budgets.js";
|
|
35
|
-
import { recordDegradationOnce } from "../degradation-ledger.js";
|
|
36
|
-
import { hasCargoWorkspaceTable, readCargoWorkspaceExclude, readCargoWorkspaceMembers, readTextFileOrUndefined, } from "../cargo-manifest.js";
|
|
37
|
-
import { resolveAstGrepNativeExe } from "./wait-policy/index.js";
|
|
38
|
-
import { hasSpawnFailureKind, isCommandAvailableAsync, safeSpawnAsync, } from "../safe-spawn.js";
|
|
39
|
-
import { launchLSP } from "./launch.js";
|
|
40
|
-
import { createLombokJdtlsArgs } from "./lombok.js";
|
|
41
|
-
import { resolveJavaRuntimeEnv } from "./jvm-runtime.js";
|
|
42
|
-
import { normalizeMapKey } from "./path-utils.js";
|
|
43
|
-
import { getRubyVersionDirNamesSync } from "./ruby-drive-dirs.js";
|
|
44
|
-
import { getProcessSingleton } from "../process-singletons.js";
|
|
45
|
-
import { createGenerationSource, } from "../generation-guard.js";
|
|
46
|
-
function withRootMarkers(root, markers) {
|
|
47
|
-
root.rootMarkers = markers;
|
|
48
|
-
return root;
|
|
49
|
-
}
|
|
50
|
-
/** Resolve a server identity cwd through the shared tool-cwd seam. */
|
|
51
|
-
export async function resolveLspServerCwd(server, filePath, sessionCwd, onRootFailure) {
|
|
52
|
-
const rootMarkers = server.rootMarkers ?? server.root.rootMarkers;
|
|
53
|
-
let serverRoot;
|
|
54
|
-
let rootFailed = false;
|
|
55
|
-
try {
|
|
56
|
-
serverRoot = await server.root(filePath);
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
60
|
-
rootFailed = true;
|
|
61
|
-
onRootFailure?.(reason);
|
|
62
|
-
}
|
|
63
|
-
const isFileDirFallback = serverRoot !== undefined &&
|
|
64
|
-
rootMarkers?.length &&
|
|
65
|
-
path.resolve(serverRoot) === path.resolve(path.dirname(filePath));
|
|
66
|
-
if (!serverRoot || isFileDirFallback) {
|
|
67
|
-
if (!serverRoot) {
|
|
68
|
-
recordDegradationOnce({
|
|
69
|
-
kind: "tool-cwd-resolution",
|
|
70
|
-
subject: server.id,
|
|
71
|
-
reason: `lsp:server-root-${rootFailed ? "failed" : "fallback"}:${filePath}`,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
if (!rootMarkers?.length)
|
|
75
|
-
return undefined;
|
|
76
|
-
return resolveToolCwd("lsp", server.id, filePath, {
|
|
77
|
-
cwd: path.dirname(path.resolve(filePath)),
|
|
78
|
-
rootMarkers,
|
|
79
|
-
}).cwd;
|
|
80
|
-
}
|
|
81
|
-
const boundedServerRoot = enforceLspRootCeiling(serverRoot, sessionCwd, filePath);
|
|
82
|
-
return resolveToolCwd("lsp", server.id, filePath, {
|
|
83
|
-
cwd: sessionCwd,
|
|
84
|
-
rootMarkers,
|
|
85
|
-
serverRoot: boundedServerRoot,
|
|
86
|
-
}).cwd;
|
|
87
|
-
}
|
|
88
|
-
const FIXTURE_ROOT_SEGMENTS = new Set(["__fixtures__", "testdata"]);
|
|
89
|
-
const FALLBACK_PROJECT_MARKERS = [
|
|
90
|
-
".git",
|
|
91
|
-
"package.json",
|
|
92
|
-
"go.work",
|
|
93
|
-
"go.mod",
|
|
94
|
-
"Cargo.toml",
|
|
95
|
-
"pyproject.toml",
|
|
96
|
-
"pom.xml",
|
|
97
|
-
"build.gradle",
|
|
98
|
-
"build.gradle.kts",
|
|
99
|
-
"mix.exs",
|
|
100
|
-
"pubspec.yaml",
|
|
101
|
-
"Package.swift",
|
|
102
|
-
];
|
|
103
|
-
/**
|
|
104
|
-
* Markers that make a nested root an independently hosted project rather than
|
|
105
|
-
* a config-only directory that should share an ancestor's LSP client.
|
|
106
|
-
*/
|
|
107
|
-
const PROJECT_BOUNDARY_MARKERS = [
|
|
108
|
-
"package.json",
|
|
109
|
-
"package-lock.json",
|
|
110
|
-
"pnpm-lock.yaml",
|
|
111
|
-
"yarn.lock",
|
|
112
|
-
"bun.lock",
|
|
113
|
-
"bun.lockb",
|
|
114
|
-
"go.work",
|
|
115
|
-
"go.mod",
|
|
116
|
-
"Cargo.toml",
|
|
117
|
-
"Cargo.lock",
|
|
118
|
-
"pyproject.toml",
|
|
119
|
-
"uv.lock",
|
|
120
|
-
"poetry.lock",
|
|
121
|
-
"Pipfile",
|
|
122
|
-
"Pipfile.lock",
|
|
123
|
-
"pom.xml",
|
|
124
|
-
"build.gradle",
|
|
125
|
-
"build.gradle.kts",
|
|
126
|
-
"settings.gradle",
|
|
127
|
-
"settings.gradle.kts",
|
|
128
|
-
"mix.exs",
|
|
129
|
-
"pubspec.yaml",
|
|
130
|
-
"Package.swift",
|
|
131
|
-
];
|
|
132
|
-
// This is process-global state, so each candidate pair is logged once per
|
|
133
|
-
// process (not once per session). Keep the root-boundary marker below aligned
|
|
134
|
-
// with FALLBACK_PROJECT_MARKERS, the shared fallback root-policy marker set.
|
|
135
|
-
const loggedRootCeilingClamps = new Set();
|
|
136
|
-
const posixCaseInsensitiveByPath = new Map();
|
|
137
|
-
function posixFilesystemIsCaseInsensitive(root) {
|
|
138
|
-
const resolved = path.resolve(root);
|
|
139
|
-
const cached = posixCaseInsensitiveByPath.get(resolved);
|
|
140
|
-
if (cached !== undefined)
|
|
141
|
-
return cached;
|
|
142
|
-
if (!existsSync(resolved)) {
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
const name = path.basename(resolved);
|
|
146
|
-
const alternate = name.toLowerCase() === name ? name.toUpperCase() : name.toLowerCase();
|
|
147
|
-
let insensitive = false;
|
|
148
|
-
const alternatePath = path.join(path.dirname(resolved), alternate);
|
|
149
|
-
if (alternate !== name && existsSync(alternatePath)) {
|
|
150
|
-
try {
|
|
151
|
-
const actual = statSync(resolved);
|
|
152
|
-
const alternateStat = statSync(alternatePath);
|
|
153
|
-
insensitive =
|
|
154
|
-
actual.dev === alternateStat.dev && actual.ino === alternateStat.ino;
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
insensitive = false;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
posixCaseInsensitiveByPath.set(resolved, insensitive);
|
|
161
|
-
return insensitive;
|
|
162
|
-
}
|
|
163
|
-
export function resetLSPCaseSensitivityState() {
|
|
164
|
-
posixCaseInsensitiveByPath.clear();
|
|
165
|
-
}
|
|
166
|
-
export function _getPosixCaseSensitivityCacheSizeForTests() {
|
|
167
|
-
return posixCaseInsensitiveByPath.size;
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Path-shape-aware containment test: is `candidate` the same path as
|
|
171
|
-
* `ancestor`, or inside it?
|
|
172
|
-
*
|
|
173
|
-
* Exported so the session-cwd registry (`clients/lsp/config.ts`) and the
|
|
174
|
-
* foreign-root decline gate (`clients/lsp/index.ts`) test containment with the
|
|
175
|
-
* SAME comparator the root ceiling uses. A second hand-rolled `path.relative`
|
|
176
|
-
* helper alongside this one is how the two drift apart on Windows-shaped
|
|
177
|
-
* paths (shape 2 / #1150): `path.isAbsolute("C:\\repo")` is false on POSIX, so
|
|
178
|
-
* a host-default comparator silently mis-answers a win32 path on Linux CI.
|
|
179
|
-
*/
|
|
180
|
-
export function isSameOrWithin(ancestor, candidate, deps = {}) {
|
|
181
|
-
const windowsShaped = isWindowsPath(ancestor) || isWindowsPath(candidate);
|
|
182
|
-
const pathApi = windowsShaped ? path.win32 : path;
|
|
183
|
-
const resolvedAncestor = pathApi.resolve(ancestor);
|
|
184
|
-
const resolvedCandidate = pathApi.resolve(candidate);
|
|
185
|
-
let caseInsensitive = false;
|
|
186
|
-
if (!windowsShaped) {
|
|
187
|
-
if (deps.caseInsensitiveProbe) {
|
|
188
|
-
const cached = posixCaseInsensitiveByPath.get(resolvedAncestor);
|
|
189
|
-
caseInsensitive = cached ?? deps.caseInsensitiveProbe(resolvedAncestor);
|
|
190
|
-
if (cached === undefined) {
|
|
191
|
-
posixCaseInsensitiveByPath.set(resolvedAncestor, caseInsensitive);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
caseInsensitive = posixFilesystemIsCaseInsensitive(resolvedAncestor);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
const relative = pathApi.relative(caseInsensitive ? resolvedAncestor.toLowerCase() : resolvedAncestor, caseInsensitive ? resolvedCandidate.toLowerCase() : resolvedCandidate);
|
|
199
|
-
return (relative === "" ||
|
|
200
|
-
(!relative.startsWith("..") && !pathApi.isAbsolute(relative)));
|
|
201
|
-
}
|
|
202
|
-
/** Enforce the session cwd as the hard boundary for LSP client root selection. */
|
|
203
|
-
export function enforceLspRootCeiling(root, sessionCwd, filePath) {
|
|
204
|
-
const windowsShaped = isWindowsPath(root) || isWindowsPath(sessionCwd);
|
|
205
|
-
const pathApi = windowsShaped ? path.win32 : path;
|
|
206
|
-
const resolvedRoot = pathApi.resolve(root);
|
|
207
|
-
const resolvedCwd = pathApi.resolve(sessionCwd);
|
|
208
|
-
// Callers may explicitly inspect an out-of-session file (notably isolated
|
|
209
|
-
// tests and API consumers). The cwd ceiling governs roots for files that are
|
|
210
|
-
// actually inside the declared session project.
|
|
211
|
-
if (filePath && !isSameOrWithin(resolvedCwd, pathApi.resolve(filePath))) {
|
|
212
|
-
return resolvedRoot;
|
|
213
|
-
}
|
|
214
|
-
if (isSameOrWithin(resolvedCwd, resolvedRoot))
|
|
215
|
-
return resolvedRoot;
|
|
216
|
-
const logKey = `${normalizeMapKey(resolvedCwd)}:${normalizeMapKey(resolvedRoot)}`;
|
|
217
|
-
if (!loggedRootCeilingClamps.has(logKey)) {
|
|
218
|
-
loggedRootCeilingClamps.add(logKey);
|
|
219
|
-
logSessionStart(`lsp root clamped to session cwd: candidate=${resolvedRoot} cwd=${resolvedCwd}`);
|
|
220
|
-
}
|
|
221
|
-
return resolvedCwd;
|
|
222
|
-
}
|
|
223
|
-
export async function hasProjectBoundaryMarker(dir) {
|
|
224
|
-
// A nested Git checkout is an independently hosted project even when it has
|
|
225
|
-
// no language manifest. Keep this directory boundary aligned with the shared
|
|
226
|
-
// FALLBACK_PROJECT_MARKERS policy used by nearestNonExcludedFallbackRoot.
|
|
227
|
-
if (await markerExists(dir, ".git"))
|
|
228
|
-
return true;
|
|
229
|
-
for (const marker of PROJECT_BOUNDARY_MARKERS) {
|
|
230
|
-
if (await markerExists(dir, marker))
|
|
231
|
-
return true;
|
|
232
|
-
}
|
|
233
|
-
return false;
|
|
234
|
-
}
|
|
235
|
-
function pathSegments(dir) {
|
|
236
|
-
const parsed = path.parse(path.resolve(dir));
|
|
237
|
-
return path
|
|
238
|
-
.relative(parsed.root, path.resolve(dir))
|
|
239
|
-
.split(path.sep)
|
|
240
|
-
.filter(Boolean)
|
|
241
|
-
.map((segment) => segment.toLowerCase());
|
|
242
|
-
}
|
|
243
|
-
function hasFixtureConvention(dir) {
|
|
244
|
-
const segments = pathSegments(dir);
|
|
245
|
-
// Go treats any directory named testdata as fixture data by convention. The
|
|
246
|
-
// exclusion is intentionally ancestor-wide so nested fixture projects cannot
|
|
247
|
-
// become independent LSP roots, but the segment match itself stays exact.
|
|
248
|
-
if (segments.some((segment) => FIXTURE_ROOT_SEGMENTS.has(segment)))
|
|
249
|
-
return true;
|
|
250
|
-
return segments.some((segment, index) => segment === "tests" && segments[index + 1] === "fixtures");
|
|
251
|
-
}
|
|
252
|
-
function hasAtomicStageSegment(dir) {
|
|
253
|
-
return pathSegments(dir).some((segment) => STAGE_TMP_PATTERN.test(segment));
|
|
254
|
-
}
|
|
255
|
-
async function findGitBoundary(dir) {
|
|
256
|
-
let current = path.resolve(dir);
|
|
257
|
-
const fsRoot = path.parse(current).root;
|
|
258
|
-
while (true) {
|
|
259
|
-
if (await markerExists(current, ".git"))
|
|
260
|
-
return current;
|
|
261
|
-
if (current === fsRoot)
|
|
262
|
-
return undefined;
|
|
263
|
-
current = path.dirname(current);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
async function isExcludedLspRoot(dir) {
|
|
267
|
-
const candidate = path.resolve(dir);
|
|
268
|
-
if (hasFixtureConvention(candidate) || hasAtomicStageSegment(candidate))
|
|
269
|
-
return true;
|
|
270
|
-
const gitRoot = await findGitBoundary(candidate);
|
|
271
|
-
if (!gitRoot || gitRoot === candidate)
|
|
272
|
-
return false;
|
|
273
|
-
// Avoid constructing the matcher when the project has no positive ignore rules.
|
|
274
|
-
// The matcher remains authoritative (including anchored rules and directory form).
|
|
275
|
-
if (getProjectIgnoreGlobs(gitRoot).length === 0)
|
|
276
|
-
return false;
|
|
277
|
-
return isPathIgnoredByProject(candidate, gitRoot, true);
|
|
278
|
-
}
|
|
279
|
-
async function nearestNonExcludedFallbackRoot(candidate) {
|
|
280
|
-
if (!(await isExcludedLspRoot(candidate)))
|
|
281
|
-
return path.resolve(candidate);
|
|
282
|
-
let current = path.dirname(path.resolve(candidate));
|
|
283
|
-
const fsRoot = path.parse(current).root;
|
|
284
|
-
let nearestAllowed;
|
|
285
|
-
while (true) {
|
|
286
|
-
if (!(await isExcludedLspRoot(current))) {
|
|
287
|
-
nearestAllowed ??= current;
|
|
288
|
-
for (const marker of FALLBACK_PROJECT_MARKERS) {
|
|
289
|
-
if (await markerExists(current, marker))
|
|
290
|
-
return current;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
if (current === fsRoot)
|
|
294
|
-
break;
|
|
295
|
-
current = path.dirname(current);
|
|
296
|
-
}
|
|
297
|
-
// No project marker was available. Keep the file attached to a stable,
|
|
298
|
-
// non-excluded ancestor rather than minting a client inside the fixture/stage.
|
|
299
|
-
return nearestAllowed ?? fsRoot;
|
|
300
|
-
}
|
|
301
|
-
function isLspInstallDisabled() {
|
|
302
|
-
return process.env.PI_LENS_DISABLE_LSP_INSTALL === "1";
|
|
303
|
-
}
|
|
304
|
-
function canInstall(allowInstall) {
|
|
305
|
-
return allowInstall !== false && !isLspInstallDisabled();
|
|
306
|
-
}
|
|
307
|
-
const DIRECT_LSP_NEGATIVE_TTL_MS = Math.max(30_000, Number.parseInt(process.env.PI_LENS_DIRECT_LSP_NEGATIVE_TTL_MS ?? "600000", 10) || 600_000);
|
|
308
|
-
const directLspCommandUnavailableUntil = new Map();
|
|
309
|
-
const directLspCommandSkipLoggedUntil = new Map();
|
|
310
|
-
// Availability rows are emitted from the same async launch path that owns the
|
|
311
|
-
// live LSP generation. A session reset can retire that generation while a
|
|
312
|
-
// managed lookup, install, or launch is still awaiting; stale work must not
|
|
313
|
-
// publish into the replacement session (#2351, shape 22).
|
|
314
|
-
const lspLaunchAvailabilityGeneration = createGenerationSource("lsp-launch-availability");
|
|
315
|
-
export function resetLspLaunchAvailabilityGeneration() {
|
|
316
|
-
lspLaunchAvailabilityGeneration.bump();
|
|
317
|
-
}
|
|
318
|
-
function staleLaunch(generation, subject, proc) {
|
|
319
|
-
if (generation.isCurrent())
|
|
320
|
-
return false;
|
|
321
|
-
try {
|
|
322
|
-
proc?.process?.kill();
|
|
323
|
-
}
|
|
324
|
-
catch {
|
|
325
|
-
// Best-effort cleanup for a process returned after service retirement.
|
|
326
|
-
}
|
|
327
|
-
generation.guardedWrite(subject, () => undefined);
|
|
328
|
-
return true;
|
|
329
|
-
}
|
|
330
|
-
async function installEvidenceForLaunch(toolId, installed, attempt) {
|
|
331
|
-
const evidence = describeInstallAttempt(attempt);
|
|
332
|
-
const confirmedManagedPath = await findManagedToolBinary(toolId);
|
|
333
|
-
return {
|
|
334
|
-
...evidence,
|
|
335
|
-
binary: path.basename(installed),
|
|
336
|
-
...(confirmedManagedPath !== undefined &&
|
|
337
|
-
pathsEqual(confirmedManagedPath, installed) && {
|
|
338
|
-
source: "managed-dir",
|
|
339
|
-
}),
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
function captureInstallAttempt(toolId) {
|
|
343
|
-
try {
|
|
344
|
-
// Capture synchronously after ensureTool resolves. Reading this later, after
|
|
345
|
-
// launch/evidence awaits, can observe another concurrent ensure's outcome.
|
|
346
|
-
return installer.getInstallAttempt?.(toolId);
|
|
347
|
-
}
|
|
348
|
-
catch {
|
|
349
|
-
// Older test doubles do not expose this production export.
|
|
350
|
-
}
|
|
351
|
-
return undefined;
|
|
352
|
-
}
|
|
353
|
-
/** Re-arm direct-command availability for the next session. */
|
|
354
|
-
export function resetDirectLspCommandAvailability() {
|
|
355
|
-
directLspCommandUnavailableUntil.clear();
|
|
356
|
-
directLspCommandSkipLoggedUntil.clear();
|
|
357
|
-
}
|
|
358
|
-
/** Test seam for seeding the real negative-cache path. */
|
|
359
|
-
export function _markDirectLspCommandUnavailableForTests(command) {
|
|
360
|
-
markDirectLspCommandUnavailable(command);
|
|
361
|
-
}
|
|
362
|
-
function pruneExpiredDirectLspNegativeEntries(now = Date.now()) {
|
|
363
|
-
for (const [command, until] of directLspCommandUnavailableUntil) {
|
|
364
|
-
if (until <= now) {
|
|
365
|
-
directLspCommandUnavailableUntil.delete(command);
|
|
366
|
-
directLspCommandSkipLoggedUntil.delete(command);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
for (const [command, until] of directLspCommandSkipLoggedUntil) {
|
|
370
|
-
if (until <= now)
|
|
371
|
-
directLspCommandSkipLoggedUntil.delete(command);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
function isSimpleCommand(command) {
|
|
375
|
-
return (!isFullyQualified(command) &&
|
|
376
|
-
!command.includes("/") &&
|
|
377
|
-
!command.includes("\\"));
|
|
378
|
-
}
|
|
379
|
-
export function isDirectLspCommandTemporarilyUnavailable(command) {
|
|
380
|
-
const now = Date.now();
|
|
381
|
-
pruneExpiredDirectLspNegativeEntries(now);
|
|
382
|
-
const until = directLspCommandUnavailableUntil.get(command);
|
|
383
|
-
if (!until || until <= now) {
|
|
384
|
-
directLspCommandUnavailableUntil.delete(command);
|
|
385
|
-
return false;
|
|
386
|
-
}
|
|
387
|
-
const loggedUntil = directLspCommandSkipLoggedUntil.get(command) ?? 0;
|
|
388
|
-
if (loggedUntil <= now) {
|
|
389
|
-
logSessionStart(`lsp direct command ${command}: skipped by negative availability cache (${Math.max(0, until - now)}ms remaining)`);
|
|
390
|
-
directLspCommandSkipLoggedUntil.set(command, until);
|
|
391
|
-
}
|
|
392
|
-
return true;
|
|
393
|
-
}
|
|
394
|
-
function markDirectLspCommandUnavailable(command) {
|
|
395
|
-
if (!isSimpleCommand(command))
|
|
396
|
-
return;
|
|
397
|
-
directLspCommandUnavailableUntil.set(command, Date.now() + DIRECT_LSP_NEGATIVE_TTL_MS);
|
|
398
|
-
directLspCommandSkipLoggedUntil.delete(command);
|
|
399
|
-
}
|
|
400
|
-
const PI_LENS_BIN_DIR = path.join(getGlobalPiLensDir(), "bin");
|
|
401
|
-
export async function resolveAndLaunch(spec, allowInstall) {
|
|
402
|
-
const generation = lspLaunchAvailabilityGeneration.capture();
|
|
403
|
-
const toolLabel = spec.managedToolId ??
|
|
404
|
-
spec.candidates[spec.candidates.length - 1] ??
|
|
405
|
-
"unknown";
|
|
406
|
-
let lastRuntimeFailure;
|
|
407
|
-
const trackRuntimeFailure = (err) => {
|
|
408
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
409
|
-
if (!hasSpawnFailureKind(err, "tool-not-found")) {
|
|
410
|
-
lastRuntimeFailure = err instanceof Error ? err : new Error(message);
|
|
411
|
-
}
|
|
412
|
-
};
|
|
413
|
-
// #2140: consult the release-managed binary directory (~/.pi-lens/bin)
|
|
414
|
-
// BEFORE the bare-PATH candidates below. A bare `candidates: ["opengrep"]`
|
|
415
|
-
// entry only ever resolves through the OS's own PATH lookup — never that
|
|
416
|
-
// directory — so a github/maven/archive-strategy tool installed there with
|
|
417
|
-
// no PATH entry ENOENTs every direct candidate first, only to have the
|
|
418
|
-
// managed-install step further down find the very same binary a few
|
|
419
|
-
// hundred ms later. `findManagedToolBinary` does a bare `fs.access`, no
|
|
420
|
-
// spawn, so this costs at most a few stat calls per server launch attempt
|
|
421
|
-
// (not per file/dispatch — launches are session-scoped). Mirrors
|
|
422
|
-
// `findManagedNodeToolBinary`'s npm-managed fast path (runner-helpers.ts)
|
|
423
|
-
// and `SecurityScanClient.probeVersion`'s equivalent fix for the CLI-scan
|
|
424
|
-
// half of this same issue (#2140, landed in PR #2148/#2137).
|
|
425
|
-
const managedProbeStartedAt = Date.now();
|
|
426
|
-
const managedCandidate = spec.managedToolId
|
|
427
|
-
? await findManagedToolBinary(spec.managedToolId)
|
|
428
|
-
: undefined;
|
|
429
|
-
if (staleLaunch(generation, `${toolLabel}:findManagedToolBinary`)) {
|
|
430
|
-
return undefined;
|
|
431
|
-
}
|
|
432
|
-
const candidates = managedCandidate && !spec.candidates.includes(managedCandidate)
|
|
433
|
-
? [managedCandidate, ...spec.candidates]
|
|
434
|
-
: spec.candidates;
|
|
435
|
-
// A candidate that fails while a LATER candidate (or managed install)
|
|
436
|
-
// succeeds is just fallback, not a failure — logging each immediately floods
|
|
437
|
-
// the logs with scary "candidate failed / npm shim failed / Run npm install"
|
|
438
|
-
// lines that read as smells even though the launch succeeded. Collect them and
|
|
439
|
-
// surface only if ALL direct candidates fail.
|
|
440
|
-
const candidateFailures = [];
|
|
441
|
-
// Step 1 & 2 — try all explicit candidates (includes bare command = PATH lookup)
|
|
442
|
-
for (const [index, command] of candidates.entries()) {
|
|
443
|
-
logLatency({
|
|
444
|
-
type: "phase",
|
|
445
|
-
phase: "lsp_launch_candidate_attempt",
|
|
446
|
-
filePath: spec.cwd,
|
|
447
|
-
durationMs: 0,
|
|
448
|
-
metadata: {
|
|
449
|
-
tool: toolLabel,
|
|
450
|
-
command,
|
|
451
|
-
index,
|
|
452
|
-
totalCandidates: candidates.length,
|
|
453
|
-
allowInstall: canInstall(allowInstall),
|
|
454
|
-
},
|
|
455
|
-
});
|
|
456
|
-
logSessionStart(`lsp launch candidate attempt tool=${toolLabel} idx=${index}/${candidates.length - 1} command=${command} cwd=${spec.cwd}`);
|
|
457
|
-
try {
|
|
458
|
-
const proc = await launchLSP(command, spec.args, {
|
|
459
|
-
cwd: spec.cwd,
|
|
460
|
-
env: spec.env,
|
|
461
|
-
});
|
|
462
|
-
if (staleLaunch(generation, `${toolLabel}:launchLSP:${command}`, proc)) {
|
|
463
|
-
return undefined;
|
|
464
|
-
}
|
|
465
|
-
logLatency({
|
|
466
|
-
type: "phase",
|
|
467
|
-
phase: "lsp_launch_candidate_success",
|
|
468
|
-
filePath: spec.cwd,
|
|
469
|
-
durationMs: 0,
|
|
470
|
-
metadata: {
|
|
471
|
-
tool: toolLabel,
|
|
472
|
-
command,
|
|
473
|
-
index,
|
|
474
|
-
source: "direct",
|
|
475
|
-
},
|
|
476
|
-
});
|
|
477
|
-
logSessionStart(`lsp launch candidate success tool=${toolLabel} idx=${index} command=${command} source=direct`);
|
|
478
|
-
// The managed-dir fast path (#2140) IS the availability probe for a
|
|
479
|
-
// release-managed tool: a real spawn just confirmed the binary
|
|
480
|
-
// findManagedToolBinary resolved actually launches. Gated on the exact
|
|
481
|
-
// managed candidate (never a later bare-PATH fallback), so a session
|
|
482
|
-
// start with the binary present emits exactly one decision and a
|
|
483
|
-
// developer-PATH-resolved copy (no managed binary at all) emits none —
|
|
484
|
-
// unchanged from before this fix.
|
|
485
|
-
if (managedCandidate !== undefined && command === managedCandidate) {
|
|
486
|
-
logAvailabilityDecision({
|
|
487
|
-
tool: toolLabel,
|
|
488
|
-
verdict: "available",
|
|
489
|
-
outcome: "success",
|
|
490
|
-
cause: "ok",
|
|
491
|
-
elapsedMs: Date.now() - managedProbeStartedAt,
|
|
492
|
-
latched: false,
|
|
493
|
-
producer: "lsp-launch",
|
|
494
|
-
classifiedBy: "probe",
|
|
495
|
-
evidence: {
|
|
496
|
-
binary: path.basename(managedCandidate),
|
|
497
|
-
source: "managed-dir",
|
|
498
|
-
correctsLatchedRow: false,
|
|
499
|
-
},
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
return { process: proc, source: "direct" };
|
|
503
|
-
}
|
|
504
|
-
catch (err) {
|
|
505
|
-
if (staleLaunch(generation, `${toolLabel}:launchLSP:${command}`)) {
|
|
506
|
-
return undefined;
|
|
507
|
-
}
|
|
508
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
509
|
-
// Defer logging: only a failure if no later candidate/install succeeds.
|
|
510
|
-
candidateFailures.push({ index, command, message, err });
|
|
511
|
-
// try next
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
// All direct candidates failed (a successful one returns above). Surface the
|
|
515
|
-
// deferred failures now so the all-failed case stays fully diagnosable.
|
|
516
|
-
for (const failure of candidateFailures) {
|
|
517
|
-
logLatency({
|
|
518
|
-
type: "phase",
|
|
519
|
-
phase: "lsp_launch_candidate_failed",
|
|
520
|
-
filePath: spec.cwd,
|
|
521
|
-
durationMs: 0,
|
|
522
|
-
metadata: {
|
|
523
|
-
tool: toolLabel,
|
|
524
|
-
command: failure.command,
|
|
525
|
-
index: failure.index,
|
|
526
|
-
error: failure.message,
|
|
527
|
-
},
|
|
528
|
-
});
|
|
529
|
-
logSessionStart(`lsp launch candidate failed tool=${toolLabel} idx=${failure.index} command=${failure.command} error=${failure.message}`);
|
|
530
|
-
trackRuntimeFailure(failure.err);
|
|
531
|
-
}
|
|
532
|
-
const hasOnlyRepairableCandidateFailures = candidateFailures.every((failure) => hasSpawnFailureKind(failure.err, "tool-not-found"));
|
|
533
|
-
if (!hasOnlyRepairableCandidateFailures) {
|
|
534
|
-
if (lastRuntimeFailure)
|
|
535
|
-
throw lastRuntimeFailure;
|
|
536
|
-
return undefined;
|
|
537
|
-
}
|
|
538
|
-
if (!canInstall(allowInstall)) {
|
|
539
|
-
logSessionStart(`lsp launch install blocked tool=${toolLabel} cwd=${spec.cwd} allowInstall=${allowInstall !== false} globalDisabled=${isLspInstallDisabled()}`);
|
|
540
|
-
logLatency({
|
|
541
|
-
type: "phase",
|
|
542
|
-
phase: "lsp_launch_install_blocked",
|
|
543
|
-
filePath: spec.cwd,
|
|
544
|
-
durationMs: 0,
|
|
545
|
-
metadata: {
|
|
546
|
-
tool: toolLabel,
|
|
547
|
-
allowInstall,
|
|
548
|
-
globalInstallDisabled: isLspInstallDisabled(),
|
|
549
|
-
},
|
|
550
|
-
});
|
|
551
|
-
return undefined;
|
|
552
|
-
}
|
|
553
|
-
// Step 3 — managed install via installer registry
|
|
554
|
-
if (spec.managedToolId) {
|
|
555
|
-
// Neither the managed-dir stat (#2140) nor any bare-PATH candidate
|
|
556
|
-
// resolved this tool — the negative counterpart to the fast-path
|
|
557
|
-
// "available" decision above, mirroring `SecurityScanClient.probeVersion`'s
|
|
558
|
-
// failure record so the negative case is visible in latency.log rather
|
|
559
|
-
// than swallowed by going straight to the install attempt.
|
|
560
|
-
const failedCandidate = candidateFailures.at(-1);
|
|
561
|
-
const classifiedFailure = classifyProbeFailure({
|
|
562
|
-
error: failedCandidate?.err instanceof Error
|
|
563
|
-
? failedCandidate.err
|
|
564
|
-
: undefined,
|
|
565
|
-
spawnFailure: {
|
|
566
|
-
kind: hasSpawnFailureKind(failedCandidate?.err, "tool-not-found")
|
|
567
|
-
? "tool-not-found"
|
|
568
|
-
: undefined,
|
|
569
|
-
},
|
|
570
|
-
}, { command: failedCandidate?.command });
|
|
571
|
-
logAvailabilityDecision({
|
|
572
|
-
tool: spec.managedToolId,
|
|
573
|
-
verdict: "unavailable",
|
|
574
|
-
outcome: classifiedFailure.outcome,
|
|
575
|
-
cause: classifiedFailure.cause,
|
|
576
|
-
elapsedMs: Date.now() - managedProbeStartedAt,
|
|
577
|
-
latched: false,
|
|
578
|
-
classifiedBy: "probe",
|
|
579
|
-
producer: "lsp-launch",
|
|
580
|
-
evidence: classifiedFailure.evidence,
|
|
581
|
-
});
|
|
582
|
-
logSessionStart(`lsp launch ensure-tool start tool=${spec.managedToolId} cwd=${spec.cwd}`);
|
|
583
|
-
const installStartedAt = Date.now();
|
|
584
|
-
const installed = await ensureTool(spec.managedToolId);
|
|
585
|
-
const installAttempt = captureInstallAttempt(spec.managedToolId);
|
|
586
|
-
if (staleLaunch(generation, `${toolLabel}:ensureTool`)) {
|
|
587
|
-
return undefined;
|
|
588
|
-
}
|
|
589
|
-
logSessionStart(`lsp launch ensure-tool result tool=${spec.managedToolId} installed=${installed ? "yes" : "no"} path=${installed ?? ""}`);
|
|
590
|
-
logLatency({
|
|
591
|
-
type: "phase",
|
|
592
|
-
phase: "lsp_launch_ensure_tool_result",
|
|
593
|
-
filePath: spec.cwd,
|
|
594
|
-
durationMs: 0,
|
|
595
|
-
metadata: {
|
|
596
|
-
tool: spec.managedToolId,
|
|
597
|
-
installed: Boolean(installed),
|
|
598
|
-
path: installed,
|
|
599
|
-
},
|
|
600
|
-
});
|
|
601
|
-
if (installed) {
|
|
602
|
-
try {
|
|
603
|
-
const proc = await launchLSP(installed, spec.args, {
|
|
604
|
-
cwd: spec.cwd,
|
|
605
|
-
env: spec.env,
|
|
606
|
-
});
|
|
607
|
-
if (staleLaunch(generation, `${toolLabel}:launchLSP:managed`, proc)) {
|
|
608
|
-
return undefined;
|
|
609
|
-
}
|
|
610
|
-
logSessionStart(`lsp launch managed success tool=${spec.managedToolId} command=${installed} source=managed`);
|
|
611
|
-
logLatency({
|
|
612
|
-
type: "phase",
|
|
613
|
-
phase: "lsp_launch_managed_success",
|
|
614
|
-
filePath: spec.cwd,
|
|
615
|
-
durationMs: 0,
|
|
616
|
-
metadata: {
|
|
617
|
-
tool: spec.managedToolId,
|
|
618
|
-
command: installed,
|
|
619
|
-
},
|
|
620
|
-
});
|
|
621
|
-
// Compensating row for the "unavailable" decision logged above (#1606
|
|
622
|
-
// shape): the install fixed exactly what the fast path found missing,
|
|
623
|
-
// so the durable record must not be left saying the tool is off.
|
|
624
|
-
// `source` is only asserted when a fresh managed-dir lookup confirms
|
|
625
|
-
// the install actually landed in ~/.pi-lens/bin (github/maven/archive
|
|
626
|
-
// strategies) rather than an npm/pip/gem install elsewhere, so the
|
|
627
|
-
// evidence never claims a resolution this call didn't derive.
|
|
628
|
-
const evidence = await installEvidenceForLaunch(spec.managedToolId, installed, installAttempt);
|
|
629
|
-
if (staleLaunch(generation, `${toolLabel}:managed-evidence`, proc)) {
|
|
630
|
-
return undefined;
|
|
631
|
-
}
|
|
632
|
-
logAvailabilityDecision({
|
|
633
|
-
tool: spec.managedToolId,
|
|
634
|
-
verdict: "available",
|
|
635
|
-
outcome: "success",
|
|
636
|
-
cause: "ok",
|
|
637
|
-
elapsedMs: Date.now() - installStartedAt,
|
|
638
|
-
latched: false,
|
|
639
|
-
classifiedBy: "caller",
|
|
640
|
-
producer: "lsp-launch",
|
|
641
|
-
evidence: {
|
|
642
|
-
...evidence,
|
|
643
|
-
correctsLatchedRow: false,
|
|
644
|
-
},
|
|
645
|
-
});
|
|
646
|
-
return { process: proc, source: "managed" };
|
|
647
|
-
}
|
|
648
|
-
catch (err) {
|
|
649
|
-
if (staleLaunch(generation, `${toolLabel}:launchLSP:managed-rejection`)) {
|
|
650
|
-
return undefined;
|
|
651
|
-
}
|
|
652
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
653
|
-
logSessionStart(`lsp launch managed failed tool=${spec.managedToolId} command=${installed} error=${message}`);
|
|
654
|
-
logLatency({
|
|
655
|
-
type: "phase",
|
|
656
|
-
phase: "lsp_launch_managed_failed",
|
|
657
|
-
filePath: spec.cwd,
|
|
658
|
-
durationMs: 0,
|
|
659
|
-
metadata: {
|
|
660
|
-
tool: spec.managedToolId,
|
|
661
|
-
command: installed,
|
|
662
|
-
error: message,
|
|
663
|
-
},
|
|
664
|
-
});
|
|
665
|
-
trackRuntimeFailure(err);
|
|
666
|
-
// force-reinstall: when a PATH-resolved tool (bare command name)
|
|
667
|
-
// fails to launch (e.g. broken symlink, missing .dll), nuke the
|
|
668
|
-
// caches and download a managed copy from the registry.
|
|
669
|
-
const looksPathResolved = !installed.includes("/") && !installed.includes("\\");
|
|
670
|
-
if (looksPathResolved && hasSpawnFailureKind(err, "tool-not-found")) {
|
|
671
|
-
logSessionStart(`lsp launch managed retry force-reinstall tool=${spec.managedToolId}`);
|
|
672
|
-
const reinstalled = await ensureTool(spec.managedToolId, {
|
|
673
|
-
forceReinstall: true,
|
|
674
|
-
});
|
|
675
|
-
const reinstallAttempt = captureInstallAttempt(spec.managedToolId);
|
|
676
|
-
if (staleLaunch(generation, `${toolLabel}:forceReinstall`)) {
|
|
677
|
-
return undefined;
|
|
678
|
-
}
|
|
679
|
-
if (reinstalled) {
|
|
680
|
-
try {
|
|
681
|
-
const proc = await launchLSP(reinstalled, spec.args, {
|
|
682
|
-
cwd: spec.cwd,
|
|
683
|
-
env: spec.env,
|
|
684
|
-
});
|
|
685
|
-
if (staleLaunch(generation, `${toolLabel}:launchLSP:forceReinstall`, proc)) {
|
|
686
|
-
return undefined;
|
|
687
|
-
}
|
|
688
|
-
logSessionStart(`lsp launch managed force-reinstall success tool=${spec.managedToolId} command=${reinstalled}`);
|
|
689
|
-
logLatency({
|
|
690
|
-
type: "phase",
|
|
691
|
-
phase: "lsp_launch_managed_force_reinstall_success",
|
|
692
|
-
filePath: spec.cwd,
|
|
693
|
-
durationMs: 0,
|
|
694
|
-
metadata: {
|
|
695
|
-
tool: spec.managedToolId,
|
|
696
|
-
command: reinstalled,
|
|
697
|
-
},
|
|
698
|
-
});
|
|
699
|
-
const evidence = await installEvidenceForLaunch(spec.managedToolId, reinstalled, reinstallAttempt);
|
|
700
|
-
if (staleLaunch(generation, `${toolLabel}:forceReinstall-evidence`, proc)) {
|
|
701
|
-
return undefined;
|
|
702
|
-
}
|
|
703
|
-
logAvailabilityDecision({
|
|
704
|
-
tool: spec.managedToolId,
|
|
705
|
-
verdict: "available",
|
|
706
|
-
outcome: "success",
|
|
707
|
-
cause: "ok",
|
|
708
|
-
elapsedMs: Date.now() - installStartedAt,
|
|
709
|
-
latched: false,
|
|
710
|
-
classifiedBy: "caller",
|
|
711
|
-
producer: "lsp-launch",
|
|
712
|
-
evidence: {
|
|
713
|
-
...evidence,
|
|
714
|
-
correctsLatchedRow: false,
|
|
715
|
-
},
|
|
716
|
-
});
|
|
717
|
-
return { process: proc, source: "managed" };
|
|
718
|
-
}
|
|
719
|
-
catch (retryErr) {
|
|
720
|
-
logSessionStart(`lsp launch managed force-reinstall failed tool=${spec.managedToolId} error=${retryErr instanceof Error ? retryErr.message : String(retryErr)}`);
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
// fall through
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
// Step 4 — language-native runtime install (go install, gem install, …)
|
|
729
|
-
const runtimeInstall = spec.runtimeInstall;
|
|
730
|
-
const runtimeAvailable = runtimeInstall
|
|
731
|
-
? await isOnPath(runtimeInstall.runtimeCommand)
|
|
732
|
-
: false;
|
|
733
|
-
if (staleLaunch(generation, `${toolLabel}:runtimeAvailability`)) {
|
|
734
|
-
return undefined;
|
|
735
|
-
}
|
|
736
|
-
if (runtimeInstall && runtimeAvailable) {
|
|
737
|
-
const ok = await runtimeInstall.install();
|
|
738
|
-
if (staleLaunch(generation, `${toolLabel}:runtimeInstall`)) {
|
|
739
|
-
return undefined;
|
|
740
|
-
}
|
|
741
|
-
if (ok) {
|
|
742
|
-
const retry = runtimeInstall.retryCandidates ?? spec.candidates;
|
|
743
|
-
for (const command of retry) {
|
|
744
|
-
try {
|
|
745
|
-
const proc = await launchLSP(command, spec.args, {
|
|
746
|
-
cwd: spec.cwd,
|
|
747
|
-
env: spec.env,
|
|
748
|
-
});
|
|
749
|
-
if (staleLaunch(generation, `${toolLabel}:launchLSP:runtime`, proc)) {
|
|
750
|
-
return undefined;
|
|
751
|
-
}
|
|
752
|
-
return { process: proc, source: "managed" };
|
|
753
|
-
}
|
|
754
|
-
catch (err) {
|
|
755
|
-
if (staleLaunch(generation, `${toolLabel}:launchLSP:runtime`)) {
|
|
756
|
-
return undefined;
|
|
757
|
-
}
|
|
758
|
-
trackRuntimeFailure(err);
|
|
759
|
-
// try next
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
if (lastRuntimeFailure) {
|
|
765
|
-
throw lastRuntimeFailure;
|
|
766
|
-
}
|
|
767
|
-
return undefined;
|
|
768
|
-
}
|
|
769
|
-
/**
|
|
770
|
-
* Launch a language server that ships as a multi-folder MODULE BUNDLE driven by a
|
|
771
|
-
* separate runtime (e.g. PowerShell Editor Services via `pwsh ...
|
|
772
|
-
* Start-EditorServices.ps1 -Stdio`), rather than a single executable on PATH.
|
|
773
|
-
*
|
|
774
|
-
* Resolution order: (1) a runtime interpreter must be on PATH — else GRACEFUL
|
|
775
|
-
* SKIP (returns undefined → the runner's coverage notice, never a hard fail);
|
|
776
|
-
* (2) the bundle must be installed (already-extracted, or installed now when
|
|
777
|
-
* `allowInstall`) — else graceful skip; (3) launch the runtime against the
|
|
778
|
-
* bundle over stdio. A launch failure is logged and also degrades to a skip.
|
|
779
|
-
*/
|
|
780
|
-
async function resolveAndLaunchBundle(spec, allowInstall) {
|
|
781
|
-
// 1. Resolve the runtime interpreter on PATH (don't spawn it bare — that would
|
|
782
|
-
// hang; just probe). No runtime → graceful skip (coverage notice).
|
|
783
|
-
let runtime;
|
|
784
|
-
for (const candidate of spec.runtimeCandidates) {
|
|
785
|
-
if (await isOnPath(candidate)) {
|
|
786
|
-
runtime = candidate;
|
|
787
|
-
break;
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
if (!runtime) {
|
|
791
|
-
logSessionStart(`lsp launch bundle skip tool=${spec.bundleToolId}: no runtime on PATH (tried ${spec.runtimeCandidates.join(", ")})`);
|
|
792
|
-
return undefined;
|
|
793
|
-
}
|
|
794
|
-
// 2. Resolve the bundle directory: already installed, else install when allowed.
|
|
795
|
-
let bundleDir = await getToolPath(spec.bundleToolId);
|
|
796
|
-
if (!bundleDir && canInstall(allowInstall)) {
|
|
797
|
-
bundleDir = await ensureTool(spec.bundleToolId);
|
|
798
|
-
}
|
|
799
|
-
if (!bundleDir) {
|
|
800
|
-
logSessionStart(`lsp launch bundle skip tool=${spec.bundleToolId}: bundle not installed (allowInstall=${allowInstall !== false})`);
|
|
801
|
-
return undefined;
|
|
802
|
-
}
|
|
803
|
-
// 3. Launch the runtime against the bundle over stdio.
|
|
804
|
-
try {
|
|
805
|
-
const proc = await launchLSP(runtime, spec.args(bundleDir), {
|
|
806
|
-
cwd: spec.cwd,
|
|
807
|
-
env: spec.env,
|
|
808
|
-
});
|
|
809
|
-
logSessionStart(`lsp launch bundle success tool=${spec.bundleToolId} runtime=${runtime} bundle=${bundleDir}`);
|
|
810
|
-
return { process: proc, source: "managed" };
|
|
811
|
-
}
|
|
812
|
-
catch (err) {
|
|
813
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
814
|
-
logSessionStart(`lsp launch bundle failed tool=${spec.bundleToolId} runtime=${runtime} error=${message}`);
|
|
815
|
-
return undefined;
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
/**
|
|
819
|
-
* Launch a language server that ships as a self-contained native TREE BUNDLE with
|
|
820
|
-
* its executable INSIDE the extracted tree (e.g. clangd: `<bundle>/bin/clangd`
|
|
821
|
-
* plus the bundled libclang headers under `lib/`), as opposed to a single binary
|
|
822
|
-
* on PATH or a runtime-driven module bundle (see {@link resolveAndLaunchBundle}).
|
|
823
|
-
*
|
|
824
|
-
* Resolution order: (1) PATH candidates first — a system install wins; (2) the
|
|
825
|
-
* managed bundle (already-extracted, or installed now when `allowInstall`), then
|
|
826
|
-
* launch the bin within it. No external runtime. Anything missing → GRACEFUL SKIP
|
|
827
|
-
* (returns undefined → the runner's coverage notice, never a hard fail).
|
|
828
|
-
*/
|
|
829
|
-
async function resolveAndLaunchTreeBinary(spec, allowInstall) {
|
|
830
|
-
// 1. PATH-first — a system install wins (user-managed, no 150MB download).
|
|
831
|
-
for (const command of spec.candidates) {
|
|
832
|
-
try {
|
|
833
|
-
const proc = await launchLSP(command, spec.args, {
|
|
834
|
-
cwd: spec.cwd,
|
|
835
|
-
env: spec.env,
|
|
836
|
-
});
|
|
837
|
-
return { process: proc, source: "direct" };
|
|
838
|
-
}
|
|
839
|
-
catch {
|
|
840
|
-
// not on PATH (or broken) — fall through to the managed bundle
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
// 2. Managed tree bundle: already-extracted, else install when allowed.
|
|
844
|
-
let bundleDir = await getToolPath(spec.bundleToolId);
|
|
845
|
-
if (!bundleDir && canInstall(allowInstall)) {
|
|
846
|
-
bundleDir = await ensureTool(spec.bundleToolId);
|
|
847
|
-
}
|
|
848
|
-
if (!bundleDir) {
|
|
849
|
-
logSessionStart(`lsp launch tree-bin skip tool=${spec.bundleToolId}: not on PATH and bundle not installed (allowInstall=${allowInstall !== false})`);
|
|
850
|
-
return undefined;
|
|
851
|
-
}
|
|
852
|
-
const suffix = process.platform === "win32" ? ".exe" : "";
|
|
853
|
-
const binPath = path.join(bundleDir, ...spec.binRelPath.split("/")) + suffix;
|
|
854
|
-
try {
|
|
855
|
-
const proc = await launchLSP(binPath, spec.args, {
|
|
856
|
-
cwd: spec.cwd,
|
|
857
|
-
env: spec.env,
|
|
858
|
-
});
|
|
859
|
-
logSessionStart(`lsp launch tree-bin success tool=${spec.bundleToolId} bin=${binPath}`);
|
|
860
|
-
return { process: proc, source: "managed" };
|
|
861
|
-
}
|
|
862
|
-
catch (err) {
|
|
863
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
864
|
-
logSessionStart(`lsp launch tree-bin failed tool=${spec.bundleToolId} bin=${binPath} error=${message}`);
|
|
865
|
-
return undefined;
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
function nodeBinLocalCandidates(root, baseName) {
|
|
869
|
-
const localBase = path.join(root, "node_modules", ".bin", baseName);
|
|
870
|
-
if (process.platform === "win32") {
|
|
871
|
-
return [`${localBase}.cmd`, `${localBase}.exe`];
|
|
872
|
-
}
|
|
873
|
-
return [localBase];
|
|
874
|
-
}
|
|
875
|
-
function nodeBinCandidates(root, baseName) {
|
|
876
|
-
return [...nodeBinLocalCandidates(root, baseName), baseName];
|
|
877
|
-
}
|
|
878
|
-
function piAgentExtensionsRootKey(file) {
|
|
879
|
-
// #1193 P3: `normalizeEphemeralMapKey` IS this module's former
|
|
880
|
-
// `normalizeSlashKey` — slash-fold, then lowercase only on win32 — and these
|
|
881
|
-
// directory keys are exactly what it is scoped for: process-local, same-run,
|
|
882
|
-
// derived from this process's own `path.resolve`, never persisted and never
|
|
883
|
-
// compared against an externally supplied spelling.
|
|
884
|
-
const dirKey = normalizeEphemeralMapKey(path.dirname(path.resolve(file)));
|
|
885
|
-
const marker = "/.pi/agent/extensions";
|
|
886
|
-
const index = dirKey.indexOf(marker);
|
|
887
|
-
if (index === -1)
|
|
888
|
-
return undefined;
|
|
889
|
-
return dirKey.slice(0, index + marker.length);
|
|
890
|
-
}
|
|
891
|
-
function IgnoreHomeRoot(primary) {
|
|
892
|
-
// #1193 P3: the former `normalizeRootKey` folded case on win32 but left the
|
|
893
|
-
// separator alone; both sides of the comparison below go through the same
|
|
894
|
-
// derivation, so adding the seam's slash fold cannot change the verdict for
|
|
895
|
-
// any spelling the old copy already agreed on, and it makes a mixed-separator
|
|
896
|
-
// root agree with the home directory it names.
|
|
897
|
-
const homeKey = normalizeEphemeralMapKey(path.resolve(os.homedir()));
|
|
898
|
-
return withRootMarkers(async (file) => {
|
|
899
|
-
const root = await primary(file);
|
|
900
|
-
if (!root)
|
|
901
|
-
return undefined;
|
|
902
|
-
return normalizeEphemeralMapKey(path.resolve(root)) === homeKey
|
|
903
|
-
? undefined
|
|
904
|
-
: root;
|
|
905
|
-
}, primary.rootMarkers ?? []);
|
|
906
|
-
}
|
|
907
|
-
function rubyBinCandidates(baseName) {
|
|
908
|
-
const candidates = [];
|
|
909
|
-
const home = os.homedir();
|
|
910
|
-
const isWin = process.platform === "win32";
|
|
911
|
-
const ext = isWin ? ".bat" : "";
|
|
912
|
-
// mise and asdf version managers — same layout on all platforms
|
|
913
|
-
candidates.push(path.join(home, ".local", "share", "mise", "installs", "ruby", "bin", `${baseName}${ext}`));
|
|
914
|
-
candidates.push(path.join(home, ".asdf", "installs", "ruby", "bin", `${baseName}${ext}`));
|
|
915
|
-
if (isWin) {
|
|
916
|
-
// Ruby installer drops versioned dirs on C: by convention, but the drive
|
|
917
|
-
// and version suffix vary — scan what's actually present instead of
|
|
918
|
-
// hardcoding. Memoized once per process (#1137): a synchronous drive-root
|
|
919
|
-
// enumeration per LSP spawn was an event-loop offender.
|
|
920
|
-
const driveRoot = path.parse(home).root; // e.g. "C:\"
|
|
921
|
-
for (const entry of getRubyVersionDirNamesSync(driveRoot)) {
|
|
922
|
-
candidates.push(path.join(driveRoot, entry, "bin", `${baseName}.bat`));
|
|
923
|
-
candidates.push(path.join(driveRoot, entry, "bin", baseName));
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
return candidates;
|
|
927
|
-
}
|
|
928
|
-
function createInteractiveServer(spec) {
|
|
929
|
-
return {
|
|
930
|
-
id: spec.id,
|
|
931
|
-
name: spec.name,
|
|
932
|
-
extensions: spec.extensions,
|
|
933
|
-
root: spec.root,
|
|
934
|
-
rootMarkers: spec.root.rootMarkers,
|
|
935
|
-
fallbackFor: spec.fallbackFor,
|
|
936
|
-
availabilityKey: typeof spec.command === "string" && isSimpleCommand(spec.command)
|
|
937
|
-
? spec.command
|
|
938
|
-
: undefined,
|
|
939
|
-
async spawn(root) {
|
|
940
|
-
const command = typeof spec.command === "function" ? spec.command(root) : spec.command;
|
|
941
|
-
const args = typeof spec.args === "function" ? spec.args(root) : spec.args || [];
|
|
942
|
-
// Try to launch directly — no auto-install for language-runtime tools
|
|
943
|
-
// (C#, Java, Swift, etc. require their SDK; cannot npm/pip install them)
|
|
944
|
-
if (isSimpleCommand(command) &&
|
|
945
|
-
isDirectLspCommandTemporarilyUnavailable(command)) {
|
|
946
|
-
return undefined;
|
|
947
|
-
}
|
|
948
|
-
// #241: the server binary is run by a language runtime (jdtls → java).
|
|
949
|
-
// When that runtime isn't on PATH, inject a discovered install's env so
|
|
950
|
-
// it launches instead of silently failing with no_clients.
|
|
951
|
-
const runtimeEnv = spec.runtime === "java" ? await resolveJavaRuntimeEnv() : undefined;
|
|
952
|
-
try {
|
|
953
|
-
const proc = await launchLSP(command, args, {
|
|
954
|
-
cwd: root,
|
|
955
|
-
...(runtimeEnv ? { env: runtimeEnv } : {}),
|
|
956
|
-
});
|
|
957
|
-
const initialization = typeof spec.initialization === "function"
|
|
958
|
-
? spec.initialization(root)
|
|
959
|
-
: spec.initialization;
|
|
960
|
-
return { process: proc, source: "direct", initialization };
|
|
961
|
-
}
|
|
962
|
-
catch (err) {
|
|
963
|
-
if (hasSpawnFailureKind(err, "tool-not-found")) {
|
|
964
|
-
markDirectLspCommandUnavailable(command);
|
|
965
|
-
}
|
|
966
|
-
return undefined;
|
|
967
|
-
}
|
|
968
|
-
},
|
|
969
|
-
};
|
|
970
|
-
}
|
|
971
|
-
export function PriorityRoot(markerGroups, excludePatterns, stopDir) {
|
|
972
|
-
const resolvers = markerGroups.map((markers) => NearestRoot(markers, excludePatterns, stopDir));
|
|
973
|
-
return withRootMarkers(async (file) => {
|
|
974
|
-
for (const resolve of resolvers) {
|
|
975
|
-
const root = await resolve(file);
|
|
976
|
-
if (root)
|
|
977
|
-
return root;
|
|
978
|
-
}
|
|
979
|
-
return undefined;
|
|
980
|
-
}, markerGroups.flat());
|
|
981
|
-
}
|
|
982
|
-
export const FileDirRoot = async (file) => {
|
|
983
|
-
const candidate = path.resolve(path.dirname(file));
|
|
984
|
-
return nearestNonExcludedFallbackRoot(candidate);
|
|
985
|
-
};
|
|
986
|
-
export function RootWithFallback(primary, fallback = FileDirRoot) {
|
|
987
|
-
return withRootMarkers(async (file) => {
|
|
988
|
-
const primaryRoot = await primary(file);
|
|
989
|
-
if (primaryRoot)
|
|
990
|
-
return primaryRoot;
|
|
991
|
-
return fallback(file);
|
|
992
|
-
}, primary.rootMarkers ?? []);
|
|
993
|
-
}
|
|
994
|
-
export function WorkspacePriorityRoot(markerGroups, excludePatterns) {
|
|
995
|
-
return withRootMarkers(async (file) => PriorityRoot(markerGroups, excludePatterns, process.cwd())(file), markerGroups.flat());
|
|
996
|
-
}
|
|
997
|
-
function isPermissionFsError(err) {
|
|
998
|
-
const code = err?.code;
|
|
999
|
-
return code === "EACCES" || code === "EPERM";
|
|
1000
|
-
}
|
|
1001
|
-
/**
|
|
1002
|
-
* The directory `markerExists(dir, pattern)` reads to answer for `pattern`:
|
|
1003
|
-
* `dir` itself for a plain basename, and the named subdirectory for a pattern
|
|
1004
|
-
* that carries a path segment (`"prisma/schema.prisma"`). `NearestRoot`'s
|
|
1005
|
-
* freshness records are keyed on these directories, so the derivation lives
|
|
1006
|
-
* HERE, next to the probe it describes — a second copy could drift and silently
|
|
1007
|
-
* stop covering a pattern shape (#3412).
|
|
1008
|
-
*/
|
|
1009
|
-
function markerProbeDir(dir, pattern) {
|
|
1010
|
-
const normalized = toPosix(pattern);
|
|
1011
|
-
const slash = normalized.lastIndexOf("/");
|
|
1012
|
-
const parentPattern = slash >= 0 ? normalized.slice(0, slash) : "";
|
|
1013
|
-
return parentPattern
|
|
1014
|
-
? path.join(dir, ...parentPattern.split("/").filter(Boolean))
|
|
1015
|
-
: dir;
|
|
1016
|
-
}
|
|
1017
|
-
async function markerExists(dir, pattern) {
|
|
1018
|
-
if (!pattern.includes("*")) {
|
|
1019
|
-
try {
|
|
1020
|
-
await stat(path.join(dir, pattern));
|
|
1021
|
-
return true;
|
|
1022
|
-
}
|
|
1023
|
-
catch (err) {
|
|
1024
|
-
if (isPermissionFsError(err)) {
|
|
1025
|
-
logSessionStart(`lsp root marker skipped: permission error stat ${path.join(dir, pattern)}`);
|
|
1026
|
-
}
|
|
1027
|
-
return false;
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
const normalized = pattern.replace(/\\/g, "/");
|
|
1031
|
-
const slash = normalized.lastIndexOf("/");
|
|
1032
|
-
const basenamePattern = slash >= 0 ? normalized.slice(slash + 1) : normalized;
|
|
1033
|
-
if (!basenamePattern)
|
|
1034
|
-
return false;
|
|
1035
|
-
const targetDir = markerProbeDir(dir, normalized);
|
|
1036
|
-
try {
|
|
1037
|
-
const entries = await readdir(targetDir, { withFileTypes: true });
|
|
1038
|
-
// Match files/symlinks only — a directory named like the marker (e.g. a
|
|
1039
|
-
// `Foo.csproj/` dir) is not a project file. Case-insensitive on win32 to
|
|
1040
|
-
// match the filesystem (and the project ignore matcher), via the shared
|
|
1041
|
-
// marker-glob helper.
|
|
1042
|
-
return direntsHaveMarkerGlobMatch(entries, basenamePattern);
|
|
1043
|
-
}
|
|
1044
|
-
catch (err) {
|
|
1045
|
-
if (isPermissionFsError(err)) {
|
|
1046
|
-
logSessionStart(`lsp root marker skipped: permission error read ${targetDir}`);
|
|
1047
|
-
}
|
|
1048
|
-
return false;
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
/**
|
|
1052
|
-
* One bound for every directory-mtime read this detector awaits. `signal` is
|
|
1053
|
-
* `undefined` on purpose: `RootFunction` takes a file path and nothing else, so
|
|
1054
|
-
* no hook signal reaches this seam until #2523 AC4 threads one — the wall clock
|
|
1055
|
-
* is the live half, and the registry entry says so. The budget is the edit
|
|
1056
|
-
* `tool_result` one because that is the hook the per-file touch path runs under
|
|
1057
|
-
* and the only hook the contract lets block the host; a tighter number would
|
|
1058
|
-
* abandon a revalidation that is strictly CHEAPER than the walk it falls back
|
|
1059
|
-
* to, making the hook slower rather than safer.
|
|
1060
|
-
*/
|
|
1061
|
-
const rootMtimeBound = (label) => ({
|
|
1062
|
-
ms: HOOK_WALL_BUDGET_MS.tool_result_edit,
|
|
1063
|
-
signal: undefined,
|
|
1064
|
-
hook: "tool_result_edit",
|
|
1065
|
-
label,
|
|
1066
|
-
});
|
|
1067
|
-
/**
|
|
1068
|
-
* Walk up the directory tree looking for project root markers.
|
|
1069
|
-
*
|
|
1070
|
-
* NearestRoot(includePatterns, excludePatterns?) → RootFunction
|
|
1071
|
-
*
|
|
1072
|
-
* - includePatterns: file/dir names that signal the project root (e.g. ["package.json"])
|
|
1073
|
-
* - excludePatterns: if any of these exist in a directory, SKIP that directory and
|
|
1074
|
-
* keep walking up toward stopDir — it does not abort resolution. A directory that
|
|
1075
|
-
* matches an exclude pattern is simply never returned as a root; the walk still
|
|
1076
|
-
* continues past it looking for an include-pattern hit higher up (#1671).
|
|
1077
|
-
* - stopDir: walk stops here (defaults to filesystem root; set to project cwd for safety)
|
|
1078
|
-
*
|
|
1079
|
-
* Equivalent to createRootDetector; exported under both names for clarity.
|
|
1080
|
-
*/
|
|
1081
|
-
export function NearestRoot(includePatterns, excludePatterns, stopDir) {
|
|
1082
|
-
// Per-instance caches — each NearestRoot(markers) call gets its own Map so
|
|
1083
|
-
// different servers (e.g. TypeScript vs Go) with different marker sets never
|
|
1084
|
-
// share entries. vi.resetModules() in tests resets module state between cases.
|
|
1085
|
-
const cache = new Map();
|
|
1086
|
-
// Only cache successful hits. Undefined results are NOT cached so that a
|
|
1087
|
-
// newly-created root marker (e.g. package.json or tsconfig.json scaffolded
|
|
1088
|
-
// mid-session by the agent) is detected on the next call — the absent →
|
|
1089
|
-
// present transition must work without a process restart. The uncached
|
|
1090
|
-
// re-walk cost for configless repos is a known trade-off; bounding the
|
|
1091
|
-
// walk with stopDir for in-cwd files is the tracked optimization (#1412).
|
|
1092
|
-
//
|
|
1093
|
-
// A HIT is revalidated instead of trusted for the session: it carries the
|
|
1094
|
-
// mtime of every directory the walk probed, and is served only while all of
|
|
1095
|
-
// them are unchanged. Without that, a nearer marker scaffolded below an
|
|
1096
|
-
// already-resolved root (`swift package init` in a subdirectory) — and a
|
|
1097
|
-
// marker REMOVED at the resolved root — stayed invisible for the whole
|
|
1098
|
-
// session, the other half of the very transition the paragraph above
|
|
1099
|
-
// handles (#3412; same shape as #2922 at the dispatch marker seam).
|
|
1100
|
-
const inFlight = new Map();
|
|
1101
|
-
// The patterns whose probe reads a subdirectory of the walked directory
|
|
1102
|
-
// instead of the walked directory itself: a pattern carrying a path segment
|
|
1103
|
-
// (`prisma/schema.prisma`) is answered from a subdirectory, so creating that
|
|
1104
|
-
// marker bumps the SUBDIRECTORY's mtime and the walked directory's own only
|
|
1105
|
-
// when the subdirectory had to be created too. Derived once from this
|
|
1106
|
-
// detector's pattern table, so a new pattern shape is covered on arrival
|
|
1107
|
-
// without a second list to maintain.
|
|
1108
|
-
const subdirPatterns = [];
|
|
1109
|
-
for (const pattern of [...includePatterns, ...(excludePatterns ?? [])]) {
|
|
1110
|
-
if (markerProbeDir("", pattern))
|
|
1111
|
-
subdirPatterns.push(pattern);
|
|
1112
|
-
}
|
|
1113
|
-
return withRootMarkers(async (file) => {
|
|
1114
|
-
// Cache key is the resolved directory — all files in the same dir share a root.
|
|
1115
|
-
const startDir = path.resolve(path.dirname(file));
|
|
1116
|
-
const dirKey = normalizeMapKey(startDir);
|
|
1117
|
-
// Fast path: already resolved for this directory, nothing the walk looked at
|
|
1118
|
-
// has changed since, and the entry is younger than the shared re-check
|
|
1119
|
-
// cadence. One stat per probed directory replaces the whole walk (markers x
|
|
1120
|
-
// directories, plus the hit directory's `isExcludedLspRoot` git-boundary
|
|
1121
|
-
// walk), so the memo still pays for itself; the cadence is what bounds the
|
|
1122
|
-
// axes a directory mtime cannot see — a same-tick create, and a `.gitignore`
|
|
1123
|
-
// above the hit changing the exclusion verdict — to one window instead of
|
|
1124
|
-
// the session (#3412 review round 1). A fired bound resolves `undefined`,
|
|
1125
|
-
// which is NOT freshness: it falls through to the walk.
|
|
1126
|
-
const cached = cache.get(dirKey);
|
|
1127
|
-
if (cached !== undefined &&
|
|
1128
|
-
Date.now() - cached.walkedAt < FRESHNESS_CADENCE_MS &&
|
|
1129
|
-
(await bounded(dirMtimesStillFreshAsync(cached.probedDirs), rootMtimeBound("lsp-root-memo-freshness"))) === true)
|
|
1130
|
-
return cached.root;
|
|
1131
|
-
// In-flight deduplication: if N parallel pipelines edit files in the same
|
|
1132
|
-
// directory simultaneously, only one stat-walk runs; the rest await the same
|
|
1133
|
-
// promise. This is the main fix for parallel-turn LSP timeout spikes.
|
|
1134
|
-
const flying = inFlight.get(dirKey);
|
|
1135
|
-
if (flying)
|
|
1136
|
-
return flying;
|
|
1137
|
-
const probedDirs = [];
|
|
1138
|
-
// Stamped before the walk so the window also covers the walk's own duration
|
|
1139
|
-
// — the conservative direction, by the walk's runtime (~0.2 ms measured).
|
|
1140
|
-
const walkedAt = Date.now();
|
|
1141
|
-
const promise = (async () => {
|
|
1142
|
-
let currentDir = startDir;
|
|
1143
|
-
const fsRoot = path.parse(currentDir).root;
|
|
1144
|
-
const stop = stopDir ? path.resolve(stopDir) : fsRoot;
|
|
1145
|
-
while (true) {
|
|
1146
|
-
if (stop !== fsRoot &&
|
|
1147
|
-
currentDir.startsWith(stop + path.sep) === false &&
|
|
1148
|
-
currentDir !== stop) {
|
|
1149
|
-
break;
|
|
1150
|
-
}
|
|
1151
|
-
// Record every directory this step is about to read, BEFORE reading
|
|
1152
|
-
// it. Ordering is the invariant: a marker created between the stat
|
|
1153
|
-
// and the probe leaves the record STALE (the next call re-walks and
|
|
1154
|
-
// finds it), whereas recording afterwards would store the mtime that
|
|
1155
|
-
// already includes the creation and hide it for the session. Only
|
|
1156
|
-
// directories up to the hit are recorded — a marker created above a
|
|
1157
|
-
// hit cannot change which root is nearest.
|
|
1158
|
-
const stepDirs = [
|
|
1159
|
-
currentDir,
|
|
1160
|
-
...subdirPatterns.map((pattern) => markerProbeDir(currentDir, pattern)),
|
|
1161
|
-
];
|
|
1162
|
-
const stepRecords = await bounded(dirMtimeRecordsAsync(stepDirs), rootMtimeBound("lsp-root-walk-dir-mtimes"));
|
|
1163
|
-
probedDirs.push(...(stepRecords ?? unknownDirMtimeRecords(stepDirs)));
|
|
1164
|
-
// Check exclude patterns — skip this dir (but keep walking up)
|
|
1165
|
-
if (excludePatterns) {
|
|
1166
|
-
let excluded = false;
|
|
1167
|
-
for (const pattern of excludePatterns) {
|
|
1168
|
-
if (await markerExists(currentDir, pattern)) {
|
|
1169
|
-
excluded = true;
|
|
1170
|
-
break;
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
if (excluded) {
|
|
1174
|
-
currentDir = path.dirname(currentDir);
|
|
1175
|
-
continue;
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
// Check include patterns. Exact marker names stay cheap (`stat`), while
|
|
1179
|
-
// glob markers like `*.csproj` match real project filenames (#201).
|
|
1180
|
-
for (const pattern of includePatterns) {
|
|
1181
|
-
if ((await markerExists(currentDir, pattern)) &&
|
|
1182
|
-
(pattern !== ".git" || currentDir !== os.tmpdir()) &&
|
|
1183
|
-
!(await isExcludedLspRoot(currentDir))) {
|
|
1184
|
-
return enforceLspRootCeiling(currentDir, process.cwd(), file);
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
if (currentDir === stop || currentDir === fsRoot) {
|
|
1188
|
-
break;
|
|
1189
|
-
}
|
|
1190
|
-
currentDir = path.dirname(currentDir);
|
|
1191
|
-
}
|
|
1192
|
-
return undefined;
|
|
1193
|
-
})();
|
|
1194
|
-
inFlight.set(dirKey, promise);
|
|
1195
|
-
try {
|
|
1196
|
-
const result = await promise;
|
|
1197
|
-
if (result !== undefined)
|
|
1198
|
-
cache.set(dirKey, { root: result, probedDirs, walkedAt });
|
|
1199
|
-
return result;
|
|
1200
|
-
}
|
|
1201
|
-
finally {
|
|
1202
|
-
inFlight.delete(dirKey);
|
|
1203
|
-
}
|
|
1204
|
-
}, includePatterns);
|
|
1205
|
-
}
|
|
1206
|
-
/** Alias kept for backward compatibility */
|
|
1207
|
-
export const createRootDetector = NearestRoot;
|
|
1208
|
-
// --- Runtime Tool Helpers ---
|
|
1209
|
-
/**
|
|
1210
|
-
* Check if a command is available on system PATH.
|
|
1211
|
-
*
|
|
1212
|
-
* Async (was a blocking `spawnSync("where"/"which")`): runs on the spawn
|
|
1213
|
-
* fall-through path (Step 4, runtime-install gate). The shared
|
|
1214
|
-
* `isCommandAvailableAsync` spawns the same finder via `safeSpawnAsync` with a
|
|
1215
|
-
* 5s timeout, so a stalled finder can no longer freeze the loop. Semantics are
|
|
1216
|
-
* preserved: true iff the finder exits 0.
|
|
1217
|
-
*/
|
|
1218
|
-
function isOnPath(command) {
|
|
1219
|
-
return isCommandAvailableAsync(command);
|
|
1220
|
-
}
|
|
1221
|
-
/**
|
|
1222
|
-
* Try to install gopls via `go install`. Resolves true if the install succeeded.
|
|
1223
|
-
*
|
|
1224
|
-
* Async (was a blocking `spawnSync`): runs on the LSP runtime-install gate, off
|
|
1225
|
-
* the event loop. `ignoreAmbientSignal` keeps the install running to completion
|
|
1226
|
-
* even if the agent turn is interrupted, matching the old uncancellable sync
|
|
1227
|
-
* behaviour. Success semantics preserved: true iff the process exits 0.
|
|
1228
|
-
*/
|
|
1229
|
-
export async function tryGoInstallGopls() {
|
|
1230
|
-
const isWindows = process.platform === "win32";
|
|
1231
|
-
const result = await safeSpawnAsync(isWindows ? "go.exe" : "go", ["install", "golang.org/x/tools/gopls@latest"], { timeout: 180000, ignoreAmbientSignal: true });
|
|
1232
|
-
return !result.error && result.status === 0;
|
|
1233
|
-
}
|
|
1234
|
-
export async function tryDotnetToolInstall(tool) {
|
|
1235
|
-
mkdirSync(PI_LENS_BIN_DIR, { recursive: true });
|
|
1236
|
-
const result = await safeSpawnAsync("dotnet", ["tool", "install", "--tool-path", PI_LENS_BIN_DIR, tool], { timeout: 180000, ignoreAmbientSignal: true });
|
|
1237
|
-
if (!result.error && result.status === 0)
|
|
1238
|
-
return true;
|
|
1239
|
-
const stderr = result.stderr ?? "";
|
|
1240
|
-
if (stderr.includes("No NuGet sources are defined or enabled")) {
|
|
1241
|
-
logSessionStart(`lsp dotnet-install: NuGet sources missing — cannot install ${tool}. ` +
|
|
1242
|
-
`Run: dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org`);
|
|
1243
|
-
return false;
|
|
1244
|
-
}
|
|
1245
|
-
const updateResult = await safeSpawnAsync("dotnet", ["tool", "update", "--tool-path", PI_LENS_BIN_DIR, tool], { timeout: 180000, ignoreAmbientSignal: true });
|
|
1246
|
-
return !updateResult.error && updateResult.status === 0;
|
|
1247
|
-
}
|
|
1248
|
-
/**
|
|
1249
|
-
* #1412 M1: walk up from `startDir` (inclusive) looking for a file at
|
|
1250
|
-
* `startDir/<relativeSegments>`, `dirname(startDir)/<relativeSegments>`, and so
|
|
1251
|
-
* on — the same ancestor walk `findNativeTypeScriptLsp` uses, bounded the same
|
|
1252
|
-
* way (`isAtOrAboveHomeDir`). A nested config root (e.g. a `cypress/tsconfig.json`
|
|
1253
|
-
* LSP root inside a repo whose `node_modules` only exists at the repo root) must
|
|
1254
|
-
* still resolve tooling installed at an ancestor, not just directly under the
|
|
1255
|
-
* LSP root — mirrors how node module resolution itself walks up.
|
|
1256
|
-
*/
|
|
1257
|
-
async function findAncestorFile(startDir, ...relativeSegments) {
|
|
1258
|
-
return findAncestorFileAmong(startDir, [relativeSegments]);
|
|
1259
|
-
}
|
|
1260
|
-
/**
|
|
1261
|
-
* Same ancestor walk as `findAncestorFile`, but checks every candidate
|
|
1262
|
-
* relative-path in `candidateSegmentLists` AT EACH LEVEL before moving up —
|
|
1263
|
-
* so the nearest ancestor wins regardless of which candidate name matched
|
|
1264
|
-
* there, matching normal node_modules resolution priority (nearest install
|
|
1265
|
-
* shadows a further one, never the reverse).
|
|
1266
|
-
*/
|
|
1267
|
-
async function findAncestorFileAmong(startDir, candidateSegmentLists) {
|
|
1268
|
-
const fs = await import("node:fs/promises");
|
|
1269
|
-
let currentDir = path.resolve(startDir);
|
|
1270
|
-
while (!isAtOrAboveHomeDir(currentDir)) {
|
|
1271
|
-
for (const segments of candidateSegmentLists) {
|
|
1272
|
-
const candidate = path.join(currentDir, ...segments);
|
|
1273
|
-
try {
|
|
1274
|
-
await fs.access(candidate);
|
|
1275
|
-
return candidate;
|
|
1276
|
-
}
|
|
1277
|
-
catch {
|
|
1278
|
-
/* not found at this level */
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
const parent = path.dirname(currentDir);
|
|
1282
|
-
if (parent === currentDir)
|
|
1283
|
-
break;
|
|
1284
|
-
currentDir = parent;
|
|
1285
|
-
}
|
|
1286
|
-
return undefined;
|
|
1287
|
-
}
|
|
1288
|
-
/**
|
|
1289
|
-
* A failed classic-compiler repair must not repeat within a session.
|
|
1290
|
-
* `ensureTool` caches successful installs, so a repair that works
|
|
1291
|
-
* short-circuits later calls on its own. A repair that fails leaves nothing
|
|
1292
|
-
* behind, and `findTsserverPath` has three call sites (TypeScript, Vue,
|
|
1293
|
-
* Svelte). Without this guard an offline or partial install re-runs a 120 s
|
|
1294
|
-
* forced reinstall on every spawn.
|
|
1295
|
-
*
|
|
1296
|
-
* The guard is process-singleton state, so without an explicit re-arm it
|
|
1297
|
-
* would latch for the whole extension-host process — a repair that failed
|
|
1298
|
-
* once (transient registry hiccup) would stay unrepairable for every later
|
|
1299
|
-
* session in that process. `resetClassicTsRepairGuard` re-arms it; callers
|
|
1300
|
-
* wire that into `session_start` alongside the other per-session resets
|
|
1301
|
-
* (#1570).
|
|
1302
|
-
*/
|
|
1303
|
-
const CLASSIC_TS_REPAIR_FAMILY = "lsp.classic-ts-repair-guard";
|
|
1304
|
-
const CLASSIC_TS_REPAIR_VERSION = 1;
|
|
1305
|
-
function classicTsRepairState() {
|
|
1306
|
-
return getProcessSingleton(CLASSIC_TS_REPAIR_FAMILY, CLASSIC_TS_REPAIR_VERSION, () => ({ attempted: false }));
|
|
1307
|
-
}
|
|
1308
|
-
/** Re-arm the classic-repair guard so a new session gets its own attempt. */
|
|
1309
|
-
export function resetClassicTsRepairGuard() {
|
|
1310
|
-
classicTsRepairState().attempted = false;
|
|
1311
|
-
}
|
|
1312
|
-
/** Test hook — clears the process-singleton classic-repair guard. */
|
|
1313
|
-
export function _resetClassicTsRepairForTests() {
|
|
1314
|
-
resetClassicTsRepairGuard();
|
|
1315
|
-
}
|
|
1316
|
-
/**
|
|
1317
|
-
* Directories that may hold the TypeScript package next to a resolved `tsc`
|
|
1318
|
-
* binary: `<bin>/../typescript` (npm-global layout) and
|
|
1319
|
-
* `<bin>/../../typescript` (managed `node_modules/.bin` layout).
|
|
1320
|
-
*/
|
|
1321
|
-
function typescriptDirsForTsc(tscPath) {
|
|
1322
|
-
const binDir = path.dirname(tscPath);
|
|
1323
|
-
return [
|
|
1324
|
-
path.join(binDir, "..", "typescript"),
|
|
1325
|
-
path.join(binDir, "..", "..", "typescript"),
|
|
1326
|
-
];
|
|
1327
|
-
}
|
|
1328
|
-
/**
|
|
1329
|
-
* Read the major version of the TypeScript package that backs a resolved
|
|
1330
|
-
* `tsc` binary. Returns undefined when the version is unknowable: `ensureTool`
|
|
1331
|
-
* returns the bare string `"tsc"` for a PATH hit, and `path.dirname("tsc")` is
|
|
1332
|
-
* `"."`, so the candidates would go cwd-relative. Callers must not repair on
|
|
1333
|
-
* an unknown version — a healthy global TypeScript 5.x would be reinstalled
|
|
1334
|
-
* for nothing.
|
|
1335
|
-
*/
|
|
1336
|
-
async function typescriptVersionForTsc(tscPath) {
|
|
1337
|
-
if (!path.isAbsolute(tscPath))
|
|
1338
|
-
return undefined;
|
|
1339
|
-
for (const dir of typescriptDirsForTsc(tscPath)) {
|
|
1340
|
-
let manifest;
|
|
1341
|
-
try {
|
|
1342
|
-
manifest = await readFile(path.join(dir, "package.json"), "utf8");
|
|
1343
|
-
}
|
|
1344
|
-
catch {
|
|
1345
|
-
continue;
|
|
1346
|
-
}
|
|
1347
|
-
let version;
|
|
1348
|
-
try {
|
|
1349
|
-
const parsed = JSON.parse(manifest);
|
|
1350
|
-
if (typeof parsed !== "object" ||
|
|
1351
|
-
parsed === null ||
|
|
1352
|
-
!("version" in parsed) ||
|
|
1353
|
-
typeof parsed.version !== "string") {
|
|
1354
|
-
return undefined;
|
|
1355
|
-
}
|
|
1356
|
-
version = parsed.version;
|
|
1357
|
-
}
|
|
1358
|
-
catch {
|
|
1359
|
-
return undefined;
|
|
1360
|
-
}
|
|
1361
|
-
const majorText = version.split(".", 1)[0] ?? "";
|
|
1362
|
-
const major = /^\d+$/.test(majorText) ? Number(majorText) : Number.NaN;
|
|
1363
|
-
if (!Number.isFinite(major))
|
|
1364
|
-
return undefined;
|
|
1365
|
-
return { version, major };
|
|
1366
|
-
}
|
|
1367
|
-
return undefined;
|
|
1368
|
-
}
|
|
1369
|
-
/**
|
|
1370
|
-
* Locate tsserver.js — tries local project (walking up from root, #1412 M1),
|
|
1371
|
-
* then process.cwd() as a last-resort fallback, then pi-lens managed
|
|
1372
|
-
* TypeScript. Returns the path to tsserver.js, or undefined if not found.
|
|
1373
|
-
*/
|
|
1374
|
-
async function findTsserverPath(root, allowInstall) {
|
|
1375
|
-
const fs = await import("node:fs/promises");
|
|
1376
|
-
const ancestorHit = await findAncestorFile(root, "node_modules", "typescript", "lib", "tsserver.js");
|
|
1377
|
-
if (ancestorHit)
|
|
1378
|
-
return ancestorHit;
|
|
1379
|
-
const cwdCandidate = path.join(process.cwd(), "node_modules", "typescript", "lib", "tsserver.js");
|
|
1380
|
-
try {
|
|
1381
|
-
await fs.access(cwdCandidate);
|
|
1382
|
-
return cwdCandidate;
|
|
1383
|
-
}
|
|
1384
|
-
catch {
|
|
1385
|
-
/* not found */
|
|
1386
|
-
}
|
|
1387
|
-
const tsserverForTsc = async (tscPath) => {
|
|
1388
|
-
if (!tscPath)
|
|
1389
|
-
return undefined;
|
|
1390
|
-
for (const dir of typescriptDirsForTsc(tscPath)) {
|
|
1391
|
-
const candidate = path.join(dir, "lib", "tsserver.js");
|
|
1392
|
-
try {
|
|
1393
|
-
await fs.access(candidate);
|
|
1394
|
-
return candidate;
|
|
1395
|
-
}
|
|
1396
|
-
catch {
|
|
1397
|
-
/* not found */
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
return undefined;
|
|
1401
|
-
};
|
|
1402
|
-
// Discover the TypeScript install (PATH / npm-global) even when installation
|
|
1403
|
-
// is disabled; only the download is gated by allowInstall.
|
|
1404
|
-
const installAllowed = canInstall(allowInstall);
|
|
1405
|
-
const discoveredTsc = await ensureTool("typescript", {
|
|
1406
|
-
allowInstall: installAllowed,
|
|
1407
|
-
});
|
|
1408
|
-
const discoveredTsserver = await tsserverForTsc(discoveredTsc);
|
|
1409
|
-
if (discoveredTsserver ||
|
|
1410
|
-
!discoveredTsc ||
|
|
1411
|
-
!installAllowed ||
|
|
1412
|
-
classicTsRepairState().attempted) {
|
|
1413
|
-
return discoveredTsserver;
|
|
1414
|
-
}
|
|
1415
|
-
// Repair only a compiler we can prove is TypeScript 7+. TypeScript 7 dropped
|
|
1416
|
-
// lib/tsserver.js, so the classic wrapper cannot start against it. Any other
|
|
1417
|
-
// version — or a version we cannot read, such as a bare PATH `tsc` — is left
|
|
1418
|
-
// alone rather than force-reinstalled.
|
|
1419
|
-
const discoveredVersion = await typescriptVersionForTsc(discoveredTsc);
|
|
1420
|
-
if (!discoveredVersion || discoveredVersion.major < 7)
|
|
1421
|
-
return undefined;
|
|
1422
|
-
// An older managed tree took `latest` before the registry pinned the classic
|
|
1423
|
-
// compiler. Reinstall the pinned version once so that tree self-heals,
|
|
1424
|
-
// without deleting user or project-local TypeScript installations.
|
|
1425
|
-
classicTsRepairState().attempted = true;
|
|
1426
|
-
logSessionStart(`lsp typescript: managed compiler resolved to TypeScript ${discoveredVersion.version}, which ships no tsserver.js; reinstalling pinned classic fallback`);
|
|
1427
|
-
const repairedTsc = await ensureTool("typescript", {
|
|
1428
|
-
allowInstall: true,
|
|
1429
|
-
forceReinstall: true,
|
|
1430
|
-
});
|
|
1431
|
-
return tsserverForTsc(repairedTsc);
|
|
1432
|
-
}
|
|
1433
|
-
/**
|
|
1434
|
-
* TypeScript 7+ ships the native typescript-go language server through the
|
|
1435
|
-
* workspace-local `tsc --lsp --stdio` entrypoint and no longer includes
|
|
1436
|
-
* `lib/tsserver.js`. Resolve the nearest TypeScript package using normal
|
|
1437
|
-
* node_modules ancestor semantics so a monorepo package can use its hoisted
|
|
1438
|
-
* compiler, while never falling through to a PATH/global `tsc`.
|
|
1439
|
-
*/
|
|
1440
|
-
async function findNativeTypeScriptLsp(root) {
|
|
1441
|
-
let currentDir = path.resolve(root);
|
|
1442
|
-
while (!isAtOrAboveHomeDir(currentDir)) {
|
|
1443
|
-
const typescriptDir = path.join(currentDir, "node_modules", "typescript");
|
|
1444
|
-
const packageJsonPath = path.join(typescriptDir, "package.json");
|
|
1445
|
-
let packageJsonText;
|
|
1446
|
-
try {
|
|
1447
|
-
packageJsonText = await readFile(packageJsonPath, "utf8");
|
|
1448
|
-
}
|
|
1449
|
-
catch (error) {
|
|
1450
|
-
if (error.code !== "ENOENT") {
|
|
1451
|
-
return undefined;
|
|
1452
|
-
}
|
|
1453
|
-
// A `node_modules/typescript/` directory that exists but has no
|
|
1454
|
-
// `package.json` is a malformed/partial install at THIS level, not an
|
|
1455
|
-
// absent one — stop here (fall back to classic) rather than walking up
|
|
1456
|
-
// to an ancestor, or a broken nearest install would let an unrelated
|
|
1457
|
-
// ancestor TS 7 binary silently shadow it (Copilot review, PR #526).
|
|
1458
|
-
try {
|
|
1459
|
-
const dirStat = await stat(typescriptDir);
|
|
1460
|
-
if (dirStat.isDirectory())
|
|
1461
|
-
return undefined;
|
|
1462
|
-
}
|
|
1463
|
-
catch {
|
|
1464
|
-
/* typescript dir itself doesn't exist here — keep walking up */
|
|
1465
|
-
}
|
|
1466
|
-
const parent = path.dirname(currentDir);
|
|
1467
|
-
if (parent === currentDir)
|
|
1468
|
-
return undefined;
|
|
1469
|
-
currentDir = parent;
|
|
1470
|
-
continue;
|
|
1471
|
-
}
|
|
1472
|
-
let version;
|
|
1473
|
-
try {
|
|
1474
|
-
const parsed = JSON.parse(packageJsonText);
|
|
1475
|
-
if (typeof parsed !== "object" ||
|
|
1476
|
-
parsed === null ||
|
|
1477
|
-
!("version" in parsed) ||
|
|
1478
|
-
typeof parsed.version !== "string") {
|
|
1479
|
-
return undefined;
|
|
1480
|
-
}
|
|
1481
|
-
version = parsed.version;
|
|
1482
|
-
}
|
|
1483
|
-
catch {
|
|
1484
|
-
return undefined;
|
|
1485
|
-
}
|
|
1486
|
-
// The nearest installed package shadows any ancestor TypeScript package,
|
|
1487
|
-
// matching Node/package-manager resolution. Never skip a local TS <=6 or
|
|
1488
|
-
// malformed install just to select an unrelated ancestor TS 7 binary.
|
|
1489
|
-
const majorText = version.split(".", 1)[0] ?? "";
|
|
1490
|
-
const major = /^\d+$/.test(majorText) ? Number(majorText) : Number.NaN;
|
|
1491
|
-
if (!Number.isFinite(major) || major < 7)
|
|
1492
|
-
return undefined;
|
|
1493
|
-
const localTsc = path.join(currentDir, "node_modules", ".bin", "tsc");
|
|
1494
|
-
const candidates = process.platform === "win32"
|
|
1495
|
-
? [`${localTsc}.cmd`, `${localTsc}.exe`, localTsc]
|
|
1496
|
-
: [localTsc];
|
|
1497
|
-
for (const command of candidates) {
|
|
1498
|
-
try {
|
|
1499
|
-
await access(command);
|
|
1500
|
-
return { command, version };
|
|
1501
|
-
}
|
|
1502
|
-
catch {
|
|
1503
|
-
/* not found */
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
return undefined;
|
|
1507
|
-
}
|
|
1508
|
-
return undefined;
|
|
1509
|
-
}
|
|
1510
|
-
function dotnetToolCandidates(tool) {
|
|
1511
|
-
const home = os.homedir();
|
|
1512
|
-
return [
|
|
1513
|
-
path.join(PI_LENS_BIN_DIR, `${tool}.exe`),
|
|
1514
|
-
path.join(PI_LENS_BIN_DIR, tool),
|
|
1515
|
-
path.join(home, ".dotnet", "tools", `${tool}.exe`),
|
|
1516
|
-
path.join(home, ".dotnet", "tools", tool),
|
|
1517
|
-
tool,
|
|
1518
|
-
].filter(Boolean);
|
|
1519
|
-
}
|
|
1520
|
-
/**
|
|
1521
|
-
* Both filename forms for a tool in a directory (`.exe` first on Windows). A
|
|
1522
|
-
* managed binary may carry the extension or not depending on how the toolchain
|
|
1523
|
-
* dropped it, so we try both.
|
|
1524
|
-
*/
|
|
1525
|
-
function binExeVariants(dir, tool) {
|
|
1526
|
-
return process.platform === "win32"
|
|
1527
|
-
? [path.join(dir, `${tool}.exe`), path.join(dir, tool)]
|
|
1528
|
-
: [path.join(dir, tool)];
|
|
1529
|
-
}
|
|
1530
|
-
/**
|
|
1531
|
-
* Canonical-bin discovery (#241): a runtime-managed server can be installed yet
|
|
1532
|
-
* absent from the shell PATH — the toolchain drops it in a well-known dir the
|
|
1533
|
-
* user's PATH often omits (fresh installs, Windows, non-login shells). Returning
|
|
1534
|
-
* the bare command FIRST keeps PATH authoritative when it resolves; the explicit
|
|
1535
|
-
* dir paths are the fallback (and the post-`go install` retry target).
|
|
1536
|
-
*
|
|
1537
|
-
* Go: `$GOPATH/bin` (first GOPATH entry) or `~/go/bin` — where `go install` lands.
|
|
1538
|
-
*/
|
|
1539
|
-
export function goBinCandidates(tool) {
|
|
1540
|
-
const gopath = process.env.GOPATH?.split(path.delimiter)[0] ||
|
|
1541
|
-
path.join(os.homedir(), "go");
|
|
1542
|
-
return [tool, ...binExeVariants(path.join(gopath, "bin"), tool)];
|
|
1543
|
-
}
|
|
1544
|
-
/** Rust: `$CARGO_HOME/bin` or `~/.cargo/bin` — cargo/rustup binaries + proxies. */
|
|
1545
|
-
export function cargoBinCandidates(tool) {
|
|
1546
|
-
const cargoHome = process.env.CARGO_HOME || path.join(os.homedir(), ".cargo");
|
|
1547
|
-
return [tool, ...binExeVariants(path.join(cargoHome, "bin"), tool)];
|
|
1548
|
-
}
|
|
1549
|
-
/**
|
|
1550
|
-
* Try to install a gem to the pi-lens bin dir. Resolves true if the install succeeded.
|
|
1551
|
-
*/
|
|
1552
|
-
export async function tryGemInstall(gem) {
|
|
1553
|
-
const { join } = await import("node:path");
|
|
1554
|
-
const binDir = join(getGlobalPiLensDir(), "bin");
|
|
1555
|
-
const { mkdir } = await import("node:fs/promises");
|
|
1556
|
-
await mkdir(binDir, { recursive: true });
|
|
1557
|
-
const result = await safeSpawnAsync("gem", ["install", gem, "--bindir", binDir, "--no-document"], { timeout: 180000, ignoreAmbientSignal: true });
|
|
1558
|
-
const ok = !result.error && result.status === 0;
|
|
1559
|
-
// Add binDir to PATH so subsequent lookups find the installed gem
|
|
1560
|
-
if (ok) {
|
|
1561
|
-
const sep = process.platform === "win32" ? ";" : ":";
|
|
1562
|
-
if (!process.env.PATH?.includes(binDir)) {
|
|
1563
|
-
process.env.PATH = `${binDir}${sep}${process.env.PATH ?? ""}`;
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
return ok;
|
|
1567
|
-
}
|
|
1568
|
-
/**
|
|
1569
|
-
* Wraps a root function so it returns undefined for files inside a Deno project.
|
|
1570
|
-
* Prevents TypeScript LSP from being spawned alongside Deno LSP for the same file,
|
|
1571
|
-
* which would produce false diagnostics for Deno-specific APIs.
|
|
1572
|
-
*/
|
|
1573
|
-
export function DenoExcludeRoot(primary) {
|
|
1574
|
-
const denoDetector = createRootDetector(["deno.json", "deno.jsonc"]);
|
|
1575
|
-
return async (file) => {
|
|
1576
|
-
const denoRoot = await denoDetector(file);
|
|
1577
|
-
if (denoRoot)
|
|
1578
|
-
return undefined;
|
|
1579
|
-
return primary(file);
|
|
1580
|
-
};
|
|
1581
|
-
}
|
|
1582
|
-
// --- Server Definitions ---
|
|
1583
|
-
const JS_TS_LSP_EXTENSIONS = KIND_EXTENSIONS["jsts"].filter((ext) => ext !== ".svelte" && ext !== ".vue");
|
|
1584
|
-
// TypeScript identity and tooling discovery deliberately use separate marker
|
|
1585
|
-
// families. A governing config wins even when a package directory supplies
|
|
1586
|
-
// hoisted binaries. Keep configs out of PROJECT_BOUNDARY_MARKERS: #1373 still
|
|
1587
|
-
// coalesces a config-only nested root when an ancestor client was hosted first
|
|
1588
|
-
// (nested-config-first remains intentionally open-order-sensitive). #1412
|
|
1589
|
-
// accepted risk (M2, not fixed here): honoring nested config roots at all
|
|
1590
|
-
// enlarges the population of directories that can independently coalesce or
|
|
1591
|
-
// diverge under #1373's open-order sensitivity — the same pre-existing
|
|
1592
|
-
// blast-radius, just triggered by more roots than before.
|
|
1593
|
-
const TS_CONFIG_MARKERS = ["tsconfig.json", "jsconfig.json"];
|
|
1594
|
-
const TS_TOOLING_MARKERS = [
|
|
1595
|
-
"package-lock.json",
|
|
1596
|
-
"bun.lockb",
|
|
1597
|
-
"bun.lock",
|
|
1598
|
-
"pnpm-lock.yaml",
|
|
1599
|
-
"yarn.lock",
|
|
1600
|
-
"package.json",
|
|
1601
|
-
];
|
|
1602
|
-
// #1412 M3: tsserver associates jsconfig.json with JS files only (its identity
|
|
1603
|
-
// probe reports a jsconfig-governed .ts file as unassociated) — so a TS-family
|
|
1604
|
-
// file under a jsconfig-only directory must NOT root there; keep walking up
|
|
1605
|
-
// for a real tsconfig.json. A JS-family file accepts either: tsconfig also
|
|
1606
|
-
// governs plain JS via `allowJs`, so accepting tsconfig for a .js file is
|
|
1607
|
-
// correct, and jsconfig obviously is too.
|
|
1608
|
-
const TS_FAMILY_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts"]);
|
|
1609
|
-
function isTsFamilyFile(file) {
|
|
1610
|
-
return TS_FAMILY_EXTENSIONS.has(path.extname(file).toLowerCase());
|
|
1611
|
-
}
|
|
1612
|
-
function tsConfigMarkersForFile(file) {
|
|
1613
|
-
return isTsFamilyFile(file)
|
|
1614
|
-
? ["tsconfig.json"]
|
|
1615
|
-
: TS_CONFIG_MARKERS;
|
|
1616
|
-
}
|
|
1617
|
-
// Two detector instances (not one parameterized by file) so each keeps its own
|
|
1618
|
-
// per-directory NearestRoot cache valid for its fixed marker set — a shared
|
|
1619
|
-
// cache keyed only by directory would conflate the TS-only and either-config
|
|
1620
|
-
// answers for the same directory.
|
|
1621
|
-
const TypeScriptConfigRootTsOnly = IgnoreHomeRoot(createRootDetector(["tsconfig.json"]));
|
|
1622
|
-
const TypeScriptConfigRootEither = IgnoreHomeRoot(createRootDetector([...TS_CONFIG_MARKERS]));
|
|
1623
|
-
const TypeScriptToolingRoot = IgnoreHomeRoot(createRootDetector([...TS_TOOLING_MARKERS]));
|
|
1624
|
-
async function findTypeScriptProjectRoot(file) {
|
|
1625
|
-
const configDetector = isTsFamilyFile(file)
|
|
1626
|
-
? TypeScriptConfigRootTsOnly
|
|
1627
|
-
: TypeScriptConfigRootEither;
|
|
1628
|
-
const [configRoot, toolingRoot] = await Promise.all([
|
|
1629
|
-
configDetector(file),
|
|
1630
|
-
TypeScriptToolingRoot(file),
|
|
1631
|
-
]);
|
|
1632
|
-
if (!configRoot)
|
|
1633
|
-
return toolingRoot;
|
|
1634
|
-
if (!toolingRoot)
|
|
1635
|
-
return configRoot;
|
|
1636
|
-
// A config inside (or beside) the nearest package governs its files. A
|
|
1637
|
-
// config above a nearer package must not erase that topology boundary.
|
|
1638
|
-
return isSameOrWithin(toolingRoot, configRoot) ? configRoot : toolingRoot;
|
|
1639
|
-
}
|
|
1640
|
-
/**
|
|
1641
|
-
* Walk up from the file's directory looking for a TypeScript project marker
|
|
1642
|
-
* (a governing config first, per-directory, per #1412 M3's extension-family
|
|
1643
|
-
* filter; else a tooling/lockfile marker), but stop at `extensionRootKey` so
|
|
1644
|
-
* we never escape the .pi/agent/extensions boundary into a higher-up project
|
|
1645
|
-
* (e.g. ~/.pi/agent/package.json which would pull every extension in the
|
|
1646
|
-
* directory into one LSP workspace).
|
|
1647
|
-
*
|
|
1648
|
-
* #1412 L4: each directory returns immediately on its first match (config or
|
|
1649
|
-
* tooling) — there is no cross-level "nearest tooling root" to carry forward,
|
|
1650
|
-
* since a match always wins on the spot. Returns the nearest directory
|
|
1651
|
-
* containing a marker, or undefined if none is found between the file and the
|
|
1652
|
-
* extensions root inclusive.
|
|
1653
|
-
*/
|
|
1654
|
-
async function findExtensionBoundedRoot(file, extensionRootKey) {
|
|
1655
|
-
const startDir = path.resolve(path.dirname(file));
|
|
1656
|
-
let currentDir = startDir;
|
|
1657
|
-
const configMarkers = tsConfigMarkersForFile(file);
|
|
1658
|
-
while (true) {
|
|
1659
|
-
for (const pattern of configMarkers) {
|
|
1660
|
-
try {
|
|
1661
|
-
await stat(path.join(currentDir, pattern));
|
|
1662
|
-
return currentDir;
|
|
1663
|
-
}
|
|
1664
|
-
catch {
|
|
1665
|
-
/* not found, try next marker */
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
for (const pattern of TS_TOOLING_MARKERS) {
|
|
1669
|
-
try {
|
|
1670
|
-
await stat(path.join(currentDir, pattern));
|
|
1671
|
-
return currentDir;
|
|
1672
|
-
}
|
|
1673
|
-
catch {
|
|
1674
|
-
/* not found, try next marker */
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1677
|
-
// Stop at or beyond the extensions root — never walk into the
|
|
1678
|
-
// pi-agent-wide scope.
|
|
1679
|
-
const currentKey = normalizeEphemeralMapKey(path.resolve(currentDir));
|
|
1680
|
-
if (currentKey === extensionRootKey)
|
|
1681
|
-
return undefined;
|
|
1682
|
-
const parent = path.dirname(currentDir);
|
|
1683
|
-
if (parent === currentDir)
|
|
1684
|
-
return undefined;
|
|
1685
|
-
currentDir = parent;
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
/**
|
|
1689
|
-
* Check whether the directory immediately containing the extensions folder
|
|
1690
|
-
* (i.e. `.pi/agent/`) holds any TypeScript project marker. This narrowly
|
|
1691
|
-
* detects the #123 scenario — pi itself installs a package.json at
|
|
1692
|
-
* `~/.pi/agent/` and the user's extension has none of its own — without
|
|
1693
|
-
* picking up accidental markers further up the filesystem.
|
|
1694
|
-
*/
|
|
1695
|
-
async function hasAgentLevelProjectMarker(extensionRootKey) {
|
|
1696
|
-
const agentDir = path.dirname(extensionRootKey);
|
|
1697
|
-
if (!agentDir || agentDir === extensionRootKey)
|
|
1698
|
-
return false;
|
|
1699
|
-
for (const pattern of [...TS_CONFIG_MARKERS, ...TS_TOOLING_MARKERS]) {
|
|
1700
|
-
try {
|
|
1701
|
-
await stat(path.join(agentDir, pattern));
|
|
1702
|
-
return true;
|
|
1703
|
-
}
|
|
1704
|
-
catch {
|
|
1705
|
-
/* not found, try next */
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
return false;
|
|
1709
|
-
}
|
|
1710
|
-
const TypeScriptRoot = withRootMarkers(DenoExcludeRoot(async (file) => {
|
|
1711
|
-
const extensionRootKey = piAgentExtensionsRootKey(file);
|
|
1712
|
-
if (extensionRootKey) {
|
|
1713
|
-
// Bounded walk so we never adopt a parent (e.g. ~/.pi/agent/) as the
|
|
1714
|
-
// LSP root.
|
|
1715
|
-
const bounded = await findExtensionBoundedRoot(file, extensionRootKey);
|
|
1716
|
-
if (bounded)
|
|
1717
|
-
return bounded;
|
|
1718
|
-
// No marker inside the extension boundary. If pi itself has a
|
|
1719
|
-
// package.json at ~/.pi/agent/ (the #123 setup), the previous code
|
|
1720
|
-
// returned undefined and the LSP silently failed to start. Fall
|
|
1721
|
-
// back to a per-file scope so the LSP at least runs.
|
|
1722
|
-
if (await hasAgentLevelProjectMarker(extensionRootKey)) {
|
|
1723
|
-
return FileDirRoot(file);
|
|
1724
|
-
}
|
|
1725
|
-
// Truly loose extension file with no project context anywhere
|
|
1726
|
-
// relevant — preserve the existing skip behavior (LSP shouldn't
|
|
1727
|
-
// analyze a lone .ts file with no package.json above or below).
|
|
1728
|
-
return undefined;
|
|
1729
|
-
}
|
|
1730
|
-
const projectRoot = await findTypeScriptProjectRoot(file);
|
|
1731
|
-
if (projectRoot)
|
|
1732
|
-
return projectRoot;
|
|
1733
|
-
return FileDirRoot(file);
|
|
1734
|
-
}), [...TS_CONFIG_MARKERS, ...TS_TOOLING_MARKERS]);
|
|
1735
|
-
export const TypeScriptServer = {
|
|
1736
|
-
id: "typescript",
|
|
1737
|
-
name: "TypeScript Language Server",
|
|
1738
|
-
extensions: JS_TS_LSP_EXTENSIONS,
|
|
1739
|
-
autoPropagateDiagnostics: true,
|
|
1740
|
-
root: TypeScriptRoot,
|
|
1741
|
-
rootMarkers: TypeScriptRoot.rootMarkers,
|
|
1742
|
-
async spawn(root, options) {
|
|
1743
|
-
const fs = await import("node:fs/promises");
|
|
1744
|
-
const nativeLsp = await findNativeTypeScriptLsp(root);
|
|
1745
|
-
if (nativeLsp) {
|
|
1746
|
-
const env = await getToolEnvironment();
|
|
1747
|
-
logSessionStart(`lsp typescript-native: version=${nativeLsp.version} command=${nativeLsp.command}`);
|
|
1748
|
-
const proc = await launchLSP(nativeLsp.command, ["--lsp", "--stdio"], {
|
|
1749
|
-
cwd: root,
|
|
1750
|
-
env,
|
|
1751
|
-
});
|
|
1752
|
-
return { process: proc, source: "direct", launchVariant: "native-ts7" };
|
|
1753
|
-
}
|
|
1754
|
-
let source = "direct";
|
|
1755
|
-
// TypeScript <=6 uses typescript-language-server + tsserver.js. Prefer a
|
|
1756
|
-
// project-local wrapper, then fall back to discovered/managed tooling.
|
|
1757
|
-
// #1412 M1: walk up from root (Windows .cmd first, then Unix at each
|
|
1758
|
-
// level) — a nested config root's node_modules/.bin lives at an ancestor,
|
|
1759
|
-
// not necessarily directly under the LSP root.
|
|
1760
|
-
let lspPath = await findAncestorFileAmong(root, [
|
|
1761
|
-
["node_modules", ".bin", "typescript-language-server.cmd"],
|
|
1762
|
-
["node_modules", ".bin", "typescript-language-server"],
|
|
1763
|
-
]);
|
|
1764
|
-
// Fall back to a discovered or managed install. ensureTool() runs PATH /
|
|
1765
|
-
// npm-global discovery even when install is disabled (only the download is
|
|
1766
|
-
// gated by canInstall), so a globally-installed typescript-language-server
|
|
1767
|
-
// resolves even without a per-project node_modules/.bin entry.
|
|
1768
|
-
if (!lspPath) {
|
|
1769
|
-
lspPath = await ensureTool("typescript-language-server", {
|
|
1770
|
-
allowInstall: canInstall(options?.allowInstall),
|
|
1771
|
-
});
|
|
1772
|
-
if (lspPath)
|
|
1773
|
-
source = "managed";
|
|
1774
|
-
if (!lspPath) {
|
|
1775
|
-
return undefined;
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
// Find tsserver.js — also try relative to the LSP binary for local installs
|
|
1779
|
-
let tsserverPath = await findTsserverPath(root, options?.allowInstall);
|
|
1780
|
-
if (!tsserverPath) {
|
|
1781
|
-
const localCandidate = path.join(path.dirname(lspPath), "..", "typescript", "lib", "tsserver.js");
|
|
1782
|
-
try {
|
|
1783
|
-
await fs.access(localCandidate);
|
|
1784
|
-
tsserverPath = localCandidate;
|
|
1785
|
-
}
|
|
1786
|
-
catch {
|
|
1787
|
-
/* not found */
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
if (tsserverPath)
|
|
1791
|
-
source = "managed";
|
|
1792
|
-
// Use absolute path and proper environment
|
|
1793
|
-
const env = await getToolEnvironment();
|
|
1794
|
-
const proc = await launchLSP(lspPath, ["--stdio"], {
|
|
1795
|
-
cwd: root,
|
|
1796
|
-
env: {
|
|
1797
|
-
...env,
|
|
1798
|
-
TSSERVER_PATH: tsserverPath,
|
|
1799
|
-
},
|
|
1800
|
-
});
|
|
1801
|
-
return {
|
|
1802
|
-
process: proc,
|
|
1803
|
-
source,
|
|
1804
|
-
initialization: tsserverPath
|
|
1805
|
-
? { tsserver: { path: tsserverPath } }
|
|
1806
|
-
: undefined,
|
|
1807
|
-
launchVariant: "classic",
|
|
1808
|
-
};
|
|
1809
|
-
},
|
|
1810
|
-
};
|
|
1811
|
-
export const DenoServer = {
|
|
1812
|
-
id: "deno",
|
|
1813
|
-
name: "Deno Language Server",
|
|
1814
|
-
fallbackFor: "typescript",
|
|
1815
|
-
extensions: JS_TS_LSP_EXTENSIONS,
|
|
1816
|
-
autoPropagateDiagnostics: true,
|
|
1817
|
-
root: createRootDetector(["deno.json", "deno.jsonc"]),
|
|
1818
|
-
async spawn(root, options) {
|
|
1819
|
-
return resolveAndLaunch({
|
|
1820
|
-
candidates: ["deno"],
|
|
1821
|
-
args: ["lsp"],
|
|
1822
|
-
cwd: root,
|
|
1823
|
-
managedToolId: "deno",
|
|
1824
|
-
}, options?.allowInstall);
|
|
1825
|
-
},
|
|
1826
|
-
};
|
|
1827
|
-
export const PythonServer = {
|
|
1828
|
-
id: "python",
|
|
1829
|
-
name: "Pyright Language Server",
|
|
1830
|
-
extensions: KIND_EXTENSIONS["python"],
|
|
1831
|
-
root: RootWithFallback(createRootDetector([
|
|
1832
|
-
".git",
|
|
1833
|
-
"pyproject.toml",
|
|
1834
|
-
"setup.py",
|
|
1835
|
-
"setup.cfg",
|
|
1836
|
-
"requirements.txt",
|
|
1837
|
-
"Pipfile",
|
|
1838
|
-
"poetry.lock",
|
|
1839
|
-
])),
|
|
1840
|
-
async spawn(root, options) {
|
|
1841
|
-
const pythonEnvironment = await detectPythonEnvironment(root);
|
|
1842
|
-
const env = augmentPythonEnvironment(await getToolEnvironment(), pythonEnvironment);
|
|
1843
|
-
let source = "direct";
|
|
1844
|
-
// openFilesOnly: true — analyse only open files rather than the full workspace.
|
|
1845
|
-
// Avoids the 5–14 s cold-start on large projects caused by workspace-wide
|
|
1846
|
-
// analysis on startup. Deep type checking is still available via the standalone
|
|
1847
|
-
// pyright CLI runner that runs in parallel.
|
|
1848
|
-
const pyrightInit = (pythonPath) => ({
|
|
1849
|
-
...(pythonPath ? { pythonPath } : {}),
|
|
1850
|
-
openFilesOnly: true,
|
|
1851
|
-
});
|
|
1852
|
-
// Project ownership outranks checker preference: exhaust explicit project
|
|
1853
|
-
// candidates before a bare command can resolve to pi-lens's managed bin or
|
|
1854
|
-
// the host PATH. Within the project tier, preserve the established
|
|
1855
|
-
// pyright → basedpyright → ty preference.
|
|
1856
|
-
const projectPyright = await resolveAndLaunch({
|
|
1857
|
-
candidates: [
|
|
1858
|
-
...pythonEnvironmentToolCandidates(pythonEnvironment, "pyright-langserver"),
|
|
1859
|
-
...nodeBinLocalCandidates(root, "pyright-langserver"),
|
|
1860
|
-
...pythonEnvironmentToolCandidates(pythonEnvironment, "basedpyright-langserver"),
|
|
1861
|
-
...nodeBinLocalCandidates(root, "basedpyright-langserver"),
|
|
1862
|
-
],
|
|
1863
|
-
args: ["--stdio"],
|
|
1864
|
-
cwd: root,
|
|
1865
|
-
env,
|
|
1866
|
-
}, false);
|
|
1867
|
-
if (projectPyright) {
|
|
1868
|
-
return {
|
|
1869
|
-
process: projectPyright.process,
|
|
1870
|
-
source: projectPyright.source,
|
|
1871
|
-
initialization: pyrightInit(pythonEnvironment?.pythonPath),
|
|
1872
|
-
};
|
|
1873
|
-
}
|
|
1874
|
-
// ty uses `ty server`, so it needs a separate launch phase from the
|
|
1875
|
-
// Pyright-compatible servers. It has no stable initializationOptions
|
|
1876
|
-
// equivalent to pyright's `pythonPath` (astral-sh/ty#2032); the child
|
|
1877
|
-
// environment and cwd provide interpreter discovery instead.
|
|
1878
|
-
const projectTy = await resolveAndLaunch({
|
|
1879
|
-
candidates: pythonEnvironmentToolCandidates(pythonEnvironment, "ty"),
|
|
1880
|
-
args: ["server"],
|
|
1881
|
-
cwd: root,
|
|
1882
|
-
env,
|
|
1883
|
-
}, false);
|
|
1884
|
-
if (projectTy) {
|
|
1885
|
-
return { process: projectTy.process, source: projectTy.source };
|
|
1886
|
-
}
|
|
1887
|
-
// With no project-owned checker, retain the existing PATH preference and
|
|
1888
|
-
// keep ty opt-in: pyright, then basedpyright, then ty. The augmented child
|
|
1889
|
-
// PATH includes pi-lens-managed bins before the inherited global PATH.
|
|
1890
|
-
const pathPyright = await resolveAndLaunch({
|
|
1891
|
-
candidates: ["pyright-langserver", "basedpyright-langserver"],
|
|
1892
|
-
args: ["--stdio"],
|
|
1893
|
-
cwd: root,
|
|
1894
|
-
env,
|
|
1895
|
-
}, false);
|
|
1896
|
-
if (pathPyright) {
|
|
1897
|
-
return {
|
|
1898
|
-
process: pathPyright.process,
|
|
1899
|
-
source: pathPyright.source,
|
|
1900
|
-
initialization: pyrightInit(pythonEnvironment?.pythonPath),
|
|
1901
|
-
};
|
|
1902
|
-
}
|
|
1903
|
-
const pathTy = await resolveAndLaunch({
|
|
1904
|
-
candidates: ["ty"],
|
|
1905
|
-
args: ["server"],
|
|
1906
|
-
cwd: root,
|
|
1907
|
-
env,
|
|
1908
|
-
}, false);
|
|
1909
|
-
if (pathTy) {
|
|
1910
|
-
return { process: pathTy.process, source: pathTy.source };
|
|
1911
|
-
}
|
|
1912
|
-
// Discover a globally-installed pyright even when install is disabled;
|
|
1913
|
-
// only the download is gated by canInstall.
|
|
1914
|
-
const pyrightPath = await ensureTool("pyright", {
|
|
1915
|
-
allowInstall: canInstall(options?.allowInstall),
|
|
1916
|
-
});
|
|
1917
|
-
if (!pyrightPath)
|
|
1918
|
-
return undefined;
|
|
1919
|
-
source = "managed";
|
|
1920
|
-
const binDir = path.dirname(pyrightPath);
|
|
1921
|
-
const isWindows = process.platform === "win32";
|
|
1922
|
-
const managedCandidates = isWindows
|
|
1923
|
-
? [
|
|
1924
|
-
path.join(binDir, "pyright-langserver.cmd"),
|
|
1925
|
-
path.join(binDir, "pyright-langserver"),
|
|
1926
|
-
"pyright-langserver",
|
|
1927
|
-
]
|
|
1928
|
-
: [path.join(binDir, "pyright-langserver"), "pyright-langserver"];
|
|
1929
|
-
const resolved = await resolveAndLaunch({ candidates: managedCandidates, args: ["--stdio"], cwd: root, env }, false);
|
|
1930
|
-
if (!resolved)
|
|
1931
|
-
return undefined;
|
|
1932
|
-
return {
|
|
1933
|
-
process: resolved.process,
|
|
1934
|
-
source,
|
|
1935
|
-
initialization: pyrightInit(pythonEnvironment?.pythonPath),
|
|
1936
|
-
};
|
|
1937
|
-
},
|
|
1938
|
-
};
|
|
1939
|
-
export const PythonJediServer = {
|
|
1940
|
-
id: "python-jedi",
|
|
1941
|
-
name: "Jedi Language Server",
|
|
1942
|
-
fallbackFor: "python",
|
|
1943
|
-
extensions: KIND_EXTENSIONS["python"],
|
|
1944
|
-
root: RootWithFallback(createRootDetector([
|
|
1945
|
-
".git",
|
|
1946
|
-
"pyproject.toml",
|
|
1947
|
-
"setup.py",
|
|
1948
|
-
"setup.cfg",
|
|
1949
|
-
"requirements.txt",
|
|
1950
|
-
"Pipfile",
|
|
1951
|
-
"poetry.lock",
|
|
1952
|
-
])),
|
|
1953
|
-
async spawn(root, options) {
|
|
1954
|
-
const launched = await resolveAndLaunch({
|
|
1955
|
-
candidates: ["jedi-language-server"],
|
|
1956
|
-
args: [],
|
|
1957
|
-
cwd: root,
|
|
1958
|
-
managedToolId: "jedi-language-server",
|
|
1959
|
-
}, options?.allowInstall);
|
|
1960
|
-
if (!launched)
|
|
1961
|
-
return undefined;
|
|
1962
|
-
const pythonPath = await detectPythonVenv(root);
|
|
1963
|
-
return {
|
|
1964
|
-
...launched,
|
|
1965
|
-
initialization: pythonPath
|
|
1966
|
-
? { workspace: { environmentPath: pythonPath } }
|
|
1967
|
-
: {},
|
|
1968
|
-
};
|
|
1969
|
-
},
|
|
1970
|
-
};
|
|
1971
|
-
export const GoServer = {
|
|
1972
|
-
id: "go",
|
|
1973
|
-
name: "gopls",
|
|
1974
|
-
extensions: KIND_EXTENSIONS["go"],
|
|
1975
|
-
root: RootWithFallback(WorkspacePriorityRoot([["go.work"], ["go.mod", "go.sum"], [".git"]])),
|
|
1976
|
-
async spawn(root, options) {
|
|
1977
|
-
const result = await resolveAndLaunch({
|
|
1978
|
-
// Canonical-bin discovery (#241): include $GOPATH/bin so a gopls that
|
|
1979
|
-
// `go install` dropped there resolves even when it isn't on PATH —
|
|
1980
|
-
// which is also the retry target after the runtimeInstall below.
|
|
1981
|
-
candidates: goBinCandidates("gopls"),
|
|
1982
|
-
args: [],
|
|
1983
|
-
cwd: root,
|
|
1984
|
-
runtimeInstall: {
|
|
1985
|
-
runtimeCommand: "go",
|
|
1986
|
-
install: tryGoInstallGopls,
|
|
1987
|
-
},
|
|
1988
|
-
}, options?.allowInstall);
|
|
1989
|
-
if (!result)
|
|
1990
|
-
return undefined;
|
|
1991
|
-
return { ...result, initialization: { ui: { semanticTokens: true } } };
|
|
1992
|
-
},
|
|
1993
|
-
};
|
|
1994
|
-
/**
|
|
1995
|
-
* Given an ancestor Cargo.toml's raw contents that already contains a
|
|
1996
|
-
* `[workspace]` table, decide whether it actually claims `childDir` as a
|
|
1997
|
-
* member. Cargo's own default is that a bare `[workspace]` with no `members`
|
|
1998
|
-
* key claims only the manifest's own package — a nested crate is NOT
|
|
1999
|
-
* implicitly swept in just because it sits underneath the workspace root on
|
|
2000
|
-
* disk. Without this check any Cargo.toml with a `[workspace]` table would
|
|
2001
|
-
* hoist every crate below it, including ones the workspace never declared
|
|
2002
|
-
* (#1671) — the same defect shape as an undeclared Maven sibling module.
|
|
2003
|
-
*
|
|
2004
|
-
* KNOWN LIMITATION (#1671 F7, documented rather than implemented): cargo also
|
|
2005
|
-
* treats a crate as an implicit workspace member when the workspace root's
|
|
2006
|
-
* OWN `[package]` has a `path = "..."` dependency on it and no explicit
|
|
2007
|
-
* `members` key is present at all. That dependency-graph inference is not
|
|
2008
|
-
* evaluated here — a workspace relying on it alone (no `members` key) reads
|
|
2009
|
-
* as claiming nothing, so its crates stay independently rooted rather than
|
|
2010
|
-
* being (correctly, per cargo) swept in. An explicit `members` list — the
|
|
2011
|
-
* common case and the one #1671's fixtures exercise — is unaffected.
|
|
2012
|
-
*/
|
|
2013
|
-
function cargoWorkspaceDeclaresMember(workspaceContent, parentDir, childDir) {
|
|
2014
|
-
const relativePath = path
|
|
2015
|
-
.relative(parentDir, childDir)
|
|
2016
|
-
.split(path.sep)
|
|
2017
|
-
.join("/");
|
|
2018
|
-
if (relativePath === "" || relativePath.startsWith(".."))
|
|
2019
|
-
return false;
|
|
2020
|
-
// Reuse the shared, `[workspace]`-scoped readers (review round 2, F4)
|
|
2021
|
-
// instead of hand-composing `extractTomlTableSection` +
|
|
2022
|
-
// `parseTomlStringArray` here — that hand-composition duplicated exactly
|
|
2023
|
-
// what `readCargoWorkspaceMembers`/`readCargoWorkspaceExclude` do, the
|
|
2024
|
-
// single-source-of-truth violation #2473 was filed to close for the OTHER
|
|
2025
|
-
// two Cargo.toml readers. Pattern matching goes through the ONE
|
|
2026
|
-
// workspace-member matcher for the same reason (#2591); cargo's dialect —
|
|
2027
|
-
// `**` never matches (#1671 F6), wildcards confined to one component, so
|
|
2028
|
-
// pattern and path must have the same component count — is
|
|
2029
|
-
// `CARGO_WORKSPACE_MEMBER_DIALECT`, not a private compiler here.
|
|
2030
|
-
const matches = (pattern) => matchesWorkspaceMemberPattern(pattern, relativePath, CARGO_WORKSPACE_MEMBER_DIALECT);
|
|
2031
|
-
const excluded = readCargoWorkspaceExclude(workspaceContent);
|
|
2032
|
-
if (excluded.some(matches))
|
|
2033
|
-
return false;
|
|
2034
|
-
return readCargoWorkspaceMembers(workspaceContent).some(matches);
|
|
2035
|
-
}
|
|
2036
|
-
/**
|
|
2037
|
-
* Bound a monorepo-hoist walk-up by the session ceiling: when `file` is inside
|
|
2038
|
-
* the session cwd, the walk must stop AT the cwd rather than climb past it —
|
|
2039
|
-
* matching enforceLspRootCeiling's clamp semantics but avoiding the wasted stat
|
|
2040
|
-
* calls of walking past a boundary we would clamp back down anyway. When
|
|
2041
|
-
* `file` is outside the session (isolated tests, out-of-session API callers),
|
|
2042
|
-
* enforceLspRootCeiling is a no-op, so the walk is left unbounded (`undefined`).
|
|
2043
|
-
*/
|
|
2044
|
-
function sessionHoistCeiling(sessionCwd, file) {
|
|
2045
|
-
return isSameOrWithin(path.resolve(sessionCwd), path.resolve(file))
|
|
2046
|
-
? path.resolve(sessionCwd)
|
|
2047
|
-
: undefined;
|
|
2048
|
-
}
|
|
2049
|
-
function RustWorkspaceRoot() {
|
|
2050
|
-
const crateRoot = createRootDetector(["Cargo.toml", "Cargo.lock"]);
|
|
2051
|
-
return withRootMarkers(async (file) => {
|
|
2052
|
-
const root = await crateRoot(file);
|
|
2053
|
-
if (!root)
|
|
2054
|
-
return undefined;
|
|
2055
|
-
const sessionCwd = process.cwd();
|
|
2056
|
-
// The walk-up for an ancestor Cargo.toml with a [workspace] table stays
|
|
2057
|
-
// bounded by the same session ceiling the crate-root lookup above already
|
|
2058
|
-
// enforces (enforceLspRootCeiling) — a monorepo hoist must never cross the
|
|
2059
|
-
// session boundary just because a workspace manifest happens to sit above
|
|
2060
|
-
// it (#1671).
|
|
2061
|
-
const stop = sessionHoistCeiling(sessionCwd, file);
|
|
2062
|
-
let current = root;
|
|
2063
|
-
const fsRoot = path.parse(current).root;
|
|
2064
|
-
while (true) {
|
|
2065
|
-
if (stop !== undefined && current === stop)
|
|
2066
|
-
break;
|
|
2067
|
-
const parent = path.dirname(current);
|
|
2068
|
-
if (parent === current || parent === fsRoot)
|
|
2069
|
-
break;
|
|
2070
|
-
const parentCargoPath = path.join(parent, "Cargo.toml");
|
|
2071
|
-
const parentCargoContent = await readTextFileOrUndefined(parentCargoPath);
|
|
2072
|
-
if (parentCargoContent !== undefined &&
|
|
2073
|
-
hasCargoWorkspaceTable(parentCargoContent)) {
|
|
2074
|
-
// Test membership against the ORIGINAL crate dir (`root`), never the
|
|
2075
|
-
// walk cursor (`current`, which may already have climbed through
|
|
2076
|
-
// gap directories that have no Cargo.toml of their own) — a
|
|
2077
|
-
// `members = ["crates/foo"]` or `["crates/*"]` entry is relative to
|
|
2078
|
-
// the workspace root, spanning the whole gap in one hop (#1671 F1).
|
|
2079
|
-
if (cargoWorkspaceDeclaresMember(parentCargoContent, parent, root)) {
|
|
2080
|
-
return enforceLspRootCeiling(parent, sessionCwd, file);
|
|
2081
|
-
}
|
|
2082
|
-
// A workspace root exists here but its `members`/`exclude` tables do
|
|
2083
|
-
// not claim this crate — stop climbing; the crate stays independently
|
|
2084
|
-
// rooted rather than being swept into a workspace it opted out of.
|
|
2085
|
-
break;
|
|
2086
|
-
}
|
|
2087
|
-
current = parent;
|
|
2088
|
-
}
|
|
2089
|
-
return root;
|
|
2090
|
-
}, crateRoot.rootMarkers ?? []);
|
|
2091
|
-
}
|
|
2092
|
-
/**
|
|
2093
|
-
* Resolve whether `parentPomPath`'s <modules> block declares `childDir` as one
|
|
2094
|
-
* of its member modules. Maven multi-module hoisting must only chain through
|
|
2095
|
-
* poms that actually declare the child (an undeclared sibling directory that
|
|
2096
|
-
* merely happens to sit next to a parent pom stays independently rooted) —
|
|
2097
|
-
* this is the "Maven module-chain verification" #1671 asks for, as opposed to
|
|
2098
|
-
* rust-analyzer's simpler "any ancestor [workspace] wins" hoist.
|
|
2099
|
-
*/
|
|
2100
|
-
/**
|
|
2101
|
-
* Strip XML comments to a fixed point rather than in one pass: a single
|
|
2102
|
-
* `.replace()` can leave a residual `<!--` behind on adversarially-nested
|
|
2103
|
-
* input (CodeQL flags this class as "incomplete multi-character
|
|
2104
|
-
* sanitization" — the removal of one comment can expose a delimiter that
|
|
2105
|
-
* was itself inside another). Looping until nothing changes closes that gap.
|
|
2106
|
-
*/
|
|
2107
|
-
function stripXmlComments(content) {
|
|
2108
|
-
let result = content;
|
|
2109
|
-
let previous;
|
|
2110
|
-
do {
|
|
2111
|
-
previous = result;
|
|
2112
|
-
result = result.replace(/<!--[\s\S]*?-->/g, "");
|
|
2113
|
-
} while (result !== previous);
|
|
2114
|
-
return result;
|
|
2115
|
-
}
|
|
2116
|
-
async function declaresMavenModule(parentDir, parentPomPath, childDir) {
|
|
2117
|
-
try {
|
|
2118
|
-
const content = stripXmlComments(await readFile(parentPomPath, "utf-8"));
|
|
2119
|
-
const modulesBlock = content.match(/<modules>([\s\S]*?)<\/modules>/);
|
|
2120
|
-
if (!modulesBlock)
|
|
2121
|
-
return false;
|
|
2122
|
-
const resolvedChild = path.resolve(childDir);
|
|
2123
|
-
for (const match of modulesBlock[1].matchAll(/<module>\s*([^<]+?)\s*<\/module>/g)) {
|
|
2124
|
-
// Case-insensitive / realpath-aware compare (#1671 F8): a declared
|
|
2125
|
-
// `<module>Foo</module>` must still match a directory actually named
|
|
2126
|
-
// `foo` on a case-insensitive filesystem, the same class of check the
|
|
2127
|
-
// rest of this codebase does via `pathsEqual` (#1139/#1150).
|
|
2128
|
-
if (pathsEqual(path.resolve(parentDir, match[1]), resolvedChild))
|
|
2129
|
-
return true;
|
|
2130
|
-
}
|
|
2131
|
-
return false;
|
|
2132
|
-
}
|
|
2133
|
-
catch {
|
|
2134
|
-
return false;
|
|
2135
|
-
}
|
|
2136
|
-
}
|
|
2137
|
-
function JavaWorkspaceRoot() {
|
|
2138
|
-
const moduleRoot = createRootDetector([
|
|
2139
|
-
"pom.xml",
|
|
2140
|
-
"build.gradle",
|
|
2141
|
-
".classpath",
|
|
2142
|
-
]);
|
|
2143
|
-
return withRootMarkers(async (file) => {
|
|
2144
|
-
const root = await moduleRoot(file);
|
|
2145
|
-
if (!root)
|
|
2146
|
-
return undefined;
|
|
2147
|
-
// Only a Maven (pom.xml) module chain-hoists here — Gradle/.classpath module
|
|
2148
|
-
// roots are returned as found; multi-module Gradle wiring (settings.gradle)
|
|
2149
|
-
// is a different shape and out of scope for #1671.
|
|
2150
|
-
if (!(await markerExists(root, "pom.xml")))
|
|
2151
|
-
return root;
|
|
2152
|
-
const sessionCwd = process.cwd();
|
|
2153
|
-
const stop = sessionHoistCeiling(sessionCwd, file);
|
|
2154
|
-
// `current` is the walk CURSOR — it climbs over gap directories (ones
|
|
2155
|
-
// with no pom.xml of their own, e.g. a `<module>sub/dir</module>` entry
|
|
2156
|
-
// spanning more than one filesystem level). `lastHop` is the last
|
|
2157
|
-
// CONFIRMED module boundary; it is what the next ancestor pom's
|
|
2158
|
-
// <modules> must declare, and it is what actually gets returned — never
|
|
2159
|
-
// the cursor, which can land on a bare gap directory that is not itself
|
|
2160
|
-
// a valid module root (#1671 F2).
|
|
2161
|
-
let current = root;
|
|
2162
|
-
let lastHop = root;
|
|
2163
|
-
const fsRoot = path.parse(current).root;
|
|
2164
|
-
while (true) {
|
|
2165
|
-
if (stop !== undefined && current === stop)
|
|
2166
|
-
break;
|
|
2167
|
-
const parent = path.dirname(current);
|
|
2168
|
-
if (parent === current || parent === fsRoot)
|
|
2169
|
-
break;
|
|
2170
|
-
if (await markerExists(parent, "pom.xml")) {
|
|
2171
|
-
const parentPom = path.join(parent, "pom.xml");
|
|
2172
|
-
if (!(await declaresMavenModule(parent, parentPom, lastHop)))
|
|
2173
|
-
break;
|
|
2174
|
-
current = parent;
|
|
2175
|
-
lastHop = parent;
|
|
2176
|
-
continue;
|
|
2177
|
-
}
|
|
2178
|
-
// No pom.xml here — a gap directory, not a module boundary. Keep
|
|
2179
|
-
// climbing without advancing `lastHop`.
|
|
2180
|
-
current = parent;
|
|
2181
|
-
}
|
|
2182
|
-
return enforceLspRootCeiling(lastHop, sessionCwd, file);
|
|
2183
|
-
}, moduleRoot.rootMarkers ?? []);
|
|
2184
|
-
}
|
|
2185
|
-
export const RustServer = {
|
|
2186
|
-
id: "rust",
|
|
2187
|
-
name: "rust-analyzer",
|
|
2188
|
-
extensions: KIND_EXTENSIONS["rust"],
|
|
2189
|
-
// No FileDirRoot fallback (#201): rust-analyzer is a heavy workspace server
|
|
2190
|
-
// that is useless without a Cargo manifest. With the fallback, every .rs file
|
|
2191
|
-
// written before a Cargo.toml exists resolved to its OWN directory as the
|
|
2192
|
-
// root, and since clients dedup by `${serverId}:${root}`, each directory
|
|
2193
|
-
// spawned a separate rust-analyzer (one per file/dir during scaffolding).
|
|
2194
|
-
// Returning undefined here skips the spawn until a Cargo.toml gives a stable,
|
|
2195
|
-
// shared crate root — then all files share one server.
|
|
2196
|
-
root: RustWorkspaceRoot(),
|
|
2197
|
-
async spawn(root, options) {
|
|
2198
|
-
// Prefer rustup-installed rust-analyzer; fall back to GitHub-downloaded
|
|
2199
|
-
// managed copy. Canonical-bin discovery (#241): include ~/.cargo/bin so a
|
|
2200
|
-
// cargo/rustup-managed rust-analyzer resolves before paying for a download
|
|
2201
|
-
// even when ~/.cargo/bin isn't on PATH.
|
|
2202
|
-
const result = await resolveAndLaunch({
|
|
2203
|
-
candidates: cargoBinCandidates("rust-analyzer"),
|
|
2204
|
-
args: [],
|
|
2205
|
-
cwd: root,
|
|
2206
|
-
managedToolId: "rust-analyzer",
|
|
2207
|
-
}, options?.allowInstall);
|
|
2208
|
-
if (!result)
|
|
2209
|
-
return undefined;
|
|
2210
|
-
return {
|
|
2211
|
-
...result,
|
|
2212
|
-
initialization: {
|
|
2213
|
-
cargo: { buildScripts: { enable: true } },
|
|
2214
|
-
procMacro: { enable: true },
|
|
2215
|
-
diagnostics: { enable: true },
|
|
2216
|
-
},
|
|
2217
|
-
};
|
|
2218
|
-
},
|
|
2219
|
-
};
|
|
2220
|
-
export const RubyServer = {
|
|
2221
|
-
id: "ruby",
|
|
2222
|
-
name: "Ruby LSP",
|
|
2223
|
-
extensions: KIND_EXTENSIONS["ruby"],
|
|
2224
|
-
root: RootWithFallback(PriorityRoot([["Gemfile", ".ruby-version"], [".git"]])),
|
|
2225
|
-
// Ruby LSP may need extra time to finish composed-bundle setup before it can
|
|
2226
|
-
// answer initialize/documentSymbol on cold start.
|
|
2227
|
-
initializeTimeoutMs: 30_000,
|
|
2228
|
-
clientWaitTimeoutMs: 30_000,
|
|
2229
|
-
async spawn(root, options) {
|
|
2230
|
-
// Try ruby-lsp first, then solargraph, then rubocop --lsp
|
|
2231
|
-
// Each has different args so we can't use a single resolveAndLaunch call
|
|
2232
|
-
const rubylsp = await resolveAndLaunch({
|
|
2233
|
-
candidates: ["ruby-lsp", ...rubyBinCandidates("ruby-lsp")],
|
|
2234
|
-
args: [],
|
|
2235
|
-
cwd: root,
|
|
2236
|
-
runtimeInstall: {
|
|
2237
|
-
runtimeCommand: "gem",
|
|
2238
|
-
install: () => tryGemInstall("ruby-lsp"),
|
|
2239
|
-
retryCandidates: ["ruby-lsp", ...rubyBinCandidates("ruby-lsp")],
|
|
2240
|
-
},
|
|
2241
|
-
}, options?.allowInstall);
|
|
2242
|
-
if (rubylsp)
|
|
2243
|
-
return rubylsp;
|
|
2244
|
-
// Solargraph fallback
|
|
2245
|
-
const solargraph = await resolveAndLaunch({
|
|
2246
|
-
candidates: ["solargraph", ...rubyBinCandidates("solargraph")],
|
|
2247
|
-
args: ["stdio"],
|
|
2248
|
-
cwd: root,
|
|
2249
|
-
}, false);
|
|
2250
|
-
if (solargraph)
|
|
2251
|
-
return solargraph;
|
|
2252
|
-
// rubocop --lsp fallback
|
|
2253
|
-
return resolveAndLaunch({
|
|
2254
|
-
candidates: ["rubocop", ...rubyBinCandidates("rubocop")],
|
|
2255
|
-
args: ["--lsp"],
|
|
2256
|
-
cwd: root,
|
|
2257
|
-
}, false);
|
|
2258
|
-
},
|
|
2259
|
-
};
|
|
2260
|
-
// NOTE: Ruby's Solargraph + RuboCop fallbacks live INSIDE RubyServer.spawn
|
|
2261
|
-
// (ruby-lsp → solargraph → rubocop --lsp). Primary selection is first-success-
|
|
2262
|
-
// wins (one server per file, see LSPService.getClientForFile), so a separate
|
|
2263
|
-
// solargraph sibling server could never be reached — RubyServer only returns
|
|
2264
|
-
// undefined when solargraph is also absent. A standalone RubySolargraphServer
|
|
2265
|
-
// would therefore be dead code; it intentionally does not exist. If a future
|
|
2266
|
-
// user-selectable preferred-server config lands, refactor RubyServer to a
|
|
2267
|
-
// single binary and register the alternatives as siblings (cf. python/jedi).
|
|
2268
|
-
export const PHPServer = {
|
|
2269
|
-
id: "php",
|
|
2270
|
-
name: "Intelephense",
|
|
2271
|
-
extensions: KIND_EXTENSIONS["php"],
|
|
2272
|
-
root: RootWithFallback(createRootDetector(["composer.json", "composer.lock"])),
|
|
2273
|
-
async spawn(root, options) {
|
|
2274
|
-
const result = await resolveAndLaunch({
|
|
2275
|
-
candidates: nodeBinCandidates(root, "intelephense"),
|
|
2276
|
-
args: ["--stdio"],
|
|
2277
|
-
cwd: root,
|
|
2278
|
-
managedToolId: "intelephense",
|
|
2279
|
-
}, options?.allowInstall);
|
|
2280
|
-
if (!result)
|
|
2281
|
-
return undefined;
|
|
2282
|
-
return {
|
|
2283
|
-
...result,
|
|
2284
|
-
initialization: {
|
|
2285
|
-
storagePath: path.join(getGlobalPiLensDir(), "intelephense"),
|
|
2286
|
-
},
|
|
2287
|
-
};
|
|
2288
|
-
},
|
|
2289
|
-
};
|
|
2290
|
-
// PowerShell Editor Services bootstrap (#278). Builds the `pwsh`/`powershell`
|
|
2291
|
-
// args that launch the bundled Start-EditorServices.ps1 over stdio. Param set
|
|
2292
|
-
// verified against the PSES v4.6.0 bundle. Each spawn gets a private session dir
|
|
2293
|
-
// for the required Log/SessionDetails paths.
|
|
2294
|
-
function buildPsesArgs(bundleDir) {
|
|
2295
|
-
const script = path.join(bundleDir, "PowerShellEditorServices", "Start-EditorServices.ps1");
|
|
2296
|
-
const sessionDir = path.join(getGlobalPiLensDir(), "pses", `${process.pid}-${Date.now()}`);
|
|
2297
|
-
mkdirSync(sessionDir, { recursive: true });
|
|
2298
|
-
const logPath = path.join(sessionDir, "pses.log");
|
|
2299
|
-
const sessionDetailsPath = path.join(sessionDir, "session.json");
|
|
2300
|
-
// Use -File with each PSES parameter as a SEPARATE argv element (the canonical
|
|
2301
|
-
// editor launch form). This deliberately avoids `-Command "& '...'"`: pwsh.exe
|
|
2302
|
-
// commonly lives under "C:\Program Files\…" (a space), which forces launchLSP's
|
|
2303
|
-
// Windows shell path, and an embedded `&`/quotes in a single -Command string
|
|
2304
|
-
// gets mangled by cmd.exe. Plain argv tokens survive shell escaping (our paths
|
|
2305
|
-
// are under ~/.pi-lens, no spaces). -Stdio makes PSES speak LSP over this
|
|
2306
|
-
// process's stdin/stdout; -LanguageServiceOnly skips the debug adapter.
|
|
2307
|
-
return [
|
|
2308
|
-
"-NoLogo",
|
|
2309
|
-
"-NoProfile",
|
|
2310
|
-
"-NonInteractive",
|
|
2311
|
-
// Unsigned bundled script + mark-of-the-web on Windows — Bypass so it runs;
|
|
2312
|
-
// ignored by non-Windows pwsh.
|
|
2313
|
-
"-ExecutionPolicy",
|
|
2314
|
-
"Bypass",
|
|
2315
|
-
"-File",
|
|
2316
|
-
script,
|
|
2317
|
-
"-HostName",
|
|
2318
|
-
"pi-lens",
|
|
2319
|
-
"-HostProfileId",
|
|
2320
|
-
"pi-lens",
|
|
2321
|
-
"-HostVersion",
|
|
2322
|
-
"1.0.0",
|
|
2323
|
-
"-BundledModulesPath",
|
|
2324
|
-
bundleDir,
|
|
2325
|
-
"-LogPath",
|
|
2326
|
-
logPath,
|
|
2327
|
-
"-LogLevel",
|
|
2328
|
-
"Warning",
|
|
2329
|
-
"-SessionDetailsPath",
|
|
2330
|
-
sessionDetailsPath,
|
|
2331
|
-
"-Stdio",
|
|
2332
|
-
"-LanguageServiceOnly",
|
|
2333
|
-
];
|
|
2334
|
-
}
|
|
2335
|
-
export const PowerShellServer = {
|
|
2336
|
-
id: "powershell",
|
|
2337
|
-
name: "PowerShell Editor Services",
|
|
2338
|
-
extensions: KIND_EXTENSIONS["powershell"],
|
|
2339
|
-
// Index at the workspace (script modules reference siblings); fall back to the
|
|
2340
|
-
// file dir.
|
|
2341
|
-
root: RootWithFallback(createRootDetector([".git"])),
|
|
2342
|
-
spawn(root, options) {
|
|
2343
|
-
// PSES is a module bundle launched via pwsh, not a binary on PATH. Resolve
|
|
2344
|
-
// pwsh/powershell + the managed bundle, then launch the bootstrap over
|
|
2345
|
-
// stdio. Graceful skip (→ coverage notice) when pwsh or the bundle is
|
|
2346
|
-
// unavailable; psscriptanalyzer remains the fallback in the dispatch group.
|
|
2347
|
-
return resolveAndLaunchBundle({
|
|
2348
|
-
runtimeCandidates: ["pwsh", "powershell"],
|
|
2349
|
-
bundleToolId: "powershell-editor-services",
|
|
2350
|
-
cwd: root,
|
|
2351
|
-
args: buildPsesArgs,
|
|
2352
|
-
}, options?.allowInstall);
|
|
2353
|
-
},
|
|
2354
|
-
};
|
|
2355
|
-
export const CSharpServer = {
|
|
2356
|
-
id: "csharp",
|
|
2357
|
-
name: "csharp-ls",
|
|
2358
|
-
extensions: KIND_EXTENSIONS["csharp"],
|
|
2359
|
-
// No FileDirRoot fallback (#201): csharp-ls is a workspace server and should
|
|
2360
|
-
// not spawn once per source directory before a .sln/.csproj exists. Glob root
|
|
2361
|
-
// markers match real project filenames such as `App.csproj` / `App.sln`
|
|
2362
|
-
// (shared marker list — see file-kinds.ts, refs #895).
|
|
2363
|
-
root: createRootDetector([...DOTNET_CSHARP_ROOT_MARKERS]),
|
|
2364
|
-
async spawn(root, options) {
|
|
2365
|
-
const candidates = dotnetToolCandidates("csharp-ls");
|
|
2366
|
-
return resolveAndLaunch({
|
|
2367
|
-
candidates,
|
|
2368
|
-
args: [],
|
|
2369
|
-
cwd: root,
|
|
2370
|
-
runtimeInstall: {
|
|
2371
|
-
runtimeCommand: "dotnet",
|
|
2372
|
-
install: () => tryDotnetToolInstall("csharp-ls"),
|
|
2373
|
-
retryCandidates: candidates,
|
|
2374
|
-
},
|
|
2375
|
-
}, options?.allowInstall);
|
|
2376
|
-
},
|
|
2377
|
-
};
|
|
2378
|
-
export const OmniSharpServer = createInteractiveServer({
|
|
2379
|
-
id: "omnisharp",
|
|
2380
|
-
name: "OmniSharp",
|
|
2381
|
-
fallbackFor: "csharp",
|
|
2382
|
-
extensions: KIND_EXTENSIONS["csharp"],
|
|
2383
|
-
root: createRootDetector([...DOTNET_CSHARP_ROOT_MARKERS]),
|
|
2384
|
-
language: "csharp",
|
|
2385
|
-
command: "OmniSharp",
|
|
2386
|
-
args: ["--languageserver"],
|
|
2387
|
-
});
|
|
2388
|
-
export const FSharpServer = {
|
|
2389
|
-
id: "fsharp",
|
|
2390
|
-
name: "FSAutocomplete",
|
|
2391
|
-
extensions: KIND_EXTENSIONS["fsharp"],
|
|
2392
|
-
root: createRootDetector([...DOTNET_FSHARP_ROOT_MARKERS]),
|
|
2393
|
-
async spawn(root, options) {
|
|
2394
|
-
// fsautocomplete is a `dotnet tool` (#241), exactly like csharp-ls: prefer a
|
|
2395
|
-
// managed/.dotnet-tools copy, else `dotnet tool install` when the .NET SDK
|
|
2396
|
-
// is on PATH. dotnetToolCandidates covers the install target so the retry
|
|
2397
|
-
// resolves it.
|
|
2398
|
-
const candidates = dotnetToolCandidates("fsautocomplete");
|
|
2399
|
-
return resolveAndLaunch({
|
|
2400
|
-
candidates,
|
|
2401
|
-
args: [],
|
|
2402
|
-
cwd: root,
|
|
2403
|
-
runtimeInstall: {
|
|
2404
|
-
runtimeCommand: "dotnet",
|
|
2405
|
-
install: () => tryDotnetToolInstall("fsautocomplete"),
|
|
2406
|
-
retryCandidates: candidates,
|
|
2407
|
-
},
|
|
2408
|
-
}, options?.allowInstall);
|
|
2409
|
-
},
|
|
2410
|
-
};
|
|
2411
|
-
export const JavaServer = createInteractiveServer({
|
|
2412
|
-
id: "java",
|
|
2413
|
-
name: "JDT Language Server",
|
|
2414
|
-
extensions: KIND_EXTENSIONS["java"],
|
|
2415
|
-
root: RootWithFallback(JavaWorkspaceRoot()),
|
|
2416
|
-
language: "java",
|
|
2417
|
-
command: () => process.env.JDTLS_PATH || "jdtls",
|
|
2418
|
-
args: (root) => createLombokJdtlsArgs(root),
|
|
2419
|
-
runtime: "java",
|
|
2420
|
-
});
|
|
2421
|
-
export const KotlinServer = {
|
|
2422
|
-
id: "kotlin",
|
|
2423
|
-
name: "Kotlin Language Server",
|
|
2424
|
-
extensions: KIND_EXTENSIONS["kotlin"],
|
|
2425
|
-
root: RootWithFallback(createRootDetector(["build.gradle.kts", "build.gradle", "pom.xml"])),
|
|
2426
|
-
async spawn(root, options) {
|
|
2427
|
-
// Prefer the newer official Kotlin LSP CLI when available, but keep
|
|
2428
|
-
// compatibility with the older fwcd kotlin-language-server command.
|
|
2429
|
-
return resolveAndLaunch({
|
|
2430
|
-
candidates: ["kotlin-lsp", "kotlin-language-server"],
|
|
2431
|
-
args: [],
|
|
2432
|
-
cwd: root,
|
|
2433
|
-
}, options?.allowInstall);
|
|
2434
|
-
},
|
|
2435
|
-
};
|
|
2436
|
-
export const SwiftServer = createInteractiveServer({
|
|
2437
|
-
id: "swift",
|
|
2438
|
-
name: "SourceKit-LSP",
|
|
2439
|
-
extensions: KIND_EXTENSIONS["swift"],
|
|
2440
|
-
root: createRootDetector(["Package.swift"]),
|
|
2441
|
-
language: "swift",
|
|
2442
|
-
command: "sourcekit-lsp",
|
|
2443
|
-
});
|
|
2444
|
-
export const DartServer = createInteractiveServer({
|
|
2445
|
-
id: "dart",
|
|
2446
|
-
name: "Dart Analysis Server",
|
|
2447
|
-
extensions: KIND_EXTENSIONS["dart"],
|
|
2448
|
-
root: RootWithFallback(createRootDetector(["pubspec.yaml"])),
|
|
2449
|
-
language: "dart",
|
|
2450
|
-
command: "dart",
|
|
2451
|
-
args: ["language-server", "--protocol=lsp"],
|
|
2452
|
-
});
|
|
2453
|
-
/**
|
|
2454
|
-
* Build an {@link LSPServerInfo} for a language server that ships as a
|
|
2455
|
-
* self-contained native TREE BUNDLE (single archive, `bin/<binary>` inside,
|
|
2456
|
-
* no external runtime — clangd #241, lua-language-server #564, and the
|
|
2457
|
-
* kotlin-language-server/elixir-ls follow-on #565). Extracted once both
|
|
2458
|
-
* `CppServer` and `LuaServer` turned out to be a near-verbatim structural
|
|
2459
|
-
* copy of each other (same `resolveAndLaunchTreeBinary` call shape) —
|
|
2460
|
-
* flagged by SonarCloud's new-code duplication gate on PR #567 — so a third
|
|
2461
|
-
* and fourth server of this shape (#565) can call this instead of
|
|
2462
|
-
* copy-pasting a `spawn` again. Each server's own "why archive-tree, why
|
|
2463
|
-
* stripComponents differs, etc." explanation stays as a comment at its own
|
|
2464
|
-
* call site below, since that reasoning is genuinely per-server.
|
|
2465
|
-
*/
|
|
2466
|
-
function createTreeBinaryServer(spec) {
|
|
2467
|
-
return {
|
|
2468
|
-
id: spec.id,
|
|
2469
|
-
name: spec.name,
|
|
2470
|
-
extensions: spec.extensions,
|
|
2471
|
-
root: spec.root,
|
|
2472
|
-
spawn(root, options) {
|
|
2473
|
-
return resolveAndLaunchTreeBinary({
|
|
2474
|
-
candidates: [spec.binaryName],
|
|
2475
|
-
bundleToolId: spec.binaryName,
|
|
2476
|
-
binRelPath: spec.binRelPath,
|
|
2477
|
-
cwd: root,
|
|
2478
|
-
args: spec.args ?? [],
|
|
2479
|
-
}, options?.allowInstall);
|
|
2480
|
-
},
|
|
2481
|
-
};
|
|
2482
|
-
}
|
|
2483
|
-
// lua-language-server ships the same self-contained native TREE BUNDLE shape
|
|
2484
|
-
// as clangd (#241/#564): bin/lua-language-server + bundled locale/meta files,
|
|
2485
|
-
// no external runtime. Prefer a system install on PATH; else auto-install the
|
|
2486
|
-
// managed bundle and launch bin/lua-language-server within it. Graceful skip
|
|
2487
|
-
// when neither is available (→ coverage notice).
|
|
2488
|
-
export const LuaServer = createTreeBinaryServer({
|
|
2489
|
-
id: "lua",
|
|
2490
|
-
name: "Lua Language Server",
|
|
2491
|
-
extensions: KIND_EXTENSIONS["lua"],
|
|
2492
|
-
root: createRootDetector([".luarc.json", ".luacheckrc"]),
|
|
2493
|
-
binaryName: "lua-language-server",
|
|
2494
|
-
binRelPath: "bin/lua-language-server",
|
|
2495
|
-
});
|
|
2496
|
-
// clangd ships a self-contained native tree bundle (bin/clangd + bundled
|
|
2497
|
-
// libclang headers). Prefer a system clangd on PATH; else auto-install the
|
|
2498
|
-
// managed bundle (#241) and launch bin/clangd within it. Graceful skip when
|
|
2499
|
-
// neither is available (→ coverage notice); cpp-check stays the fallback.
|
|
2500
|
-
export const CppServer = createTreeBinaryServer({
|
|
2501
|
-
id: "cpp",
|
|
2502
|
-
name: "clangd",
|
|
2503
|
-
extensions: KIND_EXTENSIONS["cxx"],
|
|
2504
|
-
root: RootWithFallback(createRootDetector([
|
|
2505
|
-
"compile_commands.json",
|
|
2506
|
-
".clangd",
|
|
2507
|
-
"CMakeLists.txt",
|
|
2508
|
-
"Makefile",
|
|
2509
|
-
])),
|
|
2510
|
-
binaryName: "clangd",
|
|
2511
|
-
binRelPath: "bin/clangd",
|
|
2512
|
-
args: ["--background-index"],
|
|
2513
|
-
});
|
|
2514
|
-
export const ZigServer = {
|
|
2515
|
-
id: "zig",
|
|
2516
|
-
name: "ZLS",
|
|
2517
|
-
extensions: KIND_EXTENSIONS["zig"],
|
|
2518
|
-
root: RootWithFallback(createRootDetector(["build.zig"])),
|
|
2519
|
-
spawn(root, options) {
|
|
2520
|
-
return resolveAndLaunch({
|
|
2521
|
-
candidates: ["zls"],
|
|
2522
|
-
args: [],
|
|
2523
|
-
cwd: root,
|
|
2524
|
-
managedToolId: "zls",
|
|
2525
|
-
}, options?.allowInstall);
|
|
2526
|
-
},
|
|
2527
|
-
};
|
|
2528
|
-
export const HaskellServer = createInteractiveServer({
|
|
2529
|
-
id: "haskell",
|
|
2530
|
-
name: "Haskell Language Server",
|
|
2531
|
-
extensions: KIND_EXTENSIONS["haskell"],
|
|
2532
|
-
root: createRootDetector(["stack.yaml", "cabal.project", "*.cabal"]),
|
|
2533
|
-
language: "haskell",
|
|
2534
|
-
command: "haskell-language-server-wrapper",
|
|
2535
|
-
args: ["--lsp"],
|
|
2536
|
-
});
|
|
2537
|
-
export const ElixirServer = createInteractiveServer({
|
|
2538
|
-
id: "elixir",
|
|
2539
|
-
name: "ElixirLS",
|
|
2540
|
-
extensions: KIND_EXTENSIONS["elixir"],
|
|
2541
|
-
root: RootWithFallback(createRootDetector(["mix.exs"])),
|
|
2542
|
-
language: "elixir",
|
|
2543
|
-
command: "elixir-ls",
|
|
2544
|
-
});
|
|
2545
|
-
export const ElixirExpertServer = {
|
|
2546
|
-
id: "expert",
|
|
2547
|
-
name: "Expert",
|
|
2548
|
-
fallbackFor: "elixir",
|
|
2549
|
-
extensions: KIND_EXTENSIONS["elixir"],
|
|
2550
|
-
root: RootWithFallback(createRootDetector(["mix.exs"])),
|
|
2551
|
-
availabilityKey: "expert",
|
|
2552
|
-
async spawn(root, options) {
|
|
2553
|
-
return resolveAndLaunch({
|
|
2554
|
-
candidates: ["expert"],
|
|
2555
|
-
args: ["--stdio"],
|
|
2556
|
-
cwd: root,
|
|
2557
|
-
managedToolId: "expert",
|
|
2558
|
-
}, options?.allowInstall);
|
|
2559
|
-
},
|
|
2560
|
-
autoInstall: async () => Boolean(await ensureTool("expert")),
|
|
2561
|
-
};
|
|
2562
|
-
export const GleamServer = {
|
|
2563
|
-
id: "gleam",
|
|
2564
|
-
name: "Gleam LSP",
|
|
2565
|
-
extensions: KIND_EXTENSIONS["gleam"],
|
|
2566
|
-
root: RootWithFallback(createRootDetector(["gleam.toml"])),
|
|
2567
|
-
async spawn(root, options) {
|
|
2568
|
-
// Prefer a PATH `gleam` (full toolchain); fall back to the managed
|
|
2569
|
-
// GitHub-release binary. `gleam lsp` is the server entrypoint either way.
|
|
2570
|
-
return resolveAndLaunch({
|
|
2571
|
-
candidates: ["gleam"],
|
|
2572
|
-
args: ["lsp"],
|
|
2573
|
-
cwd: root,
|
|
2574
|
-
managedToolId: "gleam",
|
|
2575
|
-
}, options?.allowInstall);
|
|
2576
|
-
},
|
|
2577
|
-
};
|
|
2578
|
-
export const TinymistServer = {
|
|
2579
|
-
id: "tinymist",
|
|
2580
|
-
name: "Tinymist",
|
|
2581
|
-
extensions: extensionsForLanguage("typst"),
|
|
2582
|
-
root: RootWithFallback(createRootDetector(["typst.toml", ".git"])),
|
|
2583
|
-
availabilityKey: "tinymist",
|
|
2584
|
-
async spawn(root, options) {
|
|
2585
|
-
return resolveAndLaunch({
|
|
2586
|
-
candidates: ["tinymist"],
|
|
2587
|
-
args: ["lsp"],
|
|
2588
|
-
cwd: root,
|
|
2589
|
-
managedToolId: "tinymist",
|
|
2590
|
-
}, options?.allowInstall);
|
|
2591
|
-
},
|
|
2592
|
-
};
|
|
2593
|
-
export const MarksmanServer = {
|
|
2594
|
-
id: "marksman",
|
|
2595
|
-
name: "Marksman",
|
|
2596
|
-
extensions: KIND_EXTENSIONS["markdown"],
|
|
2597
|
-
// Index at the workspace root so cross-file checks (broken intra-repo links,
|
|
2598
|
-
// missing/renamed anchors, heading refs) see the whole tree; fall back to the
|
|
2599
|
-
// file's directory when there's no project marker.
|
|
2600
|
-
root: RootWithFallback(createRootDetector([".marksman.toml", ".git"])),
|
|
2601
|
-
spawn(root, options) {
|
|
2602
|
-
// Prefer a PATH `marksman`; fall back to the managed GitHub-release binary.
|
|
2603
|
-
// `marksman server` is the stdio LSP entrypoint either way.
|
|
2604
|
-
return resolveAndLaunch({
|
|
2605
|
-
candidates: ["marksman"],
|
|
2606
|
-
args: ["server"],
|
|
2607
|
-
cwd: root,
|
|
2608
|
-
managedToolId: "marksman",
|
|
2609
|
-
}, options?.allowInstall);
|
|
2610
|
-
},
|
|
2611
|
-
};
|
|
2612
|
-
export const OCamlServer = createInteractiveServer({
|
|
2613
|
-
id: "ocaml",
|
|
2614
|
-
name: "ocamllsp",
|
|
2615
|
-
extensions: KIND_EXTENSIONS["ocaml"],
|
|
2616
|
-
root: createRootDetector(["dune-project", "opam"]),
|
|
2617
|
-
language: "ocaml",
|
|
2618
|
-
command: "ocamllsp",
|
|
2619
|
-
});
|
|
2620
|
-
export const ClojureServer = {
|
|
2621
|
-
id: "clojure",
|
|
2622
|
-
name: "Clojure LSP",
|
|
2623
|
-
extensions: KIND_EXTENSIONS["clojure"],
|
|
2624
|
-
root: createRootDetector(["deps.edn", "project.clj"]),
|
|
2625
|
-
async spawn(root, options) {
|
|
2626
|
-
// Prefer a PATH `clojure-lsp`; fall back to the managed self-contained
|
|
2627
|
-
// native (GraalVM) GitHub-release binary — no JVM needed either way.
|
|
2628
|
-
return resolveAndLaunch({
|
|
2629
|
-
candidates: ["clojure-lsp"],
|
|
2630
|
-
args: [],
|
|
2631
|
-
cwd: root,
|
|
2632
|
-
managedToolId: "clojure-lsp",
|
|
2633
|
-
}, options?.allowInstall);
|
|
2634
|
-
},
|
|
2635
|
-
};
|
|
2636
|
-
export const CueServer = {
|
|
2637
|
-
id: "cue",
|
|
2638
|
-
name: "CUE Language Server",
|
|
2639
|
-
extensions: KIND_EXTENSIONS["cue"],
|
|
2640
|
-
root: RootWithFallback(createRootDetector(["cue.mod", ".git"])),
|
|
2641
|
-
async spawn(root, options) {
|
|
2642
|
-
return resolveAndLaunch({
|
|
2643
|
-
candidates: ["cue"],
|
|
2644
|
-
args: ["lsp", "serve"],
|
|
2645
|
-
cwd: root,
|
|
2646
|
-
managedToolId: "cue",
|
|
2647
|
-
}, options?.allowInstall);
|
|
2648
|
-
},
|
|
2649
|
-
};
|
|
2650
|
-
export const TerraformServer = {
|
|
2651
|
-
id: "terraform",
|
|
2652
|
-
name: "Terraform LSP",
|
|
2653
|
-
extensions: KIND_EXTENSIONS["terraform"],
|
|
2654
|
-
root: RootWithFallback(createRootDetector([".terraform.lock.hcl", ".terraform"])),
|
|
2655
|
-
spawn(root, options) {
|
|
2656
|
-
return resolveAndLaunch({
|
|
2657
|
-
candidates: ["terraform-ls"],
|
|
2658
|
-
args: ["serve"],
|
|
2659
|
-
cwd: root,
|
|
2660
|
-
managedToolId: "terraform-ls",
|
|
2661
|
-
}, options?.allowInstall);
|
|
2662
|
-
},
|
|
2663
|
-
};
|
|
2664
|
-
export const NixServer = createInteractiveServer({
|
|
2665
|
-
id: "nix",
|
|
2666
|
-
name: "nixd",
|
|
2667
|
-
extensions: KIND_EXTENSIONS["nix"],
|
|
2668
|
-
root: createRootDetector(["flake.nix"]),
|
|
2669
|
-
language: "nix",
|
|
2670
|
-
command: "nixd",
|
|
2671
|
-
});
|
|
2672
|
-
export const BashServer = {
|
|
2673
|
-
id: "bash",
|
|
2674
|
-
name: "Bash Language Server",
|
|
2675
|
-
extensions: [".bash", ".sh", ".zsh"],
|
|
2676
|
-
root: FileDirRoot,
|
|
2677
|
-
// #2194 bounded the installer's own verification at 20s; the dispatch
|
|
2678
|
-
// touch's client-wait floor stayed at the shared 5s default, so a cold
|
|
2679
|
-
// spawn could still race that budget and read as unavailable even after
|
|
2680
|
-
// installer verification cleared it. Match the installer bound (#2169).
|
|
2681
|
-
// `initializeTimeoutMs` matches it too: the measured cold start
|
|
2682
|
-
// (9,667ms, #2194) has headroom under the unraised 15s default, but
|
|
2683
|
-
// leaving the two fields mismatched is the same latent shape that made
|
|
2684
|
-
// the Prisma/Vue raise a no-op (fix-round F1, #2233) — a slower host
|
|
2685
|
-
// than the one measured would still hit the 15s inner kill despite the
|
|
2686
|
-
// caller being willing to wait 20s. Equalizing costs nothing on the
|
|
2687
|
-
// success path and removes the mismatch outright.
|
|
2688
|
-
clientWaitTimeoutMs: 20_000,
|
|
2689
|
-
initializeTimeoutMs: 20_000,
|
|
2690
|
-
spawn(root, options) {
|
|
2691
|
-
return resolveAndLaunch({
|
|
2692
|
-
candidates: nodeBinCandidates(root, "bash-language-server"),
|
|
2693
|
-
args: ["start"],
|
|
2694
|
-
cwd: root,
|
|
2695
|
-
managedToolId: "bash-language-server",
|
|
2696
|
-
}, options?.allowInstall);
|
|
2697
|
-
},
|
|
2698
|
-
};
|
|
2699
|
-
export const FishServer = {
|
|
2700
|
-
id: "fish",
|
|
2701
|
-
name: "Fish Language Server",
|
|
2702
|
-
extensions: KIND_EXTENSIONS["fish"],
|
|
2703
|
-
root: RootWithFallback(createRootDetector([".git"])),
|
|
2704
|
-
spawn(root, options) {
|
|
2705
|
-
return resolveAndLaunch({
|
|
2706
|
-
candidates: nodeBinCandidates(root, "fish-lsp"),
|
|
2707
|
-
args: ["start", "--stdio"],
|
|
2708
|
-
cwd: root,
|
|
2709
|
-
managedToolId: "fish-lsp",
|
|
2710
|
-
}, options?.allowInstall);
|
|
2711
|
-
},
|
|
2712
|
-
};
|
|
2713
|
-
export const CMakeServer = {
|
|
2714
|
-
id: "cmake",
|
|
2715
|
-
name: "CMake Language Server",
|
|
2716
|
-
// CMake's canonical project file has no .cmake suffix. The configured-server
|
|
2717
|
-
// matcher supports exact basenames as well as extensions.
|
|
2718
|
-
extensions: [...KIND_EXTENSIONS["cmake"], "CMakeLists.txt"],
|
|
2719
|
-
root: RootWithFallback(createRootDetector(["CMakeLists.txt", ".git"])),
|
|
2720
|
-
spawn(root, options) {
|
|
2721
|
-
return resolveAndLaunch({
|
|
2722
|
-
candidates: ["cmake-language-server"],
|
|
2723
|
-
args: [],
|
|
2724
|
-
cwd: root,
|
|
2725
|
-
managedToolId: "cmake-language-server",
|
|
2726
|
-
}, options?.allowInstall);
|
|
2727
|
-
},
|
|
2728
|
-
};
|
|
2729
|
-
export const DockerServer = {
|
|
2730
|
-
id: "docker",
|
|
2731
|
-
name: "Dockerfile Language Server",
|
|
2732
|
-
extensions: [".dockerfile", "Dockerfile"],
|
|
2733
|
-
root: RootWithFallback(PriorityRoot([
|
|
2734
|
-
[
|
|
2735
|
-
"docker-compose.yml",
|
|
2736
|
-
"docker-compose.yaml",
|
|
2737
|
-
"compose.yml",
|
|
2738
|
-
"compose.yaml",
|
|
2739
|
-
],
|
|
2740
|
-
[".git"],
|
|
2741
|
-
])),
|
|
2742
|
-
spawn(root, options) {
|
|
2743
|
-
return resolveAndLaunch({
|
|
2744
|
-
candidates: nodeBinCandidates(root, "docker-langserver"),
|
|
2745
|
-
args: ["--stdio"],
|
|
2746
|
-
cwd: root,
|
|
2747
|
-
managedToolId: "dockerfile-language-server-nodejs",
|
|
2748
|
-
}, options?.allowInstall);
|
|
2749
|
-
},
|
|
2750
|
-
};
|
|
2751
|
-
export const YamlServer = {
|
|
2752
|
-
id: "yaml",
|
|
2753
|
-
name: "YAML Language Server",
|
|
2754
|
-
extensions: KIND_EXTENSIONS["yaml"],
|
|
2755
|
-
root: RootWithFallback(PriorityRoot([
|
|
2756
|
-
[".yamllint", "yamllint.yml", "yamllint.yaml", "pyproject.toml"],
|
|
2757
|
-
[".git"],
|
|
2758
|
-
])),
|
|
2759
|
-
spawn(root, options) {
|
|
2760
|
-
return resolveAndLaunch({
|
|
2761
|
-
candidates: nodeBinCandidates(root, "yaml-language-server"),
|
|
2762
|
-
args: ["--stdio"],
|
|
2763
|
-
cwd: root,
|
|
2764
|
-
managedToolId: "yaml-language-server",
|
|
2765
|
-
}, options?.allowInstall);
|
|
2766
|
-
},
|
|
2767
|
-
};
|
|
2768
|
-
export const JsonServer = {
|
|
2769
|
-
id: "json",
|
|
2770
|
-
name: "VSCode JSON Language Server",
|
|
2771
|
-
extensions: KIND_EXTENSIONS["json"],
|
|
2772
|
-
root: RootWithFallback(WorkspacePriorityRoot([
|
|
2773
|
-
["package.json", "tsconfig.json", "jsconfig.json"],
|
|
2774
|
-
[".git"],
|
|
2775
|
-
])),
|
|
2776
|
-
// See BashServer above: the installer's 20s verification bound (#2194)
|
|
2777
|
-
// does not, on its own, raise the dispatch touch's cold-spawn wait floor.
|
|
2778
|
-
// Mirror it here so a cold spawn cannot lose that race either (#2169).
|
|
2779
|
-
// `initializeTimeoutMs` matches it for the same reason as BashServer
|
|
2780
|
-
// above (fix-round F1, #2233).
|
|
2781
|
-
clientWaitTimeoutMs: 20_000,
|
|
2782
|
-
initializeTimeoutMs: 20_000,
|
|
2783
|
-
spawn(root, options) {
|
|
2784
|
-
return resolveAndLaunch({
|
|
2785
|
-
candidates: ["vscode-json-language-server"],
|
|
2786
|
-
args: ["--stdio"],
|
|
2787
|
-
cwd: root,
|
|
2788
|
-
managedToolId: "vscode-json-language-server",
|
|
2789
|
-
}, options?.allowInstall);
|
|
2790
|
-
},
|
|
2791
|
-
};
|
|
2792
|
-
export const HtmlServer = {
|
|
2793
|
-
id: "html",
|
|
2794
|
-
name: "VSCode HTML Language Server",
|
|
2795
|
-
extensions: KIND_EXTENSIONS["html"],
|
|
2796
|
-
root: RootWithFallback(IgnoreHomeRoot(PriorityRoot([["package.json", "index.html", "vite.config.ts"]]))),
|
|
2797
|
-
spawn(root, options) {
|
|
2798
|
-
return resolveAndLaunch({
|
|
2799
|
-
candidates: nodeBinCandidates(root, "vscode-html-language-server"),
|
|
2800
|
-
args: ["--stdio"],
|
|
2801
|
-
cwd: root,
|
|
2802
|
-
managedToolId: "vscode-html-languageserver-bin",
|
|
2803
|
-
}, options?.allowInstall);
|
|
2804
|
-
},
|
|
2805
|
-
};
|
|
2806
|
-
export const TomlServer = {
|
|
2807
|
-
id: "toml",
|
|
2808
|
-
name: "Taplo",
|
|
2809
|
-
extensions: KIND_EXTENSIONS["toml"],
|
|
2810
|
-
root: RootWithFallback(PriorityRoot([["pyproject.toml", "Cargo.toml", "taplo.toml"], [".git"]])),
|
|
2811
|
-
spawn(root, options) {
|
|
2812
|
-
return resolveAndLaunch({
|
|
2813
|
-
candidates: ["taplo"],
|
|
2814
|
-
args: ["lsp", "stdio"],
|
|
2815
|
-
cwd: root,
|
|
2816
|
-
managedToolId: "taplo",
|
|
2817
|
-
}, options?.allowInstall);
|
|
2818
|
-
},
|
|
2819
|
-
};
|
|
2820
|
-
export const PrismaServer = {
|
|
2821
|
-
id: "prisma",
|
|
2822
|
-
name: "Prisma Language Server",
|
|
2823
|
-
extensions: KIND_EXTENSIONS["prisma"],
|
|
2824
|
-
root: RootWithFallback(createRootDetector(["prisma/schema.prisma", "schema.prisma"])),
|
|
2825
|
-
// Matches the installer's 40s verification bound above (#2169): the
|
|
2826
|
-
// dispatch touch's cold-spawn wait floor defaults to 5s and would
|
|
2827
|
-
// otherwise time the client out well before the binary answers.
|
|
2828
|
-
// `initializeTimeoutMs` MUST be set alongside it (RubyServer's own
|
|
2829
|
-
// precedent below): unset, it falls back to the 15s
|
|
2830
|
-
// `INITIALIZE_TIMEOUT_MS` default in `clients/lsp/client.ts`, which
|
|
2831
|
-
// hard-kills the child mid-handshake — a real cold Prisma run measured
|
|
2832
|
-
// up to 27.3s, so the spawn would die there before this wait ever gets
|
|
2833
|
-
// a chance to matter (fix-round F1, #2233).
|
|
2834
|
-
clientWaitTimeoutMs: 40_000,
|
|
2835
|
-
initializeTimeoutMs: 40_000,
|
|
2836
|
-
spawn(root, options) {
|
|
2837
|
-
return resolveAndLaunch({
|
|
2838
|
-
candidates: nodeBinCandidates(root, "prisma-language-server"),
|
|
2839
|
-
args: ["--stdio"],
|
|
2840
|
-
cwd: root,
|
|
2841
|
-
managedToolId: "@prisma/language-server",
|
|
2842
|
-
}, options?.allowInstall);
|
|
2843
|
-
},
|
|
2844
|
-
};
|
|
2845
|
-
// --- Web Framework & Styling Servers ---
|
|
2846
|
-
export const VueServer = {
|
|
2847
|
-
id: "vue",
|
|
2848
|
-
name: "Vue Language Server",
|
|
2849
|
-
extensions: [".vue"],
|
|
2850
|
-
root: RootWithFallback(IgnoreHomeRoot(createRootDetector([
|
|
2851
|
-
"package.json",
|
|
2852
|
-
"package-lock.json",
|
|
2853
|
-
"bun.lockb",
|
|
2854
|
-
"bun.lock",
|
|
2855
|
-
"pnpm-lock.yaml",
|
|
2856
|
-
"yarn.lock",
|
|
2857
|
-
]))),
|
|
2858
|
-
// Vue's launcher loads the full language-service bundle before answering,
|
|
2859
|
-
// matching the installer's 30s verification bound (#2176). The dispatch
|
|
2860
|
-
// touch's cold-spawn wait floor defaults to 5s and needs the same raise so
|
|
2861
|
-
// a cold spawn cannot time out there instead. `initializeTimeoutMs` MUST
|
|
2862
|
-
// match: unset, it falls back to the 15s default in `clients/lsp/client.ts`
|
|
2863
|
-
// and hard-kills a cold spawn (measured up to 22.6s in #2188, 16.2s here)
|
|
2864
|
-
// mid-handshake before this wait can matter (fix-round F1, #2233).
|
|
2865
|
-
clientWaitTimeoutMs: 30_000,
|
|
2866
|
-
initializeTimeoutMs: 30_000,
|
|
2867
|
-
async spawn(root, options) {
|
|
2868
|
-
const tsserverPath = await findTsserverPath(root, options?.allowInstall);
|
|
2869
|
-
// Vue Language Server needs Vue dependencies installed to resolve types.
|
|
2870
|
-
// Without node_modules, navigation requests will timeout or return empty.
|
|
2871
|
-
const hasPackageJson = existsSync(path.join(root, "package.json"));
|
|
2872
|
-
const hasNodeModules = existsSync(path.join(root, "node_modules"));
|
|
2873
|
-
if (hasPackageJson && !hasNodeModules) {
|
|
2874
|
-
logSessionStart(`lsp vue: node_modules missing in ${root} — Vue navigation may be limited. ` +
|
|
2875
|
-
`Run: npm install (or pnpm/yarn install) in this project.`);
|
|
2876
|
-
}
|
|
2877
|
-
const proc = await resolveAndLaunch({
|
|
2878
|
-
candidates: nodeBinCandidates(root, "vue-language-server"),
|
|
2879
|
-
args: ["--stdio"],
|
|
2880
|
-
cwd: root,
|
|
2881
|
-
managedToolId: "@vue/language-server",
|
|
2882
|
-
}, options?.allowInstall);
|
|
2883
|
-
if (!proc)
|
|
2884
|
-
return undefined;
|
|
2885
|
-
return {
|
|
2886
|
-
process: proc.process,
|
|
2887
|
-
source: proc.source,
|
|
2888
|
-
initialization: tsserverPath
|
|
2889
|
-
? { typescript: { tsdk: path.dirname(tsserverPath) } }
|
|
2890
|
-
: undefined,
|
|
2891
|
-
};
|
|
2892
|
-
},
|
|
2893
|
-
};
|
|
2894
|
-
export const SvelteServer = {
|
|
2895
|
-
id: "svelte",
|
|
2896
|
-
name: "Svelte Language Server",
|
|
2897
|
-
extensions: [".svelte"],
|
|
2898
|
-
root: RootWithFallback(IgnoreHomeRoot(createRootDetector([
|
|
2899
|
-
"package.json",
|
|
2900
|
-
"package-lock.json",
|
|
2901
|
-
"bun.lockb",
|
|
2902
|
-
"bun.lock",
|
|
2903
|
-
"pnpm-lock.yaml",
|
|
2904
|
-
"yarn.lock",
|
|
2905
|
-
]))),
|
|
2906
|
-
// Matches the installer's 20s verification bound above (#2169): the
|
|
2907
|
-
// dispatch touch's cold-spawn wait floor defaults to 5s and would
|
|
2908
|
-
// otherwise time the client out well before the binary answers.
|
|
2909
|
-
// `initializeTimeoutMs` matches it for the same reason as BashServer
|
|
2910
|
-
// above (fix-round F1, #2233).
|
|
2911
|
-
clientWaitTimeoutMs: 20_000,
|
|
2912
|
-
initializeTimeoutMs: 20_000,
|
|
2913
|
-
async spawn(root, options) {
|
|
2914
|
-
const tsserverPath = await findTsserverPath(root, options?.allowInstall);
|
|
2915
|
-
const proc = await resolveAndLaunch({
|
|
2916
|
-
candidates: [
|
|
2917
|
-
...nodeBinCandidates(root, "svelteserver"),
|
|
2918
|
-
...nodeBinCandidates(root, "svelte-language-server"),
|
|
2919
|
-
],
|
|
2920
|
-
args: ["--stdio"],
|
|
2921
|
-
cwd: root,
|
|
2922
|
-
managedToolId: "svelte-language-server",
|
|
2923
|
-
}, options?.allowInstall);
|
|
2924
|
-
if (!proc)
|
|
2925
|
-
return undefined;
|
|
2926
|
-
return {
|
|
2927
|
-
process: proc.process,
|
|
2928
|
-
source: proc.source,
|
|
2929
|
-
initialization: tsserverPath
|
|
2930
|
-
? { typescript: { tsdk: path.dirname(tsserverPath) } }
|
|
2931
|
-
: undefined,
|
|
2932
|
-
};
|
|
2933
|
-
},
|
|
2934
|
-
};
|
|
2935
|
-
export const CssServer = {
|
|
2936
|
-
id: "css",
|
|
2937
|
-
name: "CSS Language Server",
|
|
2938
|
-
extensions: KIND_EXTENSIONS["css"],
|
|
2939
|
-
root: RootWithFallback(IgnoreHomeRoot(PriorityRoot([
|
|
2940
|
-
[
|
|
2941
|
-
"package.json",
|
|
2942
|
-
"postcss.config.js",
|
|
2943
|
-
"tailwind.config.js",
|
|
2944
|
-
"vite.config.ts",
|
|
2945
|
-
],
|
|
2946
|
-
]))),
|
|
2947
|
-
spawn(root, options) {
|
|
2948
|
-
return resolveAndLaunch({
|
|
2949
|
-
candidates: nodeBinCandidates(root, "vscode-css-language-server"),
|
|
2950
|
-
args: ["--stdio"],
|
|
2951
|
-
cwd: root,
|
|
2952
|
-
managedToolId: "vscode-css-languageserver",
|
|
2953
|
-
}, options?.allowInstall);
|
|
2954
|
-
},
|
|
2955
|
-
};
|
|
2956
|
-
// --- Registry ---
|
|
2957
|
-
// Opengrep — a cross-language security scanner that speaks LSP. Unlike the
|
|
2958
|
-
// per-language servers it attaches to MANY file kinds (the aggregation layer
|
|
2959
|
-
// merges its diagnostics with the file's real language server). Running it as a
|
|
2960
|
-
// warm LSP server compiles the ruleset once per session instead of paying it on
|
|
2961
|
-
// every file (the ~8s CLI-per-file cost #111), bringing per-file scans to ~1.3s.
|
|
2962
|
-
// Rules load via `initializationOptions.scan.configuration` (a local rule file
|
|
2963
|
-
// if the repo has one, else Opengrep's login-free `auto` set).
|
|
2964
|
-
const OPENGREP_KINDS = [
|
|
2965
|
-
"csharp",
|
|
2966
|
-
"css",
|
|
2967
|
-
"cxx",
|
|
2968
|
-
"dart",
|
|
2969
|
-
"docker",
|
|
2970
|
-
"go",
|
|
2971
|
-
"html",
|
|
2972
|
-
"java",
|
|
2973
|
-
"json",
|
|
2974
|
-
"jsts",
|
|
2975
|
-
"kotlin",
|
|
2976
|
-
"lua",
|
|
2977
|
-
"php",
|
|
2978
|
-
"python",
|
|
2979
|
-
"ruby",
|
|
2980
|
-
"rust",
|
|
2981
|
-
"shell",
|
|
2982
|
-
"swift",
|
|
2983
|
-
"terraform",
|
|
2984
|
-
"yaml",
|
|
2985
|
-
];
|
|
2986
|
-
const OPENGREP_EXTENSIONS = Array.from(new Set(OPENGREP_KINDS.flatMap((k) => KIND_EXTENSIONS[k] ?? [])));
|
|
2987
|
-
function opengrepInitialization(root) {
|
|
2988
|
-
// As an always-on LSP server, enablement is structural (the server is
|
|
2989
|
-
// registered); resolveOpengrepConfig here only chooses WHICH rules — a local
|
|
2990
|
-
// rule file if present, otherwise `auto`.
|
|
2991
|
-
const resolved = resolveOpengrepConfig(root, { enabled: true });
|
|
2992
|
-
return {
|
|
2993
|
-
scan: {
|
|
2994
|
-
configuration: [resolved.configArg ?? "auto"],
|
|
2995
|
-
onlyGitDirty: false,
|
|
2996
|
-
jobs: 16,
|
|
2997
|
-
},
|
|
2998
|
-
metrics: { enabled: false },
|
|
2999
|
-
doHover: false,
|
|
3000
|
-
};
|
|
3001
|
-
}
|
|
3002
|
-
export const OpengrepServer = {
|
|
3003
|
-
id: "opengrep",
|
|
3004
|
-
name: "Opengrep Security Scanner",
|
|
3005
|
-
role: "auxiliary",
|
|
3006
|
-
extensions: OPENGREP_EXTENSIONS,
|
|
3007
|
-
// Stable per-repo root so ONE warm server serves the whole project (a
|
|
3008
|
-
// per-directory root would spawn a fresh server — and re-pay rule load —
|
|
3009
|
-
// for every folder).
|
|
3010
|
-
root: RootWithFallback(NearestRoot([".git"]), async () => process.cwd()),
|
|
3011
|
-
availabilityKey: "opengrep",
|
|
3012
|
-
// Rule compilation can take a few seconds on the first scan of a session.
|
|
3013
|
-
initializeTimeoutMs: 15000,
|
|
3014
|
-
async spawn(root, options) {
|
|
3015
|
-
const launched = await resolveAndLaunch({
|
|
3016
|
-
candidates: ["opengrep"],
|
|
3017
|
-
args: ["lsp", "--experimental"],
|
|
3018
|
-
cwd: root,
|
|
3019
|
-
managedToolId: "opengrep",
|
|
3020
|
-
}, options?.allowInstall);
|
|
3021
|
-
if (!launched)
|
|
3022
|
-
return undefined;
|
|
3023
|
-
return { ...launched, initialization: opengrepInitialization(root) };
|
|
3024
|
-
},
|
|
3025
|
-
autoInstall: async () => Boolean(await ensureTool("opengrep")),
|
|
3026
|
-
};
|
|
3027
|
-
// ast-grep — a polyglot structural linter that speaks LSP. Like Opengrep it is a
|
|
3028
|
-
// cross-cutting, diagnostic-only auxiliary (never a file's primary language
|
|
3029
|
-
// server). It attaches EVERYWHERE (#239 Phase 2): a project `sgconfig.y[a]ml`
|
|
3030
|
-
// surfaces the team's OWN curated rules (auto-discovered), and absent one it
|
|
3031
|
-
// launches with `--config <shipped baseline>` so pi-lens's bundled ruleset runs
|
|
3032
|
-
// anyway — superseding the in-process napi runner, which steps aside when this
|
|
3033
|
-
// server's binary is available (and resumes as the fallback when it isn't —
|
|
3034
|
-
// Gate B). NOTE: the napi runner is NOT a subset — it delegates to napi's native
|
|
3035
|
-
// engine via root.findAll({rule}) (#206), the SAME Rust core as this LSP and the
|
|
3036
|
-
// ast-grep CLI, so rule semantics are identical across all three. The LSP's edge
|
|
3037
|
-
// is engine-driven codeAction fixes, not faithfulness of matching. #2347 closed
|
|
3038
|
-
// the one known divergence under Gate B: the LSP/CLI resolve embedded `<script>`
|
|
3039
|
-
// bodies in HTML and run `language: JavaScript` rules inside them, and the napi
|
|
3040
|
-
// fallback now mirrors that (each script body is reparsed as JS and findings are
|
|
3041
|
-
// translated back to file coordinates). A future ast-grep embedded-content
|
|
3042
|
-
// surface (for example `<style>` bodies, which 0.45.1 does NOT inject) must land
|
|
3043
|
-
// on both routes together or be recorded here as an accepted divergence.
|
|
3044
|
-
const AST_GREP_KINDS = [
|
|
3045
|
-
"csharp",
|
|
3046
|
-
"cxx",
|
|
3047
|
-
"css",
|
|
3048
|
-
"elixir",
|
|
3049
|
-
"go",
|
|
3050
|
-
"haskell",
|
|
3051
|
-
"html",
|
|
3052
|
-
"java",
|
|
3053
|
-
"json",
|
|
3054
|
-
"jsts",
|
|
3055
|
-
"kotlin",
|
|
3056
|
-
"lua",
|
|
3057
|
-
"nix",
|
|
3058
|
-
"php",
|
|
3059
|
-
"python",
|
|
3060
|
-
"ruby",
|
|
3061
|
-
"rust",
|
|
3062
|
-
"scala",
|
|
3063
|
-
"shell",
|
|
3064
|
-
"swift",
|
|
3065
|
-
"yaml",
|
|
3066
|
-
];
|
|
3067
|
-
const AST_GREP_EXTENSIONS = Array.from(new Set(AST_GREP_KINDS.flatMap((k) => KIND_EXTENSIONS[k] ?? [])));
|
|
3068
|
-
export const AstGrepServer = {
|
|
3069
|
-
id: "ast-grep",
|
|
3070
|
-
name: "ast-grep structural linter",
|
|
3071
|
-
role: "auxiliary",
|
|
3072
|
-
extensions: AST_GREP_EXTENSIONS,
|
|
3073
|
-
// Attaches everywhere (#239 Phase 2): prefer a project `sgconfig.y[a]ml` root,
|
|
3074
|
-
// else the repo root (.git) or cwd — like Opengrep. When there's no project
|
|
3075
|
-
// sgconfig the spawn launches with `--config <shipped baseline>` so the team's
|
|
3076
|
-
// rules still run; the napi runner steps aside when this server is available
|
|
3077
|
-
// (it falls back to napi when the ast-grep binary is absent — Gate B).
|
|
3078
|
-
root: RootWithFallback(createRootDetector(["sgconfig.yml", "sgconfig.yaml"]), RootWithFallback(NearestRoot([".git"]), async () => process.cwd())),
|
|
3079
|
-
availabilityKey: "ast-grep",
|
|
3080
|
-
// #1714: the one auxiliary with a measured wedge ceiling. A `lens_diagnostics
|
|
3081
|
-
// mode=full` sweep of 225 files drove this server into an unrecoverable stall
|
|
3082
|
-
// twice in two exposures (writes outstanding 5.9 s and 9.4 s, then a forced
|
|
3083
|
-
// shutdown that timed out both the request and the exit notify). It re-parses
|
|
3084
|
-
// the whole file on every didOpen, so it absorbs a sweep more slowly than the
|
|
3085
|
-
// other scanners — hold it to half the shared default.
|
|
3086
|
-
notifyInflightLimit: 4,
|
|
3087
|
-
// First scan of a session compiles the rules.
|
|
3088
|
-
initializeTimeoutMs: 15000,
|
|
3089
|
-
async spawn(root, options) {
|
|
3090
|
-
// A project sgconfig wins (the team's curated ruleset, auto-discovered from
|
|
3091
|
-
// cwd). Otherwise point `--config` at pi-lens's shipped baseline ruleset.
|
|
3092
|
-
const projectSgconfig = findLocalSgconfig(root);
|
|
3093
|
-
let args = ["lsp"];
|
|
3094
|
-
if (!projectSgconfig) {
|
|
3095
|
-
const baseline = resolveBaselineSgconfig(root);
|
|
3096
|
-
if (baseline)
|
|
3097
|
-
args = ["lsp", "--config", baseline];
|
|
3098
|
-
}
|
|
3099
|
-
// #472: prefer the platform-native exe directly (one less orphanable
|
|
3100
|
-
// node-bin-wrapper layer). Prepended as the first candidate; falls back
|
|
3101
|
-
// to the existing "ast-grep" PATH/global-bin resolution when the
|
|
3102
|
-
// optional native package isn't installed for this platform/arch.
|
|
3103
|
-
const nativeExe = resolveAstGrepNativeExe();
|
|
3104
|
-
const candidates = nativeExe ? [nativeExe, "ast-grep"] : ["ast-grep"];
|
|
3105
|
-
return resolveAndLaunch({
|
|
3106
|
-
candidates,
|
|
3107
|
-
args,
|
|
3108
|
-
cwd: root,
|
|
3109
|
-
managedToolId: "ast-grep",
|
|
3110
|
-
}, options?.allowInstall);
|
|
3111
|
-
},
|
|
3112
|
-
autoInstall: async () => Boolean(await ensureTool("ast-grep")),
|
|
3113
|
-
};
|
|
3114
|
-
// zizmor — a GitHub Actions workflow-security scanner that speaks LSP (#272).
|
|
3115
|
-
// Like Opengrep/ast-grep it is a cross-cutting, diagnostic-only auxiliary. Its
|
|
3116
|
-
// extension match (any YAML) is intentionally broad — actual candidacy is
|
|
3117
|
-
// narrowed by `pathFilter` (`isZizmorAuditTarget`, #636) to the exact paths
|
|
3118
|
-
// zizmor's own input collection audits (`.github/workflows/*`, `action.yml`,
|
|
3119
|
-
// `.github/dependabot.yaml`); every other YAML file is a guaranteed no-op —
|
|
3120
|
-
// measured directly against a real `zizmor --lsp` process, a non-matching
|
|
3121
|
-
// file gets NO `publishDiagnostics` at all, so without the path gate every
|
|
3122
|
-
// edit of e.g. a `docker-compose.yml` would burn zizmor's full
|
|
3123
|
-
// diagnostics-wait budget for zero signal. Its audit set ("regular" persona)
|
|
3124
|
-
// is compiled-in and runs with NO config; a repo `zizmor.yml` only
|
|
3125
|
-
// tunes/ignores rules (the blocking opt-in, see the auxiliary profile).
|
|
3126
|
-
// Online audits (known-vulnerable-actions, unpinned-uses, …) need a GitHub
|
|
3127
|
-
// token — resolveZizmorGitHubToken forwards one (env, else `gh auth token`);
|
|
3128
|
-
// without it zizmor runs its offline audit subset.
|
|
3129
|
-
const ZIZMOR_EXTENSIONS = KIND_EXTENSIONS["yaml"];
|
|
3130
|
-
export const ZizmorServer = {
|
|
3131
|
-
id: "zizmor",
|
|
3132
|
-
name: "zizmor Actions Security Scanner",
|
|
3133
|
-
role: "auxiliary",
|
|
3134
|
-
extensions: ZIZMOR_EXTENSIONS,
|
|
3135
|
-
pathFilter: isZizmorAuditTarget,
|
|
3136
|
-
// Stable per-repo root so ONE warm server serves the whole project (like
|
|
3137
|
-
// Opengrep) — config + workflow discovery is repo-relative.
|
|
3138
|
-
root: RootWithFallback(NearestRoot([".git"]), async () => process.cwd()),
|
|
3139
|
-
availabilityKey: "zizmor",
|
|
3140
|
-
async spawn(root, options) {
|
|
3141
|
-
// Forward a token so the online audits run; absent one, zizmor self-selects
|
|
3142
|
-
// offline mode (the env vars + `gh auth token` are resolved once and merged
|
|
3143
|
-
// over process.env by launchLSP).
|
|
3144
|
-
const ghToken = await resolveZizmorGitHubToken();
|
|
3145
|
-
return resolveAndLaunch({
|
|
3146
|
-
candidates: ["zizmor"],
|
|
3147
|
-
args: ["--lsp"],
|
|
3148
|
-
cwd: root,
|
|
3149
|
-
managedToolId: "zizmor",
|
|
3150
|
-
...(ghToken ? { env: { GH_TOKEN: ghToken } } : {}),
|
|
3151
|
-
}, options?.allowInstall);
|
|
3152
|
-
},
|
|
3153
|
-
autoInstall: async () => Boolean(await ensureTool("zizmor")),
|
|
3154
|
-
};
|
|
3155
|
-
// typos — a source-code spell checker that speaks LSP (#283). Cross-cutting,
|
|
3156
|
-
// diagnostic-only auxiliary like Opengrep/ast-grep/zizmor: it attaches to many
|
|
3157
|
-
// code kinds AND markdown/docs (option B — a spell checker that skips prose
|
|
3158
|
-
// misses its highest-value target; typos is ALLOW-LIST based, so it only flags
|
|
3159
|
-
// known misspellings with a known correction, keeping the false-positive rate on
|
|
3160
|
-
// technical vocab low). Its built-in dictionary is compiled in — NO config needed
|
|
3161
|
-
// to run; a repo `typos.toml`/`_typos.toml`/`.typos.toml` only tunes the
|
|
3162
|
-
// dictionary/severity (and is the blocking opt-in, see the auxiliary profile).
|
|
3163
|
-
// `typos-lsp` takes NO subcommand/flag — it wires stdin/stdout straight into the
|
|
3164
|
-
// LSP server. Default severity is WARNING, so findings are advisory by default.
|
|
3165
|
-
const TYPOS_EXTENSIONS = Array.from(new Set([...OPENGREP_EXTENSIONS, ...KIND_EXTENSIONS["markdown"]]));
|
|
3166
|
-
// #967: typos-lsp's `initializationOptions.config` is a filesystem PATH to a
|
|
3167
|
-
// config file (confirmed against upstream source — crates/typos-lsp/src/lsp.rs
|
|
3168
|
-
// reads `config` as a string and tilde-expands it into a PathBuf; it is never
|
|
3169
|
-
// an inline TOML string nor a parsed table). typos-lsp then MERGES that config
|
|
3170
|
-
// with any repo-local one it discovers itself, with the injected config
|
|
3171
|
-
// taking precedence on key collisions — so a project's own config must never
|
|
3172
|
-
// be injected alongside ours (see findLocalTyposConfig below): honoring an
|
|
3173
|
-
// existing project config means injecting NOTHING, letting typos-lsp read the
|
|
3174
|
-
// project's file untouched.
|
|
3175
|
-
function typosInitialization(root) {
|
|
3176
|
-
const localConfig = findLocalTyposConfig(root);
|
|
3177
|
-
if (localConfig) {
|
|
3178
|
-
logLatency({
|
|
3179
|
-
type: "phase",
|
|
3180
|
-
phase: "typos_config_resolved",
|
|
3181
|
-
filePath: root,
|
|
3182
|
-
durationMs: 0,
|
|
3183
|
-
metadata: { mode: "project_config", configPath: localConfig },
|
|
3184
|
-
});
|
|
3185
|
-
logSessionStart(`typos config resolved mode=project_config configPath=${localConfig}`);
|
|
3186
|
-
return undefined;
|
|
3187
|
-
}
|
|
3188
|
-
const configPath = resolvePackagePath(import.meta.url, "rules", "typos", "_typos.toml");
|
|
3189
|
-
logLatency({
|
|
3190
|
-
type: "phase",
|
|
3191
|
-
phase: "typos_config_resolved",
|
|
3192
|
-
filePath: root,
|
|
3193
|
-
durationMs: 0,
|
|
3194
|
-
metadata: { mode: "injected_default", configPath },
|
|
3195
|
-
});
|
|
3196
|
-
logSessionStart(`typos config resolved mode=injected_default configPath=${configPath}`);
|
|
3197
|
-
return { config: configPath };
|
|
3198
|
-
}
|
|
3199
|
-
export const TyposServer = {
|
|
3200
|
-
id: "typos",
|
|
3201
|
-
name: "typos Spell Checker",
|
|
3202
|
-
role: "auxiliary",
|
|
3203
|
-
extensions: TYPOS_EXTENSIONS,
|
|
3204
|
-
// Stable per-repo root so ONE warm server serves the whole project (like the
|
|
3205
|
-
// other auxiliaries) — typos.toml discovery is repo-relative.
|
|
3206
|
-
root: RootWithFallback(NearestRoot([".git"]), async () => process.cwd()),
|
|
3207
|
-
availabilityKey: "typos-lsp",
|
|
3208
|
-
async spawn(root, options) {
|
|
3209
|
-
const launched = await resolveAndLaunch({
|
|
3210
|
-
candidates: ["typos-lsp"],
|
|
3211
|
-
args: [],
|
|
3212
|
-
cwd: root,
|
|
3213
|
-
managedToolId: "typos-lsp",
|
|
3214
|
-
}, options?.allowInstall);
|
|
3215
|
-
if (!launched)
|
|
3216
|
-
return undefined;
|
|
3217
|
-
const initialization = typosInitialization(root);
|
|
3218
|
-
return initialization ? { ...launched, initialization } : launched;
|
|
3219
|
-
},
|
|
3220
|
-
autoInstall: async () => Boolean(await ensureTool("typos-lsp")),
|
|
3221
|
-
};
|
|
3222
|
-
export const LSP_SERVERS = [
|
|
3223
|
-
TypeScriptServer,
|
|
3224
|
-
DenoServer,
|
|
3225
|
-
PythonServer, // pyright / basedpyright — preferred; openFilesOnly avoids cold-start; ty (#717) is a local-only opt-in fallback
|
|
3226
|
-
PythonJediServer, // fallback when neither pyright nor basedpyright is available
|
|
3227
|
-
GoServer,
|
|
3228
|
-
RustServer,
|
|
3229
|
-
RubyServer,
|
|
3230
|
-
PHPServer,
|
|
3231
|
-
PowerShellServer, // PowerShell Editor Services — pwsh-bootstrapped module bundle (#278)
|
|
3232
|
-
CSharpServer,
|
|
3233
|
-
OmniSharpServer,
|
|
3234
|
-
FSharpServer,
|
|
3235
|
-
JavaServer,
|
|
3236
|
-
KotlinServer,
|
|
3237
|
-
SwiftServer,
|
|
3238
|
-
DartServer,
|
|
3239
|
-
LuaServer,
|
|
3240
|
-
CppServer,
|
|
3241
|
-
ZigServer,
|
|
3242
|
-
HaskellServer,
|
|
3243
|
-
ElixirServer,
|
|
3244
|
-
ElixirExpertServer,
|
|
3245
|
-
GleamServer,
|
|
3246
|
-
TinymistServer,
|
|
3247
|
-
MarksmanServer,
|
|
3248
|
-
OCamlServer,
|
|
3249
|
-
ClojureServer,
|
|
3250
|
-
CueServer,
|
|
3251
|
-
TerraformServer,
|
|
3252
|
-
NixServer,
|
|
3253
|
-
BashServer,
|
|
3254
|
-
FishServer,
|
|
3255
|
-
CMakeServer,
|
|
3256
|
-
DockerServer,
|
|
3257
|
-
YamlServer,
|
|
3258
|
-
JsonServer,
|
|
3259
|
-
HtmlServer,
|
|
3260
|
-
TomlServer,
|
|
3261
|
-
PrismaServer,
|
|
3262
|
-
// Web frameworks & styling
|
|
3263
|
-
VueServer,
|
|
3264
|
-
SvelteServer,
|
|
3265
|
-
CssServer,
|
|
3266
|
-
// Auxiliary (cross-cutting, diagnostic-only) servers go last — never primary.
|
|
3267
|
-
OpengrepServer,
|
|
3268
|
-
AstGrepServer,
|
|
3269
|
-
ZizmorServer,
|
|
3270
|
-
TyposServer,
|
|
3271
|
-
];
|
|
3272
|
-
/**
|
|
3273
|
-
* Get server for a file extension
|
|
3274
|
-
*/
|
|
3275
|
-
export function getServerForExtension(ext) {
|
|
3276
|
-
return LSP_SERVERS.find((server) => server.extensions.includes(ext));
|
|
3277
|
-
}
|
|
3278
|
-
/**
|
|
3279
|
-
* Get server by ID
|
|
3280
|
-
*/
|
|
3281
|
-
export function getServerById(id) {
|
|
3282
|
-
return LSP_SERVERS.find((server) => server.id === id);
|
|
3283
|
-
}
|
|
3284
|
-
/**
|
|
3285
|
-
* Get all servers for a file (may have multiple matches)
|
|
3286
|
-
*/
|
|
3287
|
-
export function getServersForFile(filePath) {
|
|
3288
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
3289
|
-
return LSP_SERVERS.filter((server) => server.extensions.includes(ext));
|
|
3290
|
-
}
|