@herbertgao/pi-extensions 2026.8.6 → 2026.8.7
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 +5 -5
- 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/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 +272 -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 +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +14 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +55 -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 +1 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +26 -10
- 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 +2 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +3 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +2 -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 +3 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +30 -7
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/state.ts +14 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +30 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +26 -5
- 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-lens/CHANGELOG.md +909 -0
- package/node_modules/pi-lens/README.md +11 -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 +285 -8
- 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 +119 -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 +192 -0
- 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 +62 -13
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +25 -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/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/helm-render.js +895 -0
- 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 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +23 -0
- 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 +14 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +20 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +22 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +10 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +18 -6
- 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 +72 -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 +9 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +18 -1
- 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 +294 -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 +82 -6
- 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 +2 -0
- 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 +102 -7
- package/node_modules/pi-lens/dist/clients/lsp/client.js +1516 -159
- 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 +2274 -147
- 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 +8 -0
- 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 +5 -0
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
- 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 +1 -1
- package/node_modules/pi-lens/dist/clients/read-guard.js +184 -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/query.js +52 -4
- 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 +308 -45
- package/node_modules/pi-lens/dist/clients/runtime-session.js +332 -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 +575 -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/sg-runner.js +116 -22
- package/node_modules/pi-lens/dist/clients/single-flight.js +158 -0
- package/node_modules/pi-lens/dist/clients/stale-marker.js +17 -0
- 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-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 +67738 -55739
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +466 -54
- 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 +10 -6
- 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/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 +23 -0
- package/node_modules/pi-web-access/README.md +27 -9
- 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/curator-page.ts +6 -3
- package/node_modules/pi-web-access/curator-server.ts +4 -1
- package/node_modules/pi-web-access/exa.ts +18 -7
- package/node_modules/pi-web-access/extract.ts +28 -3
- package/node_modules/pi-web-access/gemini-search.ts +16 -4
- package/node_modules/pi-web-access/index.ts +41 -57
- 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/query-rewrite.ts +51 -0
- package/node_modules/pi-web-access/serper.ts +211 -0
- 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 +13 -13
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Inspired by OpenCode's formatter.ts pattern
|
|
10
10
|
*/
|
|
11
11
|
import { logExtension } from "./extension-log.js";
|
|
12
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
12
13
|
import * as fs from "node:fs/promises";
|
|
13
14
|
import * as path from "node:path";
|
|
14
15
|
import { BoundedLruCache } from "./bounded-cache.js";
|
|
@@ -16,47 +17,26 @@ import { normalizeMapKey } from "./path-utils.js";
|
|
|
16
17
|
import { TERRAGRUNT_FILENAMES } from "./file-kinds.js";
|
|
17
18
|
import { detectIndentation, hasDetectableIndentation, } from "./dispatch/indent-detect.js";
|
|
18
19
|
import { logLatency } from "./latency-logger.js";
|
|
19
|
-
import {
|
|
20
|
+
import { classifyProbeFailure, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
|
|
21
|
+
import { findGlobalBinary, findLocalBinUpwards } from "./package-manager.js";
|
|
20
22
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
21
23
|
import { assertInstallAllowed } from "./project-trust.js";
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
+
import { tryLazyInstallForFormatter } from "./dispatch/runners/utils/lazy-installer.js";
|
|
25
|
+
import { findPSScriptAnalyzerConfigPath, getAutoInstallToolIdForFormatter, getFormatterPolicyForFile, getSmartDefaultFormatterName, hasBiomeConfig, hasBlackConfig, hasClangFormatConfig, hasCljfmtConfig, hasCmakeFormatConfig, hasCsharpierConfig, hasFantomasConfig, hasGoogleJavaFormatConfig, hasKtfmtConfig, hasKtlintConfig, hasMixFormatConfig, hasNearestPackageJsonDependency, hasNearestPackageJsonField, hasOcamlformatConfig, hasOrmoluConfig, hasOxfmtConfig, hasOxfmtSvelteConfig, hasPhpCsFixerConfig, hasPrettierConfig, hasPSScriptAnalyzerConfig, hasRubocopConfig, hasRuffConfig, hasSqlfluffConfig, hasStandardrbConfig, hasStyluaConfig, hasSwiftformatConfig, hasTaploConfig, hasTerraformConfig, hasVitePlusConfig, OXFMT_SUPPORTED_EXTENSIONS, } from "./tool-policy.js";
|
|
26
|
+
/**
|
|
27
|
+
* Lazy-install a formatter's tool, through the shared seam (#1537).
|
|
28
|
+
*
|
|
29
|
+
* This used to own a second copy of the attempt guard — `_lazyInstallAttempts`,
|
|
30
|
+
* a Set keyed before the install ran and never cleared — so a `gem install
|
|
31
|
+
* rubocop` that died on a network blip was never retried for the session. The
|
|
32
|
+
* state, the transient/durable classification and the retry ladder now live in
|
|
33
|
+
* `lazy-installer.ts`; what stays here is the one thing that is formatter
|
|
34
|
+
* business: a fresh binary on PATH invalidates every "not found" verdict (#1495).
|
|
35
|
+
*/
|
|
24
36
|
export async function tryLazyInstallFormatterTool(tool, cwd) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (_lazyInstallAttempts.has(attemptKey))
|
|
29
|
-
return false;
|
|
30
|
-
_lazyInstallAttempts.add(attemptKey);
|
|
31
|
-
if (tool === "rubocop") {
|
|
32
|
-
const res = await safeSpawnAsync("gem", ["install", "rubocop", "--no-document"], {
|
|
33
|
-
timeout: 180000,
|
|
34
|
-
cwd,
|
|
35
|
-
ignoreAmbientSignal: true,
|
|
36
|
-
});
|
|
37
|
-
const ok = !res.error && res.status === 0;
|
|
38
|
-
if (!ok) {
|
|
39
|
-
logExtension({
|
|
40
|
-
subsystem: "format",
|
|
41
|
-
message: `lazy-install rubocop failed: ${res.error?.message ?? res.stderr ?? "exit " + res.status}`,
|
|
42
|
-
metadata: { tool: "rubocop", cwd },
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
return ok;
|
|
46
|
-
}
|
|
47
|
-
const res = await safeSpawnAsync("rustup", ["component", "add", "rustfmt"], {
|
|
48
|
-
timeout: 180000,
|
|
49
|
-
cwd,
|
|
50
|
-
ignoreAmbientSignal: true,
|
|
51
|
-
});
|
|
52
|
-
const ok = !res.error && res.status === 0;
|
|
53
|
-
if (!ok) {
|
|
54
|
-
logExtension({
|
|
55
|
-
subsystem: "format",
|
|
56
|
-
message: `lazy-install rustfmt failed: ${res.error?.message ?? res.stderr ?? "exit " + res.status}`,
|
|
57
|
-
metadata: { tool: "rustfmt", cwd },
|
|
58
|
-
});
|
|
59
|
-
}
|
|
37
|
+
const ok = await tryLazyInstallForFormatter(tool, cwd);
|
|
38
|
+
if (ok)
|
|
39
|
+
resetWhichLatches();
|
|
60
40
|
return ok;
|
|
61
41
|
}
|
|
62
42
|
// --- Types ---
|
|
@@ -95,11 +75,236 @@ async function findUp(targets, startDir, stopDir = path.parse(startDir).root) {
|
|
|
95
75
|
}
|
|
96
76
|
return found;
|
|
97
77
|
}
|
|
78
|
+
const WHICH_BUDGET_MS = 5000;
|
|
79
|
+
/**
|
|
80
|
+
* PATH lookups, governed by the shared availability policy (#1495).
|
|
81
|
+
*
|
|
82
|
+
* `which()` is a spawn on a 5 s budget, not a filesystem check, and around a
|
|
83
|
+
* dozen `detect*` implementations gate on it. A transient timeout used to drop
|
|
84
|
+
* the formatter AND get written to `detectionCache` as an empty enabled-list —
|
|
85
|
+
* a cache invalidated only by a config file's mtime or size, never by time. One
|
|
86
|
+
* stalled `which rustfmt` therefore disabled Rust formatting for the session
|
|
87
|
+
* unless the user happened to edit a config file.
|
|
88
|
+
*
|
|
89
|
+
* Now: only a genuine absence latches, a stall expires on a cooldown, and the
|
|
90
|
+
* detection pass refuses to cache an empty result a stall caused.
|
|
91
|
+
*/
|
|
92
|
+
const whichLatchByCommand = new Map();
|
|
93
|
+
/**
|
|
94
|
+
* Commands whose CURRENT `which` verdict is transient — a probe that never got a
|
|
95
|
+
* fair hearing, still inside its cooldown.
|
|
96
|
+
*
|
|
97
|
+
* Scoped per command on purpose (#1495 review). The first cut counted transient
|
|
98
|
+
* verdicts process-wide and compared the count across a detection pass, which
|
|
99
|
+
* over-suppressed: one stalled `which rustfmt` stopped an unrelated Python or
|
|
100
|
+
* shell detection from caching, and stamped `reason: "probe-timeout"` on
|
|
101
|
+
* selections where nothing had timed out. A command leaves the set as soon as
|
|
102
|
+
* its verdict becomes trustworthy — a resolved path or a genuine absence.
|
|
103
|
+
*/
|
|
104
|
+
const whichTransientCommands = new Set();
|
|
105
|
+
/**
|
|
106
|
+
* The binaries `which()` was asked about inside the current `detect()` call
|
|
107
|
+
* (#1539).
|
|
108
|
+
*
|
|
109
|
+
* This replaces the `command[0]` approximation the poison guard used to run on.
|
|
110
|
+
* `command[0]` is the formatter's own binary, which is exact for most
|
|
111
|
+
* `detect()`s and wrong for the ones that consult a second binary — and the
|
|
112
|
+
* "extras are harmless" argument does not hold: `rustfmt` answering a GENUINE
|
|
113
|
+
* absence while `which rustup` stalls skips the lazy install and leaves nothing
|
|
114
|
+
* in the transient set for `command[0]` to match. Recording the actual probes
|
|
115
|
+
* makes the guard exact in both directions: it also stops a leftover transient
|
|
116
|
+
* verdict for some other pass's binary from being blamed on this decision.
|
|
117
|
+
*
|
|
118
|
+
* `AsyncLocalStorage` because `detect()` is async and two selection passes can
|
|
119
|
+
* be in flight at once (`tests/clients/formatters-which-latch.test.ts` runs
|
|
120
|
+
* exactly that); a module-level set would let one pass's probes leak into the
|
|
121
|
+
* other's verdict. Built eagerly — unlike `extension-log.ts`'s lazy console
|
|
122
|
+
* capture, this store is entered on every detection pass, so there is no window
|
|
123
|
+
* where paying for it is avoidable.
|
|
124
|
+
*/
|
|
125
|
+
const probedCommandsStorage = new AsyncLocalStorage();
|
|
126
|
+
/**
|
|
127
|
+
* Run one candidate's `detect()` and read the per-candidate verdict back out of
|
|
128
|
+
* the PATH latch (#1539). `detected` is the plain boolean the caller still needs
|
|
129
|
+
* for selection; `verdict` is why.
|
|
130
|
+
*/
|
|
131
|
+
async function detectCandidate(formatter, cwd) {
|
|
132
|
+
const probed = new Set();
|
|
133
|
+
let detected = false;
|
|
134
|
+
let error;
|
|
135
|
+
try {
|
|
136
|
+
detected = await probedCommandsStorage.run(probed, () => formatter.detect(cwd));
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
// A `detect()` that threw still probed whatever it probed first, and those
|
|
140
|
+
// probes are the fact the guard needs. Swallowing them here would let a
|
|
141
|
+
// stall inside a throwing detection cache as a clean "not this project".
|
|
142
|
+
error = err;
|
|
143
|
+
}
|
|
144
|
+
const stalledCommands = [...probed].filter((command) => whichTransientCommands.has(command));
|
|
145
|
+
return {
|
|
146
|
+
detected,
|
|
147
|
+
...(error !== undefined && { error }),
|
|
148
|
+
outcome: {
|
|
149
|
+
name: formatter.name,
|
|
150
|
+
verdict: detected
|
|
151
|
+
? "available"
|
|
152
|
+
: stalledCommands.length > 0
|
|
153
|
+
? "unreachable"
|
|
154
|
+
: "missing",
|
|
155
|
+
stalledCommands,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Drop the PATH verdicts, so a newly installed binary is visible at once.
|
|
161
|
+
*
|
|
162
|
+
* Module-private on purpose. Clearing the latches alone does NOT re-arm
|
|
163
|
+
* formatter availability: `getFormattersForFile` answers from `detectionCache`
|
|
164
|
+
* before it ever probes PATH, so a caller that wants a genuine re-probe must
|
|
165
|
+
* drop both. `clearFormatterCache` is that pair, and it is the only reset a
|
|
166
|
+
* session boundary should call (#1895 review round).
|
|
167
|
+
*/
|
|
168
|
+
function resetWhichLatches() {
|
|
169
|
+
whichLatchByCommand.clear();
|
|
170
|
+
whichTransientCommands.clear();
|
|
171
|
+
cooldownRecordedForRetryAtMs.clear();
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Last `retryAtMs` a cooldown-served record was emitted for, per command.
|
|
175
|
+
*
|
|
176
|
+
* The bound (#1539): the memo branch used to return before
|
|
177
|
+
* `logAvailabilityDecision`, so a formatter held off by a transient cooldown
|
|
178
|
+
* produced ONE record for arbitrarily many decisions — a reader counting
|
|
179
|
+
* `availability_decision` rows undercounted how long it had been off. Logging
|
|
180
|
+
* every cache hit is not an option either: this seam is consulted per save.
|
|
181
|
+
* One extra row per cooldown WINDOW is the compromise, and `retryAtMs` is the
|
|
182
|
+
* window's identity, so the ladder itself does the rate limiting.
|
|
183
|
+
*/
|
|
184
|
+
const cooldownRecordedForRetryAtMs = new Map();
|
|
185
|
+
/**
|
|
186
|
+
* Record that a still-cooling verdict was served from the latch, at most once
|
|
187
|
+
* per cooldown window (#1539's second defect).
|
|
188
|
+
*/
|
|
189
|
+
function noteCooldownServedVerdict(command, latch) {
|
|
190
|
+
if (latch.getOutcome() !== "transient")
|
|
191
|
+
return;
|
|
192
|
+
const retryAtMs = latch.getRetryAtMs();
|
|
193
|
+
if (retryAtMs <= 0)
|
|
194
|
+
return;
|
|
195
|
+
if (cooldownRecordedForRetryAtMs.get(command) === retryAtMs)
|
|
196
|
+
return;
|
|
197
|
+
cooldownRecordedForRetryAtMs.set(command, retryAtMs);
|
|
198
|
+
const cause = latch.getCause();
|
|
199
|
+
if (cause === null) {
|
|
200
|
+
// `getOutcome() === "transient"` is only ever set by `noteUnavailable` in
|
|
201
|
+
// the same call that sets `cause`, so this is unreachable today. It is
|
|
202
|
+
// still not a throw (#1539 review F2): this runs inside `which()`, which
|
|
203
|
+
// runs inside `detect()`, and the smart-default branch of the selection
|
|
204
|
+
// pass rethrows a detection failure — so an invariant break in a LOGGING
|
|
205
|
+
// helper could take down formatting for the file. Nothing is fabricated
|
|
206
|
+
// either (#1535's rule: a made-up cause would mislabel WHY a formatter is
|
|
207
|
+
// off, in the record this fix exists to make honest). The row is dropped
|
|
208
|
+
// and the anomaly is reported, bounded by the same once-per-window gate
|
|
209
|
+
// this function already passed.
|
|
210
|
+
logExtension({
|
|
211
|
+
subsystem: "format",
|
|
212
|
+
message: `which latch: transient outcome with no cause for ${command}; cooldown-served record dropped`,
|
|
213
|
+
metadata: { tool: command, retryAtMs },
|
|
214
|
+
});
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
logAvailabilityDecision({
|
|
218
|
+
tool: command,
|
|
219
|
+
verdict: "unavailable",
|
|
220
|
+
outcome: "transient",
|
|
221
|
+
cause,
|
|
222
|
+
elapsedMs: 0,
|
|
223
|
+
latched: false,
|
|
224
|
+
retryAfterMs: Math.max(1, retryAtMs - Date.now()),
|
|
225
|
+
budgetMs: WHICH_BUDGET_MS,
|
|
226
|
+
servedFromCooldown: true,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
98
229
|
async function which(command) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
230
|
+
probedCommandsStorage.getStore()?.add(command);
|
|
231
|
+
let entry = whichLatchByCommand.get(command);
|
|
232
|
+
if (!entry) {
|
|
233
|
+
entry = { latch: createAvailabilityLatch(), resolved: null };
|
|
234
|
+
whichLatchByCommand.set(command, entry);
|
|
235
|
+
}
|
|
236
|
+
const memo = entry.latch.read();
|
|
237
|
+
if (memo !== null) {
|
|
238
|
+
if (!memo)
|
|
239
|
+
noteCooldownServedVerdict(command, entry.latch);
|
|
240
|
+
return memo ? entry.resolved : null;
|
|
241
|
+
}
|
|
242
|
+
const stallSampler = startHostStallSampler();
|
|
243
|
+
const startedAt = Date.now();
|
|
244
|
+
const result = await safeSpawnAsync(process.platform === "win32" ? "where" : "which", [command], { timeout: WHICH_BUDGET_MS });
|
|
245
|
+
const hostStallMs = stallSampler.stop();
|
|
246
|
+
const elapsedMs = Date.now() - startedAt;
|
|
247
|
+
const resolved = !result.error && result.status === 0
|
|
248
|
+
? (result.stdout?.trim().split(/\r?\n/)[0] ?? null)
|
|
249
|
+
: null;
|
|
250
|
+
if (resolved) {
|
|
251
|
+
entry.resolved = resolved;
|
|
252
|
+
entry.latch.noteAvailable();
|
|
253
|
+
whichTransientCommands.delete(command);
|
|
254
|
+
cooldownRecordedForRetryAtMs.delete(command);
|
|
255
|
+
logAvailabilityDecision({
|
|
256
|
+
tool: command,
|
|
257
|
+
verdict: "available",
|
|
258
|
+
outcome: "success",
|
|
259
|
+
cause: "ok",
|
|
260
|
+
elapsedMs,
|
|
261
|
+
latched: true,
|
|
262
|
+
hostStallMs,
|
|
263
|
+
budgetMs: WHICH_BUDGET_MS,
|
|
264
|
+
});
|
|
265
|
+
return resolved;
|
|
266
|
+
}
|
|
267
|
+
entry.resolved = null;
|
|
268
|
+
// A `which`/`where` that RAN and found nothing exits nonzero with NO spawn
|
|
269
|
+
// error, and only that is a genuine absence an install would fix.
|
|
270
|
+
//
|
|
271
|
+
// An UNSPAWNABLE prober is the opposite (#1495 review): EACCES, `spawn
|
|
272
|
+
// UNKNOWN`, EMFILE, an unresolvable cwd, or `where`/`which` itself missing
|
|
273
|
+
// says nothing about the tool that was asked for. Claiming `missing` there
|
|
274
|
+
// was the worst version of this bug, because the prober is shared by every
|
|
275
|
+
// which-gated formatter — one EACCES would have latched a dozen of them off
|
|
276
|
+
// for the session. So the `missing` override is granted only to a prober that
|
|
277
|
+
// ran, and anything that stopped it from running stays transient.
|
|
278
|
+
const proberRan = !result.error;
|
|
279
|
+
const classified = classifyProbeFailure(result, {
|
|
280
|
+
hostStallMs,
|
|
281
|
+
...(proberRan && { unclassifiedFailureOutcome: "missing" }),
|
|
282
|
+
});
|
|
283
|
+
let { outcome, cause } = classified;
|
|
284
|
+
if (!proberRan && outcome !== "transient") {
|
|
285
|
+
outcome = "transient";
|
|
286
|
+
cause = "probe-rejected";
|
|
287
|
+
}
|
|
288
|
+
const retryAfterMs = entry.latch.noteUnavailable(outcome, cause);
|
|
289
|
+
if (outcome === "transient")
|
|
290
|
+
whichTransientCommands.add(command);
|
|
291
|
+
else
|
|
292
|
+
whichTransientCommands.delete(command);
|
|
293
|
+
// A fresh probe opens a new cooldown window (or ends the cooldown), so the
|
|
294
|
+
// next cache hit is entitled to its own record.
|
|
295
|
+
cooldownRecordedForRetryAtMs.delete(command);
|
|
296
|
+
logAvailabilityDecision({
|
|
297
|
+
tool: command,
|
|
298
|
+
verdict: "unavailable",
|
|
299
|
+
outcome,
|
|
300
|
+
cause,
|
|
301
|
+
elapsedMs,
|
|
302
|
+
latched: retryAfterMs === 0,
|
|
303
|
+
hostStallMs,
|
|
304
|
+
...(retryAfterMs > 0 && { retryAfterMs }),
|
|
305
|
+
budgetMs: WHICH_BUDGET_MS,
|
|
306
|
+
});
|
|
307
|
+
return null;
|
|
103
308
|
}
|
|
104
309
|
async function resolveGoFmtBinary() {
|
|
105
310
|
const inPath = await which("gofmt");
|
|
@@ -214,61 +419,85 @@ async function resolveManagedSmartDefaultCommand(formatterName, filePath, args)
|
|
|
214
419
|
return null;
|
|
215
420
|
return [installed, ...args, filePath];
|
|
216
421
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
422
|
+
/**
|
|
423
|
+
* One entry per formatter that can be selected via explicit project config
|
|
424
|
+
* (the `formatterPolicy` "explicit-config" branch of `getFormattersForFile`).
|
|
425
|
+
* A formatter with NO entry here can never win that branch — the switch this
|
|
426
|
+
* table replaced had exactly that failure mode for `psscriptanalyzer-format`
|
|
427
|
+
* (#1572: `.ps1`'s policy sets `defaultWhenUnconfigured: false` AND the
|
|
428
|
+
* formatter had no config check, so neither selection branch could ever pick
|
|
429
|
+
* it).
|
|
430
|
+
*
|
|
431
|
+
* This table IS the source of truth for "which formatters have an explicit-
|
|
432
|
+
* config check" — `FORMATTERS_WITH_EXPLICIT_CONFIG_CHECK` below derives its
|
|
433
|
+
* membership from these keys rather than hand-listing them a second time, so
|
|
434
|
+
* the coverage guard in formatter-policy-consistency.test.ts (#1572) can never
|
|
435
|
+
* drift from what this function actually does.
|
|
436
|
+
*/
|
|
437
|
+
// A `Map`, not an object literal: an object literal's lookup inherits
|
|
438
|
+
// `Object.prototype`, so `checks["toString"]` or `checks["constructor"]`
|
|
439
|
+
// resolves to a real (non-formatter) function instead of `undefined` —
|
|
440
|
+
// a formatter genuinely NAMED one of those prototype properties would read
|
|
441
|
+
// as "has an explicit-config check" when it has none (review finding, #1572).
|
|
442
|
+
// `Map.prototype.get` has no such inherited-key hazard.
|
|
443
|
+
const EXPLICIT_FORMATTER_CONFIG_CHECKS = new Map([
|
|
444
|
+
["biome", (cwd) => hasBiomeConfig(cwd)],
|
|
445
|
+
[
|
|
446
|
+
"prettier",
|
|
447
|
+
(cwd) => hasPrettierConfig(cwd) || hasNearestPackageJsonField(cwd, "prettier"),
|
|
448
|
+
],
|
|
449
|
+
// .svelte is conditional beyond "an oxfmt config exists": oxfmt requires the
|
|
450
|
+
// `svelte` package installed AND the config's `svelte` flag enabled
|
|
451
|
+
// (verified empirically — see hasOxfmtSvelteConfig). The generic checks
|
|
452
|
+
// below are NOT sufficient for .svelte — an oxfmt.toml with no svelte flag,
|
|
453
|
+
// or an oxfmt dependency alone, both fail at runtime for .svelte
|
|
454
|
+
// specifically (other extensions are unaffected by this stricter gate).
|
|
455
|
+
[
|
|
456
|
+
"oxfmt",
|
|
457
|
+
(cwd, ext) => ext === ".svelte"
|
|
458
|
+
? hasOxfmtSvelteConfig(cwd)
|
|
459
|
+
: hasOxfmtConfig(cwd) ||
|
|
235
460
|
hasVitePlusConfig(cwd) ||
|
|
236
461
|
// The published package is `oxfmt`; `@oxc-project/oxfmt` does not
|
|
237
462
|
// exist on npm. Accept both (scoped kept for forward-compat).
|
|
238
463
|
hasNearestPackageJsonDependency(cwd, "oxfmt") ||
|
|
239
|
-
hasNearestPackageJsonDependency(cwd, "@oxc-project/oxfmt")
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
464
|
+
hasNearestPackageJsonDependency(cwd, "@oxc-project/oxfmt"),
|
|
465
|
+
],
|
|
466
|
+
["ruff", (cwd) => hasRuffConfig(cwd)],
|
|
467
|
+
["black", (cwd) => hasBlackConfig(cwd)],
|
|
468
|
+
["sqlfluff", (cwd) => hasSqlfluffConfig(cwd)],
|
|
469
|
+
["rubocop", (cwd) => hasRubocopConfig(cwd)],
|
|
470
|
+
["standardrb", (cwd) => hasStandardrbConfig(cwd)],
|
|
471
|
+
["clang-format", (cwd) => hasClangFormatConfig(cwd)],
|
|
472
|
+
["php-cs-fixer", (cwd) => hasPhpCsFixerConfig(cwd)],
|
|
473
|
+
["stylua", (cwd) => hasStyluaConfig(cwd)],
|
|
474
|
+
["ocamlformat", (cwd) => hasOcamlformatConfig(cwd)],
|
|
475
|
+
["google-java-format", (cwd) => hasGoogleJavaFormatConfig(cwd)],
|
|
476
|
+
["ktfmt", (cwd) => hasKtfmtConfig(cwd)],
|
|
477
|
+
["ktlint", (cwd) => hasKtlintConfig(cwd)],
|
|
478
|
+
["cljfmt", (cwd) => hasCljfmtConfig(cwd)],
|
|
479
|
+
["cmake-format", (cwd) => hasCmakeFormatConfig(cwd)],
|
|
480
|
+
["psscriptanalyzer-format", (cwd) => hasPSScriptAnalyzerConfig(cwd)],
|
|
481
|
+
// #1595 sweep — see the comment above hasCsharpierConfig et al. in
|
|
482
|
+
// tool-policy.ts for why nixfmt (the 8th formatter #1572 flagged) is NOT
|
|
483
|
+
// wired here: it has no config-file convention and no manifest-marker
|
|
484
|
+
// equivalent to `.terraform.lock.hcl`, so there is no honest opt-in signal.
|
|
485
|
+
["csharpier", (cwd) => hasCsharpierConfig(cwd)],
|
|
486
|
+
["ormolu", (cwd) => hasOrmoluConfig(cwd)],
|
|
487
|
+
["taplo", (cwd) => hasTaploConfig(cwd)],
|
|
488
|
+
["terraform", (cwd) => hasTerraformConfig(cwd)],
|
|
489
|
+
["swiftformat", (cwd) => hasSwiftformatConfig(cwd)],
|
|
490
|
+
["fantomas", (cwd) => hasFantomasConfig(cwd)],
|
|
491
|
+
["mix", (cwd) => hasMixFormatConfig(cwd)],
|
|
492
|
+
]);
|
|
493
|
+
function hasExplicitFormatterConfig(formatterName, cwd, ext) {
|
|
494
|
+
return EXPLICIT_FORMATTER_CONFIG_CHECKS.get(formatterName)?.(cwd, ext) ?? false;
|
|
271
495
|
}
|
|
496
|
+
// Exported for the "every registered formatter is selectable" coverage guard
|
|
497
|
+
// (formatter-policy-consistency.test.ts, #1572): derived from the table above,
|
|
498
|
+
// not hand-listed, so it cannot drift from what `hasExplicitFormatterConfig`
|
|
499
|
+
// actually checks.
|
|
500
|
+
export const FORMATTERS_WITH_EXPLICIT_CONFIG_CHECK = new Set(EXPLICIT_FORMATTER_CONFIG_CHECKS.keys());
|
|
272
501
|
// --- Formatter Definitions ---
|
|
273
502
|
async function hasEditorConfig(cwd) {
|
|
274
503
|
try {
|
|
@@ -851,8 +1080,17 @@ export const styluaFormatter = {
|
|
|
851
1080
|
name: "stylua",
|
|
852
1081
|
command: ["stylua", "$FILE"],
|
|
853
1082
|
extensions: [".lua"],
|
|
1083
|
+
async resolveCommand(filePath, cwd) {
|
|
1084
|
+
// Project binary first (#1731, discipline B): stylua has no pi-lens
|
|
1085
|
+
// managed install, so before this the ONLY resolution was a bare
|
|
1086
|
+
// `stylua` PATH lookup — a project-local install via npm
|
|
1087
|
+
// `@johnnymorganz/stylua` (`node_modules/.bin/stylua`) was invisible.
|
|
1088
|
+
const local = findLocalBinUpwards("stylua", cwd);
|
|
1089
|
+
return local ? [local, filePath] : null;
|
|
1090
|
+
},
|
|
854
1091
|
async detect(cwd) {
|
|
855
|
-
|
|
1092
|
+
const local = findLocalBinUpwards("stylua", cwd);
|
|
1093
|
+
if (!local && (await which("stylua")) === null)
|
|
856
1094
|
return false;
|
|
857
1095
|
// Prefer explicit config but also run if binary is present in a Lua project
|
|
858
1096
|
const configs = ["stylua.toml", ".stylua.toml"];
|
|
@@ -915,6 +1153,18 @@ export const cmakeFormatFormatter = {
|
|
|
915
1153
|
return hasCmakeFormatConfig(cwd);
|
|
916
1154
|
},
|
|
917
1155
|
};
|
|
1156
|
+
export const cueFormatter = {
|
|
1157
|
+
name: "cue",
|
|
1158
|
+
// `cue fmt <file>` rewrites in place already. There is no `-w`: the only
|
|
1159
|
+
// flags are --check, -d/--diff and --files, so `cue fmt -w` aborts with
|
|
1160
|
+
// "unknown shorthand flag: 'w'" on every .cue write (verified on cue
|
|
1161
|
+
// v0.17.1).
|
|
1162
|
+
command: ["cue", "fmt", "$FILE"],
|
|
1163
|
+
extensions: [".cue"],
|
|
1164
|
+
async detect(_cwd) {
|
|
1165
|
+
return (await which("cue")) !== null;
|
|
1166
|
+
},
|
|
1167
|
+
};
|
|
918
1168
|
/**
|
|
919
1169
|
* The PowerShell one-liner behind `psscriptanalyzer-format`.
|
|
920
1170
|
*
|
|
@@ -930,16 +1180,25 @@ export const cmakeFormatFormatter = {
|
|
|
930
1180
|
* - single-quoted interpolation broke on any path containing an apostrophe.
|
|
931
1181
|
* An empty/whitespace-only file exits 0 without touching anything, so "nothing
|
|
932
1182
|
* to format" stays a clean no-op rather than a reported failure.
|
|
1183
|
+
*
|
|
1184
|
+
* `settingsPath`, when the project has one (#1572 review F2), is passed
|
|
1185
|
+
* straight to `Invoke-Formatter -Settings`. Gating selection on the file's
|
|
1186
|
+
* presence without ever reading its rules would run the project through the
|
|
1187
|
+
* stock `CodeFormatting` ruleset regardless of what it declared — the same
|
|
1188
|
+
* stock-style imposition #1144 banned for the other config-first formatters.
|
|
933
1189
|
*/
|
|
934
|
-
function psScriptAnalyzerCommand(filePath) {
|
|
1190
|
+
function psScriptAnalyzerCommand(filePath, settingsPath) {
|
|
935
1191
|
// PowerShell single-quoted strings escape an apostrophe by doubling it.
|
|
936
1192
|
const quoted = filePath.replace(/'/g, "''");
|
|
1193
|
+
const settingsArg = settingsPath
|
|
1194
|
+
? ` -Settings '${settingsPath.replace(/'/g, "''")}'`
|
|
1195
|
+
: "";
|
|
937
1196
|
return [
|
|
938
1197
|
"$ErrorActionPreference = 'Stop'",
|
|
939
1198
|
`$p = '${quoted}'`,
|
|
940
1199
|
"$content = Get-Content -Raw -LiteralPath $p",
|
|
941
1200
|
"if ([string]::IsNullOrWhiteSpace($content)) { exit 0 }",
|
|
942
|
-
|
|
1201
|
+
`$formatted = Invoke-Formatter -ScriptDefinition $content${settingsArg}`,
|
|
943
1202
|
"if ($null -eq $formatted) { throw 'Invoke-Formatter returned no output' }",
|
|
944
1203
|
"Set-Content -LiteralPath $p -Value $formatted",
|
|
945
1204
|
].join("; ");
|
|
@@ -948,11 +1207,17 @@ export const psscriptanalyzerFormatFormatter = {
|
|
|
948
1207
|
name: "psscriptanalyzer-format",
|
|
949
1208
|
command: ["pwsh", "-NoProfile", "-Command", psScriptAnalyzerCommand("$FILE")],
|
|
950
1209
|
extensions: [".ps1", ".psm1", ".psd1"],
|
|
951
|
-
async resolveCommand(filePath,
|
|
1210
|
+
async resolveCommand(filePath, cwd) {
|
|
952
1211
|
const pwsh = (await which("pwsh")) ?? (await which("powershell"));
|
|
953
1212
|
if (!pwsh)
|
|
954
1213
|
return null;
|
|
955
|
-
|
|
1214
|
+
const settingsPath = findPSScriptAnalyzerConfigPath(cwd);
|
|
1215
|
+
return [
|
|
1216
|
+
pwsh,
|
|
1217
|
+
"-NoProfile",
|
|
1218
|
+
"-Command",
|
|
1219
|
+
psScriptAnalyzerCommand(filePath, settingsPath),
|
|
1220
|
+
];
|
|
956
1221
|
},
|
|
957
1222
|
async detect(_cwd) {
|
|
958
1223
|
const pwsh = (await which("pwsh")) ?? (await which("powershell"));
|
|
@@ -1005,6 +1270,7 @@ export const ALL_FORMATTERS = [
|
|
|
1005
1270
|
googleJavaFormatFormatter,
|
|
1006
1271
|
cljfmtFormatter,
|
|
1007
1272
|
cmakeFormatFormatter,
|
|
1273
|
+
cueFormatter,
|
|
1008
1274
|
psscriptanalyzerFormatFormatter,
|
|
1009
1275
|
];
|
|
1010
1276
|
// Basenames claimed by a filename-keyed formatter, e.g. terragrunt.hcl.
|
|
@@ -1013,17 +1279,35 @@ const FILENAME_FORMATTER_BASENAMES = new Set(ALL_FORMATTERS.flatMap((f) => f.fil
|
|
|
1013
1279
|
const detectionCache = new BoundedLruCache(32);
|
|
1014
1280
|
// These are the formatter configuration files consulted by the policy helpers
|
|
1015
1281
|
// above. Their metadata is part of detection cache identity: changing a file
|
|
1016
|
-
// must re-run detection even when PATH and installed tools are unchanged.
|
|
1282
|
+
// must re-run detection even when PATH and installed tools are unchanged. A
|
|
1283
|
+
// filename a `has*Config` check reads but this list omits is invisible to the
|
|
1284
|
+
// cache: the signature never moves when that file is added, so a project that
|
|
1285
|
+
// opts in AFTER the first `getFormattersForFile` call for its cwd keeps
|
|
1286
|
+
// getting the stale (pre-opt-in) cached answer for the rest of the session
|
|
1287
|
+
// (#1572 review F1 — proved for psscriptanalyzer-format's settings file;
|
|
1288
|
+
// swept against every `EXPLICIT_FORMATTER_CONFIG_CHECKS` entry below, which
|
|
1289
|
+
// turned up the same gap for google-java-format, cljfmt, cmake-format, the
|
|
1290
|
+
// Kotlin/Spotless gradle files, sqlfluff's setup.cfg, and oxfmt's
|
|
1291
|
+
// vite-plus.json / additional vite.config extensions).
|
|
1017
1292
|
const FORMATTER_CONFIG_FILES = [
|
|
1018
1293
|
"package.json", "biome.json", "biome.jsonc", ".prettierrc", ".prettierrc.json",
|
|
1019
1294
|
".prettierrc.yml", ".prettierrc.yaml", ".prettierrc.js", ".prettierrc.cjs",
|
|
1020
1295
|
".prettierrc.mjs", "prettier.config.js", "prettier.config.cjs", "prettier.config.mjs",
|
|
1021
1296
|
"prettier.config.ts", "pyproject.toml", "ruff.toml", ".ruff.toml", "black.toml",
|
|
1022
|
-
".black", "tox.ini", "requirements.txt", "Pipfile", ".sqlfluff", ".rubocop.yml", ".rubocop.yaml", "Gemfile", ".clang-format",
|
|
1297
|
+
".black", "tox.ini", "setup.cfg", "requirements.txt", "Pipfile", ".sqlfluff", ".rubocop.yml", ".rubocop.yaml", "Gemfile", ".clang-format",
|
|
1023
1298
|
"_clang-format", ".php-cs-fixer.php", ".php-cs-fixer.dist.php", "stylua.toml",
|
|
1024
|
-
".stylua.toml", ".ocamlformat", ".editorconfig", ".
|
|
1025
|
-
".
|
|
1026
|
-
"
|
|
1299
|
+
".stylua.toml", ".ocamlformat", ".editorconfig", ".google-java-format",
|
|
1300
|
+
".ktfmt", ".ktfmt.kts", "build.gradle.kts", "build.gradle", "settings.gradle.kts", "settings.gradle",
|
|
1301
|
+
".cljfmt.edn", "cljfmt.edn", ".cljfmt",
|
|
1302
|
+
".cmake-format", ".cmake-format.yaml", ".cmake-format.yml", ".cmake-format.json", ".cmake-format.py",
|
|
1303
|
+
"cmake-format.py", "cmake-format.yaml", "cmake-format.yml",
|
|
1304
|
+
"oxfmt.toml", ".oxfmtrc.json", "vite-plus.json",
|
|
1305
|
+
"vite.config.ts", "vite.config.mts", "vite.config.cts", "vite.config.js", "vite.config.mjs", "vite.config.cjs",
|
|
1306
|
+
"PSScriptAnalyzerSettings.psd1", "ScriptAnalyzerSettings.psd1",
|
|
1307
|
+
// #1595 sweep additions.
|
|
1308
|
+
".csharpierrc", ".csharpierrc.json", ".csharpierrc.yaml", ".csharpierrc.yml",
|
|
1309
|
+
".ormolu", "taplo.toml", ".taplo.toml", ".terraform.lock.hcl", ".swiftformat",
|
|
1310
|
+
".fantomasignore", ".formatter.exs",
|
|
1027
1311
|
];
|
|
1028
1312
|
async function formatterConfigSignature(cwd) {
|
|
1029
1313
|
const paths = await findUp(FORMATTER_CONFIG_FILES, cwd);
|
|
@@ -1054,9 +1338,16 @@ export async function getFormattersForFile(filePath, cwd) {
|
|
|
1054
1338
|
const configSignature = await formatterConfigSignature(cwd);
|
|
1055
1339
|
// Check cache
|
|
1056
1340
|
let cached = detectionCache.get(normalizedCwd);
|
|
1057
|
-
if (cached
|
|
1058
|
-
|
|
1341
|
+
if (cached && cached.signature !== configSignature) {
|
|
1342
|
+
// An EXISTING entry whose signature moved is the user telling us the world
|
|
1343
|
+
// changed. Re-probe PATH too, so "install the tool, touch the config" still
|
|
1344
|
+
// works within one session now that a durable absence latches (#1495).
|
|
1345
|
+
// Scoped to a real change on purpose: keying this off "no entry yet" would
|
|
1346
|
+
// drop every PATH verdict on the first save in each new directory, which in
|
|
1347
|
+
// a monorepo means re-probing forever.
|
|
1059
1348
|
detectionCache.delete(normalizedCwd);
|
|
1349
|
+
cached = undefined;
|
|
1350
|
+
resetWhichLatches();
|
|
1060
1351
|
}
|
|
1061
1352
|
if (!cached) {
|
|
1062
1353
|
cached = { signature: configSignature, entries: new Map() };
|
|
@@ -1077,6 +1368,13 @@ export async function getFormattersForFile(filePath, cwd) {
|
|
|
1077
1368
|
? matching.filter((f) => formatterPolicy.formatterNames.includes(f.name))
|
|
1078
1369
|
: matching;
|
|
1079
1370
|
let selected;
|
|
1371
|
+
/**
|
|
1372
|
+
* One entry per candidate this pass actually ASKED, in the order it asked
|
|
1373
|
+
* them (#1539). Candidates eliminated without a probe — an explicit-config
|
|
1374
|
+
* decision is pure filesystem — contribute nothing, which is the point: a
|
|
1375
|
+
* decision that never probed cannot have been degraded by a probe.
|
|
1376
|
+
*/
|
|
1377
|
+
const candidateOutcomes = [];
|
|
1080
1378
|
if (formatterPolicy) {
|
|
1081
1379
|
const explicitlyConfigured = candidateFormatters.filter((formatter) => hasExplicitFormatterConfig(formatter.name, cwd, ext));
|
|
1082
1380
|
if (explicitlyConfigured.length > 0) {
|
|
@@ -1093,33 +1391,93 @@ export async function getFormattersForFile(filePath, cwd) {
|
|
|
1093
1391
|
const smartDefaultFormatter = candidateFormatters.find((f) => f.name === smartDefaultFormatterName);
|
|
1094
1392
|
if (smartDefaultFormatter) {
|
|
1095
1393
|
const autoInstallToolId = getAutoInstallToolIdForFormatter(smartDefaultFormatter.name);
|
|
1096
|
-
if (autoInstallToolId
|
|
1394
|
+
if (autoInstallToolId) {
|
|
1097
1395
|
selected = smartDefaultFormatter;
|
|
1098
1396
|
}
|
|
1397
|
+
else {
|
|
1398
|
+
const { detected, error, outcome } = await detectCandidate(smartDefaultFormatter, cwd);
|
|
1399
|
+
candidateOutcomes.push(outcome);
|
|
1400
|
+
// This branch never caught a throwing detection, and still
|
|
1401
|
+
// does not: `detectCandidate` catches only so the probes survive.
|
|
1402
|
+
if (error !== undefined)
|
|
1403
|
+
throw error;
|
|
1404
|
+
if (detected)
|
|
1405
|
+
selected = smartDefaultFormatter;
|
|
1406
|
+
}
|
|
1099
1407
|
}
|
|
1100
1408
|
}
|
|
1101
1409
|
}
|
|
1102
1410
|
else {
|
|
1103
1411
|
for (const formatter of candidateFormatters) {
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
break;
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
catch (err) {
|
|
1412
|
+
const { detected, error, outcome } = await detectCandidate(formatter, cwd);
|
|
1413
|
+
candidateOutcomes.push(outcome);
|
|
1414
|
+
if (error !== undefined) {
|
|
1111
1415
|
// pi-lens-ignore: missing-error-propagation — optional formatter detection, skip on failure
|
|
1112
1416
|
logExtension({
|
|
1113
1417
|
subsystem: "format",
|
|
1114
|
-
message: `Detection failed for ${formatter.name}: ${
|
|
1418
|
+
message: `Detection failed for ${formatter.name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1115
1419
|
metadata: { formatter: formatter.name, cwd },
|
|
1116
1420
|
});
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
if (detected) {
|
|
1424
|
+
selected = formatter;
|
|
1425
|
+
break;
|
|
1117
1426
|
}
|
|
1118
1427
|
}
|
|
1119
1428
|
}
|
|
1120
1429
|
const enabled = selected ? [selected] : [];
|
|
1430
|
+
// The #925/#1467 `wouldPoisonCache` shape: an empty result produced while a
|
|
1431
|
+
// PATH probe was timing out is not a finding about this project. Caching it
|
|
1432
|
+
// would survive until a config file's mtime or size changed, so leave the
|
|
1433
|
+
// cache untouched and let the next turn re-detect.
|
|
1434
|
+
// Which of THIS file's candidates are currently un-answered? Read off the
|
|
1435
|
+
// binaries the candidates' own `detect()`s actually probed (#1539), so a
|
|
1436
|
+
// stalled `which rustfmt` cannot stop a shell or Python detection from
|
|
1437
|
+
// caching (#1495 review) AND a leftover transient verdict for a binary this
|
|
1438
|
+
// pass never consulted cannot be blamed on this decision.
|
|
1439
|
+
//
|
|
1440
|
+
// This replaces the `command[0]` approximation, which missed both shapes of
|
|
1441
|
+
// extra probe: a co-equal ALTERNATIVE (`pwsh` ?? `powershell`) and — the
|
|
1442
|
+
// case the old comment wrongly called harmless — an install FALLBACK reached
|
|
1443
|
+
// after the primary answered a GENUINE absence. `rustfmt` missing plus a
|
|
1444
|
+
// stalled `which rustup` skips the lazy install, and nothing named `rustfmt`
|
|
1445
|
+
// is transient, so the empty result used to cache for the session.
|
|
1446
|
+
//
|
|
1447
|
+
// Residual: the record covers this module's own `which()` only. A detection
|
|
1448
|
+
// that reaches PATH some other way is invisible to it — ktlint's `detect()`
|
|
1449
|
+
// falls back to the installer's `getToolPath("ktlint")`, which has its own
|
|
1450
|
+
// probe and its own 24-hour cache. Widening the record to those is deferred to
|
|
1451
|
+
// the installer-side follow-up rather than bolted on here.
|
|
1452
|
+
const stalledCandidateCommands = [
|
|
1453
|
+
...new Set(candidateOutcomes.flatMap((outcome) => outcome.stalledCommands)),
|
|
1454
|
+
];
|
|
1455
|
+
const poisonedByTransientProbe = enabled.length === 0 && stalledCandidateCommands.length > 0;
|
|
1456
|
+
// A NON-empty result the poison guard cannot see (#1539): the winner won only
|
|
1457
|
+
// because a candidate ahead of it was never asked. `candidateOutcomes` is in
|
|
1458
|
+
// ask order and the loop stops at the winner, so every entry before the
|
|
1459
|
+
// winner's is a candidate that lost — and an `unreachable` one did not lose
|
|
1460
|
+
// on the merits. Caching this would hand the session to the runner-up until
|
|
1461
|
+
// a config file's mtime or size changed.
|
|
1462
|
+
const winnerIndex = selected
|
|
1463
|
+
? candidateOutcomes.findIndex((outcome) => outcome.name === selected.name)
|
|
1464
|
+
: -1;
|
|
1465
|
+
const unreachablePreferred = candidateOutcomes
|
|
1466
|
+
.slice(0, winnerIndex === -1 ? 0 : winnerIndex)
|
|
1467
|
+
.filter((outcome) => outcome.verdict === "unreachable")
|
|
1468
|
+
.map((outcome) => outcome.name);
|
|
1469
|
+
const degradedSelection = unreachablePreferred.length > 0;
|
|
1121
1470
|
let selectionReason;
|
|
1122
|
-
if (
|
|
1471
|
+
if (poisonedByTransientProbe) {
|
|
1472
|
+
selectionReason = "probe-timeout";
|
|
1473
|
+
}
|
|
1474
|
+
else if (degradedSelection) {
|
|
1475
|
+
// The reason now describes how the winner WON, not just what the config
|
|
1476
|
+
// looks like. "explicit-config" on a selection whose preferred candidate
|
|
1477
|
+
// was never reachable was the most misleading record in this seam.
|
|
1478
|
+
selectionReason = "preferred-unreachable";
|
|
1479
|
+
}
|
|
1480
|
+
else if (!selected) {
|
|
1123
1481
|
selectionReason = "none";
|
|
1124
1482
|
}
|
|
1125
1483
|
else if (!formatterPolicy) {
|
|
@@ -1130,6 +1488,7 @@ export async function getFormattersForFile(filePath, cwd) {
|
|
|
1130
1488
|
? "explicit-config"
|
|
1131
1489
|
: "smart-default";
|
|
1132
1490
|
}
|
|
1491
|
+
const provisional = poisonedByTransientProbe || degradedSelection;
|
|
1133
1492
|
logLatency({
|
|
1134
1493
|
type: "phase",
|
|
1135
1494
|
phase: "formatter_selected",
|
|
@@ -1139,19 +1498,45 @@ export async function getFormattersForFile(filePath, cwd) {
|
|
|
1139
1498
|
formatter: selected?.name ?? null,
|
|
1140
1499
|
reason: selectionReason,
|
|
1141
1500
|
cwd,
|
|
1501
|
+
...(provisional && {
|
|
1502
|
+
cached: false,
|
|
1503
|
+
stalledProbes: stalledCandidateCommands,
|
|
1504
|
+
}),
|
|
1505
|
+
...(degradedSelection && { unreachablePreferred }),
|
|
1142
1506
|
},
|
|
1143
1507
|
});
|
|
1508
|
+
// Provisional either way: not cached, so the next pass re-detects once the
|
|
1509
|
+
// cooldown expires and the preferred formatter's recovery takes effect
|
|
1510
|
+
// without waiting on a config-file edit.
|
|
1511
|
+
if (provisional)
|
|
1512
|
+
return enabled;
|
|
1144
1513
|
// Store the list of enabled formatter names in cache
|
|
1145
1514
|
const enabledNames = enabled.map((f) => f.name);
|
|
1146
1515
|
cached.entries.set(cacheKey, enabledNames);
|
|
1147
1516
|
return enabled;
|
|
1148
1517
|
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Re-arm formatter availability: drop both the per-cwd selection cache and the
|
|
1520
|
+
* PATH latches.
|
|
1521
|
+
*
|
|
1522
|
+
* #1895: this is the session-boundary reset. Both halves are required. The
|
|
1523
|
+
* latches decide whether `which` runs at all, but `detectionCache` short-
|
|
1524
|
+
* circuits ahead of them — a same-cwd lookup returns the previous verdict's
|
|
1525
|
+
* formatter names without reaching a probe. Clearing only the latches
|
|
1526
|
+
* therefore re-arms every directory EXCEPT the one the user is working in.
|
|
1527
|
+
*/
|
|
1149
1528
|
export function clearFormatterCache() {
|
|
1150
1529
|
detectionCache.clear();
|
|
1530
|
+
resetWhichLatches();
|
|
1151
1531
|
}
|
|
1152
1532
|
export function clearFormatterRuntimeState() {
|
|
1153
1533
|
detectionCache.clear();
|
|
1154
|
-
|
|
1534
|
+
resetWhichLatches();
|
|
1535
|
+
// NO `resetLazyInstallAttempts()` here (#1537 review F1). This function runs
|
|
1536
|
+
// from `resetFormatService()`, which `handleTurnEnd` calls every turn — so
|
|
1537
|
+
// clearing the lazy-install hold here made "held for the session" mean "held
|
|
1538
|
+
// for a turn", and a failing install re-ran every turn. The hold's only reset
|
|
1539
|
+
// is `session_start`'s block in runtime-session.ts.
|
|
1155
1540
|
}
|
|
1156
1541
|
// ESC is built via fromCharCode so no raw control byte sits in the source.
|
|
1157
1542
|
const ANSI_ESCAPE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[A-Za-z]`, "g");
|