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
|
@@ -10,16 +10,20 @@ import * as fs from "node:fs";
|
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import { logSessionStart } from "../../../sessionstart-logger.js";
|
|
13
|
+
import { incrementDegradationCount } from "../../../degradation-ledger.js";
|
|
13
14
|
import { getGlobalPiLensDir } from "../../../file-utils.js";
|
|
14
15
|
import { createGenerationSource, } from "../../../generation-guard.js";
|
|
15
16
|
import { PathKeyedMap } from "../../../path-keyed-map.js";
|
|
16
17
|
import { normalizeEphemeralMapKey, normalizeMapKey, } from "../../../path-utils.js";
|
|
17
|
-
import { ensureTool, getInstallAttempt, getLastEnsureResolutionSource, getToolInstallStrategy, isSpawnableCommand, resetPathWalkMemo, } from "../../../installer/index.js";
|
|
18
|
+
import { ensureTool, findManagedToolBinary, getInstallAttempt, getLastEnsureResolutionSource, getToolInstallStrategy, isSpawnableCommand, resetPathWalkMemo, } from "../../../installer/index.js";
|
|
18
19
|
import { getServersForFileWithConfig, isServerDisabled, } from "../../../lsp/config.js";
|
|
19
|
-
import { findGlobalBinary } from "../../../package-manager.js";
|
|
20
|
+
import { findGlobalBinary, findLocalBinAt, findLocalBinsUpwards, findLocalBinUpwards, VENDOR_BIN_DIRS, VENV_BIN_DIRS, } from "../../../package-manager.js";
|
|
21
|
+
import { logLatency } from "../../../latency-logger.js";
|
|
20
22
|
import { safeSpawnAsync } from "../../../safe-spawn.js";
|
|
23
|
+
import { compareOrdinal } from "../../../string-utils.js";
|
|
21
24
|
import { getToolCommandSpec, shouldAutoInstallTool, } from "../../../tool-policy.js";
|
|
22
25
|
import { isInSpawnTimeoutCooldown } from "../../../spawn-timeout-cooldown.js";
|
|
26
|
+
import { createAvailabilityProbeFlight } from "../../../availability-probe-flight.js";
|
|
23
27
|
import { classifyProbeFailure, createAvailabilityLatch, describeInstallAttempt, describeProbeEvidence, isLatchingOutcome, logAvailabilityDecision, startHostStallSampler, transientRetryDelayMs, } from "./availability-policy.js";
|
|
24
28
|
export { createAvailabilityLatch, classifyProbeFailure, describeProbeEvidence, describeUnavailability, isTransientDecision, logAvailabilityDecision, startHostStallSampler, } from "./availability-policy.js";
|
|
25
29
|
/**
|
|
@@ -42,29 +46,21 @@ export function lspPrimaryCoversFile(ctx, serverId) {
|
|
|
42
46
|
const primary = getServersForFileWithConfig(ctx.filePath).find((s) => s.role !== "auxiliary");
|
|
43
47
|
return primary?.id === serverId;
|
|
44
48
|
}
|
|
49
|
+
const _realThisDir = (() => {
|
|
50
|
+
if (typeof __dirname !== "undefined")
|
|
51
|
+
return __dirname;
|
|
52
|
+
return path.dirname(fileURLToPath(import.meta.url));
|
|
53
|
+
})();
|
|
54
|
+
let _thisDir = _realThisDir;
|
|
45
55
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
56
|
+
* Test-only override for pi-lens's OWN install-tree root (#2544 round 4 F1).
|
|
57
|
+
* `_thisDir` is fixed at module load from the running file's own location, so
|
|
58
|
+
* a test cannot otherwise place it under a synthetic HOME to prove the
|
|
59
|
+
* own-install walk's `ceiling: false` exemption without writing into this
|
|
60
|
+
* process's REAL, shared `node_modules` tree.
|
|
49
61
|
*/
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
let current = startDir;
|
|
53
|
-
const fsRoot = path.parse(current).root;
|
|
54
|
-
while (current !== fsRoot) {
|
|
55
|
-
if (fs.existsSync(path.join(current, "node_modules", ".bin"))) {
|
|
56
|
-
roots.push(current);
|
|
57
|
-
}
|
|
58
|
-
const parent = path.dirname(current);
|
|
59
|
-
if (parent === current)
|
|
60
|
-
break;
|
|
61
|
-
current = parent;
|
|
62
|
-
}
|
|
63
|
-
return roots;
|
|
64
|
-
}
|
|
65
|
-
let _thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
66
|
-
if (typeof __dirname !== "undefined") {
|
|
67
|
-
_thisDir = __dirname;
|
|
62
|
+
export function _setThisDirForTests(dir) {
|
|
63
|
+
_thisDir = dir ?? _realThisDir;
|
|
68
64
|
}
|
|
69
65
|
// Managed tools directory (~/.pi-lens/tools) — where ensureTool() installs binaries
|
|
70
66
|
const _managedToolsDir = path.join(getGlobalPiLensDir(), "tools");
|
|
@@ -86,9 +82,10 @@ function managedNodeToolCandidates(tool) {
|
|
|
86
82
|
* Verification verdicts for managed shims, keyed by path + mtime + size, so a
|
|
87
83
|
* reinstall re-verifies and a session start re-arms.
|
|
88
84
|
*
|
|
89
|
-
* A plain `Map`, not a `PathKeyedMap`:
|
|
90
|
-
*
|
|
91
|
-
*
|
|
85
|
+
* A plain `Map`, not a `PathKeyedMap`: every path keyed here is produced by
|
|
86
|
+
* `path.join` from one of two producers — `managedNodeToolCandidates` and the
|
|
87
|
+
* installer's `findManagedToolBinary` (#2140) — so the write and read forms are
|
|
88
|
+
* the same string by construction and cannot diverge on case or separator.
|
|
92
89
|
*/
|
|
93
90
|
const managedBinaryVerdicts = new Map();
|
|
94
91
|
/**
|
|
@@ -118,14 +115,14 @@ const MANAGED_VERIFY_COOLDOWN_MAX_MS = 15 * 60_000;
|
|
|
118
115
|
function managedVerifyCooldownMs(attempts) {
|
|
119
116
|
return Math.min(MANAGED_VERIFY_COOLDOWN_MAX_MS, MANAGED_VERIFY_COOLDOWN_MS * 2 ** (attempts - 1));
|
|
120
117
|
}
|
|
121
|
-
async function runManagedVerification(candidate, stamp, priorAttempts, generation) {
|
|
118
|
+
async function runManagedVerification(candidate, stamp, priorAttempts, generation, verificationArgs = ["--version"]) {
|
|
122
119
|
let transient = false;
|
|
123
120
|
let ok;
|
|
124
121
|
try {
|
|
125
122
|
const { verifyToolBinary } = await import("../../../installer/index.js");
|
|
126
123
|
ok = await verifyToolBinary(candidate, undefined, () => {
|
|
127
124
|
transient = true;
|
|
128
|
-
}, MANAGED_VERIFY_TIMEOUT_MS);
|
|
125
|
+
}, MANAGED_VERIFY_TIMEOUT_MS, verificationArgs);
|
|
129
126
|
}
|
|
130
127
|
catch {
|
|
131
128
|
// The verifier itself could not run — installer-isolated unit tests mock
|
|
@@ -164,7 +161,7 @@ async function runManagedVerification(candidate, stamp, priorAttempts, generatio
|
|
|
164
161
|
}
|
|
165
162
|
return ok ? "ok" : "broken";
|
|
166
163
|
}
|
|
167
|
-
async function verifyManagedCandidate(candidate) {
|
|
164
|
+
async function verifyManagedCandidate(candidate, verificationArgs = ["--version"]) {
|
|
168
165
|
let stamp;
|
|
169
166
|
try {
|
|
170
167
|
const stat = fs.statSync(candidate);
|
|
@@ -186,7 +183,7 @@ async function verifyManagedCandidate(candidate) {
|
|
|
186
183
|
if (existing)
|
|
187
184
|
return existing;
|
|
188
185
|
const generation = availabilityGeneration.capture();
|
|
189
|
-
const probe = runManagedVerification(candidate, stamp, priorAttempts, generation).finally(() => {
|
|
186
|
+
const probe = runManagedVerification(candidate, stamp, priorAttempts, generation, verificationArgs).finally(() => {
|
|
190
187
|
// A settling old-session probe must not evict the live entry a new
|
|
191
188
|
// session already started for the same shim (#1674 review F5).
|
|
192
189
|
generation.guardedWrite(stamp, () => {
|
|
@@ -218,14 +215,40 @@ async function verifyManagedCandidate(candidate) {
|
|
|
218
215
|
* stalls on a cold cache still gets re-probed later (#1674 review F1).
|
|
219
216
|
* Concurrent first touches share one probe (#1674 review F2).
|
|
220
217
|
*/
|
|
221
|
-
export async function findManagedNodeToolBinary(tool) {
|
|
218
|
+
export async function findManagedNodeToolBinary(tool, verificationArgs = ["--version"]) {
|
|
222
219
|
for (const candidate of managedNodeToolCandidates(tool)) {
|
|
223
|
-
const verdict = await verifyManagedCandidate(candidate);
|
|
220
|
+
const verdict = await verifyManagedCandidate(candidate, verificationArgs);
|
|
224
221
|
if (verdict === "ok" || verdict === "unverified")
|
|
225
222
|
return candidate;
|
|
226
223
|
}
|
|
227
224
|
return null;
|
|
228
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* The release-managed binary for a tool (`~/.pi-lens/bin/<tool>`), or null when
|
|
228
|
+
* the registry does not put this tool there / nothing on disk actually runs.
|
|
229
|
+
*
|
|
230
|
+
* `findManagedToolBinary` is the INSTALLER's own lookup — the one `getToolPath`
|
|
231
|
+
* runs before PATH for every github/maven/archive-strategy tool, and the one
|
|
232
|
+
* `getToolEnvironment` puts at the front of a spawn's PATH. Calling it here is
|
|
233
|
+
* what makes probe and spawn resolve through a single definition instead of two
|
|
234
|
+
* (#2140): before this, the probe fell straight to the bare command name, missed
|
|
235
|
+
* PATH, and latched `missing` — while the install fallback a few hundred ms
|
|
236
|
+
* later handed the caller the very binary sitting in that directory. Every
|
|
237
|
+
* session, for every tool the user already had (7 unavailable/available pairs in
|
|
238
|
+
* one 3h dogfood window).
|
|
239
|
+
*
|
|
240
|
+
* Verification is the same `verifyManagedCandidate` the npm-shim rung above
|
|
241
|
+
* uses, for the same reason (#1657): `findManagedToolBinary` answers from a bare
|
|
242
|
+
* `fs.access`, and an on-disk binary that cannot run must not shadow a working
|
|
243
|
+
* PATH binary — it falls through instead.
|
|
244
|
+
*/
|
|
245
|
+
async function findManagedReleaseBinary(tool, verificationArgs = ["--version"]) {
|
|
246
|
+
const candidate = await findManagedToolBinary(tool);
|
|
247
|
+
if (!candidate)
|
|
248
|
+
return null;
|
|
249
|
+
const verdict = await verifyManagedCandidate(candidate, verificationArgs);
|
|
250
|
+
return verdict === "ok" || verdict === "unverified" ? candidate : null;
|
|
251
|
+
}
|
|
229
252
|
// =============================================================================
|
|
230
253
|
// VENV-AWARE COMMAND FINDER
|
|
231
254
|
// =============================================================================
|
|
@@ -236,21 +259,30 @@ export async function findManagedNodeToolBinary(tool) {
|
|
|
236
259
|
* The resolved path is returned verbatim: every spawn consumer runs with
|
|
237
260
|
* `shell: false` (safe-spawn, #817), so wrapping it in quotes would make it
|
|
238
261
|
* a literal filename that ENOENTs on every platform (#1508).
|
|
262
|
+
*
|
|
263
|
+
* The venv directory list is `VENV_BIN_DIRS` (`package-manager.ts`), shared
|
|
264
|
+
* with `formatters.ts`'s `findInVenv` and `dead-code-client.ts`'s vulture
|
|
265
|
+
* candidates — three independent spellings of the same four directories before
|
|
266
|
+
* #2544 review F1.
|
|
267
|
+
*
|
|
268
|
+
* Resolution is a fixed lookup at `cwd` ONLY (`findLocalBinAt`, no ancestor
|
|
269
|
+
* walk). #2544 review round 3 folded this onto the shared ANCESTOR walker,
|
|
270
|
+
* on the reasoning that a monorepo package should find its repo-root venv —
|
|
271
|
+
* but that was an unrequested behaviour change with no #2514 defect behind
|
|
272
|
+
* it (#2514 is about the HOME ceiling, not about climbing at all), and the
|
|
273
|
+
* inversion is real: `~/code/app` with no venv of its own would resolve
|
|
274
|
+
* `~/code/.venv/bin/ruff` — an unrelated SIBLING project's interpreter —
|
|
275
|
+
* ahead of PATH. Reverted in round 4 F2. The HOME ceiling is moot here: a
|
|
276
|
+
* lookup at exactly `cwd` can never climb past it.
|
|
239
277
|
*/
|
|
240
|
-
export function createVenvFinder(command, windowsExt = "") {
|
|
278
|
+
export function createVenvFinder(command, windowsExt = "", verificationArgs = ["--version"]) {
|
|
241
279
|
return async (cwd) => {
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
for (const venvPath of venvPaths) {
|
|
249
|
-
const fullPath = path.join(cwd, venvPath);
|
|
250
|
-
if (fs.existsSync(fullPath)) {
|
|
251
|
-
return fullPath;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
280
|
+
const venvBin = findLocalBinAt(command, cwd, {
|
|
281
|
+
windowsExt,
|
|
282
|
+
binDirs: VENV_BIN_DIRS,
|
|
283
|
+
});
|
|
284
|
+
if (venvBin)
|
|
285
|
+
return venvBin;
|
|
254
286
|
// Managed-dir install (~/.pi-lens/tools/node_modules/.bin/<command>) — the
|
|
255
287
|
// same shim `ensureTool()` installs npm-strategy tools into. Checked BEFORE
|
|
256
288
|
// the bare-name PATH fallback (#1638): without this, a tool that only
|
|
@@ -261,9 +293,15 @@ export function createVenvFinder(command, windowsExt = "") {
|
|
|
261
293
|
// check settles it without a spawn — after one verification per shim per
|
|
262
294
|
// session, so a shim that cannot run falls through to PATH instead of
|
|
263
295
|
// shadowing a working binary (#1657).
|
|
264
|
-
const managed = await findManagedNodeToolBinary(command);
|
|
296
|
+
const managed = await findManagedNodeToolBinary(command, verificationArgs);
|
|
265
297
|
if (managed)
|
|
266
298
|
return managed;
|
|
299
|
+
// Release-managed install (`~/.pi-lens/bin/<command>`) — where `ensureTool`
|
|
300
|
+
// puts every github/maven/archive-strategy tool. Same rung, same reason as
|
|
301
|
+
// the npm shim above, for the other half of the managed families (#2140).
|
|
302
|
+
const release = await findManagedReleaseBinary(command, verificationArgs);
|
|
303
|
+
if (release)
|
|
304
|
+
return release;
|
|
267
305
|
// Fall back to global
|
|
268
306
|
return command;
|
|
269
307
|
};
|
|
@@ -366,6 +404,19 @@ const latchedUnavailableByCwd = new PathKeyedMap(normalizeMapKey);
|
|
|
366
404
|
// instead of five hand-rolled ones, and a dropped straddling write is visible
|
|
367
405
|
// in the degradation ledger instead of silent.
|
|
368
406
|
const availabilityGeneration = createGenerationSource("dispatch-availability");
|
|
407
|
+
export const getDispatchAvailabilityGeneration = () => availabilityGeneration.current();
|
|
408
|
+
const checkerProbeFlights = createAvailabilityProbeFlight({ generation: getDispatchAvailabilityGeneration });
|
|
409
|
+
export function recordAvailabilityProbeOverrun(tool, key, elapsedMs, budgetMs, failure) {
|
|
410
|
+
if (budgetMs === undefined || elapsedMs <= budgetMs)
|
|
411
|
+
return;
|
|
412
|
+
if (failure === "timeout" && elapsedMs <= budgetMs * 2)
|
|
413
|
+
return;
|
|
414
|
+
incrementDegradationCount({
|
|
415
|
+
kind: "availability-probe-overrun",
|
|
416
|
+
subject: `${tool}:${key}`,
|
|
417
|
+
reason: `probe took ${Math.round(elapsedMs)}ms; budget was ${Math.round(budgetMs)}ms`,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
369
420
|
function installStateFor(cwd, toolId) {
|
|
370
421
|
let states = installAttemptsByCwd.get(cwd);
|
|
371
422
|
if (!states) {
|
|
@@ -488,6 +539,41 @@ function sourceTagForToolId(toolId) {
|
|
|
488
539
|
return undefined;
|
|
489
540
|
}
|
|
490
541
|
}
|
|
542
|
+
/**
|
|
543
|
+
* `binary`/`source` for a probe that resolved through one of pi-lens's OWN
|
|
544
|
+
* managed installs rather than through PATH or a project venv (#2140). A reader
|
|
545
|
+
* of latency.log could otherwise not tell the two apart, and the whole point of
|
|
546
|
+
* the fix is that the managed directories now answer where PATH used to miss.
|
|
547
|
+
*
|
|
548
|
+
* BOTH managed rungs are asked, in the order `createVenvFinder` walks them
|
|
549
|
+
* (#2140 review F1). The first version asked only about the release directory,
|
|
550
|
+
* so every npm-shim hit — knip, jscpd, madge, pyright, biome, htmlhint,
|
|
551
|
+
* stylelint — logged an evidence-free row indistinguishable from a PATH hit,
|
|
552
|
+
* while the doc claimed the opposite. Each rung is asked of THE function that
|
|
553
|
+
* produced its own paths (`managedNodeToolCandidates`, `findManagedToolBinary`)
|
|
554
|
+
* and settled by string identity, never by a path-prefix predicate over a
|
|
555
|
+
* directory: a second opinion about which paths are managed is exactly the
|
|
556
|
+
* parallel-list drift `sourceTagForToolId` exists to avoid, and it would answer
|
|
557
|
+
* differently for case or separator variants.
|
|
558
|
+
*
|
|
559
|
+
* `binary` present IS the managed hit; `source` names the family whenever the
|
|
560
|
+
* command is a registry id (`sourceTagForToolId` reads the registry's own
|
|
561
|
+
* `installStrategy`). A managed shim whose COMMAND is not itself a registry id
|
|
562
|
+
* — `markdownlint-cli2`, installed under registry id `markdownlint` — carries
|
|
563
|
+
* `binary` alone rather than a guessed family. `binary` is a BASENAME, never
|
|
564
|
+
* the absolute path — same rule as every other evidence field (#1568 review).
|
|
565
|
+
*/
|
|
566
|
+
async function describeManagedResolution(tool, resolved) {
|
|
567
|
+
const managed = managedNodeToolCandidates(tool).includes(resolved) ||
|
|
568
|
+
(await findManagedToolBinary(tool)) === resolved;
|
|
569
|
+
if (!managed)
|
|
570
|
+
return {};
|
|
571
|
+
const source = sourceTagForToolId(tool);
|
|
572
|
+
return {
|
|
573
|
+
binary: path.basename(resolved),
|
|
574
|
+
...(source !== undefined && { source }),
|
|
575
|
+
};
|
|
576
|
+
}
|
|
491
577
|
/**
|
|
492
578
|
* How to tag a not-fresh install resolution in the compensating row's
|
|
493
579
|
* evidence (#1636 review, carried over from #1615's verify). `getInstallAttempt`
|
|
@@ -598,6 +684,9 @@ export function resetDispatchAvailabilityState() {
|
|
|
598
684
|
managedVerifyInFlight.clear();
|
|
599
685
|
resetPathWalkMemo();
|
|
600
686
|
availabilityGeneration.bump();
|
|
687
|
+
checkerProbeFlights.clear();
|
|
688
|
+
cwdProbeFlights.clear();
|
|
689
|
+
// The generation bump invalidates dispatcherProbeFlights in its owner module.
|
|
601
690
|
}
|
|
602
691
|
/**
|
|
603
692
|
* Create a cached availability checker for a command.
|
|
@@ -620,18 +709,21 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
620
709
|
const cacheByCwd = new PathKeyedMap(normalizeEphemeralMapKey);
|
|
621
710
|
const inFlightByCwd = new PathKeyedMap(normalizeEphemeralMapKey);
|
|
622
711
|
let checkerGeneration = availabilityGeneration.current();
|
|
623
|
-
|
|
712
|
+
let checkerFlightGeneration = 0;
|
|
713
|
+
const findCommand = createVenvFinder(command, windowsExt, versionArgs);
|
|
624
714
|
function ensureCurrentGeneration() {
|
|
625
715
|
if (checkerGeneration === availabilityGeneration.current())
|
|
626
716
|
return;
|
|
627
717
|
cacheByCwd.clear();
|
|
628
718
|
inFlightByCwd.clear();
|
|
629
719
|
checkerGeneration = availabilityGeneration.current();
|
|
720
|
+
checkerFlightGeneration++;
|
|
630
721
|
}
|
|
631
722
|
const reset = () => {
|
|
632
723
|
cacheByCwd.clear();
|
|
633
724
|
inFlightByCwd.clear();
|
|
634
725
|
checkerGeneration = availabilityGeneration.current();
|
|
726
|
+
checkerFlightGeneration++;
|
|
635
727
|
};
|
|
636
728
|
function getCache(cwd) {
|
|
637
729
|
ensureCurrentGeneration();
|
|
@@ -692,9 +784,9 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
692
784
|
cause: verdict.cause,
|
|
693
785
|
elapsedMs: verdict.elapsedMs,
|
|
694
786
|
latched,
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
787
|
+
// Every caller passes this (#2209); a direct key, not a conditional
|
|
788
|
+
// spread, keeps it visible to the call-shaped classifiedBy sweep.
|
|
789
|
+
classifiedBy: verdict.classifiedBy,
|
|
698
790
|
...(verdict.evidence !== undefined && { evidence: verdict.evidence }),
|
|
699
791
|
...(verdict.hostStallMs !== undefined && {
|
|
700
792
|
hostStallMs: verdict.hostStallMs,
|
|
@@ -777,7 +869,16 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
777
869
|
});
|
|
778
870
|
return false;
|
|
779
871
|
}
|
|
872
|
+
// Resolution is measured separately from the spawn (#2140 review F2).
|
|
873
|
+
// It is no longer free — the managed rungs stat two directories and,
|
|
874
|
+
// on a first touch, pay their own verification spawn — and it runs
|
|
875
|
+
// BEFORE `startedAt`, so neither `durationMs` nor
|
|
876
|
+
// `recordAvailabilityProbeOverrun` can see it. Folding it into
|
|
877
|
+
// `durationMs` would instead charge the probe budget for work that
|
|
878
|
+
// budget does not govern, so the two spans are reported side by side.
|
|
879
|
+
const resolveStartedAt = Date.now();
|
|
780
880
|
const cmd = await findCommand(resolvedCwd);
|
|
881
|
+
const resolveMs = Date.now() - resolveStartedAt;
|
|
781
882
|
// #1995: a command cooling down after a RUNTIME timeout (lint or
|
|
782
883
|
// autofix lane blew its real budget) must not re-probe on every
|
|
783
884
|
// edit - the positive verdict is effectively cooled. Consult-only:
|
|
@@ -807,17 +908,22 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
807
908
|
const startedAt = Date.now();
|
|
808
909
|
let result;
|
|
809
910
|
let hostStallMs;
|
|
911
|
+
let probeJoined = false;
|
|
810
912
|
try {
|
|
811
|
-
|
|
913
|
+
const shared = checkerProbeFlights.run(`checker:${command}|${versionArgs.join("|")}|${key}|${checkerFlightGeneration}|${windowsExt}|${cmd}|${JSON.stringify(Object.entries(env ?? {}).sort(([a], [b]) => compareOrdinal(a, b)))}`, () => safeSpawnAsync(cmd, versionArgs, {
|
|
812
914
|
timeout: options.probeTimeout ?? 5000,
|
|
813
915
|
cwd: resolvedCwd,
|
|
814
916
|
env,
|
|
815
|
-
|
|
917
|
+
input: "",
|
|
918
|
+
}));
|
|
919
|
+
probeJoined = shared.joined;
|
|
920
|
+
result = await shared.promise;
|
|
816
921
|
}
|
|
817
922
|
finally {
|
|
818
923
|
hostStallMs = stallSampler.stop();
|
|
819
924
|
}
|
|
820
925
|
const elapsedMs = Date.now() - startedAt;
|
|
926
|
+
recordAvailabilityProbeOverrun(command, resolvedCwd, elapsedMs, options.probeTimeout ?? 5000, result.failure);
|
|
821
927
|
if (!result.error && result.status === 0) {
|
|
822
928
|
cache.command = cmd;
|
|
823
929
|
noteDecision(cache, resolvedCwd, {
|
|
@@ -826,8 +932,12 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
826
932
|
cause: "ok",
|
|
827
933
|
elapsedMs,
|
|
828
934
|
hostStallMs,
|
|
829
|
-
classifiedBy: "probe",
|
|
830
|
-
evidence:
|
|
935
|
+
classifiedBy: probeJoined ? "joined" : "probe",
|
|
936
|
+
evidence: {
|
|
937
|
+
...describeProbeEvidence(result),
|
|
938
|
+
...(await describeManagedResolution(command, cmd)),
|
|
939
|
+
resolveMs,
|
|
940
|
+
},
|
|
831
941
|
});
|
|
832
942
|
return true;
|
|
833
943
|
}
|
|
@@ -846,7 +956,7 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
846
956
|
cause,
|
|
847
957
|
elapsedMs,
|
|
848
958
|
hostStallMs,
|
|
849
|
-
classifiedBy: "probe",
|
|
959
|
+
classifiedBy: probeJoined ? "joined" : "probe",
|
|
850
960
|
evidence,
|
|
851
961
|
});
|
|
852
962
|
return false;
|
|
@@ -885,6 +995,9 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
885
995
|
}
|
|
886
996
|
return { isAvailableAsync, getCommand, getOutcome, getVerdict, reset };
|
|
887
997
|
}
|
|
998
|
+
const cwdProbeFlights = createAvailabilityProbeFlight({
|
|
999
|
+
generation: getDispatchAvailabilityGeneration,
|
|
1000
|
+
});
|
|
888
1001
|
/**
|
|
889
1002
|
* Per-cwd cached availability probe for spawn signatures that don't fit
|
|
890
1003
|
* `createAvailabilityChecker` — multi-arg subcommands like `cargo clippy
|
|
@@ -957,7 +1070,7 @@ export function createCwdCachedProbe(probe, options) {
|
|
|
957
1070
|
...(retryAfterMs !== undefined && { retryAfterMs }),
|
|
958
1071
|
...(options.budgetMs !== undefined && { budgetMs: options.budgetMs }),
|
|
959
1072
|
// Every verdict here is derived from the probe this seam just ran.
|
|
960
|
-
classifiedBy: "probe",
|
|
1073
|
+
classifiedBy: verdict.classifiedBy ?? "probe",
|
|
961
1074
|
...(verdict.evidence !== undefined && { evidence: verdict.evidence }),
|
|
962
1075
|
}, key);
|
|
963
1076
|
}
|
|
@@ -981,20 +1094,24 @@ export function createCwdCachedProbe(probe, options) {
|
|
|
981
1094
|
const startedAt = Date.now();
|
|
982
1095
|
let result;
|
|
983
1096
|
let thrown;
|
|
1097
|
+
let probeJoined = false;
|
|
984
1098
|
try {
|
|
985
|
-
|
|
1099
|
+
const shared = cwdProbeFlights.run(`cwd:${options.tool}|${options.flightKeyComponent ?? ""}|${key}`, () => probe(key));
|
|
1100
|
+
probeJoined = shared.joined;
|
|
1101
|
+
result = await shared.promise;
|
|
986
1102
|
}
|
|
987
1103
|
catch (error) {
|
|
988
1104
|
thrown = error;
|
|
989
1105
|
}
|
|
990
1106
|
const hostStallMs = stallSampler.stop();
|
|
991
1107
|
const elapsedMs = Date.now() - startedAt;
|
|
992
|
-
// A probe that threw carries its errno in the Error, which is exactly
|
|
993
|
-
// what the taxonomy reads — so a thrown EAGAIN stays transient instead
|
|
994
|
-
// of collapsing into an untyped `false`.
|
|
995
1108
|
const shape = result ?? {
|
|
996
1109
|
error: thrown instanceof Error ? thrown : new Error(String(thrown)),
|
|
997
1110
|
};
|
|
1111
|
+
recordAvailabilityProbeOverrun(options.tool, key, elapsedMs, options.budgetMs, shape.failure);
|
|
1112
|
+
// A probe that threw carries its errno in the Error, which is exactly
|
|
1113
|
+
// what the taxonomy reads — so a thrown EAGAIN stays transient instead
|
|
1114
|
+
// of collapsing into an untyped `false`.
|
|
998
1115
|
if (result && !result.error && result.status === 0) {
|
|
999
1116
|
note(latch, key, {
|
|
1000
1117
|
available: true,
|
|
@@ -1002,6 +1119,7 @@ export function createCwdCachedProbe(probe, options) {
|
|
|
1002
1119
|
cause: "ok",
|
|
1003
1120
|
elapsedMs,
|
|
1004
1121
|
hostStallMs,
|
|
1122
|
+
classifiedBy: probeJoined ? "joined" : "probe",
|
|
1005
1123
|
evidence: describeProbeEvidence(result, options.tool),
|
|
1006
1124
|
});
|
|
1007
1125
|
return true;
|
|
@@ -1017,6 +1135,7 @@ export function createCwdCachedProbe(probe, options) {
|
|
|
1017
1135
|
cause,
|
|
1018
1136
|
elapsedMs,
|
|
1019
1137
|
hostStallMs,
|
|
1138
|
+
classifiedBy: probeJoined ? "joined" : "probe",
|
|
1020
1139
|
evidence,
|
|
1021
1140
|
});
|
|
1022
1141
|
return false;
|
|
@@ -1069,30 +1188,20 @@ export function resolveToolCommand(cwd, toolId) {
|
|
|
1069
1188
|
return null;
|
|
1070
1189
|
return resolveNodeToolCommand(cwd, spec.command, spec.windowsExt ?? ".cmd");
|
|
1071
1190
|
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Composer's `vendor/bin/<tool>` from the nearest ancestor that has one.
|
|
1193
|
+
*
|
|
1194
|
+
* Delegates to THE shared bin walker (#2544 review F1): this was a fifth
|
|
1195
|
+
* private copy of that climb, unceilinged, so a `vendor/bin` at or above
|
|
1196
|
+
* `$HOME` — which can never be this project's own Composer install — was
|
|
1197
|
+
* resolved as the project's tool (#2514, same class as `findInVendorBin`,
|
|
1198
|
+
* which had already been folded).
|
|
1199
|
+
*/
|
|
1072
1200
|
export function resolveVendorToolCommand(cwd, toolName, windowsExt = ".bat") {
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
path.join("vendor", "bin", toolName),
|
|
1078
|
-
]
|
|
1079
|
-
: [path.join("vendor", "bin", toolName)];
|
|
1080
|
-
let dir = cwd;
|
|
1081
|
-
const root = path.parse(dir).root;
|
|
1082
|
-
while (true) {
|
|
1083
|
-
for (const candidate of candidates) {
|
|
1084
|
-
const full = path.join(dir, candidate);
|
|
1085
|
-
if (fs.existsSync(full))
|
|
1086
|
-
return full;
|
|
1087
|
-
}
|
|
1088
|
-
if (dir === root)
|
|
1089
|
-
break;
|
|
1090
|
-
const parent = path.dirname(dir);
|
|
1091
|
-
if (parent === dir)
|
|
1092
|
-
break;
|
|
1093
|
-
dir = parent;
|
|
1094
|
-
}
|
|
1095
|
-
return null;
|
|
1201
|
+
return (findLocalBinUpwards(toolName, cwd, {
|
|
1202
|
+
windowsExt,
|
|
1203
|
+
binDirs: VENDOR_BIN_DIRS,
|
|
1204
|
+
}) ?? null);
|
|
1096
1205
|
}
|
|
1097
1206
|
export async function resolveToolCommandWithInstallFallback(cwd, toolId, timeout = 5000) {
|
|
1098
1207
|
const spec = getToolCommandSpec(toolId);
|
|
@@ -1108,6 +1217,7 @@ async function verifyOrInstallCommand(command, toolId, cwd, versionArgs = ["--ve
|
|
|
1108
1217
|
const versionCheck = await safeSpawnAsync(command, versionArgs, {
|
|
1109
1218
|
timeout,
|
|
1110
1219
|
cwd,
|
|
1220
|
+
input: "",
|
|
1111
1221
|
});
|
|
1112
1222
|
if (!versionCheck.error && versionCheck.status === 0) {
|
|
1113
1223
|
return command;
|
|
@@ -1141,7 +1251,7 @@ async function verifyOrInstallCommand(command, toolId, cwd, versionArgs = ["--ve
|
|
|
1141
1251
|
return null;
|
|
1142
1252
|
}
|
|
1143
1253
|
export async function resolveCommandArgsWithInstallFallback(command, toolId, cwd, versionArgs = ["--version"], timeout = 5000) {
|
|
1144
|
-
const versionCheck = await safeSpawnAsync(command.cmd, [...command.args, ...versionArgs], { timeout, cwd });
|
|
1254
|
+
const versionCheck = await safeSpawnAsync(command.cmd, [...command.args, ...versionArgs], { timeout, cwd, input: "" });
|
|
1145
1255
|
if (!versionCheck.error && versionCheck.status === 0) {
|
|
1146
1256
|
return command;
|
|
1147
1257
|
}
|
|
@@ -1318,17 +1428,40 @@ function buildSgLocalBins() {
|
|
|
1318
1428
|
: [".cmd", ".exe", ""]
|
|
1319
1429
|
: [""];
|
|
1320
1430
|
const binaryCandidates = ["ast-grep", "sg"].flatMap((base) => extensions.map((ext) => `${base}${ext}`));
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1431
|
+
// THE shared bin walk (#2544 review F1). This used to be a private
|
|
1432
|
+
// `findNodeBinRoots` climb with no HOME ceiling, so on a box with a
|
|
1433
|
+
// home-level `node_modules` (the pi-extensions manifest installs its own
|
|
1434
|
+
// bins, #2514) `$HOME` itself came back as a bin root and an unrelated
|
|
1435
|
+
// ast-grep was probed as if it were this project's. `windowsExt: ""`
|
|
1436
|
+
// because the candidate names already carry their extensions — in a
|
|
1437
|
+
// shell-dependent ORDER (`hasBash` above) that a single `windowsExt`
|
|
1438
|
+
// cannot express.
|
|
1439
|
+
//
|
|
1440
|
+
// The PROJECT walk (from `process.cwd()`) keeps the default HOME ceiling —
|
|
1441
|
+
// a `node_modules/.bin/ast-grep` at or above `$HOME` can never be THIS
|
|
1442
|
+
// project's own dependency. pi-lens's OWN install-tree walk (from
|
|
1443
|
+
// `_thisDir`) is exempt (`ceiling: false`, #2544 round 4 F1): `pi install
|
|
1444
|
+
// npm:pi-lens` installs into the HOME-level manifest
|
|
1445
|
+
// (`~/package.json` → `~/node_modules/pi-lens`), so pi-lens's bundled
|
|
1446
|
+
// `@ast-grep/cli` shim can sit AT `~/node_modules/.bin/ast-grep` — exactly
|
|
1447
|
+
// the directory the PROJECT ceiling exists to stop at. A previous version
|
|
1448
|
+
// of this comment claimed the own install always lives "under"
|
|
1449
|
+
// `~/.pi/extensions/…`, below the ceiling; that is false for the
|
|
1450
|
+
// HOME-level manifest layout, and the round-2 ceiling silently made this
|
|
1451
|
+
// walk return nothing for it (PATH, `findGlobalBinary` and npx do not
|
|
1452
|
+
// cover this location either). `_managedToolsDir` (`~/.pi-lens/tools`) is
|
|
1453
|
+
// a fixed location, not a walk, so it is checked directly as before —
|
|
1454
|
+
// also own-install, since `ensureTool()` puts npm-strategy installs there.
|
|
1455
|
+
const projectWalkOptions = { windowsExt: "" };
|
|
1456
|
+
const ownInstallWalkOptions = { windowsExt: "", ceiling: false };
|
|
1457
|
+
const bins = [
|
|
1458
|
+
...findLocalBinsUpwards(binaryCandidates, _thisDir, ownInstallWalkOptions).map((binPath) => ({ path: binPath, source: "own-install" })),
|
|
1459
|
+
...findLocalBinsUpwards(binaryCandidates, process.cwd(), projectWalkOptions).map((binPath) => ({ path: binPath, source: "project" })),
|
|
1325
1460
|
];
|
|
1326
|
-
const
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
if (fs.existsSync(localBin))
|
|
1331
|
-
bins.push(localBin);
|
|
1461
|
+
for (const candidate of binaryCandidates) {
|
|
1462
|
+
const managedBin = path.join(_managedToolsDir, "node_modules", ".bin", candidate);
|
|
1463
|
+
if (fs.existsSync(managedBin)) {
|
|
1464
|
+
bins.push({ path: managedBin, source: "own-install" });
|
|
1332
1465
|
}
|
|
1333
1466
|
}
|
|
1334
1467
|
return bins;
|
|
@@ -1353,19 +1486,19 @@ export async function isSgAvailableAsync() {
|
|
|
1353
1486
|
return memo;
|
|
1354
1487
|
if (sgAvailableInFlight)
|
|
1355
1488
|
return sgAvailableInFlight;
|
|
1356
|
-
|
|
1489
|
+
const flight = (async () => {
|
|
1357
1490
|
const startedAt = Date.now();
|
|
1358
1491
|
sgSweepSawTransient = false;
|
|
1359
1492
|
sgSweepTransientCause = "probe-timeout";
|
|
1360
1493
|
sgSweepHostStallMs = 0;
|
|
1361
1494
|
sgSweepUnreachable = [];
|
|
1362
1495
|
sgSweepDurablyMissing = [];
|
|
1363
|
-
// 1. Local node_modules/.bin
|
|
1364
|
-
for (const
|
|
1365
|
-
if (await probeAstGrepCommandAsync(
|
|
1366
|
-
sgCmd =
|
|
1496
|
+
// 1. Local node_modules/.bin (own-install tree, then project tree)
|
|
1497
|
+
for (const candidate of buildSgLocalBins()) {
|
|
1498
|
+
if (await probeAstGrepCommandAsync(candidate.path)) {
|
|
1499
|
+
sgCmd = candidate.path;
|
|
1367
1500
|
sgCmdArgs = [];
|
|
1368
|
-
noteSgAvailable(startedAt);
|
|
1501
|
+
noteSgAvailable(startedAt, { source: candidate.source });
|
|
1369
1502
|
return true;
|
|
1370
1503
|
}
|
|
1371
1504
|
}
|
|
@@ -1374,7 +1507,7 @@ export async function isSgAvailableAsync() {
|
|
|
1374
1507
|
if (await probeAstGrepCommandAsync(cmd)) {
|
|
1375
1508
|
sgCmd = cmd;
|
|
1376
1509
|
sgCmdArgs = [];
|
|
1377
|
-
noteSgAvailable(startedAt);
|
|
1510
|
+
noteSgAvailable(startedAt, { source: "global" });
|
|
1378
1511
|
return true;
|
|
1379
1512
|
}
|
|
1380
1513
|
}
|
|
@@ -1385,7 +1518,7 @@ export async function isSgAvailableAsync() {
|
|
|
1385
1518
|
if (globalBin && (await probeAstGrepCommandAsync(globalBin))) {
|
|
1386
1519
|
sgCmd = globalBin;
|
|
1387
1520
|
sgCmdArgs = [];
|
|
1388
|
-
noteSgAvailable(startedAt);
|
|
1521
|
+
noteSgAvailable(startedAt, { source: "global" });
|
|
1389
1522
|
return true;
|
|
1390
1523
|
}
|
|
1391
1524
|
}
|
|
@@ -1393,7 +1526,7 @@ export async function isSgAvailableAsync() {
|
|
|
1393
1526
|
if (await probeAstGrepCommandAsync("npx", ["--no", "--", "ast-grep"])) {
|
|
1394
1527
|
sgCmd = "npx";
|
|
1395
1528
|
sgCmdArgs = ["--no", "--", "ast-grep"];
|
|
1396
|
-
noteSgAvailable(startedAt);
|
|
1529
|
+
noteSgAvailable(startedAt, { source: "npx" });
|
|
1397
1530
|
return true;
|
|
1398
1531
|
}
|
|
1399
1532
|
// Nothing answered, and nothing answered TRANSIENTLY, while the verdict we
|
|
@@ -1415,9 +1548,19 @@ export async function isSgAvailableAsync() {
|
|
|
1415
1548
|
noteSgUnavailable(startedAt, sgSweepSawTransient ? "transient" : "missing", sgSweepSawTransient ? sgSweepTransientCause : "not-found");
|
|
1416
1549
|
return false;
|
|
1417
1550
|
})().finally(() => {
|
|
1418
|
-
|
|
1551
|
+
// Identity-guarded release (#1968's pattern): clear the slot only if THIS
|
|
1552
|
+
// flight still owns it. `ensureCurrentSgGeneration()` above nulls the slot
|
|
1553
|
+
// on a generation bump (a session reset arriving mid-flight) so the NEXT
|
|
1554
|
+
// caller starts a fresh flight rather than sharing this one's stale-generation
|
|
1555
|
+
// answer — that fresh flight is a live successor. Without the guard, this
|
|
1556
|
+
// flight's own late settlement would stomp the slot back to null out from
|
|
1557
|
+
// under that successor, and the caller after it would start a redundant
|
|
1558
|
+
// THIRD probe instead of sharing the still-running second one.
|
|
1559
|
+
if (sgAvailableInFlight === flight)
|
|
1560
|
+
sgAvailableInFlight = null;
|
|
1419
1561
|
});
|
|
1420
|
-
|
|
1562
|
+
sgAvailableInFlight = flight;
|
|
1563
|
+
return flight;
|
|
1421
1564
|
}
|
|
1422
1565
|
/**
|
|
1423
1566
|
* Record a successful shared-ast-grep sweep, with one decision record.
|
|
@@ -1432,6 +1575,11 @@ export async function isSgAvailableAsync() {
|
|
|
1432
1575
|
* `retained` marks the other provisional case (#1568 review F1): no candidate
|
|
1433
1576
|
* answered at all, so the winner being reported is the one the previous sweep
|
|
1434
1577
|
* found, kept rather than discarded on a timeout.
|
|
1578
|
+
*
|
|
1579
|
+
* `source` (omitted for a `retained` win, which resolved nothing new this
|
|
1580
|
+
* sweep) logs one `tool_bin_resolved` phase so F1's own-install-vs-project
|
|
1581
|
+
* ceiling distinction is visible in `latency.log` instead of only inferable
|
|
1582
|
+
* from a bin's absence (#2544 round 4 F4).
|
|
1435
1583
|
*/
|
|
1436
1584
|
function noteSgAvailable(startedAt, opts = {}) {
|
|
1437
1585
|
const provisional = sgSweepSawTransient;
|
|
@@ -1442,6 +1590,19 @@ function noteSgAvailable(startedAt, opts = {}) {
|
|
|
1442
1590
|
else {
|
|
1443
1591
|
sgLatch.noteAvailable();
|
|
1444
1592
|
}
|
|
1593
|
+
if (opts.source) {
|
|
1594
|
+
logLatency({
|
|
1595
|
+
type: "phase",
|
|
1596
|
+
phase: "tool_bin_resolved",
|
|
1597
|
+
filePath: "<pi-lens>",
|
|
1598
|
+
durationMs: Math.round(Date.now() - startedAt),
|
|
1599
|
+
metadata: {
|
|
1600
|
+
tool: "ast-grep",
|
|
1601
|
+
source: opts.source,
|
|
1602
|
+
ceilingApplied: opts.source === "project",
|
|
1603
|
+
},
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1445
1606
|
logAvailabilityDecision({
|
|
1446
1607
|
tool: "ast-grep",
|
|
1447
1608
|
verdict: "available",
|
|
@@ -1449,6 +1610,7 @@ function noteSgAvailable(startedAt, opts = {}) {
|
|
|
1449
1610
|
cause: provisional ? sgSweepTransientCause : "ok",
|
|
1450
1611
|
elapsedMs: Date.now() - startedAt,
|
|
1451
1612
|
latched: !provisional,
|
|
1613
|
+
classifiedBy: "probe",
|
|
1452
1614
|
hostStallMs: sgSweepHostStallMs,
|
|
1453
1615
|
budgetMs: 5000,
|
|
1454
1616
|
...(provisional && {
|
|
@@ -1472,6 +1634,9 @@ function noteSgUnavailable(startedAt, outcome, cause) {
|
|
|
1472
1634
|
hostStallMs: sgSweepHostStallMs,
|
|
1473
1635
|
...(retryAfterMs > 0 && { retryAfterMs }),
|
|
1474
1636
|
budgetMs: 5000,
|
|
1637
|
+
// Sibling of noteSgAvailable's "probe": both report the same
|
|
1638
|
+
// candidate sweep this function ran (#2209).
|
|
1639
|
+
classifiedBy: "probe",
|
|
1475
1640
|
});
|
|
1476
1641
|
}
|
|
1477
1642
|
export function getSgCommand() {
|