@herbertgao/pi-extensions 2026.8.13 → 2026.8.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -14
- package/THIRD_PARTY_NOTICES.md +0 -1
- package/node_modules/@czottmann/pi-automode/CHANGELOG.md +19 -0
- package/node_modules/@czottmann/pi-automode/README.md +5 -1
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +126 -13
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +7 -3
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +2 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +134 -23
- package/node_modules/@czottmann/pi-automode/package.json +1 -1
- package/node_modules/@herbertgao/{pi-stash → pi-bark}/LICENSE +4 -5
- package/node_modules/@herbertgao/pi-bark/README.md +44 -0
- package/node_modules/@herbertgao/pi-bark/assets/pi-icon.png +0 -0
- package/node_modules/@herbertgao/pi-bark/package.json +53 -0
- package/node_modules/@herbertgao/pi-bark/src/index.ts +178 -0
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +2 -1
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +8 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +125 -88
- package/node_modules/@narumitw/pi-btw/dist/index.ts +140 -45
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +2 -2
- package/node_modules/@narumitw/pi-btw/package.json +5 -8
- package/node_modules/@narumitw/pi-btw/src/btw.ts +31 -26
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +106 -17
- package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
- package/node_modules/@narumitw/pi-btw/src/settings.ts +16 -0
- package/node_modules/@tifan/pi-handoff/README.md +36 -13
- package/node_modules/@tifan/pi-handoff/package.json +2 -2
- package/node_modules/@tifan/pi-handoff/src/index.ts +160 -4
- package/node_modules/@tifan/pi-preferred-thinking/README.md +1 -1
- package/node_modules/@tifan/pi-preferred-thinking/package.json +1 -1
- package/node_modules/@tifan/pi-preferred-thinking/src/index.ts +15 -2
- package/node_modules/@tifan/pi-recap/README.md +1 -1
- package/node_modules/@tifan/pi-recap/package.json +2 -2
- package/node_modules/@tifan/pi-recap/src/index.ts +15 -0
- package/node_modules/@tifan/pi-rename/README.md +42 -21
- package/node_modules/@tifan/pi-rename/package.json +1 -1
- package/node_modules/@tifan/pi-rename/src/index.ts +62 -11
- package/node_modules/@tifan/pi-rename/src/language.ts +18 -0
- package/node_modules/@tifan/pi-rename/src/models.ts +69 -13
- package/node_modules/@tifan/pi-rename/src/naming.ts +36 -7
- package/node_modules/@tifan/pi-rename/src/sanitize.ts +22 -14
- package/node_modules/pi-lens/CHANGELOG.md +320 -0
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +79 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +16 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +13 -6
- package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +12 -0
- package/node_modules/pi-lens/dist/clients/biome-client.js +7 -0
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +20 -0
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +56 -19
- package/node_modules/pi-lens/dist/clients/build-identity.js +95 -0
- package/node_modules/pi-lens/dist/clients/bus-publish.js +3 -0
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +2 -1
- package/node_modules/pi-lens/dist/clients/cache-observability.js +2 -1
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +15 -1
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +14 -11
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +56 -4
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +14 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +20 -3
- package/node_modules/pi-lens/dist/clients/deps/minimatch.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +39 -0
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +150 -9
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +231 -9
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +205 -100
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +87 -0
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +21 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +174 -55
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +16 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +54 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +1 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +37 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +11 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +8 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +76 -25
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +7 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +66 -1
- package/node_modules/pi-lens/dist/clients/file-utils.js +157 -7
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +5 -0
- package/node_modules/pi-lens/dist/clients/formatters.js +28 -3
- package/node_modules/pi-lens/dist/clients/git-guard.js +197 -47
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +32 -1
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +5 -1
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +7 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +131 -34
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +3 -1
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +4 -13
- package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +145 -0
- package/node_modules/pi-lens/dist/clients/instance-registry.js +432 -122
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +6 -1
- package/node_modules/pi-lens/dist/clients/language-profile.js +4 -2
- package/node_modules/pi-lens/dist/clients/latency-logger.js +19 -1
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/lens-map.js +3 -1
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +4 -0
- package/node_modules/pi-lens/dist/clients/lsp/client.js +516 -31
- package/node_modules/pi-lens/dist/clients/lsp/config.js +25 -2
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +9 -4
- package/node_modules/pi-lens/dist/clients/lsp/index.js +509 -182
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +5 -0
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +36 -8
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +116 -4
- package/node_modules/pi-lens/dist/clients/lsp/server.js +142 -13
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +99 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +16 -6
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +32 -23
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +16 -2
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +12 -6
- package/node_modules/pi-lens/dist/clients/mcp/session.js +14 -1
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +23 -1
- package/node_modules/pi-lens/dist/clients/message-end-attribution.js +30 -0
- package/node_modules/pi-lens/dist/clients/metrics-history.js +21 -4
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +232 -18
- package/node_modules/pi-lens/dist/clients/package-manager.js +6 -18
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +14 -1
- package/node_modules/pi-lens/dist/clients/path-utils.js +32 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +0 -8
- package/node_modules/pi-lens/dist/clients/process-singletons.js +210 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +10 -4
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +33 -11
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +24 -2
- package/node_modules/pi-lens/dist/clients/read-guard.js +74 -7
- package/node_modules/pi-lens/dist/clients/recent-touches.js +17 -5
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +639 -62
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +5 -1
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +21 -1
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +68 -0
- package/node_modules/pi-lens/dist/clients/runtime-session.js +30 -12
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +15 -0
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +106 -7
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +242 -27
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +158 -18
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +48 -17
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +238 -42
- package/node_modules/pi-lens/dist/clients/sg-runner.js +27 -15
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +338 -0
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +37 -0
- package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +15 -1
- package/node_modules/pi-lens/dist/clients/startup-scan.js +6 -7
- package/node_modules/pi-lens/dist/clients/startup-timing.js +69 -4
- package/node_modules/pi-lens/dist/clients/string-utils.js +20 -0
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +419 -88
- package/node_modules/pi-lens/dist/clients/tool-policy.js +1 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +11 -5
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +17 -1
- package/node_modules/pi-lens/dist/clients/warm-attach.js +8 -10
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +13 -1
- package/node_modules/pi-lens/dist/clients/word-index-store.js +448 -0
- package/node_modules/pi-lens/dist/clients/word-index.js +582 -104
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +17 -0
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -6
- package/node_modules/pi-lens/dist/index.js +11243 -6651
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +8 -7
- package/node_modules/pi-lens/dist/mcp/cli.js +3 -1
- package/node_modules/pi-lens/dist/mcp/server.js +6 -1
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +4 -0
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +14 -5
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +3 -1
- package/node_modules/pi-lens/docs/agent-guide.md +4 -2
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +41 -10
- package/node_modules/pi-lens/docs/astplayground.md +26 -17
- package/node_modules/pi-lens/package.json +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-important.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +35 -0
- package/node_modules/pi-mcp-adapter/README.md +5 -2
- package/node_modules/pi-mcp-adapter/cli.js +4 -4
- package/node_modules/pi-mcp-adapter/commands.ts +1 -1
- package/node_modules/pi-mcp-adapter/config.ts +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.js +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.d.ts +24 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js +336 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +3 -1
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/index.ts +90 -6
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +124 -32
- package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +0 -2
- package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +89 -0
- package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -1
- package/node_modules/pi-mcp-adapter/package.json +1 -1
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +62 -22
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +1 -1
- package/node_modules/pi-mcp-adapter/server-manager.ts +6 -0
- package/node_modules/pi-mcp-adapter/types.ts +3 -1
- package/node_modules/pi-web-access/CHANGELOG.md +42 -0
- package/node_modules/pi-web-access/README.md +26 -10
- package/node_modules/pi-web-access/curator-page.ts +12 -3
- package/node_modules/pi-web-access/curator-server.ts +3 -1
- package/node_modules/pi-web-access/extract.ts +88 -13
- package/node_modules/pi-web-access/gemini-search.ts +10 -5
- package/node_modules/pi-web-access/github-extract.ts +47 -3
- package/node_modules/pi-web-access/index.ts +40 -40
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/page-query.ts +61 -8
- package/node_modules/pi-web-access/xcrawl.ts +264 -0
- package/package.json +19 -22
- package/node_modules/@herbertgao/pi-stash/README.md +0 -34
- package/node_modules/@herbertgao/pi-stash/package.json +0 -51
- package/node_modules/@herbertgao/pi-stash/src/index.ts +0 -118
- package/node_modules/@tifan/pi-titlebar-spinner/README.md +0 -24
- package/node_modules/@tifan/pi-titlebar-spinner/package.json +0 -39
- package/node_modules/@tifan/pi-titlebar-spinner/src/index.ts +0 -49
|
@@ -383,6 +383,12 @@ export const TOOLS = [
|
|
|
383
383
|
name: "Bash Language Server",
|
|
384
384
|
checkCommand: "bash-language-server",
|
|
385
385
|
checkArgs: ["--version"],
|
|
386
|
+
// The #2188 sweep measured a 9,667ms cold `--version` start with closed
|
|
387
|
+
// stdin — close enough to the 10s installer default that modest host
|
|
388
|
+
// contention pushes it over and emits a false verification degradation.
|
|
389
|
+
// 20s gives the same kind of headroom Vue's 30s bound gives its own
|
|
390
|
+
// slower cold start (#2176), without inventing a shared literal (#2194).
|
|
391
|
+
verificationTimeoutMs: 20_000,
|
|
386
392
|
installStrategy: "npm",
|
|
387
393
|
packageName: "bash-language-server",
|
|
388
394
|
binaryName: "bash-language-server",
|
|
@@ -419,6 +425,11 @@ export const TOOLS = [
|
|
|
419
425
|
name: "VSCode JSON Language Server",
|
|
420
426
|
checkCommand: "vscode-json-language-server",
|
|
421
427
|
checkArgs: ["--version"],
|
|
428
|
+
// The #2188 sweep measured an 11,047ms cold `--version` start with closed
|
|
429
|
+
// stdin — over the 10s installer default, so a cold or contended host can
|
|
430
|
+
// see a false verification degradation before the binary ever answers.
|
|
431
|
+
// 20s mirrors the bash-language-server bound above (#2194).
|
|
432
|
+
verificationTimeoutMs: 20_000,
|
|
422
433
|
installStrategy: "npm",
|
|
423
434
|
packageName: "vscode-langservers-extracted",
|
|
424
435
|
binaryName: "vscode-json-language-server",
|
|
@@ -465,7 +476,8 @@ export const TOOLS = [
|
|
|
465
476
|
id: "helm",
|
|
466
477
|
name: "Helm",
|
|
467
478
|
checkCommand: "helm",
|
|
468
|
-
|
|
479
|
+
// intended: version --short — unverified against real binary (shape 16), do not wire until probed
|
|
480
|
+
checkArgs: ["--version"],
|
|
469
481
|
installStrategy: "github",
|
|
470
482
|
binaryName: "helm",
|
|
471
483
|
github: {
|
|
@@ -543,6 +555,11 @@ export const TOOLS = [
|
|
|
543
555
|
name: "Prisma Language Server",
|
|
544
556
|
checkCommand: "prisma-language-server",
|
|
545
557
|
checkArgs: ["--version"],
|
|
558
|
+
// #2169: a real closed-stdin cold run measured 27,265ms, and a warm-cache
|
|
559
|
+
// rerun still took 9,860ms — well past the 10s installer default. 40s
|
|
560
|
+
// keeps the same margin-over-worst-observed ratio Vue's 30s bound uses
|
|
561
|
+
// (#2176) rather than trimming it for a slower binary.
|
|
562
|
+
verificationTimeoutMs: 40_000,
|
|
546
563
|
installStrategy: "npm",
|
|
547
564
|
packageName: "@prisma/language-server",
|
|
548
565
|
binaryName: "prisma-language-server",
|
|
@@ -552,6 +569,10 @@ export const TOOLS = [
|
|
|
552
569
|
name: "Vue Language Server",
|
|
553
570
|
checkCommand: "vue-language-server",
|
|
554
571
|
checkArgs: ["--version"],
|
|
572
|
+
// Vue's launcher loads the full language-service bundle before answering
|
|
573
|
+
// --version. Its cold start exceeds the dispatch probe budget on some hosts
|
|
574
|
+
// even though warm starts complete quickly (#2176).
|
|
575
|
+
verificationTimeoutMs: 30_000,
|
|
555
576
|
installStrategy: "npm",
|
|
556
577
|
packageName: "@vue/language-server",
|
|
557
578
|
binaryName: "vue-language-server",
|
|
@@ -561,6 +582,10 @@ export const TOOLS = [
|
|
|
561
582
|
name: "Svelte Language Server",
|
|
562
583
|
checkCommand: "svelteserver",
|
|
563
584
|
checkArgs: ["--version"],
|
|
585
|
+
// #2169: a real closed-stdin cold run measured 12,410ms — over the 10s
|
|
586
|
+
// installer default, matching the bash/JSON class of false verification
|
|
587
|
+
// degradation from a cold-cache host (#2194). 20s mirrors that bound.
|
|
588
|
+
verificationTimeoutMs: 20_000,
|
|
564
589
|
installStrategy: "npm",
|
|
565
590
|
packageName: "svelte-language-server",
|
|
566
591
|
binaryName: "svelteserver",
|
|
@@ -569,7 +594,9 @@ export const TOOLS = [
|
|
|
569
594
|
id: "markdownlint",
|
|
570
595
|
name: "markdownlint-cli2",
|
|
571
596
|
checkCommand: "markdownlint-cli2",
|
|
572
|
-
|
|
597
|
+
// `--version` is interpreted as a file glob by markdownlint-cli2. Use
|
|
598
|
+
// stdin with globs disabled so verification cannot scan the workspace.
|
|
599
|
+
checkArgs: ["--no-globs", "-"],
|
|
573
600
|
installStrategy: "npm",
|
|
574
601
|
packageName: "markdownlint-cli2",
|
|
575
602
|
binaryName: "markdownlint-cli2",
|
|
@@ -784,7 +811,8 @@ export const TOOLS = [
|
|
|
784
811
|
id: "spotbugs",
|
|
785
812
|
name: "SpotBugs",
|
|
786
813
|
checkCommand: "spotbugs",
|
|
787
|
-
|
|
814
|
+
// intended: -version — unverified against real binary (shape 16), do not wire until probed
|
|
815
|
+
checkArgs: ["--version"],
|
|
788
816
|
installStrategy: "archive",
|
|
789
817
|
binaryName: "spotbugs",
|
|
790
818
|
archive: {
|
|
@@ -1030,7 +1058,8 @@ export const TOOLS = [
|
|
|
1030
1058
|
id: "gitleaks",
|
|
1031
1059
|
name: "gitleaks",
|
|
1032
1060
|
checkCommand: "gitleaks",
|
|
1033
|
-
|
|
1061
|
+
// intended: version — unverified against real binary (shape 16), do not wire until probed
|
|
1062
|
+
checkArgs: ["--version"],
|
|
1034
1063
|
installStrategy: "github",
|
|
1035
1064
|
binaryName: "gitleaks",
|
|
1036
1065
|
github: {
|
|
@@ -1144,7 +1173,8 @@ export const TOOLS = [
|
|
|
1144
1173
|
id: "terraform-ls",
|
|
1145
1174
|
name: "terraform-ls",
|
|
1146
1175
|
checkCommand: "terraform-ls",
|
|
1147
|
-
|
|
1176
|
+
// intended: version — unverified against real binary (shape 16), do not wire until probed
|
|
1177
|
+
checkArgs: ["--version"],
|
|
1148
1178
|
installStrategy: "github",
|
|
1149
1179
|
binaryName: "terraform-ls",
|
|
1150
1180
|
github: {
|
|
@@ -1216,6 +1246,7 @@ export const TOOLS = [
|
|
|
1216
1246
|
id: "cue",
|
|
1217
1247
|
name: "CUE",
|
|
1218
1248
|
checkCommand: "cue",
|
|
1249
|
+
// Probed locally with CUE v0.17.1: `cue version` exits 0.
|
|
1219
1250
|
checkArgs: ["version"],
|
|
1220
1251
|
installStrategy: "github",
|
|
1221
1252
|
binaryName: "cue",
|
|
@@ -1324,6 +1355,10 @@ export const TOOLS = [
|
|
|
1324
1355
|
},
|
|
1325
1356
|
},
|
|
1326
1357
|
];
|
|
1358
|
+
/** Return the bounded managed-binary verification budget for a registered tool. */
|
|
1359
|
+
export function getToolVerificationTimeout(tool) {
|
|
1360
|
+
return tool.verificationTimeoutMs ?? 10_000;
|
|
1361
|
+
}
|
|
1327
1362
|
const ensureInFlight = new Map();
|
|
1328
1363
|
const installFailureReasons = new Map();
|
|
1329
1364
|
/** Last honest install refusal/failure reason for callers that need diagnostics. */
|
|
@@ -1621,8 +1656,13 @@ export async function checkProbeCache(toolId) {
|
|
|
1621
1656
|
try {
|
|
1622
1657
|
await fs.access(entry.path);
|
|
1623
1658
|
const stat = await fs.stat(entry.path);
|
|
1624
|
-
|
|
1625
|
-
|
|
1659
|
+
// #2300: size alongside mtime, from the same stat — a binary replaced
|
|
1660
|
+
// in-place within the same coarse-granularity mtime tick would otherwise
|
|
1661
|
+
// pass this check unnoticed. `entry.sizeBytes` absent (pre-#2300 entry)
|
|
1662
|
+
// fails open to the mtime-only check (see the field's doc comment).
|
|
1663
|
+
if (stat.mtimeMs !== entry.mtimeMs ||
|
|
1664
|
+
(entry.sizeBytes !== undefined && stat.size !== entry.sizeBytes)) {
|
|
1665
|
+
logSessionStart(`auto-install probe-cache ${toolId}: miss (mtime/size changed)`);
|
|
1626
1666
|
delete cache[toolId];
|
|
1627
1667
|
markProbeCacheChange(toolId, null);
|
|
1628
1668
|
return undefined;
|
|
@@ -1650,6 +1690,7 @@ export async function updateProbeCache(toolId, resolvedPath, transient = false)
|
|
|
1650
1690
|
const entry = {
|
|
1651
1691
|
path: resolvedPath,
|
|
1652
1692
|
mtimeMs: stat.mtimeMs,
|
|
1693
|
+
sizeBytes: stat.size,
|
|
1653
1694
|
cachedAt: Date.now(),
|
|
1654
1695
|
...(transient && { transient: true }),
|
|
1655
1696
|
};
|
|
@@ -1681,6 +1722,14 @@ export function resetProbeCacheStateForTesting() {
|
|
|
1681
1722
|
_probeCacheFlushTimer = null;
|
|
1682
1723
|
}
|
|
1683
1724
|
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Exported for testing only. Read the `ensureTool` in-flight map directly
|
|
1727
|
+
* (#1968's ABA regression: a second writer replacing an entry mid-flight,
|
|
1728
|
+
* then a late-settling first run evicting it with a bare delete-by-key).
|
|
1729
|
+
*/
|
|
1730
|
+
export function _peekEnsureInFlightForTesting() {
|
|
1731
|
+
return ensureInFlight;
|
|
1732
|
+
}
|
|
1684
1733
|
// --- Check Functions ---
|
|
1685
1734
|
const pathWalkMemo = new Map();
|
|
1686
1735
|
function hashSync(value) {
|
|
@@ -1765,6 +1814,7 @@ export async function isSpawnableCommand(command) {
|
|
|
1765
1814
|
export function isLspTransportRequiredError(output) {
|
|
1766
1815
|
return /Connection (?:input|output) stream is not set|Use arguments of createConnection/i.test(output);
|
|
1767
1816
|
}
|
|
1817
|
+
const lspTransportRequiredMatcher = /Connection (?:input|output) stream is not set|Use arguments of createConnection/i;
|
|
1768
1818
|
/**
|
|
1769
1819
|
* Parse the exact version pinned in a TOOLS `packageName` spec, e.g.
|
|
1770
1820
|
* `"jscpd@3.5.10"` -> `"3.5.10"`. Scoped packages (`"@ast-grep/cli"`) have no
|
|
@@ -1796,7 +1846,7 @@ function extractVersionToken(output) {
|
|
|
1796
1846
|
*/
|
|
1797
1847
|
const lastManagedInstallVersion = new Map();
|
|
1798
1848
|
/**
|
|
1799
|
-
* Verify a tool binary actually works by running
|
|
1849
|
+
* Verify a tool binary actually works by running its configured checkArgs.
|
|
1800
1850
|
* This catches broken symlinks, partial installs, and corrupted binaries.
|
|
1801
1851
|
* `onVersionOutput`, when provided, receives the raw stdout on a successful
|
|
1802
1852
|
* (exit 0) probe — used to piggyback version-pin drift detection onto this
|
|
@@ -1822,7 +1872,7 @@ onTransient,
|
|
|
1822
1872
|
* sensitive caller on the dispatch hot path passes a shorter one and treats
|
|
1823
1873
|
* the expiry as transient rather than as a verdict (#1657).
|
|
1824
1874
|
*/
|
|
1825
|
-
timeoutMs = 10000) {
|
|
1875
|
+
timeoutMs = 10000, verificationArgs = ["--version"]) {
|
|
1826
1876
|
// #2015: safeSpawnAsync instead of raw spawn. Raw spawn's timeout
|
|
1827
1877
|
// SIGTERMed only cmd.exe on Windows (.cmd shims run shell:true), orphaning
|
|
1828
1878
|
// the grandchild node process - which kept scanning, held handles against
|
|
@@ -1849,16 +1899,29 @@ timeoutMs = 10000) {
|
|
|
1849
1899
|
// there), so no shell option exists or is needed here.
|
|
1850
1900
|
void useShell;
|
|
1851
1901
|
try {
|
|
1852
|
-
const result = await safeSpawnAsync(execPath,
|
|
1902
|
+
const result = await safeSpawnAsync(execPath, verificationArgs, {
|
|
1853
1903
|
timeout: timeoutMs,
|
|
1904
|
+
input: "",
|
|
1905
|
+
// A broken or version-blind language server can emit a bundled
|
|
1906
|
+
// megabytes-long diagnostic on stderr. Keep verification bounded even
|
|
1907
|
+
// when the child reaches its normal timeout first.
|
|
1908
|
+
maxOutputBytes: 64 * 1024,
|
|
1909
|
+
matchWhileStreaming: lspTransportRequiredMatcher,
|
|
1854
1910
|
});
|
|
1855
1911
|
const output = `${result.stdout}\n${result.stderr}`;
|
|
1912
|
+
if (result.outputTruncated) {
|
|
1913
|
+
recordDegradationOnce({
|
|
1914
|
+
kind: "installer-verification-output-truncated",
|
|
1915
|
+
subject: binPath,
|
|
1916
|
+
reason: `verification output exceeded 65536 bytes (${verificationArgs.join(" ")})`,
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1856
1919
|
if (result.status === 0 && !result.error) {
|
|
1857
|
-
debugLog(`Verified: ${binPath} (
|
|
1920
|
+
debugLog(`Verified: ${binPath} (${verificationArgs.join(" ")}: ${result.stdout.trim()})`);
|
|
1858
1921
|
onVersionOutput?.(result.stdout);
|
|
1859
1922
|
return true;
|
|
1860
1923
|
}
|
|
1861
|
-
if (isLspTransportRequiredError(output)) {
|
|
1924
|
+
if (result.streamingMatch || isLspTransportRequiredError(output)) {
|
|
1862
1925
|
// Valid stdio LSP server that rejects `--version` (#208) — the
|
|
1863
1926
|
// transport-required error proves the binary works.
|
|
1864
1927
|
debugLog(`Verified (stdio LSP, transport-required): ${binPath}`);
|
|
@@ -1868,7 +1931,13 @@ timeoutMs = 10000) {
|
|
|
1868
1931
|
// verdict from the binary (#1569 transient semantics).
|
|
1869
1932
|
if (result.signal !== undefined || result.spawnFailure)
|
|
1870
1933
|
onTransient?.();
|
|
1871
|
-
|
|
1934
|
+
const kind = result.spawnFailure?.kind ??
|
|
1935
|
+
(result.signal
|
|
1936
|
+
? `killed-signal=${result.signal}`
|
|
1937
|
+
: result.error
|
|
1938
|
+
? "spawn-error"
|
|
1939
|
+
: "exit-nonzero");
|
|
1940
|
+
logSessionStart(`auto-install verify: failed for ${binPath} (check=${verificationArgs.join(" ")}, kind=${kind}${result.status !== null ? `, exit=${result.status}` : ""})`);
|
|
1872
1941
|
return false;
|
|
1873
1942
|
}
|
|
1874
1943
|
catch (err) {
|
|
@@ -1938,7 +2007,7 @@ export async function getAllToolStatuses() {
|
|
|
1938
2007
|
}
|
|
1939
2008
|
// 2. Check npm global
|
|
1940
2009
|
if (tool.installStrategy === "npm") {
|
|
1941
|
-
const npmPath = await findNpmGlobalToolPath(tool.binaryName || tool.id);
|
|
2010
|
+
const npmPath = await findNpmGlobalToolPath(tool.binaryName || tool.id, undefined, tool.checkArgs, getToolVerificationTimeout(tool));
|
|
1942
2011
|
if (npmPath) {
|
|
1943
2012
|
status.installed = true;
|
|
1944
2013
|
status.source = "npm-global";
|
|
@@ -1949,7 +2018,7 @@ export async function getAllToolStatuses() {
|
|
|
1949
2018
|
}
|
|
1950
2019
|
// 3. Check pip user install
|
|
1951
2020
|
if (tool.installStrategy === "pip") {
|
|
1952
|
-
const pipPath = await findPipUserToolPath(tool.binaryName || tool.id);
|
|
2021
|
+
const pipPath = await findPipUserToolPath(tool.binaryName || tool.id, undefined, tool.checkArgs, getToolVerificationTimeout(tool));
|
|
1953
2022
|
if (pipPath) {
|
|
1954
2023
|
status.installed = true;
|
|
1955
2024
|
status.source = "pip-user";
|
|
@@ -1981,7 +2050,7 @@ export async function getAllToolStatuses() {
|
|
|
1981
2050
|
const localPath = installerPlatform() === "win32" ? `${localBase}.cmd` : localBase;
|
|
1982
2051
|
try {
|
|
1983
2052
|
await fs.access(localPath);
|
|
1984
|
-
if (await verifyToolBinary(localPath)) {
|
|
2053
|
+
if (await verifyToolBinary(localPath, undefined, undefined, getToolVerificationTimeout(tool), tool.checkArgs)) {
|
|
1985
2054
|
status.installed = true;
|
|
1986
2055
|
status.source = "pi-lens-auto";
|
|
1987
2056
|
status.path = localPath;
|
|
@@ -2150,7 +2219,7 @@ async function getToolPathResolved(toolId, onTransient) {
|
|
|
2150
2219
|
const cmdPath = `${localBase}.cmd`;
|
|
2151
2220
|
try {
|
|
2152
2221
|
await fs.access(cmdPath);
|
|
2153
|
-
if (await verifyToolBinary(cmdPath, recordVersion, onTransient)) {
|
|
2222
|
+
if (await verifyToolBinary(cmdPath, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs)) {
|
|
2154
2223
|
return cmdPath;
|
|
2155
2224
|
}
|
|
2156
2225
|
logSessionStart(`auto-install verify: ${cmdPath} exists but is broken, will reinstall`);
|
|
@@ -2163,7 +2232,7 @@ async function getToolPathResolved(toolId, onTransient) {
|
|
|
2163
2232
|
const exePath = `${localBase}.exe`;
|
|
2164
2233
|
try {
|
|
2165
2234
|
await fs.access(exePath);
|
|
2166
|
-
if (await verifyToolBinary(exePath, recordVersion, onTransient)) {
|
|
2235
|
+
if (await verifyToolBinary(exePath, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs)) {
|
|
2167
2236
|
return exePath;
|
|
2168
2237
|
}
|
|
2169
2238
|
logSessionStart(`auto-install verify: ${exePath} exists but is broken, will reinstall`);
|
|
@@ -2175,7 +2244,7 @@ async function getToolPathResolved(toolId, onTransient) {
|
|
|
2175
2244
|
if (installerPlatform() !== "win32") {
|
|
2176
2245
|
try {
|
|
2177
2246
|
await fs.access(localBase);
|
|
2178
|
-
if (await verifyToolBinary(localBase, recordVersion, onTransient)) {
|
|
2247
|
+
if (await verifyToolBinary(localBase, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs)) {
|
|
2179
2248
|
return localBase;
|
|
2180
2249
|
}
|
|
2181
2250
|
logSessionStart(`auto-install verify: ${localBase} exists but is broken, will reinstall`);
|
|
@@ -2191,7 +2260,7 @@ async function getToolPathResolved(toolId, onTransient) {
|
|
|
2191
2260
|
if (tool.platformPackage) {
|
|
2192
2261
|
const platformBin = resolvePlatformPackageBinary(tool);
|
|
2193
2262
|
if (platformBin &&
|
|
2194
|
-
(await verifyToolBinary(platformBin, undefined, onTransient))) {
|
|
2263
|
+
(await verifyToolBinary(platformBin, undefined, onTransient, getToolVerificationTimeout(tool), tool.checkArgs))) {
|
|
2195
2264
|
logSessionStart(`auto-install ${toolId}: resolved platform-package binary at ${platformBin}`);
|
|
2196
2265
|
return platformBin;
|
|
2197
2266
|
}
|
|
@@ -2213,14 +2282,14 @@ async function getToolPathResolved(toolId, onTransient) {
|
|
|
2213
2282
|
return tool.checkCommand;
|
|
2214
2283
|
}
|
|
2215
2284
|
if (tool.installStrategy === "npm") {
|
|
2216
|
-
const npmPath = await findNpmGlobalToolPath(tool.binaryName || tool.id, onTransient);
|
|
2285
|
+
const npmPath = await findNpmGlobalToolPath(tool.binaryName || tool.id, onTransient, tool.checkArgs, getToolVerificationTimeout(tool));
|
|
2217
2286
|
if (npmPath) {
|
|
2218
2287
|
return npmPath;
|
|
2219
2288
|
}
|
|
2220
2289
|
}
|
|
2221
2290
|
// For pip tools, also probe user-level script locations
|
|
2222
2291
|
if (tool.installStrategy === "pip") {
|
|
2223
|
-
const pipPath = await findPipUserToolPath(tool.binaryName || tool.id, onTransient);
|
|
2292
|
+
const pipPath = await findPipUserToolPath(tool.binaryName || tool.id, onTransient, tool.checkArgs, getToolVerificationTimeout(tool));
|
|
2224
2293
|
if (pipPath) {
|
|
2225
2294
|
return pipPath;
|
|
2226
2295
|
}
|
|
@@ -2248,6 +2317,18 @@ async function findGitHubToolPath(binaryName) {
|
|
|
2248
2317
|
}
|
|
2249
2318
|
return undefined;
|
|
2250
2319
|
}
|
|
2320
|
+
/** Resolve a release-managed binary without probing PATH first. */
|
|
2321
|
+
export async function findManagedToolBinary(toolId) {
|
|
2322
|
+
const tool = TOOLS.find((candidate) => candidate.id === toolId);
|
|
2323
|
+
if (!tool)
|
|
2324
|
+
return undefined;
|
|
2325
|
+
if (tool.installStrategy !== "github" &&
|
|
2326
|
+
tool.installStrategy !== "maven" &&
|
|
2327
|
+
tool.installStrategy !== "archive") {
|
|
2328
|
+
return undefined;
|
|
2329
|
+
}
|
|
2330
|
+
return findGitHubToolPath(tool.binaryName || tool.id);
|
|
2331
|
+
}
|
|
2251
2332
|
function hasExecutableExtension(name) {
|
|
2252
2333
|
return /\.(exe|bat|cmd|ps1)$/i.test(name);
|
|
2253
2334
|
}
|
|
@@ -2278,7 +2359,7 @@ function getArchiveBinaryCandidates(binaryName, platform, assetName) {
|
|
|
2278
2359
|
candidates.add(`${binaryName}.cmd`);
|
|
2279
2360
|
return [...candidates];
|
|
2280
2361
|
}
|
|
2281
|
-
async function findNpmGlobalToolPath(binaryName, onTransient) {
|
|
2362
|
+
async function findNpmGlobalToolPath(binaryName, onTransient, verificationArgs = ["--version"], verificationTimeoutMs = 10_000) {
|
|
2282
2363
|
const isWindows = process.platform === "win32";
|
|
2283
2364
|
const binDirs = await getNpmGlobalBinCandidates(onTransient);
|
|
2284
2365
|
for (const dir of binDirs) {
|
|
@@ -2291,7 +2372,7 @@ async function findNpmGlobalToolPath(binaryName, onTransient) {
|
|
|
2291
2372
|
for (const candidate of candidates) {
|
|
2292
2373
|
try {
|
|
2293
2374
|
await fs.access(candidate);
|
|
2294
|
-
if (await verifyToolBinary(candidate, undefined, onTransient)) {
|
|
2375
|
+
if (await verifyToolBinary(candidate, undefined, onTransient, verificationTimeoutMs, verificationArgs)) {
|
|
2295
2376
|
return candidate;
|
|
2296
2377
|
}
|
|
2297
2378
|
}
|
|
@@ -2332,7 +2413,7 @@ async function getNpmGlobalBinCandidates(onTransient) {
|
|
|
2332
2413
|
}
|
|
2333
2414
|
return dirs;
|
|
2334
2415
|
}
|
|
2335
|
-
async function findPipUserToolPath(binaryName, onTransient) {
|
|
2416
|
+
async function findPipUserToolPath(binaryName, onTransient, verificationArgs = ["--version"], verificationTimeoutMs = 10_000) {
|
|
2336
2417
|
const isWindows = process.platform === "win32";
|
|
2337
2418
|
const userBaseCandidates = await getPythonUserBaseCandidates();
|
|
2338
2419
|
for (const userBase of userBaseCandidates) {
|
|
@@ -2365,7 +2446,7 @@ async function findPipUserToolPath(binaryName, onTransient) {
|
|
|
2365
2446
|
for (const candidate of candidates) {
|
|
2366
2447
|
try {
|
|
2367
2448
|
await fs.access(candidate);
|
|
2368
|
-
if (await verifyToolBinary(candidate, undefined, onTransient)) {
|
|
2449
|
+
if (await verifyToolBinary(candidate, undefined, onTransient, verificationTimeoutMs, verificationArgs)) {
|
|
2369
2450
|
return candidate;
|
|
2370
2451
|
}
|
|
2371
2452
|
}
|
|
@@ -2837,8 +2918,10 @@ export function getRefreshableManagedTools() {
|
|
|
2837
2918
|
refreshable.push({
|
|
2838
2919
|
toolId: tool.id,
|
|
2839
2920
|
strategy: "npm",
|
|
2921
|
+
checkArgs: tool.checkArgs,
|
|
2840
2922
|
packageName: tool.packageName,
|
|
2841
2923
|
binaryName: tool.binaryName,
|
|
2924
|
+
verificationTimeoutMs: tool.verificationTimeoutMs,
|
|
2842
2925
|
});
|
|
2843
2926
|
break;
|
|
2844
2927
|
}
|
|
@@ -2849,6 +2932,7 @@ export function getRefreshableManagedTools() {
|
|
|
2849
2932
|
refreshable.push({
|
|
2850
2933
|
toolId: tool.id,
|
|
2851
2934
|
strategy: tool.installStrategy,
|
|
2935
|
+
checkArgs: tool.checkArgs,
|
|
2852
2936
|
packageName: tool.packageName,
|
|
2853
2937
|
});
|
|
2854
2938
|
break;
|
|
@@ -2856,7 +2940,11 @@ export function getRefreshableManagedTools() {
|
|
|
2856
2940
|
case "github": {
|
|
2857
2941
|
if (!tool.github)
|
|
2858
2942
|
continue;
|
|
2859
|
-
refreshable.push({
|
|
2943
|
+
refreshable.push({
|
|
2944
|
+
toolId: tool.id,
|
|
2945
|
+
strategy: "github",
|
|
2946
|
+
checkArgs: tool.checkArgs,
|
|
2947
|
+
});
|
|
2860
2948
|
break;
|
|
2861
2949
|
}
|
|
2862
2950
|
case "maven": {
|
|
@@ -2865,6 +2953,7 @@ export function getRefreshableManagedTools() {
|
|
|
2865
2953
|
refreshable.push({
|
|
2866
2954
|
toolId: tool.id,
|
|
2867
2955
|
strategy: "maven",
|
|
2956
|
+
checkArgs: tool.checkArgs,
|
|
2868
2957
|
pinnedCoordinate: mavenCoordinate(tool.maven),
|
|
2869
2958
|
});
|
|
2870
2959
|
break;
|
|
@@ -2880,6 +2969,7 @@ export function getRefreshableManagedTools() {
|
|
|
2880
2969
|
refreshable.push({
|
|
2881
2970
|
toolId: tool.id,
|
|
2882
2971
|
strategy: "archive",
|
|
2972
|
+
checkArgs: tool.checkArgs,
|
|
2883
2973
|
pinnedCoordinate: url,
|
|
2884
2974
|
});
|
|
2885
2975
|
break;
|
|
@@ -2951,7 +3041,8 @@ async function probeManagedToolVersion(tool) {
|
|
|
2951
3041
|
return undefined;
|
|
2952
3042
|
try {
|
|
2953
3043
|
const result = await safeSpawnAsync(cached.path, tool.checkArgs, {
|
|
2954
|
-
timeout:
|
|
3044
|
+
timeout: getToolVerificationTimeout(tool),
|
|
3045
|
+
input: "",
|
|
2955
3046
|
ignoreAmbientSignal: true,
|
|
2956
3047
|
resourceLabel: `tool-refresh-version:${tool.id}`,
|
|
2957
3048
|
});
|
|
@@ -3187,8 +3278,8 @@ async function verifyRefreshedArtifact(tool, installedPath) {
|
|
|
3187
3278
|
await updateProbeCache(tool.id, installedPath);
|
|
3188
3279
|
return true;
|
|
3189
3280
|
}
|
|
3190
|
-
if (!(await verifyToolBinary(installedPath))) {
|
|
3191
|
-
logSessionStart(`managed-tool-refresh ${tool.id}: refreshed artifact at ${installedPath} failed its
|
|
3281
|
+
if (!(await verifyToolBinary(installedPath, undefined, undefined, getToolVerificationTimeout(tool), tool.checkArgs))) {
|
|
3282
|
+
logSessionStart(`managed-tool-refresh ${tool.id}: refreshed artifact at ${installedPath} failed its verification check`);
|
|
3192
3283
|
return false;
|
|
3193
3284
|
}
|
|
3194
3285
|
// Record the NEW mtime rather than leaving the persisted entry to miss its
|
|
@@ -3643,7 +3734,7 @@ async function installArchiveTool(tool) {
|
|
|
3643
3734
|
return undefined;
|
|
3644
3735
|
}
|
|
3645
3736
|
}
|
|
3646
|
-
async function installNpmTool(packageName, binaryName) {
|
|
3737
|
+
async function installNpmTool(packageName, binaryName, verificationArgs = ["--version"], verificationTimeoutMs = 10_000) {
|
|
3647
3738
|
try {
|
|
3648
3739
|
// Ensure tools directory exists
|
|
3649
3740
|
await fs.mkdir(TOOLS_DIR, { recursive: true });
|
|
@@ -3727,7 +3818,7 @@ async function installNpmTool(packageName, binaryName) {
|
|
|
3727
3818
|
lastAttemptTransient = false;
|
|
3728
3819
|
isValid = await verifyToolBinary(binPath, undefined, () => {
|
|
3729
3820
|
lastAttemptTransient = true;
|
|
3730
|
-
});
|
|
3821
|
+
}, verificationTimeoutMs, verificationArgs);
|
|
3731
3822
|
if (isValid)
|
|
3732
3823
|
break;
|
|
3733
3824
|
if (attempt < 3) {
|
|
@@ -4022,7 +4113,7 @@ export async function installTool(toolId) {
|
|
|
4022
4113
|
case "npm": {
|
|
4023
4114
|
if (!tool.packageName || !tool.binaryName)
|
|
4024
4115
|
return false;
|
|
4025
|
-
const npmPath = await installNpmTool(tool.packageName, tool.binaryName);
|
|
4116
|
+
const npmPath = await installNpmTool(tool.packageName, tool.binaryName, tool.checkArgs, getToolVerificationTimeout(tool));
|
|
4026
4117
|
if (npmPath !== undefined) {
|
|
4027
4118
|
// #1746 review F4: an install just resolved this package's range
|
|
4028
4119
|
// against the registry, so record it as freshly checked. Otherwise a
|
|
@@ -4316,7 +4407,13 @@ async function ensureToolResolved(toolId, opts) {
|
|
|
4316
4407
|
return await ensurePromise;
|
|
4317
4408
|
}
|
|
4318
4409
|
finally {
|
|
4319
|
-
|
|
4410
|
+
// Identity-guarded release (#1968's pattern): delete only if THIS run is
|
|
4411
|
+
// still the registered one. A bare delete-by-key lets a late-settling run
|
|
4412
|
+
// evict a live successor a second writer registered under the same key
|
|
4413
|
+
// mid-flight, after which the next caller starts a duplicate ensure/install.
|
|
4414
|
+
if (ensureInFlight.get(inFlightKey) === ensurePromise) {
|
|
4415
|
+
ensureInFlight.delete(inFlightKey);
|
|
4416
|
+
}
|
|
4320
4417
|
}
|
|
4321
4418
|
}
|
|
4322
4419
|
// --- Integration Helpers ---
|
|
@@ -259,6 +259,8 @@ async function installedRefreshCandidates() {
|
|
|
259
259
|
candidates.push({
|
|
260
260
|
toolId: tool.toolId,
|
|
261
261
|
strategy: tool.strategy,
|
|
262
|
+
checkArgs: tool.checkArgs,
|
|
263
|
+
verificationTimeoutMs: tool.verificationTimeoutMs ?? 10_000,
|
|
262
264
|
...(tool.packageName !== undefined && { packageName: tool.packageName }),
|
|
263
265
|
...(tool.binaryName !== undefined && { binaryName: tool.binaryName }),
|
|
264
266
|
});
|
|
@@ -513,7 +515,7 @@ async function performNpmRefresh(candidate, packageName, now) {
|
|
|
513
515
|
// drops npm entries that do not.
|
|
514
516
|
const binaryName = candidate.binaryName;
|
|
515
517
|
const binPath = resolveManagedNpmBinPath(binaryName);
|
|
516
|
-
const verified = await verifyToolBinary(binPath);
|
|
518
|
+
const verified = await verifyToolBinary(binPath, undefined, undefined, candidate.verificationTimeoutMs, candidate.checkArgs);
|
|
517
519
|
if (!verified) {
|
|
518
520
|
recordDegradationOnce({
|
|
519
521
|
kind: "managed-tool-refresh",
|
|
@@ -70,7 +70,7 @@ import { acquireQuarantinePidFileLock } from "./bounded-pid-file-lock.js";
|
|
|
70
70
|
import { spawnCollectStdoutResult, unrefChildAndPipes, } from "./child-unref.js";
|
|
71
71
|
import { incrementDegradationCount, recordDegradationOnce, } from "./degradation-ledger.js";
|
|
72
72
|
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
73
|
-
import { isInstanceRegistryEnabled, readInstanceRegistry, } from "./instance-registry.js";
|
|
73
|
+
import { isInstanceRegistryEnabled, pruneDeadInstances as pruneDeadRegistryInstances, readInstanceRegistry, } from "./instance-registry.js";
|
|
74
74
|
import { logLatency } from "./latency-logger.js";
|
|
75
75
|
const isWindows = process.platform === "win32";
|
|
76
76
|
/**
|
|
@@ -1226,8 +1226,8 @@ export async function sweepOrphans() {
|
|
|
1226
1226
|
/** Drop dead-parent AND stale-heartbeat (#525, record-cleanup-only)
|
|
1227
1227
|
* instances from the registry. Re-reads immediately before
|
|
1228
1228
|
* writing (rather than reusing the earlier `readInstanceRegistry()` snapshot)
|
|
1229
|
-
*
|
|
1230
|
-
*
|
|
1229
|
+
* under the instance-registry lock so the reaper cannot lose a registration
|
|
1230
|
+
* or heartbeat while pruning.
|
|
1231
1231
|
*
|
|
1232
1232
|
* Exported for the #1217 concurrency regression test; `sweepOrphans` is the
|
|
1233
1233
|
* only production caller.
|
|
@@ -1244,17 +1244,8 @@ export async function sweepOrphans() {
|
|
|
1244
1244
|
* rename, which the hand-rolled copy did not, and puts this writer back on
|
|
1245
1245
|
* the same scheme as the other writer of this same file. */
|
|
1246
1246
|
export async function pruneDeadInstances(deadPids) {
|
|
1247
|
-
const target = path.join(getGlobalPiLensDir(), "instances.json");
|
|
1248
1247
|
try {
|
|
1249
|
-
|
|
1250
|
-
const parsed = JSON.parse(raw);
|
|
1251
|
-
if (!parsed || !Array.isArray(parsed.instances))
|
|
1252
|
-
return;
|
|
1253
|
-
const remaining = parsed.instances.filter((entry) => !deadPids.has(entry.pid));
|
|
1254
|
-
if (remaining.length === parsed.instances.length)
|
|
1255
|
-
return;
|
|
1256
|
-
await fs.promises.mkdir(getGlobalPiLensDir(), { recursive: true });
|
|
1257
|
-
await writeFileAtomicAsync(target, JSON.stringify({ instances: remaining }));
|
|
1248
|
+
await pruneDeadRegistryInstances(deadPids);
|
|
1258
1249
|
}
|
|
1259
1250
|
catch {
|
|
1260
1251
|
// best-effort
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/** Cross-process mutual exclusion for the machine-global instance registry. */
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import { randomInt } from "node:crypto";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { incrementDegradationCount } from "./degradation-ledger.js";
|
|
6
|
+
const LOCK_STALE_MS = 5_000;
|
|
7
|
+
const LOCK_WAIT_MS = 500;
|
|
8
|
+
const LOCK_MIN_BACKOFF_MS = 5;
|
|
9
|
+
const LOCK_MAX_BACKOFF_MS = 25;
|
|
10
|
+
function lockPath(target) {
|
|
11
|
+
return `${target}.lock`;
|
|
12
|
+
}
|
|
13
|
+
function ownerPid(lock) {
|
|
14
|
+
try {
|
|
15
|
+
const pid = Number(fs.readFileSync(lock, "utf8").trim().split(/\s+/)[0]);
|
|
16
|
+
return Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function isPidAlive(pid) {
|
|
23
|
+
if (pid === undefined)
|
|
24
|
+
return false;
|
|
25
|
+
try {
|
|
26
|
+
process.kill(pid, 0);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return error?.code !== "ESRCH";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function staleLock(lock) {
|
|
34
|
+
try {
|
|
35
|
+
const stat = fs.statSync(lock);
|
|
36
|
+
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS)
|
|
37
|
+
return true;
|
|
38
|
+
const pid = ownerPid(lock);
|
|
39
|
+
return pid !== undefined && !isPidAlive(pid);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function recordLockTimeout(target) {
|
|
46
|
+
incrementDegradationCount({
|
|
47
|
+
kind: "instance-registry-lock-timeout",
|
|
48
|
+
subject: path.resolve(target),
|
|
49
|
+
reason: `lock acquisition exhausted for ${path.basename(target)}`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function backoffMs() {
|
|
53
|
+
return randomInt(LOCK_MIN_BACKOFF_MS, LOCK_MAX_BACKOFF_MS + 1);
|
|
54
|
+
}
|
|
55
|
+
function backoff() {
|
|
56
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, backoffMs());
|
|
57
|
+
}
|
|
58
|
+
function ownsLock(lock) {
|
|
59
|
+
return ownerPid(lock) === process.pid;
|
|
60
|
+
}
|
|
61
|
+
async function releaseLock(lock) {
|
|
62
|
+
// Residual TOCTOU: ownership and unlink are separate syscalls, so a stale
|
|
63
|
+
// takeover can still replace the lock between them.
|
|
64
|
+
if (ownsLock(lock))
|
|
65
|
+
await fs.promises.unlink(lock).catch(() => { });
|
|
66
|
+
}
|
|
67
|
+
function releaseLockSync(lock) {
|
|
68
|
+
if (!ownsLock(lock))
|
|
69
|
+
return;
|
|
70
|
+
try {
|
|
71
|
+
fs.unlinkSync(lock);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// A stale takeover may have displaced the lock after ownership was read.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function takeOverStale(lock) {
|
|
78
|
+
if (!staleLock(lock))
|
|
79
|
+
return;
|
|
80
|
+
const displaced = `${lock}.stale-${process.pid}-${Date.now()}`;
|
|
81
|
+
try {
|
|
82
|
+
fs.renameSync(lock, displaced);
|
|
83
|
+
fs.unlinkSync(displaced);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Another owner may have released or taken over it first.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function isLockContention(error) {
|
|
90
|
+
const code = error?.code;
|
|
91
|
+
return code === "EEXIST" || code === "EPERM" || code === "EBUSY";
|
|
92
|
+
}
|
|
93
|
+
export async function withInstanceRegistryLock(target, op) {
|
|
94
|
+
const lock = lockPath(target);
|
|
95
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
96
|
+
await fs.promises.mkdir(path.dirname(target), { recursive: true });
|
|
97
|
+
while (Date.now() <= deadline) {
|
|
98
|
+
try {
|
|
99
|
+
await fs.promises.writeFile(lock, `${process.pid} ${Date.now()}\n`, {
|
|
100
|
+
flag: "wx",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (!isLockContention(error))
|
|
105
|
+
throw error;
|
|
106
|
+
takeOverStale(lock);
|
|
107
|
+
if (Date.now() <= deadline)
|
|
108
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs()));
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
return await op();
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
await releaseLock(lock);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
recordLockTimeout(target);
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
export function withInstanceRegistryLockSync(target, op) {
|
|
122
|
+
const lock = lockPath(target);
|
|
123
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
124
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
125
|
+
while (Date.now() <= deadline) {
|
|
126
|
+
try {
|
|
127
|
+
fs.writeFileSync(lock, `${process.pid} ${Date.now()}\n`, { flag: "wx" });
|
|
128
|
+
try {
|
|
129
|
+
return op();
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
releaseLockSync(lock);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (!isLockContention(error))
|
|
137
|
+
throw error;
|
|
138
|
+
takeOverStale(lock);
|
|
139
|
+
if (Date.now() <= deadline)
|
|
140
|
+
backoff();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
recordLockTimeout(target);
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|