@herbertgao/pi-extensions 2026.8.6 → 2026.8.8
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 +8 -6
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +19 -7
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +30 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +320 -22
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +94 -77
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +79 -17
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +27 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +66 -17
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +41 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +75 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +7 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +64 -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/README.md +30 -22
- package/node_modules/@herbertgao/pi-subagents/examples/agent-tool-description.md +5 -5
- package/node_modules/@herbertgao/pi-subagents/package.json +7 -7
- package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +7 -1
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +48 -19
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +17 -13
- package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +26 -2
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +219 -40
- package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +101 -3
- package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +18 -4
- package/node_modules/@herbertgao/pi-subagents/src/settings.ts +99 -0
- package/node_modules/@herbertgao/pi-subagents/src/types.ts +22 -3
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +51 -3
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +20 -2
- package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +21 -6
- package/node_modules/@herbertgao/pi-subagents/src/usage.ts +129 -1
- package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +20 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +5 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +174 -100
- package/node_modules/@juicesharp/rpiv-ask-user-question/config.ts +25 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/configuration.md +13 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +5 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +10 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +10 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/tool-schema.md +11 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +5 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/zh.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +10 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +140 -97
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +63 -28
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/selectors/projections.ts +22 -15
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/state-reducer.ts +11 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/state.ts +14 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/response-envelope.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/types.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/multi-select-view.ts +31 -17
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/preview/preview-block-renderer.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/wrapping-select.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +86 -38
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +76 -9
- package/node_modules/@narumitw/pi-btw/README.md +120 -111
- package/node_modules/@narumitw/pi-btw/dist/index.ts +2898 -0
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +7 -0
- package/node_modules/@narumitw/pi-btw/package.json +10 -6
- package/node_modules/@narumitw/pi-btw/src/btw.ts +36 -40
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +55 -10
- package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +390 -0
- package/node_modules/@narumitw/pi-btw/src/menu.ts +45 -2
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +136 -45
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +44 -1856
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +4 -6
- package/node_modules/pi-lens/CHANGELOG.md +999 -0
- package/node_modules/pi-lens/README.md +117 -3
- package/node_modules/pi-lens/config/ruff/core.toml +42 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +130 -27
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +126 -13
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +67 -11
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +9 -3
- package/node_modules/pi-lens/dist/clients/atomic-write.js +9 -3
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +381 -13
- package/node_modules/pi-lens/dist/clients/biome-client.js +61 -30
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +428 -0
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +129 -0
- package/node_modules/pi-lens/dist/clients/bootstrap.js +12 -1
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +12 -3
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +206 -0
- package/node_modules/pi-lens/dist/clients/cache-observability.js +353 -76
- package/node_modules/pi-lens/dist/clients/cascade-budget.js +196 -0
- package/node_modules/pi-lens/dist/clients/cascade-format.js +20 -1
- package/node_modules/pi-lens/dist/clients/cascade-types.js +9 -1
- package/node_modules/pi-lens/dist/clients/child-unref.js +54 -6
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +76 -24
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +123 -31
- package/node_modules/pi-lens/dist/clients/deadline-utils.js +8 -1
- package/node_modules/pi-lens/dist/clients/debug-handles.js +5 -0
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +84 -14
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +155 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +25 -3
- package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +8 -2
- package/node_modules/pi-lens/dist/clients/deps/typebox.js +8 -3
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +147 -33
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +298 -0
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +52 -2
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +256 -88
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +7 -4
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +5 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +234 -18
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +187 -19
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +6 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +11 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +398 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +9 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +11 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +12 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +18 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +9 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +895 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +8 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +16 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +60 -53
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +16 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +25 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +20 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +44 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +331 -55
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +38 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +97 -19
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +17 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +32 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +47 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +14 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +127 -28
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +12 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +73 -12
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +275 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +21 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +260 -27
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +734 -64
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +86 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +188 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +9 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +21 -10
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +18 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +4 -2
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +28 -5
- package/node_modules/pi-lens/dist/clients/extension-log.js +22 -1
- package/node_modules/pi-lens/dist/clients/file-kinds.js +8 -2
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +316 -0
- package/node_modules/pi-lens/dist/clients/finding-identity.js +88 -0
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +3 -2
- package/node_modules/pi-lens/dist/clients/formatters.js +500 -115
- package/node_modules/pi-lens/dist/clients/generation-guard.js +216 -0
- package/node_modules/pi-lens/dist/clients/git-guard.js +31 -0
- package/node_modules/pi-lens/dist/clients/gitleaks-client.js +181 -8
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +109 -9
- package/node_modules/pi-lens/dist/clients/grammar-source.js +333 -10
- package/node_modules/pi-lens/dist/clients/host-ports.js +8 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +1114 -100
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +58 -0
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +715 -0
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +643 -97
- package/node_modules/pi-lens/dist/clients/instance-registry.js +87 -17
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +87 -8
- package/node_modules/pi-lens/dist/clients/knip-client.js +310 -10
- package/node_modules/pi-lens/dist/clients/language-policy.js +8 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +9 -2
- package/node_modules/pi-lens/dist/clients/latency-logger.js +440 -11
- package/node_modules/pi-lens/dist/clients/lazy-import.js +21 -0
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +21 -0
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +128 -8
- package/node_modules/pi-lens/dist/clients/lsp/client.js +1660 -164
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +83 -1
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +389 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +2423 -151
- package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +215 -0
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +146 -14
- package/node_modules/pi-lens/dist/clients/lsp/language.js +48 -4
- package/node_modules/pi-lens/dist/clients/lsp/server.js +277 -16
- package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +22 -0
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +43 -0
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +79 -3
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +43 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +422 -28
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +30 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +175 -0
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +3 -2
- package/node_modules/pi-lens/dist/clients/mcp/session.js +10 -2
- package/node_modules/pi-lens/dist/clients/module-report.js +22 -1
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +114 -23
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +15 -1
- package/node_modules/pi-lens/dist/clients/package-manager.js +170 -14
- package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +27 -0
- package/node_modules/pi-lens/dist/clients/path-utils.js +160 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +154 -33
- package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +37 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +17 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +89 -22
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +84 -14
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +15 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +50 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +23 -9
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
- package/node_modules/pi-lens/dist/clients/project-report.js +62 -27
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +299 -0
- package/node_modules/pi-lens/dist/clients/quiet-window-config.js +47 -0
- package/node_modules/pi-lens/dist/clients/quiet-window.js +11 -25
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +8 -3
- package/node_modules/pi-lens/dist/clients/read-guard.js +253 -10
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +49 -17
- package/node_modules/pi-lens/dist/clients/reverse-deps.js +50 -11
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +194 -31
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +52 -4
- package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +21 -0
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +6 -3
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +96 -7
- package/node_modules/pi-lens/dist/clients/runtime-context.js +8 -1
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +352 -45
- package/node_modules/pi-lens/dist/clients/runtime-session.js +373 -9
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +217 -22
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +212 -79
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +620 -67
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +220 -7
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +161 -0
- package/node_modules/pi-lens/dist/clients/search-read-registration.js +34 -6
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +157 -24
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +184 -0
- package/node_modules/pi-lens/dist/clients/sg-runner.js +116 -22
- package/node_modules/pi-lens/dist/clients/single-flight.js +158 -0
- package/node_modules/pi-lens/dist/clients/source-filter.js +6 -2
- package/node_modules/pi-lens/dist/clients/stale-marker.js +17 -0
- package/node_modules/pi-lens/dist/clients/startup-scan.js +5 -2
- package/node_modules/pi-lens/dist/clients/startup-timing.js +19 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +151 -16
- package/node_modules/pi-lens/dist/clients/tool-event.js +56 -5
- package/node_modules/pi-lens/dist/clients/tool-policy.js +125 -20
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +75 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +769 -64
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +1 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +11 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +90 -0
- package/node_modules/pi-lens/dist/clients/trivy-client.js +19 -1
- package/node_modules/pi-lens/dist/clients/widget-state.js +336 -36
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +214 -14
- package/node_modules/pi-lens/dist/index.js +61407 -48577
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +476 -57
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +132 -43
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -7
- package/node_modules/pi-lens/docs/agent-guide.md +6 -1
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +515 -506
- package/node_modules/pi-lens/docs/dispositions.md +21 -0
- package/node_modules/pi-lens/docs/features.md +19 -5
- package/node_modules/pi-lens/docs/globalconfig.md +3 -0
- package/node_modules/pi-lens/docs/language-coverage.md +71 -5
- package/node_modules/pi-lens/docs/mcp.md +2 -2
- package/node_modules/pi-lens/docs/servercapabilities.md +4 -1
- package/node_modules/pi-lens/docs/settings.md +19 -1
- package/node_modules/pi-lens/docs/usage.md +1 -0
- package/node_modules/pi-lens/package.json +27 -10
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-js-snapshot.yml +23 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-snapshot.yml +23 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/ts-void-zero-snapshot.yml +23 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-object-param-test.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +15 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-compile-call-test.yml +29 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-dupe-class-members-test.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-js-test.yml +7 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-test.yml +7 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-known-value-widening-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-non-null-assertion-test.yml +216 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +2 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +2 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-return-value-in-generator-test.yml +12 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-runtime-typeof-test.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-server-bind-wildcard-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-shape-in-symbol-names-test.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-js-test.yml +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-test.yml +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-parameters-test.yml +12 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-returns-test.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-any-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-unknown-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/redundant-unsafe-function-test.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/require-safety-comment-for-as-unknown-as-test.yml +177 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ts-void-zero-test.yml +1 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member-js.yml +13 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member.yml +13 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-object-param.yml +60 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +57 -12
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-compile-call.yml +63 -5
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-dupe-class-members.yml +52 -8
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html-js.yml +27 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html.yml +27 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-known-value-widening.yml +71 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-non-null-assertion.yml +316 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +22 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +23 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +33 -5
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +34 -8
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-return-value-in-generator.yml +22 -5
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-runtime-typeof.yml +121 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-server-bind-wildcard.yml +23 -7
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-shape-in-symbol-names.yml +68 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +41 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code.yml +41 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +6 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-parameters.yml +67 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-returns.yml +106 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-any.yml +50 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-unknown.yml +56 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/redundant-unsafe-function.yml +32 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/require-safety-comment-for-as-unknown-as.yml +184 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/ts-void-zero.yml +5 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/self-scan-baseline.json +5 -0
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +53 -1
- package/node_modules/pi-lens/scripts/download-grammars.js +25 -0
- package/node_modules/pi-lens/scripts/grammars.lock.json +9 -0
- package/node_modules/pi-lens/scripts/install-selftest.mjs +99 -11
- package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +75 -0
- package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +285 -0
- package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +233 -0
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +2 -0
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +80 -3
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +41 -0
- package/node_modules/pi-lens/vendor/grammars/tree-sitter-cue.wasm +0 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +36 -0
- package/node_modules/pi-mcp-adapter/README.md +47 -5
- package/node_modules/pi-mcp-adapter/cli.js +163 -1
- package/node_modules/pi-mcp-adapter/commands.ts +128 -22
- package/node_modules/pi-mcp-adapter/config.ts +25 -7
- package/node_modules/pi-mcp-adapter/direct-tools.ts +5 -4
- package/node_modules/pi-mcp-adapter/index.ts +219 -32
- package/node_modules/pi-mcp-adapter/init.ts +36 -4
- package/node_modules/pi-mcp-adapter/lifecycle.ts +315 -24
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +39 -6
- package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +387 -0
- package/node_modules/pi-mcp-adapter/mcp-callback-server.ts +27 -3
- package/node_modules/pi-mcp-adapter/mcp-code.ts +4 -0
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +42 -13
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +32 -21
- package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +168 -0
- package/node_modules/pi-mcp-adapter/package.json +3 -1
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +5 -4
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +31 -37
- package/node_modules/pi-mcp-adapter/search-ranking.ts +92 -22
- package/node_modules/pi-mcp-adapter/server-manager.ts +140 -2
- package/node_modules/pi-mcp-adapter/session-recovery.ts +7 -0
- package/node_modules/pi-mcp-adapter/tool-approval.ts +15 -2
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +44 -4
- package/node_modules/pi-mcp-adapter/types.ts +2 -0
- package/node_modules/pi-mcp-adapter/ui-server.ts +18 -16
- package/node_modules/pi-mcp-adapter/utils.ts +70 -0
- package/node_modules/pi-web-access/CHANGELOG.md +37 -0
- package/node_modules/pi-web-access/README.md +32 -13
- package/node_modules/pi-web-access/anysearch.ts +4 -2
- package/node_modules/pi-web-access/brave.ts +16 -4
- package/node_modules/pi-web-access/chrome-cookies.ts +86 -15
- package/node_modules/pi-web-access/curator-page.ts +6 -3
- package/node_modules/pi-web-access/curator-server.ts +4 -1
- package/node_modules/pi-web-access/data-uri-sanitize.ts +406 -0
- package/node_modules/pi-web-access/exa.ts +18 -7
- package/node_modules/pi-web-access/extract.ts +40 -5
- package/node_modules/pi-web-access/firecrawl.ts +18 -2
- package/node_modules/pi-web-access/gemini-search.ts +16 -4
- package/node_modules/pi-web-access/github-extract.ts +65 -36
- package/node_modules/pi-web-access/index.ts +60 -61
- package/node_modules/pi-web-access/openai-search.ts +17 -6
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/page-query.ts +4 -2
- package/node_modules/pi-web-access/parallel-mcp.ts +249 -0
- package/node_modules/pi-web-access/parallel.ts +2 -2
- package/node_modules/pi-web-access/pdf-extract.ts +18 -5
- package/node_modules/pi-web-access/query-rewrite.ts +51 -0
- package/node_modules/pi-web-access/serper.ts +211 -0
- package/node_modules/pi-web-access/ssrf-protection.ts +12 -2
- package/node_modules/pi-web-access/summary-model-scope.ts +12 -4
- package/node_modules/pi-web-access/summary-review.ts +44 -22
- package/node_modules/pi-web-access/tavily.ts +16 -4
- package/node_modules/pi-web-access/utils.ts +87 -0
- package/node_modules/pi-web-access/valyu.ts +199 -0
- package/package.json +14 -14
|
@@ -11,15 +11,16 @@ import * as path from "node:path";
|
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import { logSessionStart } from "../../../sessionstart-logger.js";
|
|
13
13
|
import { getGlobalPiLensDir } from "../../../file-utils.js";
|
|
14
|
+
import { createGenerationSource, } from "../../../generation-guard.js";
|
|
14
15
|
import { PathKeyedMap } from "../../../path-keyed-map.js";
|
|
15
16
|
import { normalizeEphemeralMapKey, normalizeMapKey, } from "../../../path-utils.js";
|
|
16
|
-
import { ensureTool, isSpawnableCommand, resetPathWalkMemo, } from "../../../installer/index.js";
|
|
17
|
+
import { ensureTool, getInstallAttempt, getLastEnsureResolutionSource, getToolInstallStrategy, isSpawnableCommand, resetPathWalkMemo, } from "../../../installer/index.js";
|
|
17
18
|
import { getServersForFileWithConfig, isServerDisabled, } from "../../../lsp/config.js";
|
|
18
19
|
import { findGlobalBinary } from "../../../package-manager.js";
|
|
19
20
|
import { safeSpawnAsync } from "../../../safe-spawn.js";
|
|
20
21
|
import { getToolCommandSpec, shouldAutoInstallTool, } from "../../../tool-policy.js";
|
|
21
|
-
import { classifyProbeFailure, createAvailabilityLatch, isLatchingOutcome, logAvailabilityDecision, startHostStallSampler, transientRetryDelayMs, } from "./availability-policy.js";
|
|
22
|
-
export { createAvailabilityLatch, classifyProbeFailure, describeUnavailability, isTransientDecision, logAvailabilityDecision, startHostStallSampler, } from "./availability-policy.js";
|
|
22
|
+
import { classifyProbeFailure, createAvailabilityLatch, describeInstallAttempt, describeProbeEvidence, isLatchingOutcome, logAvailabilityDecision, startHostStallSampler, transientRetryDelayMs, } from "./availability-policy.js";
|
|
23
|
+
export { createAvailabilityLatch, classifyProbeFailure, describeProbeEvidence, describeUnavailability, isTransientDecision, logAvailabilityDecision, startHostStallSampler, } from "./availability-policy.js";
|
|
23
24
|
/**
|
|
24
25
|
* True when the LSP runner will cover `ctx.filePath` via the given PRIMARY server
|
|
25
26
|
* id. Used by CLI runners that duplicate a linter a warm LSP already wraps
|
|
@@ -67,34 +68,176 @@ if (typeof __dirname !== "undefined") {
|
|
|
67
68
|
// Managed tools directory (~/.pi-lens/tools) — where ensureTool() installs binaries
|
|
68
69
|
const _managedToolsDir = path.join(getGlobalPiLensDir(), "tools");
|
|
69
70
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* When the shim exists the tool IS installed, so availability needs no spawn at
|
|
74
|
-
* all — and a spawn that cannot happen cannot time out (#1467). knip and jscpd
|
|
75
|
-
* each carried a line-for-line copy of this resolver; #1476 folds them into one
|
|
76
|
-
* definition so the next managed tool inherits the fast path instead of a
|
|
77
|
-
* fourth copy.
|
|
71
|
+
* Managed-shim candidates for a Node CLI tool, in the installer's own
|
|
72
|
+
* preference order (`getToolPath`'s managed-first branch: `.cmd`, then `.exe`,
|
|
73
|
+
* then extensionless on Windows; extensionless elsewhere).
|
|
78
74
|
*
|
|
79
75
|
* The pi-lens dir is read per call, never memoized at module load, so tests that
|
|
80
76
|
* point `getGlobalPiLensDir` at a temp home still see their own tree.
|
|
81
77
|
*/
|
|
82
|
-
|
|
78
|
+
function managedNodeToolCandidates(tool) {
|
|
83
79
|
const base = path.join(getGlobalPiLensDir(), "tools", "node_modules", ".bin", tool);
|
|
84
|
-
|
|
80
|
+
return process.platform === "win32"
|
|
81
|
+
? [`${base}.cmd`, `${base}.exe`, base]
|
|
82
|
+
: [base];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Verification verdicts for managed shims, keyed by path + mtime + size, so a
|
|
86
|
+
* reinstall re-verifies and a session start re-arms.
|
|
87
|
+
*
|
|
88
|
+
* A plain `Map`, not a `PathKeyedMap`: `managedNodeToolCandidates` is the ONLY
|
|
89
|
+
* producer of these paths, so the write and read forms are the same string by
|
|
90
|
+
* construction and cannot diverge on case or separator.
|
|
91
|
+
*/
|
|
92
|
+
const managedBinaryVerdicts = new Map();
|
|
93
|
+
/**
|
|
94
|
+
* Verifications in progress, keyed the same way. Concurrent first touches of
|
|
95
|
+
* one shim share the single probe instead of each spawning their own — the
|
|
96
|
+
* same in-flight-share shape `resolveInstallInFlightByCwd` uses for installs
|
|
97
|
+
* (#1674 review F2).
|
|
98
|
+
*/
|
|
99
|
+
const managedVerifyInFlight = new Map();
|
|
100
|
+
/**
|
|
101
|
+
* Budget for the managed-shim verification spawn.
|
|
102
|
+
*
|
|
103
|
+
* 5s, not the checker's default probe budget: a cold npm shim on a cold file
|
|
104
|
+
* cache measured just over 2s in the #1674 review, so a tighter budget spends
|
|
105
|
+
* the whole wait and still learns nothing. This runs once per shim per session
|
|
106
|
+
* on the success path, and the installer's own verification keeps its more
|
|
107
|
+
* generous 10s.
|
|
108
|
+
*/
|
|
109
|
+
const MANAGED_VERIFY_TIMEOUT_MS = 5000;
|
|
110
|
+
/** First cooldown after a verification that never got a fair run. */
|
|
111
|
+
const MANAGED_VERIFY_COOLDOWN_MS = 60_000;
|
|
112
|
+
/**
|
|
113
|
+
* Ceiling for the cooldown ladder — a shim that keeps stalling is still
|
|
114
|
+
* re-probed every 15 minutes.
|
|
115
|
+
*/
|
|
116
|
+
const MANAGED_VERIFY_COOLDOWN_MAX_MS = 15 * 60_000;
|
|
117
|
+
function managedVerifyCooldownMs(attempts) {
|
|
118
|
+
return Math.min(MANAGED_VERIFY_COOLDOWN_MAX_MS, MANAGED_VERIFY_COOLDOWN_MS * 2 ** (attempts - 1));
|
|
119
|
+
}
|
|
120
|
+
async function runManagedVerification(candidate, stamp, priorAttempts, generation) {
|
|
121
|
+
let transient = false;
|
|
122
|
+
let ok;
|
|
85
123
|
try {
|
|
86
|
-
|
|
124
|
+
const { verifyToolBinary } = await import("../../../installer/index.js");
|
|
125
|
+
ok = await verifyToolBinary(candidate, undefined, () => {
|
|
126
|
+
transient = true;
|
|
127
|
+
}, MANAGED_VERIFY_TIMEOUT_MS);
|
|
87
128
|
}
|
|
88
129
|
catch {
|
|
89
|
-
|
|
130
|
+
// The verifier itself could not run — installer-isolated unit tests mock
|
|
131
|
+
// this module without it, and a throw here says nothing about the shim.
|
|
132
|
+
// "Cannot verify" is not "broken": keep the on-disk answer.
|
|
133
|
+
transient = true;
|
|
134
|
+
ok = false;
|
|
135
|
+
}
|
|
136
|
+
// An unspawnable prober is never a durable verdict (#1569): a timeout or a
|
|
137
|
+
// spawn-boundary failure says nothing about the shim, so it never demotes
|
|
138
|
+
// the candidate. It IS remembered, under a cooldown, so the wait is paid
|
|
139
|
+
// once per window rather than on every resolve (#1674 review F1).
|
|
140
|
+
// A verification that straddles a session boundary answers ITS caller, but
|
|
141
|
+
// its verdict belongs to the session that asked. Writing it into the fresh
|
|
142
|
+
// session would hand the new session the old one's cooldown — the exact
|
|
143
|
+
// re-arm this state clears at `session_start` (#1674 review F5). Same guard
|
|
144
|
+
// the install seam's in-flight share uses.
|
|
145
|
+
if (!ok && transient) {
|
|
146
|
+
const attempts = priorAttempts + 1;
|
|
147
|
+
generation.guardedWrite(stamp, () => {
|
|
148
|
+
managedBinaryVerdicts.set(stamp, {
|
|
149
|
+
verdict: "unverified",
|
|
150
|
+
attempts,
|
|
151
|
+
retryAtMs: Date.now() + managedVerifyCooldownMs(attempts),
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
return "unverified";
|
|
90
155
|
}
|
|
156
|
+
generation.guardedWrite(stamp, () => {
|
|
157
|
+
managedBinaryVerdicts.set(stamp, { verdict: ok ? "ok" : "broken" });
|
|
158
|
+
});
|
|
159
|
+
if (!ok) {
|
|
160
|
+
// Once per shim per session, at the moment the verdict is reached — the
|
|
161
|
+
// memo answers every later call, so this cannot become per-dispatch spam.
|
|
162
|
+
logSessionStart(`dispatch availability: managed shim ${candidate} exists but does not run; falling through to PATH`);
|
|
163
|
+
}
|
|
164
|
+
return ok ? "ok" : "broken";
|
|
91
165
|
}
|
|
166
|
+
async function verifyManagedCandidate(candidate) {
|
|
167
|
+
let stamp;
|
|
168
|
+
try {
|
|
169
|
+
const stat = fs.statSync(candidate);
|
|
170
|
+
stamp = `${candidate}:${stat.mtimeMs}:${stat.size}`;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return "absent";
|
|
174
|
+
}
|
|
175
|
+
const memo = managedBinaryVerdicts.get(stamp);
|
|
176
|
+
let priorAttempts = 0;
|
|
177
|
+
if (memo) {
|
|
178
|
+
if (memo.verdict !== "unverified")
|
|
179
|
+
return memo.verdict;
|
|
180
|
+
if (Date.now() < memo.retryAtMs)
|
|
181
|
+
return "unverified";
|
|
182
|
+
priorAttempts = memo.attempts;
|
|
183
|
+
}
|
|
184
|
+
const existing = managedVerifyInFlight.get(stamp);
|
|
185
|
+
if (existing)
|
|
186
|
+
return existing;
|
|
187
|
+
const generation = availabilityGeneration.capture();
|
|
188
|
+
const probe = runManagedVerification(candidate, stamp, priorAttempts, generation).finally(() => {
|
|
189
|
+
// A settling old-session probe must not evict the live entry a new
|
|
190
|
+
// session already started for the same shim (#1674 review F5).
|
|
191
|
+
generation.guardedWrite(stamp, () => {
|
|
192
|
+
if (managedVerifyInFlight.get(stamp) === probe) {
|
|
193
|
+
managedVerifyInFlight.delete(stamp);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
managedVerifyInFlight.set(stamp, probe);
|
|
198
|
+
return probe;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* The managed shim for a Node CLI tool (`~/.pi-lens/tools/node_modules/.bin/<tool>`),
|
|
202
|
+
* or null when no candidate on disk actually runs.
|
|
203
|
+
*
|
|
204
|
+
* Verification uses the installer's own `verifyToolBinary`, the same check its
|
|
205
|
+
* managed-first branch runs before returning a managed path. A bare
|
|
206
|
+
* `existsSync` let a broken shim shadow a working PATH binary — the on-disk
|
|
207
|
+
* file wins the race and then fails every spawn behind it (#1657). knip and
|
|
208
|
+
* jscpd each carried a line-for-line copy of this resolver; #1476 folds them
|
|
209
|
+
* into one definition so the next managed tool inherits the fast path instead
|
|
210
|
+
* of a fourth copy.
|
|
211
|
+
*
|
|
212
|
+
* A settled verdict is memoized per file identity for the session, so the fast
|
|
213
|
+
* path answers without a spawn after the first call (#1467). A probe that
|
|
214
|
+
* cannot run keeps the optimistic answer rather than turning a stall into a
|
|
215
|
+
* "missing tool", and is memoized under a bounded cooldown ladder so the wait
|
|
216
|
+
* is paid once per window instead of on every resolve — while a shim that
|
|
217
|
+
* stalls on a cold cache still gets re-probed later (#1674 review F1).
|
|
218
|
+
* Concurrent first touches share one probe (#1674 review F2).
|
|
219
|
+
*/
|
|
220
|
+
export async function findManagedNodeToolBinary(tool) {
|
|
221
|
+
for (const candidate of managedNodeToolCandidates(tool)) {
|
|
222
|
+
const verdict = await verifyManagedCandidate(candidate);
|
|
223
|
+
if (verdict === "ok" || verdict === "unverified")
|
|
224
|
+
return candidate;
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
// =============================================================================
|
|
229
|
+
// VENV-AWARE COMMAND FINDER
|
|
230
|
+
// =============================================================================
|
|
92
231
|
/**
|
|
93
232
|
* Find a command in venv first, then fall back to global.
|
|
94
233
|
* Checks common venv locations (.venv, venv) before trying global.
|
|
234
|
+
*
|
|
235
|
+
* The resolved path is returned verbatim: every spawn consumer runs with
|
|
236
|
+
* `shell: false` (safe-spawn, #817), so wrapping it in quotes would make it
|
|
237
|
+
* a literal filename that ENOENTs on every platform (#1508).
|
|
95
238
|
*/
|
|
96
|
-
export function createVenvFinder(command, windowsExt = ""
|
|
97
|
-
return (cwd) => {
|
|
239
|
+
export function createVenvFinder(command, windowsExt = "") {
|
|
240
|
+
return async (cwd) => {
|
|
98
241
|
const venvPaths = [
|
|
99
242
|
`.venv/bin/${command}`,
|
|
100
243
|
`venv/bin/${command}`,
|
|
@@ -104,9 +247,22 @@ export function createVenvFinder(command, windowsExt = "", quoteWindows = false)
|
|
|
104
247
|
for (const venvPath of venvPaths) {
|
|
105
248
|
const fullPath = path.join(cwd, venvPath);
|
|
106
249
|
if (fs.existsSync(fullPath)) {
|
|
107
|
-
return
|
|
250
|
+
return fullPath;
|
|
108
251
|
}
|
|
109
252
|
}
|
|
253
|
+
// Managed-dir install (~/.pi-lens/tools/node_modules/.bin/<command>) — the
|
|
254
|
+
// same shim `ensureTool()` installs npm-strategy tools into. Checked BEFORE
|
|
255
|
+
// the bare-name PATH fallback (#1638): without this, a tool that only
|
|
256
|
+
// exists in the managed dir never resolves here, so `resolveAvailableOrInstall`
|
|
257
|
+
// clears the checker's cache on every install recovery for nothing — the
|
|
258
|
+
// very next probe misses PATH again, spawns a doomed `--version` process,
|
|
259
|
+
// and only THEN falls through to `ensureTool`'s own cache. One managed-dir
|
|
260
|
+
// check settles it without a spawn — after one verification per shim per
|
|
261
|
+
// session, so a shim that cannot run falls through to PATH instead of
|
|
262
|
+
// shadowing a working binary (#1657).
|
|
263
|
+
const managed = await findManagedNodeToolBinary(command);
|
|
264
|
+
if (managed)
|
|
265
|
+
return managed;
|
|
110
266
|
// Fall back to global
|
|
111
267
|
return command;
|
|
112
268
|
};
|
|
@@ -171,10 +327,44 @@ export async function discoverManagedTool(toolId) {
|
|
|
171
327
|
// install must not become an install attempt on every eligible file/turn.
|
|
172
328
|
const installAttemptsByCwd = new PathKeyedMap(normalizeMapKey);
|
|
173
329
|
const resolveInstallInFlightByCwd = new PathKeyedMap(normalizeEphemeralMapKey);
|
|
330
|
+
// Which (cwd, toolId) pairs already got their ONE compensating `available`
|
|
331
|
+
// row this session for a GENUINE correction (#1612 review F2).
|
|
332
|
+
// `checker.reset()` clears the checker's own cache on every install recovery,
|
|
333
|
+
// so the NEXT call re-probes PATH from scratch — and that probe keeps missing
|
|
334
|
+
// for a managed-dir-only install (createVenvFinder never searches the managed
|
|
335
|
+
// tools dir; tracked separately as its own bug). Without this memo, every one
|
|
336
|
+
// of those repeat probe failures looks like a fresh correction and re-logs
|
|
337
|
+
// "available", even though only the first call actually ran an install.
|
|
338
|
+
//
|
|
339
|
+
// A correction is genuine only when a latched `unavailable` row for the same
|
|
340
|
+
// pair stood before it. `verifyOrInstallCommand` also emits through this seam
|
|
341
|
+
// with NO prior latch behind it (biome-check and oxlint reach it with no
|
|
342
|
+
// checker probe at all), and burning this memo on such a row silences the
|
|
343
|
+
// LATER genuine latch-then-recover for the same pair — the #1606 defect back
|
|
344
|
+
// through the #1612 seam (#1657). Those rows dedupe against
|
|
345
|
+
// `uncorrectedEmissionsByCwd` instead, so repeats stay suppressed without
|
|
346
|
+
// pre-empting the real correction.
|
|
347
|
+
const correctedAvailabilityByCwd = new PathKeyedMap(normalizeMapKey);
|
|
348
|
+
// Which (cwd, toolId) pairs already emitted an `available` row that corrected
|
|
349
|
+
// nothing (no latched `unavailable` stood before it). Same one-row-per-session
|
|
350
|
+
// bound as the memo above, kept in a SEPARATE scope so it cannot answer for a
|
|
351
|
+
// correction that has not happened yet (#1657).
|
|
352
|
+
const uncorrectedEmissionsByCwd = new PathKeyedMap(normalizeMapKey);
|
|
353
|
+
// Which (cwd, tool) pairs currently stand latched `unavailable` in the durable
|
|
354
|
+
// log. Recorded where such a row is actually written, so "was there anything
|
|
355
|
+
// to correct" is read from the log's own history rather than assumed by the
|
|
356
|
+
// caller. Session-scoped: `resetDispatchAvailabilityState` clears it, so a new
|
|
357
|
+
// session re-arms instead of inheriting the last one's latches (#1657).
|
|
358
|
+
const latchedUnavailableByCwd = new PathKeyedMap(normalizeMapKey);
|
|
174
359
|
// Checkers are created by runner modules and may also be created dynamically.
|
|
175
360
|
// Keep the session reset as a generation rather than retaining every checker
|
|
176
361
|
// reset closure forever.
|
|
177
|
-
|
|
362
|
+
//
|
|
363
|
+
// #1754: the counter itself is now the shared `GenerationSource` primitive, so
|
|
364
|
+
// the capture-before-await/check-after guards below are one implementation
|
|
365
|
+
// instead of five hand-rolled ones, and a dropped straddling write is visible
|
|
366
|
+
// in the degradation ledger instead of silent.
|
|
367
|
+
const availabilityGeneration = createGenerationSource("dispatch-availability");
|
|
178
368
|
function installStateFor(cwd, toolId) {
|
|
179
369
|
let states = installAttemptsByCwd.get(cwd);
|
|
180
370
|
if (!states) {
|
|
@@ -200,12 +390,213 @@ function noteInstallSuccess(toolId, cwd) {
|
|
|
200
390
|
if (states?.size === 0)
|
|
201
391
|
installAttemptsByCwd.delete(cwd);
|
|
202
392
|
}
|
|
393
|
+
function hasId(map, cwd, id) {
|
|
394
|
+
return map.get(cwd)?.has(id) ?? false;
|
|
395
|
+
}
|
|
396
|
+
function addId(map, cwd, id) {
|
|
397
|
+
let ids = map.get(cwd);
|
|
398
|
+
if (!ids) {
|
|
399
|
+
ids = new Set();
|
|
400
|
+
map.set(cwd, ids);
|
|
401
|
+
}
|
|
402
|
+
ids.add(id);
|
|
403
|
+
}
|
|
404
|
+
function deleteIds(map, cwd, ids) {
|
|
405
|
+
const existing = map.get(cwd);
|
|
406
|
+
if (!existing)
|
|
407
|
+
return;
|
|
408
|
+
for (const id of ids)
|
|
409
|
+
existing.delete(id);
|
|
410
|
+
if (existing.size === 0)
|
|
411
|
+
map.delete(cwd);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* True once a compensating `available` row has already fired for this
|
|
415
|
+
* (cwd, toolId) pair this session; `noteAvailabilityCorrected` records it the
|
|
416
|
+
* first time. #1612 review F2's "once per correction" — see
|
|
417
|
+
* `correctedAvailabilityByCwd`'s comment for why repeats need suppressing.
|
|
418
|
+
*/
|
|
419
|
+
function hasCorrectedAvailability(cwd, toolId) {
|
|
420
|
+
return hasId(correctedAvailabilityByCwd, cwd, toolId);
|
|
421
|
+
}
|
|
422
|
+
function noteAvailabilityCorrected(cwd, toolId) {
|
|
423
|
+
addId(correctedAvailabilityByCwd, cwd, toolId);
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Record that a latched `unavailable` row now stands for this pair. Called
|
|
427
|
+
* from each seam that writes one (the availability checker and the shared
|
|
428
|
+
* probe latch), so a later compensating row can tell a genuine correction
|
|
429
|
+
* from a no-op emission (#1657).
|
|
430
|
+
*/
|
|
431
|
+
function noteLatchedUnavailable(cwd, tool) {
|
|
432
|
+
addId(latchedUnavailableByCwd, cwd, tool);
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Drop the latch record for a pair, because the row it described no longer
|
|
436
|
+
* stands. Called on every `available` verdict as well as on the compensating
|
|
437
|
+
* row itself, so a latch cannot outlive the state it describes and later back
|
|
438
|
+
* a "correction" that corrects nothing (#1674 review F3).
|
|
439
|
+
*/
|
|
440
|
+
function clearLatchedUnavailable(cwd, ...tools) {
|
|
441
|
+
deleteIds(latchedUnavailableByCwd, cwd, tools);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Every name a latched row for this pair could have been written under. A
|
|
445
|
+
* checker logs its COMMAND (sometimes a resolved absolute path), the install
|
|
446
|
+
* seam knows a toolId, and the two are not always the same string. One
|
|
447
|
+
* derivation feeds BOTH the lookup and the clear, so the two can never
|
|
448
|
+
* disagree about which key the latch lives under (#1674 review F3).
|
|
449
|
+
*/
|
|
450
|
+
function latchKeysFor(toolId, command) {
|
|
451
|
+
const keys = [toolId];
|
|
452
|
+
if (command)
|
|
453
|
+
keys.push(command, path.basename(command));
|
|
454
|
+
return keys;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* True when a latched `unavailable` row stands for any of these keys. A false
|
|
458
|
+
* positive only restores the pre-#1657 behavior (the row emits and burns the
|
|
459
|
+
* correction memo); a false negative still emits the row, under the
|
|
460
|
+
* uncorrected scope. Neither can swallow a genuine correction.
|
|
461
|
+
*/
|
|
462
|
+
function hasLatchedUnavailable(cwd, tools) {
|
|
463
|
+
return tools.some((tool) => hasId(latchedUnavailableByCwd, cwd, tool));
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Which installer family resolved a tool, for the compensating row's
|
|
467
|
+
* `evidence.source` (#1612 review F1). Reads the registry's own
|
|
468
|
+
* `installStrategy` rather than hand-mapping per toolId — the two cannot
|
|
469
|
+
* drift apart because there is only one list.
|
|
470
|
+
*/
|
|
471
|
+
function sourceTagForToolId(toolId) {
|
|
472
|
+
switch (getToolInstallStrategy(toolId)) {
|
|
473
|
+
case "npm":
|
|
474
|
+
return "managed-dir";
|
|
475
|
+
case "pip":
|
|
476
|
+
return "pip-user";
|
|
477
|
+
case "github":
|
|
478
|
+
return "github-release";
|
|
479
|
+
case "archive":
|
|
480
|
+
return "archive-dist";
|
|
481
|
+
case "maven":
|
|
482
|
+
return "maven-jar";
|
|
483
|
+
default:
|
|
484
|
+
// "gem" has no CLI runner behind this seam yet, and an unknown toolId
|
|
485
|
+
// means the registry lookup itself missed — either way, no tag beats
|
|
486
|
+
// a guessed one.
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* How to tag a not-fresh install resolution in the compensating row's
|
|
492
|
+
* evidence (#1636 review, carried over from #1615's verify). `getInstallAttempt`
|
|
493
|
+
* answers `undefined` for BOTH a genuine cache hit and a plain PATH/managed-dir
|
|
494
|
+
* discovery, and answers `declined`/`skipped` for a policy refusal that still
|
|
495
|
+
* hands back whatever discovery found — three different facts a single
|
|
496
|
+
* `"cache"` tag used to flatten into one. `declined`/`skipped` take priority
|
|
497
|
+
* over the source map: a project-trust decline overwrites whatever the
|
|
498
|
+
* discovery pass underneath it found (see `ensureTool`'s trust-gate branch in
|
|
499
|
+
* installer/index.ts), so by the time this reads it, "declined" is the honest
|
|
500
|
+
* answer regardless of how the binary was actually found.
|
|
501
|
+
*/
|
|
502
|
+
function resolvedTagForAttempt(toolId, attempt) {
|
|
503
|
+
if (attempt?.outcome === "declined" || attempt?.outcome === "skipped") {
|
|
504
|
+
return "declined";
|
|
505
|
+
}
|
|
506
|
+
return getLastEnsureResolutionSource(toolId) === "path" ? "path" : "cache";
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Emit the ONE compensating `available` row for a (cwd, toolId) correction,
|
|
510
|
+
* after a latched unavailable probe recovers via `ensureTool`. This is the
|
|
511
|
+
* SINGLE row constructor for every caller behind this seam —
|
|
512
|
+
* `resolveAvailableOrInstallUnshared` (#1612) and `verifyOrInstallCommand`
|
|
513
|
+
* (#1636) both call it rather than each hand-rolling their own, which is
|
|
514
|
+
* exactly the drift #1610's own review caught the first time this shape
|
|
515
|
+
* shipped twice.
|
|
516
|
+
*
|
|
517
|
+
* Gated on the once-per-correction memo (`correctedAvailabilityByCwd`,
|
|
518
|
+
* #1612 review F2): `checker.reset()` / a cleared probe cache means the NEXT
|
|
519
|
+
* call for the same (cwd, toolId) can resolve the same tool again with no
|
|
520
|
+
* fresh attempt behind it, and that must not re-log a "fresh correction"
|
|
521
|
+
* every time.
|
|
522
|
+
*
|
|
523
|
+
* `latchKeys` are every name a latched `unavailable` row for this pair could
|
|
524
|
+
* carry. A row that clears one of them is a genuine correction and burns the
|
|
525
|
+
* correction memo; a row with nothing to correct dedupes in its own scope, so
|
|
526
|
+
* it cannot silence the genuine latch-then-recover that follows it for the
|
|
527
|
+
* same pair (#1657).
|
|
528
|
+
*/
|
|
529
|
+
function emitCompensatingAvailableRow(cwd, toolId, installStartedAt, installedPath, latchKeys) {
|
|
530
|
+
if (hasCorrectedAvailability(cwd, toolId))
|
|
531
|
+
return;
|
|
532
|
+
const correctsLatchedRow = hasLatchedUnavailable(cwd, latchKeys);
|
|
533
|
+
if (correctsLatchedRow) {
|
|
534
|
+
noteAvailabilityCorrected(cwd, toolId);
|
|
535
|
+
// Clear every key the latch could live under, not just the toolId: the
|
|
536
|
+
// checker records the row under its COMMAND, and a leftover entry would
|
|
537
|
+
// later back a "correction" with nothing behind it (#1674 review F3).
|
|
538
|
+
clearLatchedUnavailable(cwd, ...latchKeys);
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
if (hasId(uncorrectedEmissionsByCwd, cwd, toolId))
|
|
542
|
+
return;
|
|
543
|
+
addId(uncorrectedEmissionsByCwd, cwd, toolId);
|
|
544
|
+
}
|
|
545
|
+
// `installed`/`installedPath` can come back truthy WITHOUT ensureTool
|
|
546
|
+
// running an install this call — its in-memory cache, its on-disk probe
|
|
547
|
+
// cache, getToolPath finding it already present, or a concurrent peer's
|
|
548
|
+
// install all return a path with no fresh attempt recorded. Deriving from
|
|
549
|
+
// `getInstallAttempt` (rather than asserting "succeeded" because the path
|
|
550
|
+
// is truthy, or hand-rolling a second mapping) is what #1610's OWN
|
|
551
|
+
// failure-path mirror bug was caught on review; read the REAL outcome
|
|
552
|
+
// through the one function that already knows how, don't assume it. Hard
|
|
553
|
+
// import, no `typeof` guard (#1636 review): a genuinely removed export
|
|
554
|
+
// must throw here, not silently read as "not attempted".
|
|
555
|
+
const attempt = getInstallAttempt(toolId);
|
|
556
|
+
const installEvidence = describeInstallAttempt(attempt);
|
|
557
|
+
const evidence = {
|
|
558
|
+
...installEvidence,
|
|
559
|
+
binary: path.basename(installedPath),
|
|
560
|
+
source: sourceTagForToolId(toolId),
|
|
561
|
+
// Set alongside any non-"succeeded" outcome: the path is truthy but
|
|
562
|
+
// nothing ran this call, so it came from an already-known-good answer
|
|
563
|
+
// rather than a fresh attempt (#1612 review F2).
|
|
564
|
+
...(installEvidence.install !== "succeeded" && {
|
|
565
|
+
resolved: resolvedTagForAttempt(toolId, attempt),
|
|
566
|
+
}),
|
|
567
|
+
// The whole point of the memo split, on the record: a reader can tell a
|
|
568
|
+
// row that cleared a latched `unavailable` from one that corrected
|
|
569
|
+
// nothing, without re-deriving it from the surrounding rows (#1674
|
|
570
|
+
// review F4).
|
|
571
|
+
correctsLatchedRow,
|
|
572
|
+
};
|
|
573
|
+
logAvailabilityDecision({
|
|
574
|
+
tool: toolId,
|
|
575
|
+
verdict: "available",
|
|
576
|
+
outcome: "success",
|
|
577
|
+
cause: "ok",
|
|
578
|
+
elapsedMs: Date.now() - installStartedAt,
|
|
579
|
+
// `checker.reset()`/a fresh probe just cleared whatever cache stood
|
|
580
|
+
// behind this call, so nothing is actually held pinned the instant
|
|
581
|
+
// this row is written — the very next probe starts fresh
|
|
582
|
+
// (#1612 review F3). Writing `true` here would claim a durability
|
|
583
|
+
// this call doesn't have.
|
|
584
|
+
latched: false,
|
|
585
|
+
classifiedBy: "caller",
|
|
586
|
+
evidence,
|
|
587
|
+
}, cwd);
|
|
588
|
+
}
|
|
203
589
|
/** Reset availability/install suppression at the session boundary. */
|
|
204
590
|
export function resetDispatchAvailabilityState() {
|
|
205
591
|
installAttemptsByCwd.clear();
|
|
206
592
|
resolveInstallInFlightByCwd.clear();
|
|
593
|
+
correctedAvailabilityByCwd.clear();
|
|
594
|
+
uncorrectedEmissionsByCwd.clear();
|
|
595
|
+
latchedUnavailableByCwd.clear();
|
|
596
|
+
managedBinaryVerdicts.clear();
|
|
597
|
+
managedVerifyInFlight.clear();
|
|
207
598
|
resetPathWalkMemo();
|
|
208
|
-
|
|
599
|
+
availabilityGeneration.bump();
|
|
209
600
|
}
|
|
210
601
|
/**
|
|
211
602
|
* Create a cached availability checker for a command.
|
|
@@ -227,19 +618,19 @@ export function resetDispatchAvailabilityState() {
|
|
|
227
618
|
export function createAvailabilityChecker(command, windowsExt = "", versionArgs = ["--version"], options = {}) {
|
|
228
619
|
const cacheByCwd = new PathKeyedMap(normalizeEphemeralMapKey);
|
|
229
620
|
const inFlightByCwd = new PathKeyedMap(normalizeEphemeralMapKey);
|
|
230
|
-
let checkerGeneration =
|
|
231
|
-
const findCommand = createVenvFinder(command, windowsExt
|
|
621
|
+
let checkerGeneration = availabilityGeneration.current();
|
|
622
|
+
const findCommand = createVenvFinder(command, windowsExt);
|
|
232
623
|
function ensureCurrentGeneration() {
|
|
233
|
-
if (checkerGeneration ===
|
|
624
|
+
if (checkerGeneration === availabilityGeneration.current())
|
|
234
625
|
return;
|
|
235
626
|
cacheByCwd.clear();
|
|
236
627
|
inFlightByCwd.clear();
|
|
237
|
-
checkerGeneration =
|
|
628
|
+
checkerGeneration = availabilityGeneration.current();
|
|
238
629
|
}
|
|
239
630
|
const reset = () => {
|
|
240
631
|
cacheByCwd.clear();
|
|
241
632
|
inFlightByCwd.clear();
|
|
242
|
-
checkerGeneration =
|
|
633
|
+
checkerGeneration = availabilityGeneration.current();
|
|
243
634
|
};
|
|
244
635
|
function getCache(cwd) {
|
|
245
636
|
ensureCurrentGeneration();
|
|
@@ -279,13 +670,31 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
279
670
|
retryAfterMs = transientRetryDelayMs(cache.transientAttempts, verdict.cause);
|
|
280
671
|
cache.retryAtMs = Date.now() + retryAfterMs;
|
|
281
672
|
}
|
|
673
|
+
const latched = verdict.available || isLatchingOutcome(verdict.outcome);
|
|
674
|
+
// Remember the pair whose durable row now says "unavailable and staying
|
|
675
|
+
// that way" — that is the row a later compensating `available` row
|
|
676
|
+
// corrects, and the only thing that makes such a row a correction
|
|
677
|
+
// (#1657).
|
|
678
|
+
if (!verdict.available && latched) {
|
|
679
|
+
noteLatchedUnavailable(resolvedCwd, command);
|
|
680
|
+
}
|
|
681
|
+
else if (verdict.available) {
|
|
682
|
+
// The latched row this pair carried no longer stands: THIS row says
|
|
683
|
+
// the tool is back. Dropping the record here keeps a stale latch from
|
|
684
|
+
// backing a later "correction" that corrects nothing (#1674 F3).
|
|
685
|
+
clearLatchedUnavailable(resolvedCwd, ...latchKeysFor(command));
|
|
686
|
+
}
|
|
282
687
|
logAvailabilityDecision({
|
|
283
688
|
tool: command,
|
|
284
689
|
verdict: verdict.available ? "available" : "unavailable",
|
|
285
690
|
outcome: verdict.outcome,
|
|
286
691
|
cause: verdict.cause,
|
|
287
692
|
elapsedMs: verdict.elapsedMs,
|
|
288
|
-
latched
|
|
693
|
+
latched,
|
|
694
|
+
...(verdict.classifiedBy !== undefined && {
|
|
695
|
+
classifiedBy: verdict.classifiedBy,
|
|
696
|
+
}),
|
|
697
|
+
...(verdict.evidence !== undefined && { evidence: verdict.evidence }),
|
|
289
698
|
...(verdict.hostStallMs !== undefined && {
|
|
290
699
|
hostStallMs: verdict.hostStallMs,
|
|
291
700
|
}),
|
|
@@ -325,7 +734,7 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
325
734
|
const promiseGeneration = checkerGeneration;
|
|
326
735
|
let promise;
|
|
327
736
|
promise = (async () => {
|
|
328
|
-
const fastPath = options.fastPath?.();
|
|
737
|
+
const fastPath = await options.fastPath?.();
|
|
329
738
|
if (fastPath) {
|
|
330
739
|
cache.command = fastPath;
|
|
331
740
|
noteDecision(cache, resolvedCwd, {
|
|
@@ -333,6 +742,9 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
333
742
|
outcome: "success",
|
|
334
743
|
cause: "fast-path",
|
|
335
744
|
elapsedMs: 0,
|
|
745
|
+
// Caller-asserted, and justifiably: an on-disk shim IS the tool, so
|
|
746
|
+
// there is no spawn to derive anything from (#1500).
|
|
747
|
+
classifiedBy: "caller",
|
|
336
748
|
});
|
|
337
749
|
return true;
|
|
338
750
|
}
|
|
@@ -347,6 +759,9 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
347
759
|
outcome: "non-installable",
|
|
348
760
|
cause: "bad-cwd",
|
|
349
761
|
elapsedMs: 0,
|
|
762
|
+
// The workspace is gone. Asserted from a stat, not a probe, and it
|
|
763
|
+
// must never read as "the tool is missing" (#1500).
|
|
764
|
+
classifiedBy: "caller",
|
|
350
765
|
});
|
|
351
766
|
return false;
|
|
352
767
|
}
|
|
@@ -357,10 +772,11 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
357
772
|
outcome: "non-installable",
|
|
358
773
|
cause: "bad-cwd",
|
|
359
774
|
elapsedMs: 0,
|
|
775
|
+
classifiedBy: "caller",
|
|
360
776
|
});
|
|
361
777
|
return false;
|
|
362
778
|
}
|
|
363
|
-
const cmd = findCommand(resolvedCwd);
|
|
779
|
+
const cmd = await findCommand(resolvedCwd);
|
|
364
780
|
const env = await options.environment?.(resolvedCwd);
|
|
365
781
|
// The probe budget is enforced by a HOST-side timer, so host event-loop
|
|
366
782
|
// stalls are charged to the child. Measure the stall that overlapped the
|
|
@@ -388,10 +804,12 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
388
804
|
cause: "ok",
|
|
389
805
|
elapsedMs,
|
|
390
806
|
hostStallMs,
|
|
807
|
+
classifiedBy: "probe",
|
|
808
|
+
evidence: describeProbeEvidence(result),
|
|
391
809
|
});
|
|
392
810
|
return true;
|
|
393
811
|
}
|
|
394
|
-
const { outcome, cause } = classifyProbeFailure(result, {
|
|
812
|
+
const { outcome, cause, evidence } = classifyProbeFailure(result, {
|
|
395
813
|
hostStallMs,
|
|
396
814
|
unclassifiedFailureOutcome: options.unclassifiedFailureOutcome,
|
|
397
815
|
});
|
|
@@ -406,6 +824,8 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
406
824
|
cause,
|
|
407
825
|
elapsedMs,
|
|
408
826
|
hostStallMs,
|
|
827
|
+
classifiedBy: "probe",
|
|
828
|
+
evidence,
|
|
409
829
|
});
|
|
410
830
|
return false;
|
|
411
831
|
})().finally(() => {
|
|
@@ -444,32 +864,173 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
444
864
|
}
|
|
445
865
|
/**
|
|
446
866
|
* Per-cwd cached availability probe for spawn signatures that don't fit
|
|
447
|
-
* `createAvailabilityChecker` — multi-arg subcommands like `
|
|
448
|
-
* --version
|
|
449
|
-
*
|
|
450
|
-
* concurrent first-time callers share the
|
|
867
|
+
* `createAvailabilityChecker` — multi-arg subcommands like `cargo clippy
|
|
868
|
+
* --version` and `mix credo --version`, or a dynamically-resolved
|
|
869
|
+
* `<cmd> --version` such as eslint's local-then-global binary. Each cwd is
|
|
870
|
+
* probed at most once per verdict; concurrent first-time callers share the
|
|
871
|
+
* in-flight promise.
|
|
872
|
+
*
|
|
873
|
+
* Those three ARE the consumers. `npx biome --version` used to be named here as
|
|
874
|
+
* one and never was: biome resolves through `resolveManagedToolClient` and its
|
|
875
|
+
* own latch in `biome-client.ts`.
|
|
451
876
|
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
877
|
+
* ## Latch policy (#1494)
|
|
878
|
+
*
|
|
879
|
+
* This used to cache the boolean forever, so one stalled `eslint --version`
|
|
880
|
+
* disabled eslint for that cwd until the process restarted. The verdict now
|
|
881
|
+
* goes through `availability-policy.ts`: the probe returns the SPAWN RESULT,
|
|
882
|
+
* the policy classifies it, and only a durable outcome (`missing` /
|
|
883
|
+
* `non-installable`) latches. A timeout, abort or host stall expires on a
|
|
884
|
+
* bounded cooldown and the next caller re-probes.
|
|
456
885
|
*/
|
|
457
|
-
export function createCwdCachedProbe(probe) {
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
886
|
+
export function createCwdCachedProbe(probe, options) {
|
|
887
|
+
const latchByCwd = new PathKeyedMap(normalizeEphemeralMapKey);
|
|
888
|
+
const inFlightByCwd = new PathKeyedMap(normalizeEphemeralMapKey);
|
|
889
|
+
let probeGeneration = availabilityGeneration.current();
|
|
890
|
+
function clear() {
|
|
891
|
+
latchByCwd.clear();
|
|
892
|
+
inFlightByCwd.clear();
|
|
893
|
+
}
|
|
894
|
+
function ensureCurrentGeneration() {
|
|
895
|
+
if (probeGeneration === availabilityGeneration.current())
|
|
896
|
+
return;
|
|
897
|
+
clear();
|
|
898
|
+
probeGeneration = availabilityGeneration.current();
|
|
899
|
+
}
|
|
900
|
+
function latchFor(key) {
|
|
901
|
+
const existing = latchByCwd.get(key);
|
|
902
|
+
if (existing)
|
|
903
|
+
return existing;
|
|
904
|
+
const created = createAvailabilityLatch();
|
|
905
|
+
latchByCwd.set(key, created);
|
|
906
|
+
return created;
|
|
907
|
+
}
|
|
908
|
+
function note(latch, key, verdict) {
|
|
909
|
+
let retryAfterMs;
|
|
910
|
+
if (verdict.available) {
|
|
911
|
+
latch.noteAvailable(verdict.cause);
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
const delay = latch.noteUnavailable(verdict.outcome, verdict.cause);
|
|
915
|
+
if (delay > 0)
|
|
916
|
+
retryAfterMs = delay;
|
|
917
|
+
}
|
|
918
|
+
const latched = verdict.available || isLatchingOutcome(verdict.outcome);
|
|
919
|
+
// Same correction bookkeeping as the checker seam above (#1657).
|
|
920
|
+
if (!verdict.available && latched) {
|
|
921
|
+
noteLatchedUnavailable(key, options.tool);
|
|
464
922
|
}
|
|
923
|
+
else if (verdict.available) {
|
|
924
|
+
clearLatchedUnavailable(key, ...latchKeysFor(options.tool));
|
|
925
|
+
}
|
|
926
|
+
logAvailabilityDecision({
|
|
927
|
+
tool: options.tool,
|
|
928
|
+
verdict: verdict.available ? "available" : "unavailable",
|
|
929
|
+
outcome: verdict.outcome,
|
|
930
|
+
cause: verdict.cause,
|
|
931
|
+
elapsedMs: verdict.elapsedMs,
|
|
932
|
+
latched,
|
|
933
|
+
hostStallMs: verdict.hostStallMs,
|
|
934
|
+
...(retryAfterMs !== undefined && { retryAfterMs }),
|
|
935
|
+
...(options.budgetMs !== undefined && { budgetMs: options.budgetMs }),
|
|
936
|
+
// Every verdict here is derived from the probe this seam just ran.
|
|
937
|
+
classifiedBy: "probe",
|
|
938
|
+
...(verdict.evidence !== undefined && { evidence: verdict.evidence }),
|
|
939
|
+
}, key);
|
|
940
|
+
}
|
|
941
|
+
const run = (cwd) => {
|
|
942
|
+
ensureCurrentGeneration();
|
|
465
943
|
const key = path.resolve(cwd || process.cwd());
|
|
466
|
-
const
|
|
944
|
+
const latch = latchFor(key);
|
|
945
|
+
const memo = latch.read();
|
|
946
|
+
if (memo !== null)
|
|
947
|
+
return Promise.resolve(memo);
|
|
948
|
+
const existing = inFlightByCwd.get(key);
|
|
467
949
|
if (existing)
|
|
468
950
|
return existing;
|
|
469
|
-
const
|
|
470
|
-
|
|
951
|
+
const promiseGeneration = probeGeneration;
|
|
952
|
+
let promise;
|
|
953
|
+
promise = (async () => {
|
|
954
|
+
// The probe budget is enforced by a HOST-side timer, so a stalled event
|
|
955
|
+
// loop expires it while the child is still healthy. Measure the stall
|
|
956
|
+
// that overlapped the window and hand it to the classifier (#1467).
|
|
957
|
+
const stallSampler = startHostStallSampler();
|
|
958
|
+
const startedAt = Date.now();
|
|
959
|
+
let result;
|
|
960
|
+
let thrown;
|
|
961
|
+
try {
|
|
962
|
+
result = await probe(key);
|
|
963
|
+
}
|
|
964
|
+
catch (error) {
|
|
965
|
+
thrown = error;
|
|
966
|
+
}
|
|
967
|
+
const hostStallMs = stallSampler.stop();
|
|
968
|
+
const elapsedMs = Date.now() - startedAt;
|
|
969
|
+
// A probe that threw carries its errno in the Error, which is exactly
|
|
970
|
+
// what the taxonomy reads — so a thrown EAGAIN stays transient instead
|
|
971
|
+
// of collapsing into an untyped `false`.
|
|
972
|
+
const shape = result ?? {
|
|
973
|
+
error: thrown instanceof Error ? thrown : new Error(String(thrown)),
|
|
974
|
+
};
|
|
975
|
+
if (result && !result.error && result.status === 0) {
|
|
976
|
+
note(latch, key, {
|
|
977
|
+
available: true,
|
|
978
|
+
outcome: "success",
|
|
979
|
+
cause: "ok",
|
|
980
|
+
elapsedMs,
|
|
981
|
+
hostStallMs,
|
|
982
|
+
evidence: describeProbeEvidence(result, options.tool),
|
|
983
|
+
});
|
|
984
|
+
return true;
|
|
985
|
+
}
|
|
986
|
+
const { outcome, cause, evidence } = classifyProbeFailure(shape, {
|
|
987
|
+
hostStallMs,
|
|
988
|
+
command: options.tool,
|
|
989
|
+
unclassifiedFailureOutcome: options.unclassifiedFailureOutcome,
|
|
990
|
+
});
|
|
991
|
+
note(latch, key, {
|
|
992
|
+
available: false,
|
|
993
|
+
outcome,
|
|
994
|
+
cause,
|
|
995
|
+
elapsedMs,
|
|
996
|
+
hostStallMs,
|
|
997
|
+
evidence,
|
|
998
|
+
});
|
|
999
|
+
return false;
|
|
1000
|
+
})().finally(() => {
|
|
1001
|
+
// A session reset clears the map and a caller may immediately start a
|
|
1002
|
+
// replacement probe for the same cwd; the old promise must not delete
|
|
1003
|
+
// that newer-generation entry when it settles.
|
|
1004
|
+
if (probeGeneration === promiseGeneration &&
|
|
1005
|
+
inFlightByCwd.get(key) === promise) {
|
|
1006
|
+
inFlightByCwd.delete(key);
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
inFlightByCwd.set(key, promise);
|
|
471
1010
|
return promise;
|
|
472
1011
|
};
|
|
1012
|
+
const cached = run;
|
|
1013
|
+
cached.getVerdict = (cwd) => {
|
|
1014
|
+
ensureCurrentGeneration();
|
|
1015
|
+
const latch = latchFor(path.resolve(cwd || process.cwd()));
|
|
1016
|
+
const outcome = latch.getOutcome();
|
|
1017
|
+
return {
|
|
1018
|
+
outcome,
|
|
1019
|
+
cause: latch.getCause(),
|
|
1020
|
+
elapsedMs: 0,
|
|
1021
|
+
// Read the OUTCOME, never `read()`: `read()` answers `null` both for a
|
|
1022
|
+
// cwd that was never probed and for a transient verdict whose cooldown
|
|
1023
|
+
// expired, and `!== false` turned both of those into `latched: true` —
|
|
1024
|
+
// the opposite of the truth in the expired case.
|
|
1025
|
+
latched: outcome !== null && isLatchingOutcome(outcome),
|
|
1026
|
+
retryAtMs: latch.getRetryAtMs(),
|
|
1027
|
+
};
|
|
1028
|
+
};
|
|
1029
|
+
cached.reset = () => {
|
|
1030
|
+
clear();
|
|
1031
|
+
probeGeneration = availabilityGeneration.current();
|
|
1032
|
+
};
|
|
1033
|
+
return cached;
|
|
473
1034
|
}
|
|
474
1035
|
export function resolveNodeToolCommand(cwd, toolName, windowsExt = ".cmd") {
|
|
475
1036
|
const isWin = process.platform === "win32";
|
|
@@ -537,9 +1098,20 @@ async function verifyOrInstallCommand(command, toolId, cwd, versionArgs = ["--ve
|
|
|
537
1098
|
const state = installStateFor(cwd, toolId);
|
|
538
1099
|
if (state.suppressed)
|
|
539
1100
|
return null;
|
|
1101
|
+
const installStartedAt = Date.now();
|
|
540
1102
|
const installed = await ensureTool(toolId);
|
|
541
1103
|
if (installed) {
|
|
542
1104
|
noteInstallSuccess(toolId, cwd);
|
|
1105
|
+
// A latched `unavailable` row MAY stand here, written by whatever probe
|
|
1106
|
+
// the caller ran first — but not always. The on-disk pre-check above
|
|
1107
|
+
// logs nothing, and two runners reach this seam with no checker probe at
|
|
1108
|
+
// all (biome-check and oxlint call
|
|
1109
|
+
// `resolveToolCommandWithInstallFallback` directly, #1657). So the row is
|
|
1110
|
+
// emitted either way, and only a call that really clears a latched row is
|
|
1111
|
+
// recorded as a correction. Without the row, the durable log keeps saying
|
|
1112
|
+
// the tool is off after the installer just brought it back — the third
|
|
1113
|
+
// seam of #1606/#1610/#1612 (#1636).
|
|
1114
|
+
emitCompensatingAvailableRow(cwd, toolId, installStartedAt, installed, latchKeysFor(toolId, command));
|
|
543
1115
|
return installed;
|
|
544
1116
|
}
|
|
545
1117
|
noteInstallFailure(toolId, cwd);
|
|
@@ -578,10 +1150,36 @@ async function resolveAvailableOrInstallUnshared(checker, toolId, cwd) {
|
|
|
578
1150
|
if (state.suppressed) {
|
|
579
1151
|
return null;
|
|
580
1152
|
}
|
|
1153
|
+
const installStartedAt = Date.now();
|
|
581
1154
|
const installed = await ensureTool(toolId);
|
|
582
1155
|
if (installed) {
|
|
583
1156
|
noteInstallSuccess(toolId, cwd);
|
|
584
1157
|
checker.reset?.();
|
|
1158
|
+
// The PATH probe above already wrote a latched `unavailable` row via
|
|
1159
|
+
// `checker.isAvailableAsync`. Without a compensating row here, the durable
|
|
1160
|
+
// log keeps saying the tool is off after the installer just brought it
|
|
1161
|
+
// back — the shape #1606/PR #1610 fixed in `ensureViaInstaller`, shared
|
|
1162
|
+
// here by ~16 runners behind this helper (#1612), and by
|
|
1163
|
+
// `verifyOrInstallCommand` for the third seam (#1636).
|
|
1164
|
+
//
|
|
1165
|
+
// `checker.reset()` above clears the checker's cache, so the NEXT call
|
|
1166
|
+
// re-probes PATH from scratch — and for a managed-dir-only install that
|
|
1167
|
+
// probe misses again unless `createVenvFinder` finds it first (#1638).
|
|
1168
|
+
// Left unguarded, every one of those repeat misses would re-fire this
|
|
1169
|
+
// row, claiming a fresh install each time — `emitCompensatingAvailableRow`
|
|
1170
|
+
// gates on the once-per-correction memo to prevent that.
|
|
1171
|
+
//
|
|
1172
|
+
// The probe above returned the latching `missing` outcome, so a latched
|
|
1173
|
+
// `unavailable` row stands for this pair right now — recorded under the
|
|
1174
|
+
// checker's own command name. Record it under the toolId too, so the
|
|
1175
|
+
// emitter can READ that a correction is happening from the same ledger
|
|
1176
|
+
// every other caller reads, instead of being told so by an argument
|
|
1177
|
+
// (#1674 review F3).
|
|
1178
|
+
noteLatchedUnavailable(cwd, toolId);
|
|
1179
|
+
// This IS a genuine correction: the probe above returned the latching
|
|
1180
|
+
// `missing` outcome, so a latched `unavailable` row stands for this pair
|
|
1181
|
+
// right now (#1657).
|
|
1182
|
+
emitCompensatingAvailableRow(cwd, toolId, installStartedAt, installed, latchKeysFor(toolId, checker.getCommand(cwd) ?? undefined));
|
|
585
1183
|
return installed;
|
|
586
1184
|
}
|
|
587
1185
|
noteInstallFailure(toolId, cwd);
|
|
@@ -598,16 +1196,19 @@ export function resolveAvailableOrInstall(checker, toolId, cwd) {
|
|
|
598
1196
|
const existing = byTool.get(toolId);
|
|
599
1197
|
if (existing)
|
|
600
1198
|
return existing;
|
|
601
|
-
|
|
1199
|
+
// Same capture-before-await guard as the managed-verify seam, via the same
|
|
1200
|
+
// primitive (#1754): a settling old-session transaction must not evict the
|
|
1201
|
+
// entry a NEW session already installed for this (cwd, tool).
|
|
1202
|
+
const generation = availabilityGeneration.capture();
|
|
602
1203
|
const promise = resolveAvailableOrInstallUnshared(checker, toolId, cwd).finally(() => {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}
|
|
1204
|
+
generation.guardedWrite(`${toolId}@${key}`, () => {
|
|
1205
|
+
const current = resolveInstallInFlightByCwd.get(key);
|
|
1206
|
+
if (current?.get(toolId) === promise) {
|
|
1207
|
+
current.delete(toolId);
|
|
1208
|
+
if (current.size === 0)
|
|
1209
|
+
resolveInstallInFlightByCwd.delete(key);
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
611
1212
|
});
|
|
612
1213
|
byTool.set(toolId, promise);
|
|
613
1214
|
return promise;
|
|
@@ -628,6 +1229,26 @@ let sgCmdArgs = [];
|
|
|
628
1229
|
let sgSweepSawTransient = false;
|
|
629
1230
|
let sgSweepTransientCause = "probe-timeout";
|
|
630
1231
|
let sgSweepHostStallMs = 0;
|
|
1232
|
+
/**
|
|
1233
|
+
* Candidates that were UNREACHABLE, in ask order (#1568).
|
|
1234
|
+
*
|
|
1235
|
+
* The sweep stops at the first candidate that answers, so at the moment of a
|
|
1236
|
+
* win this list is exactly the set of candidates ahead of the winner that never
|
|
1237
|
+
* got a fair hearing — i.e. the preferred tiers the winner did not really beat.
|
|
1238
|
+
*
|
|
1239
|
+
* Basenames, because tier 1 is an absolute `node_modules/.bin` path and this
|
|
1240
|
+
* list is written to latency.log (#1568 review F3).
|
|
1241
|
+
*/
|
|
1242
|
+
let sgSweepUnreachable = [];
|
|
1243
|
+
/**
|
|
1244
|
+
* Candidates that were probed and answered DURABLY missing this sweep — a
|
|
1245
|
+
* real ENOENT/non-installable verdict, not a stall (#1593). The retained-arm
|
|
1246
|
+
* fallback below only knows the sweep saw SOME transient candidate; without
|
|
1247
|
+
* this list it cannot tell "the memoized winner itself just proved absent"
|
|
1248
|
+
* from "an unrelated sibling merely stalled", and re-serves a command this
|
|
1249
|
+
* very sweep disproved.
|
|
1250
|
+
*/
|
|
1251
|
+
let sgSweepDurablyMissing = [];
|
|
631
1252
|
function isAstGrepVersionOutput(output) {
|
|
632
1253
|
return /\bast[- ]grep\b/i.test(output);
|
|
633
1254
|
}
|
|
@@ -653,6 +1274,12 @@ async function probeAstGrepCommandAsync(cmd, argsPrefix = []) {
|
|
|
653
1274
|
if (outcome === "transient") {
|
|
654
1275
|
sgSweepSawTransient = true;
|
|
655
1276
|
sgSweepTransientCause = cause;
|
|
1277
|
+
const name = path.basename(cmd);
|
|
1278
|
+
if (!sgSweepUnreachable.includes(name))
|
|
1279
|
+
sgSweepUnreachable.push(name);
|
|
1280
|
+
}
|
|
1281
|
+
else if (!sgSweepDurablyMissing.includes(cmd)) {
|
|
1282
|
+
sgSweepDurablyMissing.push(cmd);
|
|
656
1283
|
}
|
|
657
1284
|
return false;
|
|
658
1285
|
}
|
|
@@ -684,15 +1311,15 @@ function buildSgLocalBins() {
|
|
|
684
1311
|
return bins;
|
|
685
1312
|
}
|
|
686
1313
|
let sgAvailableInFlight = null;
|
|
687
|
-
let sgAvailabilityGeneration =
|
|
1314
|
+
let sgAvailabilityGeneration = availabilityGeneration.current();
|
|
688
1315
|
function ensureCurrentSgGeneration() {
|
|
689
|
-
if (sgAvailabilityGeneration ===
|
|
1316
|
+
if (sgAvailabilityGeneration === availabilityGeneration.current())
|
|
690
1317
|
return;
|
|
691
1318
|
sgLatch.reset();
|
|
692
1319
|
sgCmd = null;
|
|
693
1320
|
sgCmdArgs = [];
|
|
694
1321
|
sgAvailableInFlight = null;
|
|
695
|
-
sgAvailabilityGeneration =
|
|
1322
|
+
sgAvailabilityGeneration = availabilityGeneration.current();
|
|
696
1323
|
}
|
|
697
1324
|
export async function isSgAvailableAsync() {
|
|
698
1325
|
ensureCurrentSgGeneration();
|
|
@@ -708,6 +1335,8 @@ export async function isSgAvailableAsync() {
|
|
|
708
1335
|
sgSweepSawTransient = false;
|
|
709
1336
|
sgSweepTransientCause = "probe-timeout";
|
|
710
1337
|
sgSweepHostStallMs = 0;
|
|
1338
|
+
sgSweepUnreachable = [];
|
|
1339
|
+
sgSweepDurablyMissing = [];
|
|
711
1340
|
// 1. Local node_modules/.bin
|
|
712
1341
|
for (const localBin of buildSgLocalBins()) {
|
|
713
1342
|
if (await probeAstGrepCommandAsync(localBin)) {
|
|
@@ -744,6 +1373,21 @@ export async function isSgAvailableAsync() {
|
|
|
744
1373
|
noteSgAvailable(startedAt);
|
|
745
1374
|
return true;
|
|
746
1375
|
}
|
|
1376
|
+
// Nothing answered, and nothing answered TRANSIENTLY, while the verdict we
|
|
1377
|
+
// are about to overwrite is a provisional win whose command we still hold.
|
|
1378
|
+
// #1476's principle applies to the result as much as to the probe: a
|
|
1379
|
+
// timeout says nothing about the tool, so it cannot erase a command this
|
|
1380
|
+
// process proved working one cooldown ago. Keep serving it and re-arm
|
|
1381
|
+
// (#1568 review F1) — UNLESS this very sweep just proved the memoized
|
|
1382
|
+
// command durably missing (#1593): a sibling tier stalling is not license
|
|
1383
|
+
// to re-serve a winner that ENOENTed a moment ago in the same pass.
|
|
1384
|
+
if (sgSweepSawTransient &&
|
|
1385
|
+
sgLatch.isProvisional() &&
|
|
1386
|
+
sgCmd !== null &&
|
|
1387
|
+
!sgSweepDurablyMissing.includes(sgCmd)) {
|
|
1388
|
+
noteSgAvailable(startedAt, { retained: true });
|
|
1389
|
+
return true;
|
|
1390
|
+
}
|
|
747
1391
|
// A timeout on ANY candidate is evidence about the host, not the tool.
|
|
748
1392
|
noteSgUnavailable(startedAt, sgSweepSawTransient ? "transient" : "missing", sgSweepSawTransient ? sgSweepTransientCause : "not-found");
|
|
749
1393
|
return false;
|
|
@@ -752,18 +1396,44 @@ export async function isSgAvailableAsync() {
|
|
|
752
1396
|
});
|
|
753
1397
|
return sgAvailableInFlight;
|
|
754
1398
|
}
|
|
755
|
-
/**
|
|
756
|
-
|
|
757
|
-
|
|
1399
|
+
/**
|
|
1400
|
+
* Record a successful shared-ast-grep sweep, with one decision record.
|
|
1401
|
+
*
|
|
1402
|
+
* A win reached while an EARLIER candidate was unreachable is provisional
|
|
1403
|
+
* (#1568). The sweep stops at the first candidate that answers, so
|
|
1404
|
+
* `sgSweepSawTransient` at this point means precisely "a tier this one is
|
|
1405
|
+
* supposed to lose to never got a fair hearing" — the winner is used now, but
|
|
1406
|
+
* caching it for the session would pin a healthy PATH ast-grep behind `npx`
|
|
1407
|
+
* until the next restart.
|
|
1408
|
+
*
|
|
1409
|
+
* `retained` marks the other provisional case (#1568 review F1): no candidate
|
|
1410
|
+
* answered at all, so the winner being reported is the one the previous sweep
|
|
1411
|
+
* found, kept rather than discarded on a timeout.
|
|
1412
|
+
*/
|
|
1413
|
+
function noteSgAvailable(startedAt, opts = {}) {
|
|
1414
|
+
const provisional = sgSweepSawTransient;
|
|
1415
|
+
let retryAfterMs = 0;
|
|
1416
|
+
if (provisional) {
|
|
1417
|
+
retryAfterMs = sgLatch.noteProvisionallyAvailable(sgSweepTransientCause);
|
|
1418
|
+
}
|
|
1419
|
+
else {
|
|
1420
|
+
sgLatch.noteAvailable();
|
|
1421
|
+
}
|
|
758
1422
|
logAvailabilityDecision({
|
|
759
1423
|
tool: "ast-grep",
|
|
760
1424
|
verdict: "available",
|
|
761
1425
|
outcome: "success",
|
|
762
|
-
cause: "ok",
|
|
1426
|
+
cause: provisional ? sgSweepTransientCause : "ok",
|
|
763
1427
|
elapsedMs: Date.now() - startedAt,
|
|
764
|
-
latched:
|
|
1428
|
+
latched: !provisional,
|
|
765
1429
|
hostStallMs: sgSweepHostStallMs,
|
|
766
1430
|
budgetMs: 5000,
|
|
1431
|
+
...(provisional && {
|
|
1432
|
+
provisional: true,
|
|
1433
|
+
unreachablePreferred: [...sgSweepUnreachable],
|
|
1434
|
+
...(opts.retained === true && { retained: true }),
|
|
1435
|
+
...(retryAfterMs > 0 && { retryAfterMs }),
|
|
1436
|
+
}),
|
|
767
1437
|
});
|
|
768
1438
|
}
|
|
769
1439
|
/** Record a failed shared-ast-grep sweep; a transient verdict expires. */
|