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
|
@@ -63,4 +63,35 @@ export class RustClient {
|
|
|
63
63
|
isRustFile(filePath) {
|
|
64
64
|
return path.extname(filePath).toLowerCase() === ".rs";
|
|
65
65
|
}
|
|
66
|
+
/** Forget the memoized cargo path and latched availability verdict — #2455. */
|
|
67
|
+
resetAvailability() {
|
|
68
|
+
this.availability.reset();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The one `RustClient` this process owns (#2455 fix round 4, F2).
|
|
73
|
+
*
|
|
74
|
+
* Same shape as `go-client.ts`'s `goClient`, and the same round-2 defect: the
|
|
75
|
+
* runner (`dispatch/runners/rust-clippy.ts`) and `bootstrap.ts` each built
|
|
76
|
+
* their own instance, so `resetRustAvailability` re-armed one latch while
|
|
77
|
+
* `handleSessionStart`'s "Active tools" line read the other's never-expiring
|
|
78
|
+
* probe-class "missing" verdict. One instance, in the module that owns the
|
|
79
|
+
* class; `tests/clients/toolchain-client-singleton.test.ts` holds the line.
|
|
80
|
+
*/
|
|
81
|
+
export const rustClient = new RustClient();
|
|
82
|
+
/**
|
|
83
|
+
* Forget `rustClient`'s memoized cargo path and latched availability verdict —
|
|
84
|
+
* #2455. Same #1496/#1535 shape as `resetZizmorTokenAvailability`; wired into
|
|
85
|
+
* `handleSessionStart` beside it (`clients/runtime-session.ts`) so a Rust
|
|
86
|
+
* toolchain installed mid-process is observed by the next session instead of
|
|
87
|
+
* staying "missing" for the rest of the process's life.
|
|
88
|
+
*
|
|
89
|
+
* This module — not `rust-clippy.ts` — because the reset must sit beside the
|
|
90
|
+
* single instance it clears (#2455 fix round 4, F2). `rust-clippy.ts`'s
|
|
91
|
+
* `clippyAvailabilityByCargo` is a separate, already-covered latch: it rides
|
|
92
|
+
* `createCwdCachedProbe`'s shared `availabilityGeneration` counter (see
|
|
93
|
+
* `runner-helpers.ts`'s `resetDispatchAvailabilityState`), not this reset.
|
|
94
|
+
*/
|
|
95
|
+
export function resetRustAvailability() {
|
|
96
|
+
rustClient.resetAvailability();
|
|
66
97
|
}
|
|
@@ -16,11 +16,13 @@
|
|
|
16
16
|
import { spawn, spawnSync, } from "node:child_process";
|
|
17
17
|
import * as fs from "node:fs";
|
|
18
18
|
import * as path from "node:path";
|
|
19
|
+
import { BoundedFifoMap } from "./bounded-cache.js";
|
|
19
20
|
import { logLatency } from "./latency-logger.js";
|
|
20
21
|
import { recordDegradation } from "./degradation-ledger.js";
|
|
21
22
|
import { logExtension } from "./extension-log.js";
|
|
22
23
|
import { isFullyQualifiedWin32 } from "./path-utils.js";
|
|
23
24
|
import { startSpawnUsageSampler } from "./resource-sampler.js";
|
|
25
|
+
import { compareOrdinal } from "./string-utils.js";
|
|
24
26
|
/** Intent-level spawn failure. `cause` retains the original OS Error/errno. */
|
|
25
27
|
export class SpawnFailureError extends Error {
|
|
26
28
|
kind;
|
|
@@ -569,23 +571,21 @@ function resolveWindowsPathEntry(entry, effectiveCwd, env) {
|
|
|
569
571
|
* cache is also count-bounded with oldest-entry eviction because a long-lived
|
|
570
572
|
* process can encounter unbounded cwd/environment combinations.
|
|
571
573
|
*/
|
|
572
|
-
const WINDOWS_COMMAND_CACHE_MAX_ENTRIES = 256;
|
|
574
|
+
export const WINDOWS_COMMAND_CACHE_MAX_ENTRIES = 256;
|
|
573
575
|
const WINDOWS_COMMAND_NEGATIVE_CACHE_TTL_MS = 1000;
|
|
574
|
-
const windowsCommandCache = new
|
|
576
|
+
const windowsCommandCache = new BoundedFifoMap(WINDOWS_COMMAND_CACHE_MAX_ENTRIES);
|
|
575
577
|
/** Reset after session replacement or a successful managed install. */
|
|
576
578
|
export function resetSafeSpawnWindowsCommandCache() {
|
|
577
579
|
windowsCommandCache.clear();
|
|
578
580
|
loggedSpawnClassifications.clear();
|
|
579
581
|
}
|
|
580
582
|
function cacheWindowsCommandResult(key, resolved) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
windowsCommandCache.delete(oldest);
|
|
588
|
-
}
|
|
583
|
+
// Resolution entries are session-scoped and cheap to recompute. The
|
|
584
|
+
// caller always deletes any stale entry for `key` before reaching here
|
|
585
|
+
// (see resolveWindowsCommandForEnvironment), so this can only grow the
|
|
586
|
+
// map; BoundedFifoMap evicts the oldest insertion first once the map
|
|
587
|
+
// exceeds capacity, so a long-lived process cannot retain every cwd /
|
|
588
|
+
// environment it has ever touched.
|
|
589
589
|
windowsCommandCache.set(key, { resolved, checkedAt: Date.now() });
|
|
590
590
|
}
|
|
591
591
|
function getPathExts(env) {
|
|
@@ -701,7 +701,7 @@ export function resolveWindowsCommandForEnvironment(command, cwd, env) {
|
|
|
701
701
|
.flatMap(([key, value]) => /^=[A-Za-z]:$/.test(key)
|
|
702
702
|
? [[key.toLowerCase(), value]]
|
|
703
703
|
: [])
|
|
704
|
-
.sort(([left], [right]) => left
|
|
704
|
+
.sort(([left], [right]) => compareOrdinal(left, right));
|
|
705
705
|
// Keep presence separate from value: absent PATHEXT means the Windows
|
|
706
706
|
// default extension list, while PATHEXT="" means no implicit extensions.
|
|
707
707
|
// The per-drive snapshot is equally important: it is provenance for
|
|
@@ -792,6 +792,13 @@ function ensureUtf8ConsoleCodePageOnce() {
|
|
|
792
792
|
spawnSync(cmdExe, ["/d", "/s", "/c", "chcp 65001 >nul 2>&1"], {
|
|
793
793
|
shell: false,
|
|
794
794
|
windowsHide: true,
|
|
795
|
+
// #1980 population sweep: a synchronous spawn with no timeout parks
|
|
796
|
+
// the event loop for as long as the child takes, and nothing here
|
|
797
|
+
// bounds that. `chcp` is normally instant, but this runs before the
|
|
798
|
+
// FIRST real spawn of the process, which is exactly when a cold or
|
|
799
|
+
// antivirus-throttled cmd.exe is slowest. Matches the 5s bound every
|
|
800
|
+
// other synchronous child-process site in this repo already carries.
|
|
801
|
+
timeout: 5000,
|
|
795
802
|
});
|
|
796
803
|
}
|
|
797
804
|
catch {
|
|
@@ -840,6 +847,30 @@ const EXIT_PIPE_IDLE_MAX_WAIT_MS = 2000;
|
|
|
840
847
|
* - Kills the process on timeout (preventing zombies)
|
|
841
848
|
* - Supports cancellation via AbortSignal
|
|
842
849
|
*
|
|
850
|
+
* ## Why this bound is NOT `bounded()` — the one documented exception (#2523)
|
|
851
|
+
*
|
|
852
|
+
* #2523 slice 2 folded every other "deadline AND abort signal" implementation
|
|
853
|
+
* in the tree onto `clients/deadline-utils.ts#bounded`. This one stays, and it
|
|
854
|
+
* is not a copy of the same idea:
|
|
855
|
+
*
|
|
856
|
+
* `bounded()` ABANDONS a promise — it stops waiting and lets the work run on
|
|
857
|
+
* unobserved, which is safe precisely because everything it wraps is in-process
|
|
858
|
+
* (a dynamic import, an LSP round trip, a `stat`). This function's work is a
|
|
859
|
+
* CHILD PROCESS, and abandoning one leaks it. So its timer does not settle a
|
|
860
|
+
* race at all: `timeoutId` sets `timedOut`, then runs `killTree()` — SIGTERM,
|
|
861
|
+
* an escalation timer, SIGKILL, and on Windows a `taskkill /T` of the whole
|
|
862
|
+
* tree — and the promise settles only when the child's own `close` arrives (or
|
|
863
|
+
* the escalation gives up). The result it produces carries the teardown
|
|
864
|
+
* EVIDENCE (`timeoutTeardown`, #2010): which signal killed it, whether the
|
|
865
|
+
* tree actually died, how long it took. Replacing that with `bounded()` would
|
|
866
|
+
* return `undefined` at the deadline and leave a live `vitest`/`gradle` behind
|
|
867
|
+
* — the orphan class #2504 measured as 59 concurrent spawns starving a turn.
|
|
868
|
+
*
|
|
869
|
+
* The rule the exception preserves: a bound that can CANCEL its work must
|
|
870
|
+
* cancel it. `bounded()` is for work that cannot be cancelled; this is not
|
|
871
|
+
* that. Anything wrapping THIS function still gets `bounded()` around its own
|
|
872
|
+
* await if it needs one.
|
|
873
|
+
*
|
|
843
874
|
* @example
|
|
844
875
|
* const result = await safeSpawnAsync("npm", ["test"], { timeout: 30000 });
|
|
845
876
|
* if (result.error) console.error("Failed:", result.error);
|
|
@@ -891,6 +922,8 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
891
922
|
let aborted = false;
|
|
892
923
|
let killed = false;
|
|
893
924
|
let outputTruncated = false;
|
|
925
|
+
let killedForOutputCap = false;
|
|
926
|
+
let streamingMatch = false;
|
|
894
927
|
// #1651 review: a single boolean the close/error handlers both check
|
|
895
928
|
// AND set, so whichever one decides the outcome first wins outright —
|
|
896
929
|
// the DECISION itself is shape-based (see the `close` handler below),
|
|
@@ -937,21 +970,116 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
937
970
|
options.maxOutputBytes > 0
|
|
938
971
|
? Math.floor(options.maxOutputBytes)
|
|
939
972
|
: undefined;
|
|
940
|
-
const
|
|
973
|
+
const outputTruncationMarker = "\n...[output truncated]...\n";
|
|
974
|
+
let retainedHead = [];
|
|
975
|
+
let retainedTail = [];
|
|
976
|
+
let retainedTailBytes = 0;
|
|
977
|
+
let retainedTailLimit = 0;
|
|
978
|
+
let truncationStream = "stderr";
|
|
979
|
+
const streamingMatcher = options?.matchWhileStreaming;
|
|
980
|
+
const streamingCarryBytes = streamingMatcher
|
|
981
|
+
? Math.max(0, streamingMatcher.source.length - 1)
|
|
982
|
+
: 0;
|
|
983
|
+
const streamingCarry = {
|
|
984
|
+
stdout: "",
|
|
985
|
+
stderr: "",
|
|
986
|
+
};
|
|
987
|
+
const bytePrefix = (text, bytes) => Buffer.from(text).subarray(0, bytes).toString();
|
|
988
|
+
const byteSuffix = (text, bytes) => {
|
|
989
|
+
const buffer = Buffer.from(text);
|
|
990
|
+
return buffer.subarray(Math.max(0, buffer.byteLength - bytes)).toString();
|
|
991
|
+
};
|
|
992
|
+
const appendTail = (part, maxBytes) => {
|
|
993
|
+
if (maxBytes <= 0)
|
|
994
|
+
return;
|
|
995
|
+
retainedTail.push({ stream: part.stream, text: part.text });
|
|
996
|
+
retainedTailBytes += Buffer.byteLength(part.text);
|
|
997
|
+
while (retainedTailBytes > maxBytes && retainedTail.length > 0) {
|
|
998
|
+
const first = retainedTail[0];
|
|
999
|
+
const excess = retainedTailBytes - maxBytes;
|
|
1000
|
+
const firstBytes = Buffer.byteLength(first.text);
|
|
1001
|
+
if (firstBytes <= excess) {
|
|
1002
|
+
retainedTail.shift();
|
|
1003
|
+
retainedTailBytes -= firstBytes;
|
|
1004
|
+
}
|
|
1005
|
+
else {
|
|
1006
|
+
first.text = byteSuffix(first.text, firstBytes - excess);
|
|
1007
|
+
retainedTailBytes -= excess;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
const renderOutput = (stream) => {
|
|
1012
|
+
const head = retainedHead
|
|
1013
|
+
.filter((part) => part.stream === stream)
|
|
1014
|
+
.map((part) => part.text)
|
|
1015
|
+
.join("");
|
|
1016
|
+
const tail = retainedTail
|
|
1017
|
+
.filter((part) => part.stream === stream)
|
|
1018
|
+
.map((part) => part.text)
|
|
1019
|
+
.join("");
|
|
1020
|
+
const marker = stream === truncationStream ? outputTruncationMarker : "";
|
|
1021
|
+
return head + marker + tail;
|
|
1022
|
+
};
|
|
1023
|
+
const refreshRetainedOutputs = () => {
|
|
1024
|
+
stdout = renderOutput("stdout");
|
|
1025
|
+
stderr = renderOutput("stderr");
|
|
1026
|
+
};
|
|
1027
|
+
const appendOutput = (stream, current, chunk) => {
|
|
941
1028
|
const text = typeof chunk === "string" ? chunk : chunk.toString();
|
|
942
|
-
if (maxOutputBytes === undefined)
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
return current;
|
|
1029
|
+
if (maxOutputBytes === undefined || outputTruncated) {
|
|
1030
|
+
if (maxOutputBytes === undefined)
|
|
1031
|
+
return current + text;
|
|
1032
|
+
appendTail({ stream, text }, retainedTailLimit);
|
|
1033
|
+
truncationStream = stream;
|
|
1034
|
+
return renderOutput(stream);
|
|
949
1035
|
}
|
|
1036
|
+
const used = Buffer.byteLength(stdout) + Buffer.byteLength(stderr);
|
|
950
1037
|
const bytes = Buffer.byteLength(text);
|
|
951
|
-
if (bytes <=
|
|
1038
|
+
if (used + bytes <= maxOutputBytes)
|
|
952
1039
|
return current + text;
|
|
953
1040
|
outputTruncated = true;
|
|
954
|
-
|
|
1041
|
+
truncationStream = stream;
|
|
1042
|
+
const available = Math.max(0, maxOutputBytes - Buffer.byteLength(outputTruncationMarker));
|
|
1043
|
+
const headBytes = Math.floor(available / 2);
|
|
1044
|
+
const tailBytes = available - headBytes;
|
|
1045
|
+
retainedTailLimit = tailBytes;
|
|
1046
|
+
const parts = [
|
|
1047
|
+
{ stream: "stdout", text: stdout },
|
|
1048
|
+
{ stream: "stderr", text: stderr },
|
|
1049
|
+
{ stream, text },
|
|
1050
|
+
];
|
|
1051
|
+
let headRemaining = headBytes;
|
|
1052
|
+
for (const part of parts) {
|
|
1053
|
+
if (headRemaining <= 0)
|
|
1054
|
+
break;
|
|
1055
|
+
const kept = bytePrefix(part.text, headRemaining);
|
|
1056
|
+
if (kept)
|
|
1057
|
+
retainedHead.push({ stream: part.stream, text: kept });
|
|
1058
|
+
headRemaining -= Buffer.byteLength(kept);
|
|
1059
|
+
}
|
|
1060
|
+
let tailRemaining = tailBytes;
|
|
1061
|
+
for (let index = parts.length - 1; index >= 0 && tailRemaining > 0; index--) {
|
|
1062
|
+
const part = parts[index];
|
|
1063
|
+
const kept = byteSuffix(part.text, tailRemaining);
|
|
1064
|
+
if (kept) {
|
|
1065
|
+
retainedTail.unshift({ stream: part.stream, text: kept });
|
|
1066
|
+
tailRemaining -= Buffer.byteLength(kept);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
retainedTailBytes = tailBytes - tailRemaining;
|
|
1070
|
+
return renderOutput(stream);
|
|
1071
|
+
};
|
|
1072
|
+
const matchStreamingChunk = (stream, chunk) => {
|
|
1073
|
+
if (streamingMatch || !streamingMatcher)
|
|
1074
|
+
return;
|
|
1075
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString();
|
|
1076
|
+
streamingMatcher.lastIndex = 0;
|
|
1077
|
+
streamingMatch = streamingMatcher.test(streamingCarry[stream] + text);
|
|
1078
|
+
if (streamingCarryBytes > 0) {
|
|
1079
|
+
streamingCarry[stream] = Buffer.from(streamingCarry[stream] + text)
|
|
1080
|
+
.subarray(-streamingCarryBytes)
|
|
1081
|
+
.toString();
|
|
1082
|
+
}
|
|
955
1083
|
};
|
|
956
1084
|
// Spawn the process (non-blocking). Keeping Node's `shell` option false
|
|
957
1085
|
// is important on every path here: shell:true concatenates tainted
|
|
@@ -1091,6 +1219,12 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
1091
1219
|
}));
|
|
1092
1220
|
return;
|
|
1093
1221
|
}
|
|
1222
|
+
// Keep historical open stdin by default. An opted-in payload is written
|
|
1223
|
+
// completely and followed by EOF; input: "" closes stdin immediately.
|
|
1224
|
+
if (options?.input !== undefined) {
|
|
1225
|
+
child.stdin?.on("error", () => { });
|
|
1226
|
+
child.stdin?.end(options.input);
|
|
1227
|
+
}
|
|
1094
1228
|
if (child.pid && (posixProcessGroup || options?.lifetimeCoupled)) {
|
|
1095
1229
|
// #2026: POSIX registers unconditionally - detached children no
|
|
1096
1230
|
// longer receive terminal signals directly, so the lifetime
|
|
@@ -1201,11 +1335,17 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
1201
1335
|
};
|
|
1202
1336
|
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
1203
1337
|
// Output-cap kills are awaited by finalize() below, just like
|
|
1204
|
-
// timeout/abort kills.
|
|
1205
|
-
//
|
|
1338
|
+
// timeout/abort kills. An armed, unmatched streaming matcher is the one
|
|
1339
|
+
// exception: retention remains bounded, but the caller's timeout is the
|
|
1340
|
+
// bound while the child gets a chance to emit its rescue line.
|
|
1206
1341
|
let killPromise;
|
|
1207
1342
|
const stopForOutputLimit = () => {
|
|
1208
|
-
|
|
1343
|
+
const waitingForStreamingMatch = streamingMatcher !== undefined && !streamingMatch;
|
|
1344
|
+
if (outputTruncated &&
|
|
1345
|
+
!waitingForStreamingMatch &&
|
|
1346
|
+
!killed &&
|
|
1347
|
+
!child.killed) {
|
|
1348
|
+
killedForOutputCap = true;
|
|
1209
1349
|
killed = true;
|
|
1210
1350
|
killPromise = killTree();
|
|
1211
1351
|
}
|
|
@@ -1214,12 +1354,18 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
1214
1354
|
child.stdout?.setEncoding("utf-8");
|
|
1215
1355
|
child.stderr?.setEncoding("utf-8");
|
|
1216
1356
|
child.stdout?.on("data", (data) => {
|
|
1217
|
-
|
|
1357
|
+
matchStreamingChunk("stdout", data);
|
|
1358
|
+
stdout = appendOutput("stdout", stdout, data);
|
|
1359
|
+
if (outputTruncated)
|
|
1360
|
+
refreshRetainedOutputs();
|
|
1218
1361
|
stopForOutputLimit();
|
|
1219
1362
|
rearmIdleGrace?.();
|
|
1220
1363
|
});
|
|
1221
1364
|
child.stderr?.on("data", (data) => {
|
|
1222
|
-
|
|
1365
|
+
matchStreamingChunk("stderr", data);
|
|
1366
|
+
stderr = appendOutput("stderr", stderr, data);
|
|
1367
|
+
if (outputTruncated)
|
|
1368
|
+
refreshRetainedOutputs();
|
|
1223
1369
|
stopForOutputLimit();
|
|
1224
1370
|
rearmIdleGrace?.();
|
|
1225
1371
|
});
|
|
@@ -1415,7 +1561,11 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
1415
1561
|
resolved = true;
|
|
1416
1562
|
await waitForPipeIdle();
|
|
1417
1563
|
const resourceUsage = finishResourceUsage();
|
|
1418
|
-
const outputInfo =
|
|
1564
|
+
const outputInfo = {
|
|
1565
|
+
...(outputTruncated ? { outputTruncated: true } : {}),
|
|
1566
|
+
...(killedForOutputCap ? { killedForOutputCap: true } : {}),
|
|
1567
|
+
};
|
|
1568
|
+
const streamingMatchInfo = streamingMatch ? { streamingMatch: true } : {};
|
|
1419
1569
|
// #1816: surface the signal name as a field on every path where one
|
|
1420
1570
|
// exists, so callers can NAME it instead of scraping `error.message`.
|
|
1421
1571
|
const signalInfo = signal ? { signal } : {};
|
|
@@ -1440,6 +1590,7 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
1440
1590
|
...(timeoutTeardown && { timeoutTeardown }),
|
|
1441
1591
|
spawnFailure: new SpawnFailureError("timeout", cause.message, cause),
|
|
1442
1592
|
...outputInfo,
|
|
1593
|
+
...streamingMatchInfo,
|
|
1443
1594
|
resourceUsage,
|
|
1444
1595
|
});
|
|
1445
1596
|
}
|
|
@@ -1454,6 +1605,7 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
1454
1605
|
...signalInfo,
|
|
1455
1606
|
spawnFailure: new SpawnFailureError("killed", cause.message, cause),
|
|
1456
1607
|
...outputInfo,
|
|
1608
|
+
...streamingMatchInfo,
|
|
1457
1609
|
resourceUsage,
|
|
1458
1610
|
});
|
|
1459
1611
|
}
|
|
@@ -1468,6 +1620,7 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
1468
1620
|
...signalInfo,
|
|
1469
1621
|
spawnFailure: new SpawnFailureError("killed", cause.message, cause),
|
|
1470
1622
|
...outputInfo,
|
|
1623
|
+
...streamingMatchInfo,
|
|
1471
1624
|
resourceUsage,
|
|
1472
1625
|
});
|
|
1473
1626
|
}
|
|
@@ -1497,11 +1650,19 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
1497
1650
|
failure: "spawn",
|
|
1498
1651
|
spawnFailure,
|
|
1499
1652
|
...outputInfo,
|
|
1653
|
+
...streamingMatchInfo,
|
|
1500
1654
|
resourceUsage,
|
|
1501
1655
|
});
|
|
1502
1656
|
}
|
|
1503
1657
|
else {
|
|
1504
|
-
resolve({
|
|
1658
|
+
resolve({
|
|
1659
|
+
stdout,
|
|
1660
|
+
stderr,
|
|
1661
|
+
status: code,
|
|
1662
|
+
...outputInfo,
|
|
1663
|
+
...streamingMatchInfo,
|
|
1664
|
+
resourceUsage,
|
|
1665
|
+
});
|
|
1505
1666
|
}
|
|
1506
1667
|
};
|
|
1507
1668
|
child.on("exit", onChildSettle);
|
|
@@ -1545,6 +1706,8 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
1545
1706
|
failure,
|
|
1546
1707
|
spawnFailure,
|
|
1547
1708
|
...(outputTruncated ? { outputTruncated: true } : {}),
|
|
1709
|
+
...(killedForOutputCap ? { killedForOutputCap: true } : {}),
|
|
1710
|
+
...(streamingMatch ? { streamingMatch: true } : {}),
|
|
1548
1711
|
resourceUsage,
|
|
1549
1712
|
});
|
|
1550
1713
|
if (controlFailure)
|
package/dist/clients/sanitize.js
CHANGED
|
@@ -6,8 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
// --- Constants ---
|
|
8
8
|
// ANSI escape codes for colors and formatting
|
|
9
|
+
// oxlint-disable-next-line no-control-regex -- ESC (\x1b) is the literal ANSI escape-sequence lead byte this pattern strips, not accidental input.
|
|
9
10
|
const ANSI_ESCAPE = /\x1b\[[0-9;]*m/g;
|
|
10
|
-
|
|
11
|
+
// Full ECMA-48 CSI grammar (parameter bytes 0-9;? , intermediate bytes
|
|
12
|
+
// space-/, final byte @-~) — matches every CSI sequence ANSI_ESCAPE does
|
|
13
|
+
// plus non-SGR ones (cursor moves, private-mode `?` sequences like
|
|
14
|
+
// `\x1b[?25l`, etc.), not just the `m`-terminated color codes.
|
|
15
|
+
// oxlint-disable-next-line no-control-regex -- ESC (\x1b) is the literal ANSI escape-sequence lead byte this pattern strips, not accidental input.
|
|
16
|
+
const ANSI_ESCAPE_EXTENDED = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
11
17
|
// Common error patterns from different tools
|
|
12
18
|
const ERROR_INDICATORS = [
|
|
13
19
|
/\berror\b/i,
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
* fully in scope.
|
|
55
55
|
*/
|
|
56
56
|
import { EXCLUDED_DIRS, getExcludedDirGlobs } from "./file-utils.js";
|
|
57
|
+
import { escapeRegExp } from "./string-utils.js";
|
|
57
58
|
/** Directory-name entries only — drops glob entries (e.g. `*.dSYM`) that a
|
|
58
59
|
* bare directory-name/regex exclude can't express. */
|
|
59
60
|
function literalExcludedDirNames() {
|
|
@@ -68,9 +69,6 @@ function literalExcludedDirNames() {
|
|
|
68
69
|
export function getScratchTreeGlobPatterns() {
|
|
69
70
|
return getExcludedDirGlobs();
|
|
70
71
|
}
|
|
71
|
-
function escapeRegExp(literal) {
|
|
72
|
-
return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
73
|
-
}
|
|
74
72
|
/** Bare directory names, for tools (opengrep/semgrep `--exclude`) that treat a
|
|
75
73
|
* slash-free pattern as "this directory name, anywhere in the tree". */
|
|
76
74
|
export function getScratchTreeDirNames() {
|
|
@@ -16,6 +16,9 @@ import * as path from "node:path";
|
|
|
16
16
|
import { createSubsystemLogger } from "./extension-log.js";
|
|
17
17
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
18
18
|
import { classifyProbeFailure, createAvailabilityLatch, describeInstallAttempt, describeProbeEvidence, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
|
|
19
|
+
import { createAvailabilityProbeFlight } from "./availability-probe-flight.js";
|
|
20
|
+
import { createSingleFlight } from "./single-flight.js";
|
|
21
|
+
const securityProbeFlights = createAvailabilityProbeFlight();
|
|
19
22
|
export class SecurityScanClient {
|
|
20
23
|
toolName;
|
|
21
24
|
/**
|
|
@@ -68,6 +71,7 @@ export class SecurityScanClient {
|
|
|
68
71
|
logDurableAbsence(evidence, elapsedMs = 0) {
|
|
69
72
|
logAvailabilityDecision({
|
|
70
73
|
tool: this.toolName,
|
|
74
|
+
producer: "security-scan",
|
|
71
75
|
verdict: "unavailable",
|
|
72
76
|
outcome: "missing",
|
|
73
77
|
cause: "not-found",
|
|
@@ -79,7 +83,8 @@ export class SecurityScanClient {
|
|
|
79
83
|
}
|
|
80
84
|
/** Outcome of the most recent `probeVersion` call. */
|
|
81
85
|
lastProbeOutcome = null;
|
|
82
|
-
|
|
86
|
+
/** Availability resolution is instance-owned; only its external probe is shared. */
|
|
87
|
+
ensureFlight = createSingleFlight();
|
|
83
88
|
inFlight = new Map();
|
|
84
89
|
binaryPath = null;
|
|
85
90
|
log;
|
|
@@ -99,15 +104,7 @@ export class SecurityScanClient {
|
|
|
99
104
|
async ensureAvailable() {
|
|
100
105
|
if (this.available !== null)
|
|
101
106
|
return this.available;
|
|
102
|
-
|
|
103
|
-
return this.ensureInFlight;
|
|
104
|
-
this.ensureInFlight = this.doEnsureAvailable();
|
|
105
|
-
try {
|
|
106
|
-
return await this.ensureInFlight;
|
|
107
|
-
}
|
|
108
|
-
finally {
|
|
109
|
-
this.ensureInFlight = null;
|
|
110
|
-
}
|
|
107
|
+
return this.ensureFlight.run("availability", () => this.doEnsureAvailable());
|
|
111
108
|
}
|
|
112
109
|
/**
|
|
113
110
|
* Spawn `toolName <versionArgs>` and report whether it answered cleanly.
|
|
@@ -128,10 +125,26 @@ export class SecurityScanClient {
|
|
|
128
125
|
const startedAt = Date.now();
|
|
129
126
|
let probe;
|
|
130
127
|
let hostStallMs;
|
|
128
|
+
let resolvedBinaryPath = null;
|
|
129
|
+
let probeJoined = false;
|
|
131
130
|
try {
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
const shared = securityProbeFlights.run(`security:${this.toolName}|${versionArgs.join("|")}`, async () => {
|
|
132
|
+
const { findManagedToolBinary } = await import("./installer/index.js");
|
|
133
|
+
const managed = await findManagedToolBinary(this.toolName);
|
|
134
|
+
return {
|
|
135
|
+
probe: await safeSpawnAsync(managed ?? this.toolName, versionArgs, {
|
|
136
|
+
timeout: 5000,
|
|
137
|
+
}),
|
|
138
|
+
binaryPath: managed ?? null,
|
|
139
|
+
};
|
|
134
140
|
});
|
|
141
|
+
probeJoined = shared.joined;
|
|
142
|
+
const flightResult = await shared.promise;
|
|
143
|
+
probe = flightResult.probe;
|
|
144
|
+
resolvedBinaryPath = flightResult.binaryPath;
|
|
145
|
+
if (!probe.error && probe.status === 0 && flightResult.binaryPath) {
|
|
146
|
+
this.binaryPath = flightResult.binaryPath;
|
|
147
|
+
}
|
|
135
148
|
}
|
|
136
149
|
finally {
|
|
137
150
|
hostStallMs = sampler.stop();
|
|
@@ -142,6 +155,7 @@ export class SecurityScanClient {
|
|
|
142
155
|
this.log(`${this.toolName} found: ${probe.stdout.trim().split("\n")[0]}`);
|
|
143
156
|
logAvailabilityDecision({
|
|
144
157
|
tool: this.toolName,
|
|
158
|
+
producer: "security-scan",
|
|
145
159
|
verdict: "available",
|
|
146
160
|
outcome: "success",
|
|
147
161
|
cause: "ok",
|
|
@@ -149,8 +163,14 @@ export class SecurityScanClient {
|
|
|
149
163
|
latched: true,
|
|
150
164
|
hostStallMs,
|
|
151
165
|
budgetMs: 5000,
|
|
152
|
-
classifiedBy: "probe",
|
|
153
|
-
evidence:
|
|
166
|
+
classifiedBy: probeJoined ? "joined" : "probe",
|
|
167
|
+
evidence: {
|
|
168
|
+
...describeProbeEvidence(probe),
|
|
169
|
+
...(resolvedBinaryPath && {
|
|
170
|
+
binary: path.basename(resolvedBinaryPath),
|
|
171
|
+
source: "managed-dir",
|
|
172
|
+
}),
|
|
173
|
+
},
|
|
154
174
|
});
|
|
155
175
|
return true;
|
|
156
176
|
}
|
|
@@ -167,6 +187,7 @@ export class SecurityScanClient {
|
|
|
167
187
|
: 0;
|
|
168
188
|
logAvailabilityDecision({
|
|
169
189
|
tool: this.toolName,
|
|
190
|
+
producer: "security-scan",
|
|
170
191
|
verdict: "unavailable",
|
|
171
192
|
outcome,
|
|
172
193
|
cause,
|
|
@@ -175,8 +196,14 @@ export class SecurityScanClient {
|
|
|
175
196
|
hostStallMs,
|
|
176
197
|
...(retryAfterMs > 0 && { retryAfterMs }),
|
|
177
198
|
budgetMs: 5000,
|
|
178
|
-
classifiedBy: "probe",
|
|
179
|
-
evidence
|
|
199
|
+
classifiedBy: probeJoined ? "joined" : "probe",
|
|
200
|
+
evidence: {
|
|
201
|
+
...evidence,
|
|
202
|
+
...(resolvedBinaryPath && {
|
|
203
|
+
binary: path.basename(resolvedBinaryPath),
|
|
204
|
+
source: "managed-dir",
|
|
205
|
+
}),
|
|
206
|
+
},
|
|
180
207
|
});
|
|
181
208
|
return false;
|
|
182
209
|
}
|
|
@@ -311,7 +338,14 @@ export class SecurityScanClient {
|
|
|
311
338
|
this.log(`Scan already in flight for ${key}; sharing result`);
|
|
312
339
|
return existing;
|
|
313
340
|
}
|
|
314
|
-
|
|
341
|
+
// Identity-guarded release (#1968's pattern): delete only if THIS run is
|
|
342
|
+
// still the registered one. A bare delete-by-key lets a late-settling run
|
|
343
|
+
// evict a live successor a second writer registered under the same key
|
|
344
|
+
// mid-flight, after which the next caller starts a duplicate scan.
|
|
345
|
+
const promise = run().finally(() => {
|
|
346
|
+
if (this.inFlight.get(key) === promise)
|
|
347
|
+
this.inFlight.delete(key);
|
|
348
|
+
});
|
|
315
349
|
this.inFlight.set(key, promise);
|
|
316
350
|
return promise;
|
|
317
351
|
}
|