pi-lens 4.1.2 → 4.1.4
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/CHANGELOG.md +1308 -0
- package/README.md +3 -0
- package/config/biome/core.jsonc +11 -2
- package/config/dependency-cruiser-eager-allowlist.json +91 -0
- package/dist/clients/actionable-warnings-logger.js +18 -3
- package/dist/clients/actionable-warnings.js +927 -80
- package/dist/clients/agent-behavior-client.js +13 -4
- package/dist/clients/ast-grep-client.js +136 -9
- package/dist/clients/ast-grep-rule-manager.js +60 -5
- package/dist/clients/ast-grep-tool-logger.js +2 -2
- package/dist/clients/availability-probe-flight.js +12 -0
- package/dist/clients/bash-file-access.js +1 -3
- package/dist/clients/biome-client.js +16 -2
- package/dist/clients/blocker-freshness.js +34 -0
- package/dist/clients/bootstrap.js +509 -73
- package/dist/clients/bounded-cache.js +152 -12
- package/dist/clients/bounded-telemetry.js +115 -23
- package/dist/clients/build-identity.js +95 -0
- package/dist/clients/bundled-resource-health.js +113 -0
- package/dist/clients/bus-events-logger.js +2 -2
- package/dist/clients/bus-publish.js +3 -0
- package/dist/clients/cache/rule-cache.js +33 -5
- package/dist/clients/cache-manager.js +105 -6
- package/dist/clients/cache-observability.js +46 -19
- package/dist/clients/cargo-manifest.js +422 -0
- package/dist/clients/cascade-logger.js +17 -3
- package/dist/clients/code-quality-warnings.js +13 -3
- package/dist/clients/complexity-client.js +16 -5
- package/dist/clients/config-core/deny.js +221 -0
- package/dist/clients/config-core/index.js +47 -0
- package/dist/clients/config-core/merge.js +357 -0
- package/dist/clients/config-core/normalize.js +340 -0
- package/dist/clients/config-core/process-spec.js +248 -0
- package/dist/clients/config-core/provenance.js +171 -0
- package/dist/clients/config-core/records.js +218 -0
- package/dist/clients/config-core/resolve.js +125 -0
- package/dist/clients/config-core/safe-object.js +78 -0
- package/dist/clients/config-core/schema.js +167 -0
- package/dist/clients/config-diagnostic-codes.js +281 -0
- package/dist/clients/config-locations.js +160 -0
- package/dist/clients/config-resolve.js +789 -0
- package/dist/clients/config-schema.js +197 -0
- package/dist/clients/config-warn.js +407 -0
- package/dist/clients/dead-code-client.js +33 -25
- package/dist/clients/dead-code-logger.js +2 -2
- package/dist/clients/deadline-utils.js +178 -0
- package/dist/clients/debug-handles.js +2 -2
- package/dist/clients/debug-heap.js +3 -3
- package/dist/clients/deferred-lsp-work.js +106 -0
- package/dist/clients/degradation-ledger.js +166 -8
- package/dist/clients/demoted-finding-render.js +14 -0
- package/dist/clients/dependency-checker.js +28 -22
- package/dist/clients/deps/minimatch.js +1 -1
- package/dist/clients/diagnostic-line-freshness.js +12 -9
- package/dist/clients/diagnostic-logger.js +14 -4
- package/dist/clients/dispatch/collect-later-tier.js +39 -0
- package/dist/clients/dispatch/dispatcher.js +304 -19
- package/dist/clients/dispatch/fact-store.js +328 -9
- package/dist/clients/dispatch/integration.js +214 -107
- package/dist/clients/dispatch/pending-runner-findings.js +87 -0
- package/dist/clients/dispatch/plan.js +21 -5
- package/dist/clients/dispatch/runners/actionlint.js +0 -1
- package/dist/clients/dispatch/runners/ast-grep-napi.js +550 -77
- package/dist/clients/dispatch/runners/biome-check.js +0 -1
- package/dist/clients/dispatch/runners/cpp-check.js +0 -1
- package/dist/clients/dispatch/runners/credo.js +0 -1
- package/dist/clients/dispatch/runners/cue-vet.js +0 -1
- package/dist/clients/dispatch/runners/dart-analyze.js +0 -1
- package/dist/clients/dispatch/runners/detekt.js +0 -1
- package/dist/clients/dispatch/runners/dotnet-build.js +0 -1
- package/dist/clients/dispatch/runners/elixir-check.js +0 -1
- package/dist/clients/dispatch/runners/eslint.js +5 -2
- package/dist/clients/dispatch/runners/fact-rules.js +0 -1
- package/dist/clients/dispatch/runners/fish-indent.js +0 -1
- package/dist/clients/dispatch/runners/gleam-check.js +0 -1
- package/dist/clients/dispatch/runners/go-vet.js +1 -3
- package/dist/clients/dispatch/runners/golangci-lint.js +0 -1
- package/dist/clients/dispatch/runners/hadolint.js +0 -1
- package/dist/clients/dispatch/runners/helm-lint.js +16 -4
- package/dist/clients/dispatch/runners/helm-render.js +54 -10
- package/dist/clients/dispatch/runners/htmlhint.js +0 -1
- package/dist/clients/dispatch/runners/index.js +1 -3
- package/dist/clients/dispatch/runners/javac.js +0 -1
- package/dist/clients/dispatch/runners/ktlint.js +0 -1
- package/dist/clients/dispatch/runners/lsp.js +37 -2
- package/dist/clients/dispatch/runners/markdownlint.js +4 -2
- package/dist/clients/dispatch/runners/mypy.js +0 -1
- package/dist/clients/dispatch/runners/oxlint.js +24 -22
- package/dist/clients/dispatch/runners/php-lint.js +0 -1
- package/dist/clients/dispatch/runners/phpstan.js +0 -1
- package/dist/clients/dispatch/runners/prisma-validate.js +0 -1
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -1
- package/dist/clients/dispatch/runners/pyright.js +7 -4
- package/dist/clients/dispatch/runners/rubocop.js +0 -1
- package/dist/clients/dispatch/runners/ruff.js +0 -1
- package/dist/clients/dispatch/runners/rust-clippy.js +9 -6
- package/dist/clients/dispatch/runners/shellcheck.js +0 -1
- package/dist/clients/dispatch/runners/shfmt.js +0 -1
- package/dist/clients/dispatch/runners/spellcheck.js +0 -1
- package/dist/clients/dispatch/runners/spotbugs.js +0 -1
- package/dist/clients/dispatch/runners/sqlfluff.js +0 -1
- package/dist/clients/dispatch/runners/stylelint.js +0 -1
- package/dist/clients/dispatch/runners/swiftlint.js +0 -1
- package/dist/clients/dispatch/runners/taplo.js +0 -1
- package/dist/clients/dispatch/runners/terragrunt.js +0 -1
- package/dist/clients/dispatch/runners/tflint.js +0 -1
- package/dist/clients/dispatch/runners/tree-sitter.js +0 -1
- package/dist/clients/dispatch/runners/trivy-config.js +0 -1
- package/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
- package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -4
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +270 -105
- package/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
- package/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
- package/dist/clients/dispatch/runners/utils/toolchain-availability.js +89 -26
- package/dist/clients/dispatch/runners/vale.js +0 -1
- package/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
- package/dist/clients/dispatch/runners/yamllint.js +0 -1
- package/dist/clients/dispatch/runners/zig-check.js +0 -1
- package/dist/clients/disposition-logger.js +3 -3
- package/dist/clients/effective-config.js +403 -0
- package/dist/clients/error-class.js +23 -0
- package/dist/clients/event-loop-hold.js +274 -0
- package/dist/clients/event-loop-monitor.js +66 -1
- package/dist/clients/extension-log.js +2 -2
- package/dist/clients/feature-hints.js +2 -1
- package/dist/clients/file-time.js +6 -1
- package/dist/clients/file-utils.js +266 -13
- package/dist/clients/finding-delivery-gate.js +55 -13
- package/dist/clients/format-service.js +6 -1
- package/dist/clients/formatters.js +303 -104
- package/dist/clients/freshness-cadence.js +17 -0
- package/dist/clients/generated-artifacts.js +114 -19
- package/dist/clients/generation-guard.js +4 -9
- package/dist/clients/git-guard.js +198 -48
- package/dist/clients/git-tracked-ignore.js +32 -1
- package/dist/clients/go-client.js +39 -0
- package/dist/clients/govulncheck-client.js +5 -1
- package/dist/clients/gradle-ktfmt-style.js +252 -0
- package/dist/clients/gzip-stage-write.js +7 -0
- package/dist/clients/hashline-anchor.js +424 -0
- package/dist/clients/hook-budgets.js +76 -0
- package/dist/clients/host-edit-normalize.js +5 -2
- package/dist/clients/host-ports.js +1 -1
- package/dist/clients/installer/index.js +200 -84
- package/dist/clients/installer/managed-tool-refresh.js +3 -1
- package/dist/clients/instance-reaper.js +108 -243
- package/dist/clients/instance-registry-lock.js +145 -0
- package/dist/clients/instance-registry.js +435 -122
- package/dist/clients/jscpd-client.js +6 -1
- package/dist/clients/language-profile.js +4 -2
- package/dist/clients/language-registry.js +595 -0
- package/dist/clients/latency-logger.js +179 -3
- package/dist/clients/ledger-bounds.js +13 -0
- package/dist/clients/lens-config.js +141 -30
- package/dist/clients/lens-engine.js +30 -37
- package/dist/clients/lens-flag-registry.js +67 -6
- package/dist/clients/lens-map.js +3 -1
- package/dist/clients/log-cleanup.js +2 -2
- package/dist/clients/lsp/aggregation.js +3 -1
- package/dist/clients/lsp/cascade-tier.js +4 -0
- package/dist/clients/lsp/client.js +672 -49
- package/dist/clients/lsp/config.js +504 -156
- package/dist/clients/lsp/diagnostic-binding.js +10 -5
- package/dist/clients/lsp/edits.js +15 -2
- package/dist/clients/lsp/index.js +1179 -247
- package/dist/clients/lsp/jvm-runtime.js +5 -0
- package/dist/clients/lsp/language.js +18 -169
- package/dist/clients/lsp/launch.js +38 -9
- package/dist/clients/lsp/pending-aux-coverage.js +113 -12
- package/dist/clients/lsp/server.js +426 -160
- package/dist/clients/lsp/session-roots.js +192 -0
- package/dist/clients/lsp/spawn-history.js +2 -7
- package/dist/clients/lsp/tsserver-sync.js +9 -2
- package/dist/clients/lsp/workspace-diagnostics-cache.js +17 -7
- package/dist/clients/lsp/workspace-sweep-hold.js +32 -23
- package/dist/clients/lsp-mutation.js +179 -23
- package/dist/clients/map-with-concurrency.js +36 -0
- package/dist/clients/mcp/analyze.js +45 -10
- package/dist/clients/mcp/session.js +19 -17
- package/dist/clients/memory-sampler.js +28 -1
- package/dist/clients/message-end-attribution.js +30 -0
- package/dist/clients/metrics-history.js +21 -4
- package/dist/clients/middle-man-analysis.js +2 -4
- package/dist/clients/module-report.js +20 -45
- package/dist/clients/mutating-tool.js +651 -0
- package/dist/clients/mutation-attribution.js +368 -0
- package/dist/clients/mutation-bridge.js +240 -0
- package/dist/clients/ndjson-logger.js +247 -20
- package/dist/clients/observed-mutation-sources.js +101 -0
- package/dist/clients/observed-mutation.js +1215 -0
- package/dist/clients/opaque-mutation-scan.js +300 -40
- package/dist/clients/opengrep-config.js +4 -0
- package/dist/clients/package-manager.js +201 -44
- package/dist/clients/partial-edit-apply.js +359 -80
- package/dist/clients/path-attribution-telemetry.js +23 -7
- package/dist/clients/path-keyed-map.js +35 -3
- package/dist/clients/path-utils.js +460 -13
- package/dist/clients/persist-debounce.js +8 -1
- package/dist/clients/php-cs-fixer-config.js +114 -0
- package/dist/clients/pipeline.js +77 -24
- package/dist/clients/probe-home-state.js +228 -0
- package/dist/clients/process-bridge.js +66 -0
- package/dist/clients/process-singletons.js +210 -0
- package/dist/clients/process-snapshot.js +68 -0
- package/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
- package/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +27 -3
- package/dist/clients/project-diagnostics/scanner.js +61 -24
- package/dist/clients/project-lens-config.js +393 -122
- package/dist/clients/project-snapshot.js +85 -35
- package/dist/clients/python-environment.js +217 -0
- package/dist/clients/python-provenance.js +639 -0
- package/dist/clients/quiet-window.js +6 -1
- package/dist/clients/read-bridge.js +13 -21
- package/dist/clients/read-expansion.js +9 -46
- package/dist/clients/read-guard-logger.js +39 -6
- package/dist/clients/read-guard-tool-lines.js +364 -212
- package/dist/clients/read-guard.js +95 -7
- package/dist/clients/recent-touches.js +17 -5
- package/dist/clients/resource-sampler.js +195 -96
- package/dist/clients/review-graph/builder.js +752 -132
- package/dist/clients/review-graph/service.js +55 -3
- package/dist/clients/review-graph/shared-extraction-ir.js +7 -15
- package/dist/clients/review-graph/tsconfig-paths.js +5 -1
- package/dist/clients/review-graph/workspace-modules.js +10 -74
- package/dist/clients/review-graph-logger.js +30 -4
- package/dist/clients/runtime-agent-end.js +18 -2
- package/dist/clients/runtime-context.js +49 -16
- package/dist/clients/runtime-coordinator.js +93 -6
- package/dist/clients/runtime-session.js +169 -34
- package/dist/clients/runtime-tool-call.js +183 -35
- package/dist/clients/runtime-tool-result.js +894 -241
- package/dist/clients/runtime-turn.js +1264 -74
- package/dist/clients/rust-client.js +31 -0
- package/dist/clients/safe-spawn.js +191 -28
- package/dist/clients/sanitize.js +7 -1
- package/dist/clients/scratch-tree-policy.js +1 -3
- package/dist/clients/security-scan-client.js +51 -17
- package/dist/clients/session-lifecycle.js +238 -42
- package/dist/clients/session-start-observability.js +79 -0
- package/dist/clients/sessionstart-logger.js +12 -3
- package/dist/clients/sg-runner.js +27 -15
- package/dist/clients/sgconfig.js +6 -1
- package/dist/clients/shared-checkout-guard.js +338 -0
- package/dist/clients/skills-resolver.js +105 -0
- package/dist/clients/smells-rollup.js +2 -2
- package/dist/clients/spawn-output-cap.js +37 -0
- package/dist/clients/spawn-timeout-cooldown.js +15 -1
- package/dist/clients/startup-scan.js +6 -7
- package/dist/clients/startup-timing.js +69 -4
- package/dist/clients/string-utils.js +33 -0
- package/dist/clients/subagent-mode.js +17 -4
- package/dist/clients/test-runner-client.js +586 -118
- package/dist/clients/test-runner-delivery.js +239 -0
- package/dist/clients/tool-definition.js +41 -1
- package/dist/clients/tool-policy.js +92 -22
- package/dist/clients/tree-sitter-cache.js +40 -40
- package/dist/clients/tree-sitter-client.js +97 -53
- package/dist/clients/tree-sitter-logger.js +19 -3
- package/dist/clients/tree-sitter-query-loader.js +81 -1
- package/dist/clients/tree-sitter-shared.js +51 -46
- package/dist/clients/tree-sitter-symbol-extractor.js +28 -0
- package/dist/clients/typos-config.js +5 -0
- package/dist/clients/user-notify.js +6 -2
- package/dist/clients/warm-attach.js +8 -10
- package/dist/clients/widget-state.js +191 -26
- package/dist/clients/word-index-logger.js +15 -3
- package/dist/clients/word-index-store.js +463 -0
- package/dist/clients/word-index.js +815 -125
- package/dist/clients/workspace-topology.js +25 -1
- package/dist/clients/zizmor-config.js +29 -6
- package/dist/index.js +68577 -53950
- package/dist/mcp/analyze-cli.js +3 -1
- package/dist/mcp/build-staleness.js +8 -7
- package/dist/mcp/cli.js +3 -1
- package/dist/mcp/server.js +133 -6
- package/dist/scripts/lib/process-scan.mjs +583 -0
- package/dist/scripts/lib/skills-predicate.mjs +129 -0
- package/dist/tools/effective-config.js +89 -0
- package/dist/tools/lens-diagnostics.js +99 -24
- package/dist/tools/lsp-diagnostics.js +81 -85
- package/dist/tools/lsp-navigation.js +40 -17
- package/dist/tools/shared.js +0 -1
- package/docs/agent-guide.md +68 -3
- package/docs/ast-grep_rules_catalog.md +41 -10
- package/docs/astplayground.md +26 -17
- package/docs/configuration.md +222 -0
- package/docs/dependencies.md +3 -3
- package/docs/features.md +44 -5
- package/docs/language-coverage.md +2 -2
- package/docs/pi-lens-fixer.md +25 -0
- package/docs/pi-lens-investigator.md +25 -0
- package/docs/pi-lens-reviewer.md +27 -0
- package/docs/pi-lens-subagent.md +38 -0
- package/docs/pi-lens-warden.md +55 -0
- package/docs/public-api-stability.md +359 -0
- package/docs/release-qa-baseline.md +182 -0
- package/docs/subagent-compat.md +110 -30
- package/docs/tree-sitter_rules_catalog.md +2 -2
- package/package.json +17 -21
- package/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
- package/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
- package/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
- package/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
- package/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
- package/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
- package/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
- package/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
- package/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
- package/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
- package/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
- package/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
- package/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
- package/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
- package/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
- package/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
- package/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
- package/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
- package/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
- package/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
- package/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
- package/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
- package/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
- package/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
- package/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
- package/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
- package/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
- package/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
- package/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
- package/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
- package/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
- package/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
- package/rules/ast-grep-rules/rules/no-important.yml +21 -0
- package/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
- package/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
- package/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
- package/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
- package/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
- package/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
- package/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
- package/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
- package/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
- package/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
- package/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
- package/scripts/analyze-pi-lens-logs.mjs +192 -1
- package/scripts/install-selftest.mjs +58 -2
- package/scripts/lib/skills-predicate.mjs +129 -0
- package/scripts/rpc-load-check.mjs +3 -1
|
@@ -6,15 +6,19 @@
|
|
|
6
6
|
* - Auto-installation strategies
|
|
7
7
|
* - Platform-specific handling
|
|
8
8
|
*/
|
|
9
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { existsSync, mkdirSync, statSync } from "node:fs";
|
|
10
10
|
import { access, readFile, readdir, stat } from "node:fs/promises";
|
|
11
11
|
import os from "node:os";
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { getGlobalPiLensDir, getProjectIgnoreGlobs, isPathIgnoredByProject, } from "../file-utils.js";
|
|
14
|
+
import { augmentPythonEnvironment, detectPythonEnvironment, detectPythonVenv, pythonEnvironmentToolCandidates, } from "../python-environment.js";
|
|
15
|
+
export { detectPythonVenv };
|
|
14
16
|
import { STAGE_TMP_PATTERN } from "../atomic-write-staging.js";
|
|
15
17
|
import { DOTNET_CSHARP_ROOT_MARKERS, DOTNET_FSHARP_ROOT_MARKERS, KIND_EXTENSIONS, } from "../file-kinds.js";
|
|
16
|
-
import { direntsHaveMarkerGlobMatch, isAtOrAboveHomeDir, isFullyQualified, isWindowsPath, pathsEqual, } from "../path-utils.js";
|
|
17
|
-
import { ensureTool, getToolEnvironment, getToolPath, } from "../installer/index.js";
|
|
18
|
+
import { CARGO_WORKSPACE_MEMBER_DIALECT, direntsHaveMarkerGlobMatch, isAtOrAboveHomeDir, isFullyQualified, isWindowsPath, matchesWorkspaceMemberPattern, pathsEqual, } from "../path-utils.js";
|
|
19
|
+
import { ensureTool, findManagedToolBinary, getToolEnvironment, getToolPath, } from "../installer/index.js";
|
|
20
|
+
import * as installer from "../installer/index.js";
|
|
21
|
+
import { classifyProbeFailure, describeInstallAttempt, logAvailabilityDecision, } from "../dispatch/runners/utils/availability-policy.js";
|
|
18
22
|
import { resolveOpengrepConfig } from "../opengrep-config.js";
|
|
19
23
|
import { isZizmorAuditTarget, resolveZizmorGitHubToken, } from "../zizmor-config.js";
|
|
20
24
|
import { logLatency } from "../latency-logger.js";
|
|
@@ -22,6 +26,7 @@ import { logSessionStart } from "../sessionstart-logger.js";
|
|
|
22
26
|
import { findLocalSgconfig, resolveBaselineSgconfig } from "../sgconfig.js";
|
|
23
27
|
import { findLocalTyposConfig } from "../typos-config.js";
|
|
24
28
|
import { resolvePackagePath } from "../package-root.js";
|
|
29
|
+
import { hasCargoWorkspaceTable, readCargoWorkspaceExclude, readCargoWorkspaceMembers, readTextFileOrUndefined, } from "../cargo-manifest.js";
|
|
25
30
|
import { resolveAstGrepNativeExe } from "./wait-policy/index.js";
|
|
26
31
|
import { hasSpawnFailureKind, isCommandAvailableAsync, safeSpawnAsync, } from "../safe-spawn.js";
|
|
27
32
|
import { launchLSP } from "./launch.js";
|
|
@@ -29,6 +34,8 @@ import { createLombokJdtlsArgs } from "./lombok.js";
|
|
|
29
34
|
import { resolveJavaRuntimeEnv } from "./jvm-runtime.js";
|
|
30
35
|
import { normalizeMapKey } from "./path-utils.js";
|
|
31
36
|
import { getRubyVersionDirNamesSync } from "./ruby-drive-dirs.js";
|
|
37
|
+
import { getProcessSingleton } from "../process-singletons.js";
|
|
38
|
+
import { createGenerationSource, } from "../generation-guard.js";
|
|
32
39
|
const FIXTURE_ROOT_SEGMENTS = new Set(["__fixtures__", "testdata"]);
|
|
33
40
|
const FALLBACK_PROJECT_MARKERS = [
|
|
34
41
|
".git",
|
|
@@ -77,10 +84,69 @@ const PROJECT_BOUNDARY_MARKERS = [
|
|
|
77
84
|
// process (not once per session). Keep the root-boundary marker below aligned
|
|
78
85
|
// with FALLBACK_PROJECT_MARKERS, the shared fallback root-policy marker set.
|
|
79
86
|
const loggedRootCeilingClamps = new Set();
|
|
80
|
-
|
|
87
|
+
const posixCaseInsensitiveByPath = new Map();
|
|
88
|
+
function posixFilesystemIsCaseInsensitive(root) {
|
|
89
|
+
const resolved = path.resolve(root);
|
|
90
|
+
const cached = posixCaseInsensitiveByPath.get(resolved);
|
|
91
|
+
if (cached !== undefined)
|
|
92
|
+
return cached;
|
|
93
|
+
if (!existsSync(resolved)) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const name = path.basename(resolved);
|
|
97
|
+
const alternate = name.toLowerCase() === name ? name.toUpperCase() : name.toLowerCase();
|
|
98
|
+
let insensitive = false;
|
|
99
|
+
const alternatePath = path.join(path.dirname(resolved), alternate);
|
|
100
|
+
if (alternate !== name && existsSync(alternatePath)) {
|
|
101
|
+
try {
|
|
102
|
+
const actual = statSync(resolved);
|
|
103
|
+
const alternateStat = statSync(alternatePath);
|
|
104
|
+
insensitive =
|
|
105
|
+
actual.dev === alternateStat.dev && actual.ino === alternateStat.ino;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
insensitive = false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
posixCaseInsensitiveByPath.set(resolved, insensitive);
|
|
112
|
+
return insensitive;
|
|
113
|
+
}
|
|
114
|
+
export function resetLSPCaseSensitivityState() {
|
|
115
|
+
posixCaseInsensitiveByPath.clear();
|
|
116
|
+
}
|
|
117
|
+
export function _getPosixCaseSensitivityCacheSizeForTests() {
|
|
118
|
+
return posixCaseInsensitiveByPath.size;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Path-shape-aware containment test: is `candidate` the same path as
|
|
122
|
+
* `ancestor`, or inside it?
|
|
123
|
+
*
|
|
124
|
+
* Exported so the session-cwd registry (`clients/lsp/config.ts`) and the
|
|
125
|
+
* foreign-root decline gate (`clients/lsp/index.ts`) test containment with the
|
|
126
|
+
* SAME comparator the root ceiling uses. A second hand-rolled `path.relative`
|
|
127
|
+
* helper alongside this one is how the two drift apart on Windows-shaped
|
|
128
|
+
* paths (shape 2 / #1150): `path.isAbsolute("C:\\repo")` is false on POSIX, so
|
|
129
|
+
* a host-default comparator silently mis-answers a win32 path on Linux CI.
|
|
130
|
+
*/
|
|
131
|
+
export function isSameOrWithin(ancestor, candidate, deps = {}) {
|
|
81
132
|
const windowsShaped = isWindowsPath(ancestor) || isWindowsPath(candidate);
|
|
82
133
|
const pathApi = windowsShaped ? path.win32 : path;
|
|
83
|
-
const
|
|
134
|
+
const resolvedAncestor = pathApi.resolve(ancestor);
|
|
135
|
+
const resolvedCandidate = pathApi.resolve(candidate);
|
|
136
|
+
let caseInsensitive = false;
|
|
137
|
+
if (!windowsShaped) {
|
|
138
|
+
if (deps.caseInsensitiveProbe) {
|
|
139
|
+
const cached = posixCaseInsensitiveByPath.get(resolvedAncestor);
|
|
140
|
+
caseInsensitive = cached ?? deps.caseInsensitiveProbe(resolvedAncestor);
|
|
141
|
+
if (cached === undefined) {
|
|
142
|
+
posixCaseInsensitiveByPath.set(resolvedAncestor, caseInsensitive);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
caseInsensitive = posixFilesystemIsCaseInsensitive(resolvedAncestor);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const relative = pathApi.relative(caseInsensitive ? resolvedAncestor.toLowerCase() : resolvedAncestor, caseInsensitive ? resolvedCandidate.toLowerCase() : resolvedCandidate);
|
|
84
150
|
return (relative === "" ||
|
|
85
151
|
(!relative.startsWith("..") && !pathApi.isAbsolute(relative)));
|
|
86
152
|
}
|
|
@@ -192,6 +258,49 @@ function canInstall(allowInstall) {
|
|
|
192
258
|
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);
|
|
193
259
|
const directLspCommandUnavailableUntil = new Map();
|
|
194
260
|
const directLspCommandSkipLoggedUntil = new Map();
|
|
261
|
+
// Availability rows are emitted from the same async launch path that owns the
|
|
262
|
+
// live LSP generation. A session reset can retire that generation while a
|
|
263
|
+
// managed lookup, install, or launch is still awaiting; stale work must not
|
|
264
|
+
// publish into the replacement session (#2351, shape 22).
|
|
265
|
+
const lspLaunchAvailabilityGeneration = createGenerationSource("lsp-launch-availability");
|
|
266
|
+
export function resetLspLaunchAvailabilityGeneration() {
|
|
267
|
+
lspLaunchAvailabilityGeneration.bump();
|
|
268
|
+
}
|
|
269
|
+
function staleLaunch(generation, subject, proc) {
|
|
270
|
+
if (generation.isCurrent())
|
|
271
|
+
return false;
|
|
272
|
+
try {
|
|
273
|
+
proc?.process?.kill();
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
// Best-effort cleanup for a process returned after service retirement.
|
|
277
|
+
}
|
|
278
|
+
generation.guardedWrite(subject, () => undefined);
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
async function installEvidenceForLaunch(toolId, installed, attempt) {
|
|
282
|
+
const evidence = describeInstallAttempt(attempt);
|
|
283
|
+
const confirmedManagedPath = await findManagedToolBinary(toolId);
|
|
284
|
+
return {
|
|
285
|
+
...evidence,
|
|
286
|
+
binary: path.basename(installed),
|
|
287
|
+
...(confirmedManagedPath !== undefined &&
|
|
288
|
+
pathsEqual(confirmedManagedPath, installed) && {
|
|
289
|
+
source: "managed-dir",
|
|
290
|
+
}),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function captureInstallAttempt(toolId) {
|
|
294
|
+
try {
|
|
295
|
+
// Capture synchronously after ensureTool resolves. Reading this later, after
|
|
296
|
+
// launch/evidence awaits, can observe another concurrent ensure's outcome.
|
|
297
|
+
return installer.getInstallAttempt?.(toolId);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// Older test doubles do not expose this production export.
|
|
301
|
+
}
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
195
304
|
/** Re-arm direct-command availability for the next session. */
|
|
196
305
|
export function resetDirectLspCommandAvailability() {
|
|
197
306
|
directLspCommandUnavailableUntil.clear();
|
|
@@ -241,6 +350,7 @@ function markDirectLspCommandUnavailable(command) {
|
|
|
241
350
|
}
|
|
242
351
|
const PI_LENS_BIN_DIR = path.join(getGlobalPiLensDir(), "bin");
|
|
243
352
|
export async function resolveAndLaunch(spec, allowInstall) {
|
|
353
|
+
const generation = lspLaunchAvailabilityGeneration.capture();
|
|
244
354
|
const toolLabel = spec.managedToolId ??
|
|
245
355
|
spec.candidates[spec.candidates.length - 1] ??
|
|
246
356
|
"unknown";
|
|
@@ -251,6 +361,28 @@ export async function resolveAndLaunch(spec, allowInstall) {
|
|
|
251
361
|
lastRuntimeFailure = err instanceof Error ? err : new Error(message);
|
|
252
362
|
}
|
|
253
363
|
};
|
|
364
|
+
// #2140: consult the release-managed binary directory (~/.pi-lens/bin)
|
|
365
|
+
// BEFORE the bare-PATH candidates below. A bare `candidates: ["opengrep"]`
|
|
366
|
+
// entry only ever resolves through the OS's own PATH lookup — never that
|
|
367
|
+
// directory — so a github/maven/archive-strategy tool installed there with
|
|
368
|
+
// no PATH entry ENOENTs every direct candidate first, only to have the
|
|
369
|
+
// managed-install step further down find the very same binary a few
|
|
370
|
+
// hundred ms later. `findManagedToolBinary` does a bare `fs.access`, no
|
|
371
|
+
// spawn, so this costs at most a few stat calls per server launch attempt
|
|
372
|
+
// (not per file/dispatch — launches are session-scoped). Mirrors
|
|
373
|
+
// `findManagedNodeToolBinary`'s npm-managed fast path (runner-helpers.ts)
|
|
374
|
+
// and `SecurityScanClient.probeVersion`'s equivalent fix for the CLI-scan
|
|
375
|
+
// half of this same issue (#2140, landed in PR #2148/#2137).
|
|
376
|
+
const managedProbeStartedAt = Date.now();
|
|
377
|
+
const managedCandidate = spec.managedToolId
|
|
378
|
+
? await findManagedToolBinary(spec.managedToolId)
|
|
379
|
+
: undefined;
|
|
380
|
+
if (staleLaunch(generation, `${toolLabel}:findManagedToolBinary`)) {
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
const candidates = managedCandidate && !spec.candidates.includes(managedCandidate)
|
|
384
|
+
? [managedCandidate, ...spec.candidates]
|
|
385
|
+
: spec.candidates;
|
|
254
386
|
// A candidate that fails while a LATER candidate (or managed install)
|
|
255
387
|
// succeeds is just fallback, not a failure — logging each immediately floods
|
|
256
388
|
// the logs with scary "candidate failed / npm shim failed / Run npm install"
|
|
@@ -258,7 +390,7 @@ export async function resolveAndLaunch(spec, allowInstall) {
|
|
|
258
390
|
// surface only if ALL direct candidates fail.
|
|
259
391
|
const candidateFailures = [];
|
|
260
392
|
// Step 1 & 2 — try all explicit candidates (includes bare command = PATH lookup)
|
|
261
|
-
for (const [index, command] of
|
|
393
|
+
for (const [index, command] of candidates.entries()) {
|
|
262
394
|
logLatency({
|
|
263
395
|
type: "phase",
|
|
264
396
|
phase: "lsp_launch_candidate_attempt",
|
|
@@ -268,16 +400,19 @@ export async function resolveAndLaunch(spec, allowInstall) {
|
|
|
268
400
|
tool: toolLabel,
|
|
269
401
|
command,
|
|
270
402
|
index,
|
|
271
|
-
totalCandidates:
|
|
403
|
+
totalCandidates: candidates.length,
|
|
272
404
|
allowInstall: canInstall(allowInstall),
|
|
273
405
|
},
|
|
274
406
|
});
|
|
275
|
-
logSessionStart(`lsp launch candidate attempt tool=${toolLabel} idx=${index}/${
|
|
407
|
+
logSessionStart(`lsp launch candidate attempt tool=${toolLabel} idx=${index}/${candidates.length - 1} command=${command} cwd=${spec.cwd}`);
|
|
276
408
|
try {
|
|
277
409
|
const proc = await launchLSP(command, spec.args, {
|
|
278
410
|
cwd: spec.cwd,
|
|
279
411
|
env: spec.env,
|
|
280
412
|
});
|
|
413
|
+
if (staleLaunch(generation, `${toolLabel}:launchLSP:${command}`, proc)) {
|
|
414
|
+
return undefined;
|
|
415
|
+
}
|
|
281
416
|
logLatency({
|
|
282
417
|
type: "phase",
|
|
283
418
|
phase: "lsp_launch_candidate_success",
|
|
@@ -291,9 +426,36 @@ export async function resolveAndLaunch(spec, allowInstall) {
|
|
|
291
426
|
},
|
|
292
427
|
});
|
|
293
428
|
logSessionStart(`lsp launch candidate success tool=${toolLabel} idx=${index} command=${command} source=direct`);
|
|
429
|
+
// The managed-dir fast path (#2140) IS the availability probe for a
|
|
430
|
+
// release-managed tool: a real spawn just confirmed the binary
|
|
431
|
+
// findManagedToolBinary resolved actually launches. Gated on the exact
|
|
432
|
+
// managed candidate (never a later bare-PATH fallback), so a session
|
|
433
|
+
// start with the binary present emits exactly one decision and a
|
|
434
|
+
// developer-PATH-resolved copy (no managed binary at all) emits none —
|
|
435
|
+
// unchanged from before this fix.
|
|
436
|
+
if (managedCandidate !== undefined && command === managedCandidate) {
|
|
437
|
+
logAvailabilityDecision({
|
|
438
|
+
tool: toolLabel,
|
|
439
|
+
verdict: "available",
|
|
440
|
+
outcome: "success",
|
|
441
|
+
cause: "ok",
|
|
442
|
+
elapsedMs: Date.now() - managedProbeStartedAt,
|
|
443
|
+
latched: false,
|
|
444
|
+
producer: "lsp-launch",
|
|
445
|
+
classifiedBy: "probe",
|
|
446
|
+
evidence: {
|
|
447
|
+
binary: path.basename(managedCandidate),
|
|
448
|
+
source: "managed-dir",
|
|
449
|
+
correctsLatchedRow: false,
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
}
|
|
294
453
|
return { process: proc, source: "direct" };
|
|
295
454
|
}
|
|
296
455
|
catch (err) {
|
|
456
|
+
if (staleLaunch(generation, `${toolLabel}:launchLSP:${command}`)) {
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
297
459
|
const message = err instanceof Error ? err.message : String(err);
|
|
298
460
|
// Defer logging: only a failure if no later candidate/install succeeds.
|
|
299
461
|
candidateFailures.push({ index, command, message, err });
|
|
@@ -341,8 +503,40 @@ export async function resolveAndLaunch(spec, allowInstall) {
|
|
|
341
503
|
}
|
|
342
504
|
// Step 3 — managed install via installer registry
|
|
343
505
|
if (spec.managedToolId) {
|
|
506
|
+
// Neither the managed-dir stat (#2140) nor any bare-PATH candidate
|
|
507
|
+
// resolved this tool — the negative counterpart to the fast-path
|
|
508
|
+
// "available" decision above, mirroring `SecurityScanClient.probeVersion`'s
|
|
509
|
+
// failure record so the negative case is visible in latency.log rather
|
|
510
|
+
// than swallowed by going straight to the install attempt.
|
|
511
|
+
const failedCandidate = candidateFailures.at(-1);
|
|
512
|
+
const classifiedFailure = classifyProbeFailure({
|
|
513
|
+
error: failedCandidate?.err instanceof Error
|
|
514
|
+
? failedCandidate.err
|
|
515
|
+
: undefined,
|
|
516
|
+
spawnFailure: {
|
|
517
|
+
kind: hasSpawnFailureKind(failedCandidate?.err, "tool-not-found")
|
|
518
|
+
? "tool-not-found"
|
|
519
|
+
: undefined,
|
|
520
|
+
},
|
|
521
|
+
}, { command: failedCandidate?.command });
|
|
522
|
+
logAvailabilityDecision({
|
|
523
|
+
tool: spec.managedToolId,
|
|
524
|
+
verdict: "unavailable",
|
|
525
|
+
outcome: classifiedFailure.outcome,
|
|
526
|
+
cause: classifiedFailure.cause,
|
|
527
|
+
elapsedMs: Date.now() - managedProbeStartedAt,
|
|
528
|
+
latched: false,
|
|
529
|
+
classifiedBy: "probe",
|
|
530
|
+
producer: "lsp-launch",
|
|
531
|
+
evidence: classifiedFailure.evidence,
|
|
532
|
+
});
|
|
344
533
|
logSessionStart(`lsp launch ensure-tool start tool=${spec.managedToolId} cwd=${spec.cwd}`);
|
|
534
|
+
const installStartedAt = Date.now();
|
|
345
535
|
const installed = await ensureTool(spec.managedToolId);
|
|
536
|
+
const installAttempt = captureInstallAttempt(spec.managedToolId);
|
|
537
|
+
if (staleLaunch(generation, `${toolLabel}:ensureTool`)) {
|
|
538
|
+
return undefined;
|
|
539
|
+
}
|
|
346
540
|
logSessionStart(`lsp launch ensure-tool result tool=${spec.managedToolId} installed=${installed ? "yes" : "no"} path=${installed ?? ""}`);
|
|
347
541
|
logLatency({
|
|
348
542
|
type: "phase",
|
|
@@ -361,6 +555,9 @@ export async function resolveAndLaunch(spec, allowInstall) {
|
|
|
361
555
|
cwd: spec.cwd,
|
|
362
556
|
env: spec.env,
|
|
363
557
|
});
|
|
558
|
+
if (staleLaunch(generation, `${toolLabel}:launchLSP:managed`, proc)) {
|
|
559
|
+
return undefined;
|
|
560
|
+
}
|
|
364
561
|
logSessionStart(`lsp launch managed success tool=${spec.managedToolId} command=${installed} source=managed`);
|
|
365
562
|
logLatency({
|
|
366
563
|
type: "phase",
|
|
@@ -372,9 +569,37 @@ export async function resolveAndLaunch(spec, allowInstall) {
|
|
|
372
569
|
command: installed,
|
|
373
570
|
},
|
|
374
571
|
});
|
|
572
|
+
// Compensating row for the "unavailable" decision logged above (#1606
|
|
573
|
+
// shape): the install fixed exactly what the fast path found missing,
|
|
574
|
+
// so the durable record must not be left saying the tool is off.
|
|
575
|
+
// `source` is only asserted when a fresh managed-dir lookup confirms
|
|
576
|
+
// the install actually landed in ~/.pi-lens/bin (github/maven/archive
|
|
577
|
+
// strategies) rather than an npm/pip/gem install elsewhere, so the
|
|
578
|
+
// evidence never claims a resolution this call didn't derive.
|
|
579
|
+
const evidence = await installEvidenceForLaunch(spec.managedToolId, installed, installAttempt);
|
|
580
|
+
if (staleLaunch(generation, `${toolLabel}:managed-evidence`, proc)) {
|
|
581
|
+
return undefined;
|
|
582
|
+
}
|
|
583
|
+
logAvailabilityDecision({
|
|
584
|
+
tool: spec.managedToolId,
|
|
585
|
+
verdict: "available",
|
|
586
|
+
outcome: "success",
|
|
587
|
+
cause: "ok",
|
|
588
|
+
elapsedMs: Date.now() - installStartedAt,
|
|
589
|
+
latched: false,
|
|
590
|
+
classifiedBy: "caller",
|
|
591
|
+
producer: "lsp-launch",
|
|
592
|
+
evidence: {
|
|
593
|
+
...evidence,
|
|
594
|
+
correctsLatchedRow: false,
|
|
595
|
+
},
|
|
596
|
+
});
|
|
375
597
|
return { process: proc, source: "managed" };
|
|
376
598
|
}
|
|
377
599
|
catch (err) {
|
|
600
|
+
if (staleLaunch(generation, `${toolLabel}:launchLSP:managed-rejection`)) {
|
|
601
|
+
return undefined;
|
|
602
|
+
}
|
|
378
603
|
const message = err instanceof Error ? err.message : String(err);
|
|
379
604
|
logSessionStart(`lsp launch managed failed tool=${spec.managedToolId} command=${installed} error=${message}`);
|
|
380
605
|
logLatency({
|
|
@@ -398,12 +623,19 @@ export async function resolveAndLaunch(spec, allowInstall) {
|
|
|
398
623
|
const reinstalled = await ensureTool(spec.managedToolId, {
|
|
399
624
|
forceReinstall: true,
|
|
400
625
|
});
|
|
626
|
+
const reinstallAttempt = captureInstallAttempt(spec.managedToolId);
|
|
627
|
+
if (staleLaunch(generation, `${toolLabel}:forceReinstall`)) {
|
|
628
|
+
return undefined;
|
|
629
|
+
}
|
|
401
630
|
if (reinstalled) {
|
|
402
631
|
try {
|
|
403
632
|
const proc = await launchLSP(reinstalled, spec.args, {
|
|
404
633
|
cwd: spec.cwd,
|
|
405
634
|
env: spec.env,
|
|
406
635
|
});
|
|
636
|
+
if (staleLaunch(generation, `${toolLabel}:launchLSP:forceReinstall`, proc)) {
|
|
637
|
+
return undefined;
|
|
638
|
+
}
|
|
407
639
|
logSessionStart(`lsp launch managed force-reinstall success tool=${spec.managedToolId} command=${reinstalled}`);
|
|
408
640
|
logLatency({
|
|
409
641
|
type: "phase",
|
|
@@ -415,6 +647,24 @@ export async function resolveAndLaunch(spec, allowInstall) {
|
|
|
415
647
|
command: reinstalled,
|
|
416
648
|
},
|
|
417
649
|
});
|
|
650
|
+
const evidence = await installEvidenceForLaunch(spec.managedToolId, reinstalled, reinstallAttempt);
|
|
651
|
+
if (staleLaunch(generation, `${toolLabel}:forceReinstall-evidence`, proc)) {
|
|
652
|
+
return undefined;
|
|
653
|
+
}
|
|
654
|
+
logAvailabilityDecision({
|
|
655
|
+
tool: spec.managedToolId,
|
|
656
|
+
verdict: "available",
|
|
657
|
+
outcome: "success",
|
|
658
|
+
cause: "ok",
|
|
659
|
+
elapsedMs: Date.now() - installStartedAt,
|
|
660
|
+
latched: false,
|
|
661
|
+
classifiedBy: "caller",
|
|
662
|
+
producer: "lsp-launch",
|
|
663
|
+
evidence: {
|
|
664
|
+
...evidence,
|
|
665
|
+
correctsLatchedRow: false,
|
|
666
|
+
},
|
|
667
|
+
});
|
|
418
668
|
return { process: proc, source: "managed" };
|
|
419
669
|
}
|
|
420
670
|
catch (retryErr) {
|
|
@@ -427,20 +677,35 @@ export async function resolveAndLaunch(spec, allowInstall) {
|
|
|
427
677
|
}
|
|
428
678
|
}
|
|
429
679
|
// Step 4 — language-native runtime install (go install, gem install, …)
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
680
|
+
const runtimeInstall = spec.runtimeInstall;
|
|
681
|
+
const runtimeAvailable = runtimeInstall
|
|
682
|
+
? await isOnPath(runtimeInstall.runtimeCommand)
|
|
683
|
+
: false;
|
|
684
|
+
if (staleLaunch(generation, `${toolLabel}:runtimeAvailability`)) {
|
|
685
|
+
return undefined;
|
|
686
|
+
}
|
|
687
|
+
if (runtimeInstall && runtimeAvailable) {
|
|
688
|
+
const ok = await runtimeInstall.install();
|
|
689
|
+
if (staleLaunch(generation, `${toolLabel}:runtimeInstall`)) {
|
|
690
|
+
return undefined;
|
|
691
|
+
}
|
|
433
692
|
if (ok) {
|
|
434
|
-
const retry =
|
|
693
|
+
const retry = runtimeInstall.retryCandidates ?? spec.candidates;
|
|
435
694
|
for (const command of retry) {
|
|
436
695
|
try {
|
|
437
696
|
const proc = await launchLSP(command, spec.args, {
|
|
438
697
|
cwd: spec.cwd,
|
|
439
698
|
env: spec.env,
|
|
440
699
|
});
|
|
700
|
+
if (staleLaunch(generation, `${toolLabel}:launchLSP:runtime`, proc)) {
|
|
701
|
+
return undefined;
|
|
702
|
+
}
|
|
441
703
|
return { process: proc, source: "managed" };
|
|
442
704
|
}
|
|
443
705
|
catch (err) {
|
|
706
|
+
if (staleLaunch(generation, `${toolLabel}:launchLSP:runtime`)) {
|
|
707
|
+
return undefined;
|
|
708
|
+
}
|
|
444
709
|
trackRuntimeFailure(err);
|
|
445
710
|
// try next
|
|
446
711
|
}
|
|
@@ -551,12 +816,15 @@ async function resolveAndLaunchTreeBinary(spec, allowInstall) {
|
|
|
551
816
|
return undefined;
|
|
552
817
|
}
|
|
553
818
|
}
|
|
554
|
-
function
|
|
819
|
+
function nodeBinLocalCandidates(root, baseName) {
|
|
555
820
|
const localBase = path.join(root, "node_modules", ".bin", baseName);
|
|
556
821
|
if (process.platform === "win32") {
|
|
557
|
-
return [`${localBase}.cmd`, `${localBase}.exe
|
|
822
|
+
return [`${localBase}.cmd`, `${localBase}.exe`];
|
|
558
823
|
}
|
|
559
|
-
return [localBase
|
|
824
|
+
return [localBase];
|
|
825
|
+
}
|
|
826
|
+
function nodeBinCandidates(root, baseName) {
|
|
827
|
+
return [...nodeBinLocalCandidates(root, baseName), baseName];
|
|
560
828
|
}
|
|
561
829
|
function normalizeSlashKey(value) {
|
|
562
830
|
const normalized = path.resolve(value).replace(/\\/g, "/");
|
|
@@ -611,6 +879,7 @@ function createInteractiveServer(spec) {
|
|
|
611
879
|
name: spec.name,
|
|
612
880
|
extensions: spec.extensions,
|
|
613
881
|
root: spec.root,
|
|
882
|
+
fallbackFor: spec.fallbackFor,
|
|
614
883
|
availabilityKey: typeof spec.command === "string" && isSimpleCommand(spec.command)
|
|
615
884
|
? spec.command
|
|
616
885
|
: undefined,
|
|
@@ -896,19 +1165,23 @@ async function findAncestorFileAmong(startDir, candidateSegmentLists) {
|
|
|
896
1165
|
* Svelte). Without this guard an offline or partial install re-runs a 120 s
|
|
897
1166
|
* forced reinstall on every spawn.
|
|
898
1167
|
*
|
|
899
|
-
* The guard is
|
|
1168
|
+
* The guard is process-singleton state, so without an explicit re-arm it
|
|
900
1169
|
* would latch for the whole extension-host process — a repair that failed
|
|
901
1170
|
* once (transient registry hiccup) would stay unrepairable for every later
|
|
902
1171
|
* session in that process. `resetClassicTsRepairGuard` re-arms it; callers
|
|
903
1172
|
* wire that into `session_start` alongside the other per-session resets
|
|
904
1173
|
* (#1570).
|
|
905
1174
|
*/
|
|
906
|
-
|
|
1175
|
+
const CLASSIC_TS_REPAIR_FAMILY = "lsp.classic-ts-repair-guard";
|
|
1176
|
+
const CLASSIC_TS_REPAIR_VERSION = 1;
|
|
1177
|
+
function classicTsRepairState() {
|
|
1178
|
+
return getProcessSingleton(CLASSIC_TS_REPAIR_FAMILY, CLASSIC_TS_REPAIR_VERSION, () => ({ attempted: false }));
|
|
1179
|
+
}
|
|
907
1180
|
/** Re-arm the classic-repair guard so a new session gets its own attempt. */
|
|
908
1181
|
export function resetClassicTsRepairGuard() {
|
|
909
|
-
|
|
1182
|
+
classicTsRepairState().attempted = false;
|
|
910
1183
|
}
|
|
911
|
-
/** Test hook — clears the
|
|
1184
|
+
/** Test hook — clears the process-singleton classic-repair guard. */
|
|
912
1185
|
export function _resetClassicTsRepairForTests() {
|
|
913
1186
|
resetClassicTsRepairGuard();
|
|
914
1187
|
}
|
|
@@ -1008,7 +1281,7 @@ async function findTsserverPath(root, allowInstall) {
|
|
|
1008
1281
|
if (discoveredTsserver ||
|
|
1009
1282
|
!discoveredTsc ||
|
|
1010
1283
|
!installAllowed ||
|
|
1011
|
-
|
|
1284
|
+
classicTsRepairState().attempted) {
|
|
1012
1285
|
return discoveredTsserver;
|
|
1013
1286
|
}
|
|
1014
1287
|
// Repair only a compiler we can prove is TypeScript 7+. TypeScript 7 dropped
|
|
@@ -1021,7 +1294,7 @@ async function findTsserverPath(root, allowInstall) {
|
|
|
1021
1294
|
// An older managed tree took `latest` before the registry pinned the classic
|
|
1022
1295
|
// compiler. Reinstall the pinned version once so that tree self-heals,
|
|
1023
1296
|
// without deleting user or project-local TypeScript installations.
|
|
1024
|
-
|
|
1297
|
+
classicTsRepairState().attempted = true;
|
|
1025
1298
|
logSessionStart(`lsp typescript: managed compiler resolved to TypeScript ${discoveredVersion.version}, which ships no tsserver.js; reinstalling pinned classic fallback`);
|
|
1026
1299
|
const repairedTsc = await ensureTool("typescript", {
|
|
1027
1300
|
allowInstall: true,
|
|
@@ -1178,33 +1451,6 @@ export function DenoExcludeRoot(primary) {
|
|
|
1178
1451
|
return primary(file);
|
|
1179
1452
|
};
|
|
1180
1453
|
}
|
|
1181
|
-
/**
|
|
1182
|
-
* Find the active Python interpreter inside the nearest virtual environment.
|
|
1183
|
-
* Search order: VIRTUAL_ENV → CONDA_PREFIX → .venv → venv (all under root).
|
|
1184
|
-
* Returns undefined when no venv python binary is found.
|
|
1185
|
-
*/
|
|
1186
|
-
export async function detectPythonVenv(root) {
|
|
1187
|
-
const isWin = process.platform === "win32";
|
|
1188
|
-
const candidates = [
|
|
1189
|
-
process.env.VIRTUAL_ENV,
|
|
1190
|
-
process.env.CONDA_PREFIX,
|
|
1191
|
-
path.join(root, ".venv"),
|
|
1192
|
-
path.join(root, "venv"),
|
|
1193
|
-
].filter((v) => Boolean(v));
|
|
1194
|
-
for (const venv of candidates) {
|
|
1195
|
-
const pythonPath = isWin
|
|
1196
|
-
? path.join(venv, "Scripts", "python.exe")
|
|
1197
|
-
: path.join(venv, "bin", "python");
|
|
1198
|
-
try {
|
|
1199
|
-
await access(pythonPath);
|
|
1200
|
-
return pythonPath;
|
|
1201
|
-
}
|
|
1202
|
-
catch {
|
|
1203
|
-
// not found — try next candidate
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
return undefined;
|
|
1207
|
-
}
|
|
1208
1454
|
// --- Server Definitions ---
|
|
1209
1455
|
const JS_TS_LSP_EXTENSIONS = KIND_EXTENSIONS["jsts"].filter((ext) => ext !== ".svelte" && ext !== ".vue");
|
|
1210
1456
|
// TypeScript identity and tooling discovery deliberately use separate marker
|
|
@@ -1436,6 +1682,7 @@ export const TypeScriptServer = {
|
|
|
1436
1682
|
export const DenoServer = {
|
|
1437
1683
|
id: "deno",
|
|
1438
1684
|
name: "Deno Language Server",
|
|
1685
|
+
fallbackFor: "typescript",
|
|
1439
1686
|
extensions: JS_TS_LSP_EXTENSIONS,
|
|
1440
1687
|
autoPropagateDiagnostics: true,
|
|
1441
1688
|
root: createRootDetector(["deno.json", "deno.jsonc"]),
|
|
@@ -1462,7 +1709,8 @@ export const PythonServer = {
|
|
|
1462
1709
|
"poetry.lock",
|
|
1463
1710
|
])),
|
|
1464
1711
|
async spawn(root, options) {
|
|
1465
|
-
const
|
|
1712
|
+
const pythonEnvironment = await detectPythonEnvironment(root);
|
|
1713
|
+
const env = augmentPythonEnvironment(await getToolEnvironment(), pythonEnvironment);
|
|
1466
1714
|
let source = "direct";
|
|
1467
1715
|
// openFilesOnly: true — analyse only open files rather than the full workspace.
|
|
1468
1716
|
// Avoids the 5–14 s cold-start on large projects caused by workspace-wide
|
|
@@ -1472,36 +1720,65 @@ export const PythonServer = {
|
|
|
1472
1720
|
...(pythonPath ? { pythonPath } : {}),
|
|
1473
1721
|
openFilesOnly: true,
|
|
1474
1722
|
});
|
|
1475
|
-
//
|
|
1476
|
-
//
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1723
|
+
// Project ownership outranks checker preference: exhaust explicit project
|
|
1724
|
+
// candidates before a bare command can resolve to pi-lens's managed bin or
|
|
1725
|
+
// the host PATH. Within the project tier, preserve the established
|
|
1726
|
+
// pyright → basedpyright → ty preference.
|
|
1727
|
+
const projectPyright = await resolveAndLaunch({
|
|
1728
|
+
candidates: [
|
|
1729
|
+
...pythonEnvironmentToolCandidates(pythonEnvironment, "pyright-langserver"),
|
|
1730
|
+
...nodeBinLocalCandidates(root, "pyright-langserver"),
|
|
1731
|
+
...pythonEnvironmentToolCandidates(pythonEnvironment, "basedpyright-langserver"),
|
|
1732
|
+
...nodeBinLocalCandidates(root, "basedpyright-langserver"),
|
|
1733
|
+
],
|
|
1734
|
+
args: ["--stdio"],
|
|
1735
|
+
cwd: root,
|
|
1736
|
+
env,
|
|
1737
|
+
}, false);
|
|
1738
|
+
if (projectPyright) {
|
|
1484
1739
|
return {
|
|
1485
|
-
process:
|
|
1486
|
-
source:
|
|
1487
|
-
initialization: pyrightInit(pythonPath),
|
|
1740
|
+
process: projectPyright.process,
|
|
1741
|
+
source: projectPyright.source,
|
|
1742
|
+
initialization: pyrightInit(pythonEnvironment?.pythonPath),
|
|
1488
1743
|
};
|
|
1489
1744
|
}
|
|
1490
|
-
// ty
|
|
1491
|
-
//
|
|
1492
|
-
//
|
|
1493
|
-
//
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1745
|
+
// ty uses `ty server`, so it needs a separate launch phase from the
|
|
1746
|
+
// Pyright-compatible servers. It has no stable initializationOptions
|
|
1747
|
+
// equivalent to pyright's `pythonPath` (astral-sh/ty#2032); the child
|
|
1748
|
+
// environment and cwd provide interpreter discovery instead.
|
|
1749
|
+
const projectTy = await resolveAndLaunch({
|
|
1750
|
+
candidates: pythonEnvironmentToolCandidates(pythonEnvironment, "ty"),
|
|
1751
|
+
args: ["server"],
|
|
1752
|
+
cwd: root,
|
|
1753
|
+
env,
|
|
1754
|
+
}, false);
|
|
1755
|
+
if (projectTy) {
|
|
1756
|
+
return { process: projectTy.process, source: projectTy.source };
|
|
1757
|
+
}
|
|
1758
|
+
// With no project-owned checker, retain the existing PATH preference and
|
|
1759
|
+
// keep ty opt-in: pyright, then basedpyright, then ty. The augmented child
|
|
1760
|
+
// PATH includes pi-lens-managed bins before the inherited global PATH.
|
|
1761
|
+
const pathPyright = await resolveAndLaunch({
|
|
1762
|
+
candidates: ["pyright-langserver", "basedpyright-langserver"],
|
|
1763
|
+
args: ["--stdio"],
|
|
1764
|
+
cwd: root,
|
|
1765
|
+
env,
|
|
1766
|
+
}, false);
|
|
1767
|
+
if (pathPyright) {
|
|
1768
|
+
return {
|
|
1769
|
+
process: pathPyright.process,
|
|
1770
|
+
source: pathPyright.source,
|
|
1771
|
+
initialization: pyrightInit(pythonEnvironment?.pythonPath),
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
const pathTy = await resolveAndLaunch({
|
|
1775
|
+
candidates: ["ty"],
|
|
1776
|
+
args: ["server"],
|
|
1777
|
+
cwd: root,
|
|
1778
|
+
env,
|
|
1779
|
+
}, false);
|
|
1780
|
+
if (pathTy) {
|
|
1781
|
+
return { process: pathTy.process, source: pathTy.source };
|
|
1505
1782
|
}
|
|
1506
1783
|
// Discover a globally-installed pyright even when install is disabled;
|
|
1507
1784
|
// only the download is gated by canInstall.
|
|
@@ -1523,17 +1800,17 @@ export const PythonServer = {
|
|
|
1523
1800
|
const resolved = await resolveAndLaunch({ candidates: managedCandidates, args: ["--stdio"], cwd: root, env }, false);
|
|
1524
1801
|
if (!resolved)
|
|
1525
1802
|
return undefined;
|
|
1526
|
-
const pythonPath = await detectPythonVenv(root);
|
|
1527
1803
|
return {
|
|
1528
1804
|
process: resolved.process,
|
|
1529
1805
|
source,
|
|
1530
|
-
initialization: pyrightInit(pythonPath),
|
|
1806
|
+
initialization: pyrightInit(pythonEnvironment?.pythonPath),
|
|
1531
1807
|
};
|
|
1532
1808
|
},
|
|
1533
1809
|
};
|
|
1534
1810
|
export const PythonJediServer = {
|
|
1535
1811
|
id: "python-jedi",
|
|
1536
1812
|
name: "Jedi Language Server",
|
|
1813
|
+
fallbackFor: "python",
|
|
1537
1814
|
extensions: KIND_EXTENSIONS["python"],
|
|
1538
1815
|
root: RootWithFallback(createRootDetector([
|
|
1539
1816
|
".git",
|
|
@@ -1585,79 +1862,6 @@ export const GoServer = {
|
|
|
1585
1862
|
return { ...result, initialization: { ui: { semanticTokens: true } } };
|
|
1586
1863
|
},
|
|
1587
1864
|
};
|
|
1588
|
-
async function readTextFileOrUndefined(filePath) {
|
|
1589
|
-
try {
|
|
1590
|
-
return await readFile(filePath, "utf-8");
|
|
1591
|
-
}
|
|
1592
|
-
catch {
|
|
1593
|
-
return undefined;
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
/**
|
|
1597
|
-
* Slice out ONE top-level TOML table's raw body — from its `[name]` heading
|
|
1598
|
-
* to the next top-level `[...]`/`[[...]]` heading or EOF. `members`/`exclude`
|
|
1599
|
-
* must be read from the `[workspace]` table specifically: `[package]` has its
|
|
1600
|
-
* OWN `exclude` key (the standard cargo-publish exclude list, conventionally
|
|
1601
|
-
* written above `[workspace]` in a virtual-manifest-less root crate), and a
|
|
1602
|
-
* whole-file regex would misread it as workspace membership (#1671 F4).
|
|
1603
|
-
*/
|
|
1604
|
-
function extractTomlTableSection(content, tableName) {
|
|
1605
|
-
const heading = new RegExp(`^\\[${tableName}\\][ \\t]*(?:#.*)?$`, "m");
|
|
1606
|
-
const match = heading.exec(content);
|
|
1607
|
-
if (!match)
|
|
1608
|
-
return "";
|
|
1609
|
-
const rest = content.slice(match.index + match[0].length);
|
|
1610
|
-
const nextHeading = rest.match(/^\[{1,2}[^\]]+\]{1,2}[ \t]*(?:#.*)?$/m);
|
|
1611
|
-
return nextHeading?.index !== undefined
|
|
1612
|
-
? rest.slice(0, nextHeading.index)
|
|
1613
|
-
: rest;
|
|
1614
|
-
}
|
|
1615
|
-
function parseTomlStringArray(content, key) {
|
|
1616
|
-
const match = content.match(new RegExp(`^[ \\t]*${key}[ \\t]*=[ \\t]*\\[([\\s\\S]*?)\\]`, "m"));
|
|
1617
|
-
if (!match)
|
|
1618
|
-
return [];
|
|
1619
|
-
return [...match[1].matchAll(/"([^"]*)"|'([^']*)'/g)].map((m) => (m[1] ?? m[2] ?? "").trim());
|
|
1620
|
-
}
|
|
1621
|
-
/** Turn one `/`-delimited glob SEGMENT into a regex source: `*` matches any
|
|
1622
|
-
* run of characters within the segment, `?` matches exactly one character,
|
|
1623
|
-
* everything else is escaped and literal. */
|
|
1624
|
-
function segmentGlobToRegExpSource(segment) {
|
|
1625
|
-
let out = "";
|
|
1626
|
-
for (const ch of segment) {
|
|
1627
|
-
if (ch === "*")
|
|
1628
|
-
out += "[^/]*";
|
|
1629
|
-
else if (ch === "?")
|
|
1630
|
-
out += "[^/]";
|
|
1631
|
-
else
|
|
1632
|
-
out += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
1633
|
-
}
|
|
1634
|
-
return out;
|
|
1635
|
-
}
|
|
1636
|
-
/**
|
|
1637
|
-
* A `members`/`exclude` entry names a path exactly, or globs it segment by
|
|
1638
|
-
* segment: `*` and `?` inside a segment work at any depth — one wildcard
|
|
1639
|
-
* segment (`crates/*`), a bare `*`, or several chained together for a
|
|
1640
|
-
* deeper fixed-depth layout — matching cargo's own `glob`-crate semantics
|
|
1641
|
-
* for a fixed number of path components.
|
|
1642
|
-
*
|
|
1643
|
-
* KNOWN LIMITATION (#1671 F6, documented rather than implemented): a
|
|
1644
|
-
* recursive `**` segment (matching a variable number of path components) is
|
|
1645
|
-
* NOT supported and never matches — cargo workspaces that rely on `**` to
|
|
1646
|
-
* pull in an arbitrarily-nested crate tree will under-hoist (the crate stays
|
|
1647
|
-
* independently rooted instead of joining the workspace). This is
|
|
1648
|
-
* deliberately out of #1671's scope: the common, and the issue's fixture,
|
|
1649
|
-
* shape is an explicit fixed-depth `members` list.
|
|
1650
|
-
*/
|
|
1651
|
-
function matchesCargoWorkspacePattern(pattern, relativePath) {
|
|
1652
|
-
const normalized = pattern.replace(/\/+$/, "");
|
|
1653
|
-
if (normalized.includes("**"))
|
|
1654
|
-
return false;
|
|
1655
|
-
const patternSegments = normalized.split("/");
|
|
1656
|
-
const pathSegments = relativePath.split("/");
|
|
1657
|
-
if (patternSegments.length !== pathSegments.length)
|
|
1658
|
-
return false;
|
|
1659
|
-
return patternSegments.every((segment, i) => new RegExp(`^${segmentGlobToRegExpSource(segment)}$`).test(pathSegments[i]));
|
|
1660
|
-
}
|
|
1661
1865
|
/**
|
|
1662
1866
|
* Given an ancestor Cargo.toml's raw contents that already contains a
|
|
1663
1867
|
* `[workspace]` table, decide whether it actually claims `childDir` as a
|
|
@@ -1684,13 +1888,21 @@ function cargoWorkspaceDeclaresMember(workspaceContent, parentDir, childDir) {
|
|
|
1684
1888
|
.join("/");
|
|
1685
1889
|
if (relativePath === "" || relativePath.startsWith(".."))
|
|
1686
1890
|
return false;
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1891
|
+
// Reuse the shared, `[workspace]`-scoped readers (review round 2, F4)
|
|
1892
|
+
// instead of hand-composing `extractTomlTableSection` +
|
|
1893
|
+
// `parseTomlStringArray` here — that hand-composition duplicated exactly
|
|
1894
|
+
// what `readCargoWorkspaceMembers`/`readCargoWorkspaceExclude` do, the
|
|
1895
|
+
// single-source-of-truth violation #2473 was filed to close for the OTHER
|
|
1896
|
+
// two Cargo.toml readers. Pattern matching goes through the ONE
|
|
1897
|
+
// workspace-member matcher for the same reason (#2591); cargo's dialect —
|
|
1898
|
+
// `**` never matches (#1671 F6), wildcards confined to one component, so
|
|
1899
|
+
// pattern and path must have the same component count — is
|
|
1900
|
+
// `CARGO_WORKSPACE_MEMBER_DIALECT`, not a private compiler here.
|
|
1901
|
+
const matches = (pattern) => matchesWorkspaceMemberPattern(pattern, relativePath, CARGO_WORKSPACE_MEMBER_DIALECT);
|
|
1902
|
+
const excluded = readCargoWorkspaceExclude(workspaceContent);
|
|
1903
|
+
if (excluded.some(matches))
|
|
1690
1904
|
return false;
|
|
1691
|
-
|
|
1692
|
-
const members = parseTomlStringArray(workspaceSection, "members");
|
|
1693
|
-
return members.some((pattern) => matchesCargoWorkspacePattern(pattern, relativePath));
|
|
1905
|
+
return readCargoWorkspaceMembers(workspaceContent).some(matches);
|
|
1694
1906
|
}
|
|
1695
1907
|
/**
|
|
1696
1908
|
* Bound a monorepo-hoist walk-up by the session ceiling: when `file` is inside
|
|
@@ -1729,7 +1941,7 @@ function RustWorkspaceRoot() {
|
|
|
1729
1941
|
const parentCargoPath = path.join(parent, "Cargo.toml");
|
|
1730
1942
|
const parentCargoContent = await readTextFileOrUndefined(parentCargoPath);
|
|
1731
1943
|
if (parentCargoContent !== undefined &&
|
|
1732
|
-
|
|
1944
|
+
hasCargoWorkspaceTable(parentCargoContent)) {
|
|
1733
1945
|
// Test membership against the ORIGINAL crate dir (`root`), never the
|
|
1734
1946
|
// walk cursor (`current`, which may already have climbed through
|
|
1735
1947
|
// gap directories that have no Cargo.toml of their own) — a
|
|
@@ -2037,6 +2249,7 @@ export const CSharpServer = {
|
|
|
2037
2249
|
export const OmniSharpServer = createInteractiveServer({
|
|
2038
2250
|
id: "omnisharp",
|
|
2039
2251
|
name: "OmniSharp",
|
|
2252
|
+
fallbackFor: "csharp",
|
|
2040
2253
|
extensions: KIND_EXTENSIONS["csharp"],
|
|
2041
2254
|
root: createRootDetector([...DOTNET_CSHARP_ROOT_MARKERS]),
|
|
2042
2255
|
language: "csharp",
|
|
@@ -2203,6 +2416,7 @@ export const ElixirServer = createInteractiveServer({
|
|
|
2203
2416
|
export const ElixirExpertServer = {
|
|
2204
2417
|
id: "expert",
|
|
2205
2418
|
name: "Expert",
|
|
2419
|
+
fallbackFor: "elixir",
|
|
2206
2420
|
extensions: KIND_EXTENSIONS["elixir"],
|
|
2207
2421
|
root: RootWithFallback(createRootDetector(["mix.exs"])),
|
|
2208
2422
|
availabilityKey: "expert",
|
|
@@ -2316,6 +2530,19 @@ export const BashServer = {
|
|
|
2316
2530
|
name: "Bash Language Server",
|
|
2317
2531
|
extensions: [".bash", ".sh", ".zsh"],
|
|
2318
2532
|
root: FileDirRoot,
|
|
2533
|
+
// #2194 bounded the installer's own verification at 20s; the dispatch
|
|
2534
|
+
// touch's client-wait floor stayed at the shared 5s default, so a cold
|
|
2535
|
+
// spawn could still race that budget and read as unavailable even after
|
|
2536
|
+
// installer verification cleared it. Match the installer bound (#2169).
|
|
2537
|
+
// `initializeTimeoutMs` matches it too: the measured cold start
|
|
2538
|
+
// (9,667ms, #2194) has headroom under the unraised 15s default, but
|
|
2539
|
+
// leaving the two fields mismatched is the same latent shape that made
|
|
2540
|
+
// the Prisma/Vue raise a no-op (fix-round F1, #2233) — a slower host
|
|
2541
|
+
// than the one measured would still hit the 15s inner kill despite the
|
|
2542
|
+
// caller being willing to wait 20s. Equalizing costs nothing on the
|
|
2543
|
+
// success path and removes the mismatch outright.
|
|
2544
|
+
clientWaitTimeoutMs: 20_000,
|
|
2545
|
+
initializeTimeoutMs: 20_000,
|
|
2319
2546
|
spawn(root, options) {
|
|
2320
2547
|
return resolveAndLaunch({
|
|
2321
2548
|
candidates: nodeBinCandidates(root, "bash-language-server"),
|
|
@@ -2402,6 +2629,13 @@ export const JsonServer = {
|
|
|
2402
2629
|
["package.json", "tsconfig.json", "jsconfig.json"],
|
|
2403
2630
|
[".git"],
|
|
2404
2631
|
])),
|
|
2632
|
+
// See BashServer above: the installer's 20s verification bound (#2194)
|
|
2633
|
+
// does not, on its own, raise the dispatch touch's cold-spawn wait floor.
|
|
2634
|
+
// Mirror it here so a cold spawn cannot lose that race either (#2169).
|
|
2635
|
+
// `initializeTimeoutMs` matches it for the same reason as BashServer
|
|
2636
|
+
// above (fix-round F1, #2233).
|
|
2637
|
+
clientWaitTimeoutMs: 20_000,
|
|
2638
|
+
initializeTimeoutMs: 20_000,
|
|
2405
2639
|
spawn(root, options) {
|
|
2406
2640
|
return resolveAndLaunch({
|
|
2407
2641
|
candidates: ["vscode-json-language-server"],
|
|
@@ -2444,6 +2678,17 @@ export const PrismaServer = {
|
|
|
2444
2678
|
name: "Prisma Language Server",
|
|
2445
2679
|
extensions: KIND_EXTENSIONS["prisma"],
|
|
2446
2680
|
root: RootWithFallback(createRootDetector(["prisma/schema.prisma", "schema.prisma"])),
|
|
2681
|
+
// Matches the installer's 40s verification bound above (#2169): the
|
|
2682
|
+
// dispatch touch's cold-spawn wait floor defaults to 5s and would
|
|
2683
|
+
// otherwise time the client out well before the binary answers.
|
|
2684
|
+
// `initializeTimeoutMs` MUST be set alongside it (RubyServer's own
|
|
2685
|
+
// precedent below): unset, it falls back to the 15s
|
|
2686
|
+
// `INITIALIZE_TIMEOUT_MS` default in `clients/lsp/client.ts`, which
|
|
2687
|
+
// hard-kills the child mid-handshake — a real cold Prisma run measured
|
|
2688
|
+
// up to 27.3s, so the spawn would die there before this wait ever gets
|
|
2689
|
+
// a chance to matter (fix-round F1, #2233).
|
|
2690
|
+
clientWaitTimeoutMs: 40_000,
|
|
2691
|
+
initializeTimeoutMs: 40_000,
|
|
2447
2692
|
spawn(root, options) {
|
|
2448
2693
|
return resolveAndLaunch({
|
|
2449
2694
|
candidates: nodeBinCandidates(root, "prisma-language-server"),
|
|
@@ -2466,6 +2711,15 @@ export const VueServer = {
|
|
|
2466
2711
|
"pnpm-lock.yaml",
|
|
2467
2712
|
"yarn.lock",
|
|
2468
2713
|
]))),
|
|
2714
|
+
// Vue's launcher loads the full language-service bundle before answering,
|
|
2715
|
+
// matching the installer's 30s verification bound (#2176). The dispatch
|
|
2716
|
+
// touch's cold-spawn wait floor defaults to 5s and needs the same raise so
|
|
2717
|
+
// a cold spawn cannot time out there instead. `initializeTimeoutMs` MUST
|
|
2718
|
+
// match: unset, it falls back to the 15s default in `clients/lsp/client.ts`
|
|
2719
|
+
// and hard-kills a cold spawn (measured up to 22.6s in #2188, 16.2s here)
|
|
2720
|
+
// mid-handshake before this wait can matter (fix-round F1, #2233).
|
|
2721
|
+
clientWaitTimeoutMs: 30_000,
|
|
2722
|
+
initializeTimeoutMs: 30_000,
|
|
2469
2723
|
async spawn(root, options) {
|
|
2470
2724
|
const tsserverPath = await findTsserverPath(root, options?.allowInstall);
|
|
2471
2725
|
// Vue Language Server needs Vue dependencies installed to resolve types.
|
|
@@ -2505,6 +2759,13 @@ export const SvelteServer = {
|
|
|
2505
2759
|
"pnpm-lock.yaml",
|
|
2506
2760
|
"yarn.lock",
|
|
2507
2761
|
]))),
|
|
2762
|
+
// Matches the installer's 20s verification bound above (#2169): the
|
|
2763
|
+
// dispatch touch's cold-spawn wait floor defaults to 5s and would
|
|
2764
|
+
// otherwise time the client out well before the binary answers.
|
|
2765
|
+
// `initializeTimeoutMs` matches it for the same reason as BashServer
|
|
2766
|
+
// above (fix-round F1, #2233).
|
|
2767
|
+
clientWaitTimeoutMs: 20_000,
|
|
2768
|
+
initializeTimeoutMs: 20_000,
|
|
2508
2769
|
async spawn(root, options) {
|
|
2509
2770
|
const tsserverPath = await findTsserverPath(root, options?.allowInstall);
|
|
2510
2771
|
const proc = await resolveAndLaunch({
|
|
@@ -2629,7 +2890,13 @@ export const OpengrepServer = {
|
|
|
2629
2890
|
// Gate B). NOTE: the napi runner is NOT a subset — it delegates to napi's native
|
|
2630
2891
|
// engine via root.findAll({rule}) (#206), the SAME Rust core as this LSP and the
|
|
2631
2892
|
// ast-grep CLI, so rule semantics are identical across all three. The LSP's edge
|
|
2632
|
-
// is engine-driven codeAction fixes, not faithfulness of matching.
|
|
2893
|
+
// is engine-driven codeAction fixes, not faithfulness of matching. #2347 closed
|
|
2894
|
+
// the one known divergence under Gate B: the LSP/CLI resolve embedded `<script>`
|
|
2895
|
+
// bodies in HTML and run `language: JavaScript` rules inside them, and the napi
|
|
2896
|
+
// fallback now mirrors that (each script body is reparsed as JS and findings are
|
|
2897
|
+
// translated back to file coordinates). A future ast-grep embedded-content
|
|
2898
|
+
// surface (for example `<style>` bodies, which 0.45.1 does NOT inject) must land
|
|
2899
|
+
// on both routes together or be recorded here as an accepted divergence.
|
|
2633
2900
|
const AST_GREP_KINDS = [
|
|
2634
2901
|
"csharp",
|
|
2635
2902
|
"cxx",
|
|
@@ -2650,7 +2917,6 @@ const AST_GREP_KINDS = [
|
|
|
2650
2917
|
"rust",
|
|
2651
2918
|
"scala",
|
|
2652
2919
|
"shell",
|
|
2653
|
-
"solidity",
|
|
2654
2920
|
"swift",
|
|
2655
2921
|
"yaml",
|
|
2656
2922
|
];
|