@herbertgao/pi-extensions 2026.9.0 → 2026.9.2
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/node_modules/@czottmann/pi-automode/CHANGELOG.md +13 -1
- package/node_modules/@czottmann/pi-automode/README.md +20 -0
- package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +1 -1
- package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +2 -1
- package/node_modules/@czottmann/pi-automode/docs/defaults.md +3 -2
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +38 -2
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +5 -3
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +11 -3
- package/node_modules/@czottmann/pi-automode/package.json +1 -1
- package/node_modules/@narumitw/pi-btw/README.md +57 -141
- package/node_modules/@narumitw/pi-btw/dist/index.ts +466 -41
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
- package/node_modules/@narumitw/pi-btw/docs/workflows.md +74 -0
- package/node_modules/@narumitw/pi-btw/package.json +2 -1
- package/node_modules/@narumitw/pi-btw/src/btw.ts +15 -1
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +32 -2
- package/node_modules/@narumitw/pi-btw/src/keybindings.ts +330 -0
- package/node_modules/@narumitw/pi-btw/src/menu.ts +136 -19
- package/node_modules/@narumitw/pi-btw/src/settings.ts +32 -0
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +57 -5
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +52 -30
- package/node_modules/@pi-plugins/fast-mode/README.md +5 -3
- package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20 -55
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +1 -1
- package/node_modules/pi-lens/CHANGELOG.md +1072 -0
- package/node_modules/pi-lens/README.md +3 -0
- package/node_modules/pi-lens/config/biome/core.jsonc +11 -2
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +13 -1
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +2 -9
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +927 -80
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +1 -1
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +13 -4
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +123 -3
- package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +60 -5
- package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +2 -9
- package/node_modules/pi-lens/dist/clients/ast-grep-types.js +2 -0
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +133 -3
- package/node_modules/pi-lens/dist/clients/biome-client.js +9 -2
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +14 -6
- package/node_modules/pi-lens/dist/clients/bootstrap.js +509 -73
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +152 -12
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +1 -1
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +59 -9
- package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +113 -0
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +3 -11
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +31 -4
- package/node_modules/pi-lens/dist/clients/cache-manager.js +105 -6
- package/node_modules/pi-lens/dist/clients/cache-observability.js +40 -19
- package/node_modules/pi-lens/dist/clients/cargo-manifest.js +422 -0
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +2 -9
- package/node_modules/pi-lens/dist/clients/child-unref.js +1 -19
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +13 -3
- package/node_modules/pi-lens/dist/clients/complexity-client.js +16 -5
- package/node_modules/pi-lens/dist/clients/config-core/deny.js +221 -0
- package/node_modules/pi-lens/dist/clients/config-core/index.js +50 -0
- package/node_modules/pi-lens/dist/clients/config-core/merge.js +357 -0
- package/node_modules/pi-lens/dist/clients/config-core/normalize.js +340 -0
- package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +248 -0
- package/node_modules/pi-lens/dist/clients/config-core/provenance.js +164 -0
- package/node_modules/pi-lens/dist/clients/config-core/records.js +218 -0
- package/node_modules/pi-lens/dist/clients/config-core/resolve.js +125 -0
- package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +78 -0
- package/node_modules/pi-lens/dist/clients/config-core/schema.js +165 -0
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +281 -0
- package/node_modules/pi-lens/dist/clients/config-locations.js +160 -0
- package/node_modules/pi-lens/dist/clients/config-resolve.js +789 -0
- package/node_modules/pi-lens/dist/clients/config-schema.js +234 -0
- package/node_modules/pi-lens/dist/clients/config-warn.js +407 -0
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +19 -14
- package/node_modules/pi-lens/dist/clients/dead-code-logger.js +2 -6
- package/node_modules/pi-lens/dist/clients/deadline-utils.js +178 -0
- package/node_modules/pi-lens/dist/clients/debug-handles.js +2 -2
- package/node_modules/pi-lens/dist/clients/debug-heap.js +3 -3
- package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +106 -0
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +113 -7
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +8 -19
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +1 -1
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +12 -9
- package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +15 -5
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -4
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +162 -44
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +97 -0
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +27 -8
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -6
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -3
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +2 -2
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +383 -27
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +4 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +1 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +1 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +14 -18
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +6 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +7 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +1 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +1 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +1 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +4 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -67
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +194 -80
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +84 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -21
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +2 -20
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +2 -2
- package/node_modules/pi-lens/dist/clients/disposition-logger.js +3 -10
- package/node_modules/pi-lens/dist/clients/effective-config.js +403 -0
- package/node_modules/pi-lens/dist/clients/error-class.js +23 -0
- package/node_modules/pi-lens/dist/clients/event-loop-hold.js +274 -0
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +2 -2
- package/node_modules/pi-lens/dist/clients/extension-log.js +2 -2
- package/node_modules/pi-lens/dist/clients/feature-hints.js +2 -1
- package/node_modules/pi-lens/dist/clients/file-kinds.js +1 -38
- package/node_modules/pi-lens/dist/clients/file-time.js +7 -2
- package/node_modules/pi-lens/dist/clients/file-utils.js +117 -10
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +50 -13
- package/node_modules/pi-lens/dist/clients/format-service.js +6 -2
- package/node_modules/pi-lens/dist/clients/formatters.js +385 -104
- package/node_modules/pi-lens/dist/clients/freshness-cadence.js +17 -0
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +116 -22
- package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -9
- package/node_modules/pi-lens/dist/clients/git-guard.js +6 -1
- package/node_modules/pi-lens/dist/clients/go-client.js +39 -0
- package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +252 -0
- package/node_modules/pi-lens/dist/clients/grammar-source.js +3 -3
- package/node_modules/pi-lens/dist/clients/hashline-anchor.js +424 -0
- package/node_modules/pi-lens/dist/clients/hook-budgets.js +76 -0
- package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +5 -2
- package/node_modules/pi-lens/dist/clients/host-ports.js +1 -1
- package/node_modules/pi-lens/dist/clients/installer/index.js +289 -59
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +84 -18
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +110 -236
- package/node_modules/pi-lens/dist/clients/instance-registry.js +6 -3
- package/node_modules/pi-lens/dist/clients/language-profile.js +0 -9
- package/node_modules/pi-lens/dist/clients/language-registry.js +599 -0
- package/node_modules/pi-lens/dist/clients/latency-logger.js +165 -9
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +34 -1
- package/node_modules/pi-lens/dist/clients/lens-config.js +160 -30
- package/node_modules/pi-lens/dist/clients/lens-engine.js +33 -41
- package/node_modules/pi-lens/dist/clients/lens-events.js +1 -1
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +77 -6
- package/node_modules/pi-lens/dist/clients/lens-map.js +1 -1
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -38
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +3 -1
- package/node_modules/pi-lens/dist/clients/lsp/client.js +172 -33
- package/node_modules/pi-lens/dist/clients/lsp/config.js +484 -158
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +18 -5
- package/node_modules/pi-lens/dist/clients/lsp/index.js +892 -96
- package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/language.js +18 -177
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +5 -18
- package/node_modules/pi-lens/dist/clients/lsp/server.js +284 -147
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +116 -23
- package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +2 -7
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -1
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +14 -7
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +6 -0
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +163 -21
- package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +36 -0
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +33 -4
- package/node_modules/pi-lens/dist/clients/mcp/session.js +5 -16
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +8 -3
- package/node_modules/pi-lens/dist/clients/metrics-history.js +28 -107
- package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +3 -5
- package/node_modules/pi-lens/dist/clients/module-report.js +21 -46
- package/node_modules/pi-lens/dist/clients/mutating-tool.js +651 -0
- package/node_modules/pi-lens/dist/clients/mutation-attribution.js +368 -0
- package/node_modules/pi-lens/dist/clients/mutation-bridge.js +240 -0
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +247 -24
- package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +101 -0
- package/node_modules/pi-lens/dist/clients/observed-mutation.js +1215 -0
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +70 -28
- package/node_modules/pi-lens/dist/clients/opengrep-config.js +5 -1
- package/node_modules/pi-lens/dist/clients/package-manager.js +195 -26
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +359 -80
- package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +23 -7
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +21 -2
- package/node_modules/pi-lens/dist/clients/path-utils.js +451 -15
- package/node_modules/pi-lens/dist/clients/performance-report.js +2 -2
- package/node_modules/pi-lens/dist/clients/persist-debounce.js +8 -1
- package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +114 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +79 -18
- package/node_modules/pi-lens/dist/clients/probe-home-state.js +228 -0
- package/node_modules/pi-lens/dist/clients/process-bridge.js +66 -0
- package/node_modules/pi-lens/dist/clients/process-snapshot.js +68 -0
- package/node_modules/pi-lens/dist/clients/project-changes.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +28 -4
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +51 -20
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +438 -122
- package/node_modules/pi-lens/dist/clients/project-scan-policy.js +3 -51
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +52 -24
- package/node_modules/pi-lens/dist/clients/python-environment.js +217 -0
- package/node_modules/pi-lens/dist/clients/python-provenance.js +639 -0
- package/node_modules/pi-lens/dist/clients/quiet-window.js +6 -1
- package/node_modules/pi-lens/dist/clients/read-bridge.js +13 -21
- package/node_modules/pi-lens/dist/clients/read-expansion.js +9 -46
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +16 -5
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +364 -212
- package/node_modules/pi-lens/dist/clients/read-guard.js +26 -2
- package/node_modules/pi-lens/dist/clients/recent-touches.js +2 -2
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +195 -96
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +155 -114
- package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +1 -1
- package/node_modules/pi-lens/dist/clients/review-graph/service.js +55 -3
- package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +7 -15
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +10 -74
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +9 -3
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +19 -3
- package/node_modules/pi-lens/dist/clients/runtime-context.js +49 -16
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +25 -6
- package/node_modules/pi-lens/dist/clients/runtime-session.js +184 -32
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +179 -36
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +797 -243
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +1028 -53
- package/node_modules/pi-lens/dist/clients/rust-client.js +31 -0
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +33 -10
- package/node_modules/pi-lens/dist/clients/sanitize.js +7 -12
- package/node_modules/pi-lens/dist/clients/scan-utils.js +1 -56
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +1 -3
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +3 -0
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +1 -1
- package/node_modules/pi-lens/dist/clients/session-start-observability.js +79 -0
- package/node_modules/pi-lens/dist/clients/session-summary.js +0 -24
- package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +12 -3
- package/node_modules/pi-lens/dist/clients/sgconfig.js +7 -6
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +1 -1
- package/node_modules/pi-lens/dist/clients/skills-resolver.js +105 -0
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +4 -4
- package/node_modules/pi-lens/dist/clients/startup-scan.js +2 -10
- package/node_modules/pi-lens/dist/clients/string-utils.js +13 -0
- package/node_modules/pi-lens/dist/clients/subagent-mode.js +17 -4
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +176 -39
- package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +244 -0
- package/node_modules/pi-lens/dist/clients/tool-definition.js +41 -1
- package/node_modules/pi-lens/dist/clients/tool-policy.js +92 -22
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +40 -40
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +87 -100
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +81 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +51 -46
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +28 -0
- package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -4
- package/node_modules/pi-lens/dist/clients/typos-config.js +5 -0
- package/node_modules/pi-lens/dist/clients/user-notify.js +6 -6
- package/node_modules/pi-lens/dist/clients/widget-state.js +191 -26
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +2 -9
- package/node_modules/pi-lens/dist/clients/word-index-store.js +23 -12
- package/node_modules/pi-lens/dist/clients/word-index.js +252 -47
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +8 -1
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +4 -1
- package/node_modules/pi-lens/dist/index.js +65899 -55340
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -1
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +1 -1
- package/node_modules/pi-lens/dist/mcp/server.js +127 -5
- package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +583 -0
- package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +129 -0
- package/node_modules/pi-lens/dist/tools/effective-config.js +89 -0
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +96 -25
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +104 -86
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +37 -16
- package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +3 -3
- package/node_modules/pi-lens/dist/tools/render-compact.js +1 -1
- package/node_modules/pi-lens/dist/tools/shared.js +0 -1
- package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -2
- package/node_modules/pi-lens/docs/agent-guide.md +64 -1
- package/node_modules/pi-lens/docs/configuration.md +222 -0
- package/node_modules/pi-lens/docs/dependencies.md +3 -3
- package/node_modules/pi-lens/docs/features.md +44 -5
- package/node_modules/pi-lens/docs/globalconfig.md +20 -0
- package/node_modules/pi-lens/docs/language-coverage.md +2 -2
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +9 -1
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +25 -0
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +25 -0
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +33 -0
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +49 -0
- package/node_modules/pi-lens/docs/pi-lens-warden.md +55 -0
- package/node_modules/pi-lens/docs/public-api-stability.md +359 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +190 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +7 -3
- package/node_modules/pi-lens/docs/subagent-compat.md +110 -30
- package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +2 -2
- package/node_modules/pi-lens/docs/word-index.md +1 -1
- package/node_modules/pi-lens/package.json +18 -35
- package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
- package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
- package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
- package/node_modules/pi-lens/rules/typos/_typos.toml +4 -2
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +192 -1
- package/node_modules/pi-lens/scripts/install-selftest.mjs +58 -2
- package/node_modules/pi-lens/scripts/lib/skills-predicate.mjs +129 -0
- package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +1 -1
- package/node_modules/pi-lens/scripts/rpc-load-check.mjs +3 -1
- package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +4 -2
- package/node_modules/pi-web-access/CHANGELOG.md +28 -0
- package/node_modules/pi-web-access/README.md +62 -15
- package/node_modules/pi-web-access/curator-page.ts +3 -1
- package/node_modules/pi-web-access/curator-server.ts +5 -1
- package/node_modules/pi-web-access/gemini-search.ts +8 -4
- package/node_modules/pi-web-access/github-extract.ts +242 -1
- package/node_modules/pi-web-access/index.ts +118 -64
- package/node_modules/pi-web-access/mistral-search.ts +281 -0
- package/node_modules/pi-web-access/package.json +2 -2
- package/node_modules/pi-web-access/perplexity.ts +14 -1
- package/node_modules/pi-web-access/utils.ts +23 -6
- package/node_modules/pi-web-access/xai-search.ts +96 -33
- package/package.json +6 -6
|
@@ -11,15 +11,21 @@
|
|
|
11
11
|
import { logExtension } from "./extension-log.js";
|
|
12
12
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
13
|
import * as fs from "node:fs/promises";
|
|
14
|
+
import * as os from "node:os";
|
|
14
15
|
import * as path from "node:path";
|
|
15
16
|
import { BoundedLruCache } from "./bounded-cache.js";
|
|
16
|
-
import {
|
|
17
|
+
import { createGenerationSource } from "./generation-guard.js";
|
|
18
|
+
import { findNearestMarkerRoot, isRealGitMarker, normalizeMapKey, } from "./path-utils.js";
|
|
19
|
+
import { resolveCargoPackageEdition } from "./cargo-manifest.js";
|
|
20
|
+
import { resolveKtfmtGradleStyle } from "./gradle-ktfmt-style.js";
|
|
21
|
+
import { resolvePhpCsFixerConfig } from "./php-cs-fixer-config.js";
|
|
17
22
|
import { TERRAGRUNT_FILENAMES } from "./file-kinds.js";
|
|
23
|
+
import { stripAnsi } from "./sanitize.js";
|
|
18
24
|
import { detectIndentation, hasDetectableIndentation, } from "./dispatch/indent-detect.js";
|
|
19
25
|
import { logLatency } from "./latency-logger.js";
|
|
20
26
|
import { compareOrdinal } from "./string-utils.js";
|
|
21
27
|
import { classifyProbeFailure, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
|
|
22
|
-
import { findGlobalBinary, findLocalBinUpwards } from "./package-manager.js";
|
|
28
|
+
import { findGlobalBinary, findLocalBinUpwards, VENDOR_BIN_DIRS, VENV_BIN_DIRS, } from "./package-manager.js";
|
|
23
29
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
24
30
|
import { assertInstallAllowed } from "./project-trust.js";
|
|
25
31
|
import { tryLazyInstallForFormatter } from "./dispatch/runners/utils/lazy-installer.js";
|
|
@@ -49,6 +55,19 @@ export async function tryLazyInstallFormatterTool(tool, cwd) {
|
|
|
49
55
|
* stock command there is exactly the stock-style imposition the fix bans.
|
|
50
56
|
*/
|
|
51
57
|
export const SKIP_FORMATTING = "skip-formatting";
|
|
58
|
+
/**
|
|
59
|
+
* Sentinel a `resolveCommand` returns when it has PROVEN the selected
|
|
60
|
+
* formatter has no executable — the binary is absent from every place the
|
|
61
|
+
* resolver probed (node_modules/.bin, a venv, a managed install dir, PATH) and
|
|
62
|
+
* no alternative resolved (#2413). Distinct from `null` ("no override, fall
|
|
63
|
+
* back to the static `command`") and from SKIP_FORMATTING ("style-preserving
|
|
64
|
+
* refusal"): the static fallback for these formatters is the SAME bare binary
|
|
65
|
+
* the resolver just proved missing, so spawning it only re-triggers the ENOENT
|
|
66
|
+
* the resolver already observed — and reporting that as a formatting failure is
|
|
67
|
+
* the exact defect this fixes (recurring defect shape 10: an unavailable
|
|
68
|
+
* producer collapsed into an ordinary failed result).
|
|
69
|
+
*/
|
|
70
|
+
export const FORMATTER_UNAVAILABLE = "formatter-unavailable";
|
|
52
71
|
// --- Utility Functions ---
|
|
53
72
|
async function fileExists(filePath) {
|
|
54
73
|
try {
|
|
@@ -63,12 +82,47 @@ async function findUp(targets, startDir, stopDir = path.parse(startDir).root) {
|
|
|
63
82
|
const found = [];
|
|
64
83
|
let currentDir = startDir;
|
|
65
84
|
while (currentDir !== stopDir) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
85
|
+
// One `readdir` per directory instead of one `access` per target: the
|
|
86
|
+
// per-target probe made this loop's cost O(targets), so a long target
|
|
87
|
+
// list (e.g. `FORMATTER_CONFIG_FILES`) paid for every entry at every
|
|
88
|
+
// ancestor directory even when almost none of them exist (#1603).
|
|
89
|
+
let entries;
|
|
90
|
+
try {
|
|
91
|
+
entries = await fs.readdir(currentDir);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
entries = [];
|
|
95
|
+
}
|
|
96
|
+
if (entries.length > 0) {
|
|
97
|
+
// On default-case-insensitive platforms the old `fs.access` probe
|
|
98
|
+
// matched a config file regardless of on-disk casing; fold so the
|
|
99
|
+
// readdir membership check keeps that behavior.
|
|
100
|
+
const foldCase = process.platform === "win32" || process.platform === "darwin";
|
|
101
|
+
const entrySet = new Set(foldCase ? entries.map((e) => e.toLowerCase()) : entries);
|
|
102
|
+
for (const target of targets) {
|
|
103
|
+
// Nested candidates (for example node_modules/.bin/biome) do
|
|
104
|
+
// not appear as direct readdir members. They retain the old
|
|
105
|
+
// bounded candidate probe; direct names use the O(depth + matches)
|
|
106
|
+
// directory-membership path above.
|
|
107
|
+
if (target.includes("/") || target.includes("\\"))
|
|
108
|
+
continue;
|
|
109
|
+
if (entrySet.has(foldCase ? target.toLowerCase() : target)) {
|
|
110
|
+
const candidate = path.join(currentDir, target);
|
|
111
|
+
// Directory membership is only a cheap candidate filter. Keep the
|
|
112
|
+
// old access check for matched entries so dangling links and entries
|
|
113
|
+
// that cannot be read do not become config evidence (#1603 R2).
|
|
114
|
+
if (await fileExists(candidate))
|
|
115
|
+
found.push(candidate);
|
|
116
|
+
}
|
|
70
117
|
}
|
|
71
118
|
}
|
|
119
|
+
for (const target of targets) {
|
|
120
|
+
if (!target.includes("/") && !target.includes("\\"))
|
|
121
|
+
continue;
|
|
122
|
+
const candidate = path.join(currentDir, target);
|
|
123
|
+
if (await fileExists(candidate))
|
|
124
|
+
found.push(candidate);
|
|
125
|
+
}
|
|
72
126
|
const parent = path.dirname(currentDir);
|
|
73
127
|
if (parent === currentDir)
|
|
74
128
|
break;
|
|
@@ -76,6 +130,10 @@ async function findUp(targets, startDir, stopDir = path.parse(startDir).root) {
|
|
|
76
130
|
}
|
|
77
131
|
return found;
|
|
78
132
|
}
|
|
133
|
+
/** Test-only access to the real filesystem walker and its candidate filter. */
|
|
134
|
+
export function _findUpForTests(targets, startDir, stopDir) {
|
|
135
|
+
return findUp(targets, startDir, stopDir);
|
|
136
|
+
}
|
|
79
137
|
const WHICH_BUDGET_MS = 5000;
|
|
80
138
|
/**
|
|
81
139
|
* PATH lookups, governed by the shared availability policy (#1495).
|
|
@@ -337,79 +395,58 @@ async function resolveGoFmtBinary() {
|
|
|
337
395
|
/**
|
|
338
396
|
* Walk up from cwd looking for a binary in .venv or venv.
|
|
339
397
|
* Returns the absolute path if found, null otherwise.
|
|
398
|
+
*
|
|
399
|
+
* Tool-resolution walker, not a config lookup (#2514/#2517 policy): escaping
|
|
400
|
+
* the project upward past HOME means STOP, not keep reading — a `.venv`/`venv`
|
|
401
|
+
* bin found at or above HOME can never be THIS project's own virtualenv. The
|
|
402
|
+
* ceiling lives in `findLocalBinUpwards` and is default-on there; the tests
|
|
403
|
+
* that pin a fake HOME drive that walker directly rather than threading a
|
|
404
|
+
* `homeDir` no production caller ever passed (#2544 review F3).
|
|
405
|
+
*
|
|
406
|
+
* On Windows the candidate order is EXT-OUTER, DIR-INNER — `.venv/Scripts/x.exe`,
|
|
407
|
+
* `venv/Scripts/x.exe`, then the bare names — which is what the pre-fold copy
|
|
408
|
+
* did and what `findLocalBinUpwards` preserves.
|
|
340
409
|
*/
|
|
341
410
|
async function findInVenv(binary, cwd) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
`venv/Scripts/${binary}.exe`,
|
|
347
|
-
`.venv/Scripts/${binary}`,
|
|
348
|
-
`venv/Scripts/${binary}`,
|
|
349
|
-
]
|
|
350
|
-
: [`.venv/bin/${binary}`, `venv/bin/${binary}`];
|
|
351
|
-
let dir = cwd;
|
|
352
|
-
const root = path.parse(dir).root;
|
|
353
|
-
while (dir !== root) {
|
|
354
|
-
for (const candidate of candidates) {
|
|
355
|
-
const full = path.join(dir, candidate);
|
|
356
|
-
if (await fileExists(full))
|
|
357
|
-
return full;
|
|
358
|
-
}
|
|
359
|
-
const parent = path.dirname(dir);
|
|
360
|
-
if (parent === dir)
|
|
361
|
-
break;
|
|
362
|
-
dir = parent;
|
|
363
|
-
}
|
|
364
|
-
return null;
|
|
411
|
+
return (findLocalBinUpwards(binary, cwd, {
|
|
412
|
+
windowsExt: ".exe",
|
|
413
|
+
binDirs: VENV_BIN_DIRS,
|
|
414
|
+
}) ?? null);
|
|
365
415
|
}
|
|
366
416
|
/**
|
|
367
417
|
* Check vendor/bin for PHP Composer-managed tools.
|
|
368
418
|
* Walks up from cwd to find vendor/bin/<binary>.
|
|
419
|
+
*
|
|
420
|
+
* Tool-resolution walker, not a config lookup (#2514/#2517 policy): escaping
|
|
421
|
+
* the project upward past HOME means STOP, not keep reading — a `vendor/bin`
|
|
422
|
+
* match found at or above HOME can never be THIS project's own Composer
|
|
423
|
+
* install. Same single walker as `findInVenv` above; only `binDirs` and the
|
|
424
|
+
* Windows extension differ.
|
|
369
425
|
*/
|
|
370
426
|
async function findInVendorBin(binary, cwd) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
while (dir !== root) {
|
|
376
|
-
for (const name of names) {
|
|
377
|
-
const full = path.join(dir, "vendor", "bin", name);
|
|
378
|
-
if (await fileExists(full))
|
|
379
|
-
return full;
|
|
380
|
-
}
|
|
381
|
-
const parent = path.dirname(dir);
|
|
382
|
-
if (parent === dir)
|
|
383
|
-
break;
|
|
384
|
-
dir = parent;
|
|
385
|
-
}
|
|
386
|
-
return null;
|
|
427
|
+
return (findLocalBinUpwards(binary, cwd, {
|
|
428
|
+
windowsExt: ".bat",
|
|
429
|
+
binDirs: VENDOR_BIN_DIRS,
|
|
430
|
+
}) ?? null);
|
|
387
431
|
}
|
|
388
432
|
/**
|
|
389
433
|
* Check node_modules/.bin for locally installed Node tools.
|
|
390
434
|
* Walks up from cwd to find node_modules/.bin/<binary>.
|
|
435
|
+
*
|
|
436
|
+
* #2514: this WAS a private, byte-for-byte duplicate of
|
|
437
|
+
* `findLocalBinUpwards` (`package-manager.ts`) — the same walk `stylua`
|
|
438
|
+
* below, `taplo.ts`, `knip-client.ts`, and (via `findNodeToolBinary`)
|
|
439
|
+
* `dependency-checker.ts`/`jscpd-client.ts` already reuse. A stray
|
|
440
|
+
* `~/node_modules/.bin/oxfmt.cmd` (the home-level pi-extensions manifest
|
|
441
|
+
* installs its own bins) was picked up as the project's formatter because
|
|
442
|
+
* this copy had no HOME ceiling; fixing only this copy would have left every
|
|
443
|
+
* other caller of `findLocalBinUpwards` with the same defect. Delegating
|
|
444
|
+
* here instead of hand-rolling a second ceiling keeps there being exactly one
|
|
445
|
+
* `node_modules/.bin` walker — see that function for the tool-resolution
|
|
446
|
+
* "escaping the project means STOP" policy (#2517).
|
|
391
447
|
*/
|
|
392
448
|
async function findInNodeModules(binary, cwd) {
|
|
393
|
-
|
|
394
|
-
let dir = cwd;
|
|
395
|
-
const root = path.parse(dir).root;
|
|
396
|
-
while (dir !== root) {
|
|
397
|
-
const candidates = isWin
|
|
398
|
-
? [
|
|
399
|
-
path.join(dir, "node_modules", ".bin", `${binary}.cmd`),
|
|
400
|
-
path.join(dir, "node_modules", ".bin", binary),
|
|
401
|
-
]
|
|
402
|
-
: [path.join(dir, "node_modules", ".bin", binary)];
|
|
403
|
-
for (const full of candidates) {
|
|
404
|
-
if (await fileExists(full))
|
|
405
|
-
return full;
|
|
406
|
-
}
|
|
407
|
-
const parent = path.dirname(dir);
|
|
408
|
-
if (parent === dir)
|
|
409
|
-
break;
|
|
410
|
-
dir = parent;
|
|
411
|
-
}
|
|
412
|
-
return null;
|
|
449
|
+
return findLocalBinUpwards(binary, cwd) ?? null;
|
|
413
450
|
}
|
|
414
451
|
/**
|
|
415
452
|
* Returns true if `bundle exec <gem>` should be used:
|
|
@@ -566,6 +603,110 @@ async function indentationArgs(filePath, tool, cwd) {
|
|
|
566
603
|
String(indentation.width),
|
|
567
604
|
];
|
|
568
605
|
}
|
|
606
|
+
const FORMATTER_MARKERS_BY_NAME = new Map([
|
|
607
|
+
["biome", ["biome.json", "biome.jsonc", "package.json"]],
|
|
608
|
+
[
|
|
609
|
+
"prettier",
|
|
610
|
+
[
|
|
611
|
+
".prettierrc",
|
|
612
|
+
".prettierrc.json",
|
|
613
|
+
".prettierrc.yaml",
|
|
614
|
+
".prettierrc.yml",
|
|
615
|
+
".prettierrc.js",
|
|
616
|
+
".prettierrc.cjs",
|
|
617
|
+
".prettierrc.mjs",
|
|
618
|
+
"prettier.config.js",
|
|
619
|
+
"prettier.config.cjs",
|
|
620
|
+
"prettier.config.mjs",
|
|
621
|
+
".prettierignore",
|
|
622
|
+
"package.json",
|
|
623
|
+
],
|
|
624
|
+
],
|
|
625
|
+
[
|
|
626
|
+
"oxfmt",
|
|
627
|
+
["oxfmt.toml", ".oxfmtrc.json", "vite-plus.json", "package.json"],
|
|
628
|
+
],
|
|
629
|
+
["ruff", ["pyproject.toml", "ruff.toml", ".ruff.toml"]],
|
|
630
|
+
["black", ["pyproject.toml", "black.toml", ".black"]],
|
|
631
|
+
["sqlfluff", [".sqlfluff", "pyproject.toml", "setup.cfg"]],
|
|
632
|
+
["rustfmt", ["rustfmt.toml", ".rustfmt.toml", "Cargo.toml"]],
|
|
633
|
+
["rubocop", [".rubocop.yml", ".rubocop.yaml"]],
|
|
634
|
+
["standardrb", [".standard.yml", ".standard.yaml"]],
|
|
635
|
+
["clang-format", [".clang-format", "_clang-format"]],
|
|
636
|
+
["php-cs-fixer", [".php-cs-fixer.php", ".php-cs-fixer.dist.php"]],
|
|
637
|
+
["stylua", ["stylua.toml", ".stylua.toml"]],
|
|
638
|
+
["ocamlformat", [".ocamlformat"]],
|
|
639
|
+
["google-java-format", [".google-java-format", ".editorconfig"]],
|
|
640
|
+
["cljfmt", [".cljfmt.edn", "cljfmt.edn", ".cljfmt"]],
|
|
641
|
+
[
|
|
642
|
+
"cmake-format",
|
|
643
|
+
[
|
|
644
|
+
".cmake-format",
|
|
645
|
+
".cmake-format.yaml",
|
|
646
|
+
".cmake-format.yml",
|
|
647
|
+
".cmake-format.json",
|
|
648
|
+
".cmake-format.py",
|
|
649
|
+
"cmake-format.yaml",
|
|
650
|
+
"cmake-format.yml",
|
|
651
|
+
".editorconfig",
|
|
652
|
+
],
|
|
653
|
+
],
|
|
654
|
+
[
|
|
655
|
+
"psscriptanalyzer-format",
|
|
656
|
+
["PSScriptAnalyzerSettings.psd1", "ScriptAnalyzerSettings.psd1"],
|
|
657
|
+
],
|
|
658
|
+
[
|
|
659
|
+
"csharpier",
|
|
660
|
+
[
|
|
661
|
+
".csharpierrc",
|
|
662
|
+
".csharpierrc.json",
|
|
663
|
+
".csharpierrc.yaml",
|
|
664
|
+
".csharpierrc.yml",
|
|
665
|
+
],
|
|
666
|
+
],
|
|
667
|
+
["ormolu", [".ormolu"]],
|
|
668
|
+
["taplo", ["taplo.toml", ".taplo.toml"]],
|
|
669
|
+
["terraform", [".terraform.lock.hcl"]],
|
|
670
|
+
["swiftformat", [".swiftformat"]],
|
|
671
|
+
["fantomas", [".fantomasignore", ".editorconfig"]],
|
|
672
|
+
["mix", [".formatter.exs"]],
|
|
673
|
+
["shfmt", [".editorconfig"]],
|
|
674
|
+
["ktlint", [".editorconfig"]],
|
|
675
|
+
[
|
|
676
|
+
"ktfmt",
|
|
677
|
+
[
|
|
678
|
+
".editorconfig",
|
|
679
|
+
".ktfmt",
|
|
680
|
+
".ktfmt.kts",
|
|
681
|
+
"build.gradle",
|
|
682
|
+
"build.gradle.kts",
|
|
683
|
+
"settings.gradle",
|
|
684
|
+
"settings.gradle.kts",
|
|
685
|
+
],
|
|
686
|
+
],
|
|
687
|
+
]);
|
|
688
|
+
/**
|
|
689
|
+
* Resolve the cwd for the formatter child, capped at the user's home
|
|
690
|
+
* directory. Files outside a project retain the historical file-directory
|
|
691
|
+
* cwd. The optional homeDir is only for the ceiling regression test.
|
|
692
|
+
*/
|
|
693
|
+
export function resolveFormatterCwd(absolutePath, formatterName, homeDir = os.homedir()) {
|
|
694
|
+
const fileDir = path.dirname(path.resolve(absolutePath));
|
|
695
|
+
const effectiveHome = homeDir || undefined;
|
|
696
|
+
const markers = formatterName
|
|
697
|
+
? [...(FORMATTER_MARKERS_BY_NAME.get(formatterName) ?? []), ".gitignore"]
|
|
698
|
+
: [".gitignore"];
|
|
699
|
+
const root = findNearestMarkerRoot(fileDir, markers, {
|
|
700
|
+
homeDir: effectiveHome,
|
|
701
|
+
});
|
|
702
|
+
if (root)
|
|
703
|
+
return root;
|
|
704
|
+
const gitRoot = findNearestMarkerRoot(fileDir, [".git"], {
|
|
705
|
+
homeDir: effectiveHome,
|
|
706
|
+
markerPredicate: isRealGitMarker,
|
|
707
|
+
});
|
|
708
|
+
return gitRoot ?? fileDir;
|
|
709
|
+
}
|
|
569
710
|
/**
|
|
570
711
|
* Every biome invocation must carry this. Biome exits 1 with "No files were
|
|
571
712
|
* processed in the specified paths" when the path is ignored by the repo's own
|
|
@@ -747,7 +888,11 @@ export const oxfmtFormatter = {
|
|
|
747
888
|
const found = await which("oxfmt");
|
|
748
889
|
if (found)
|
|
749
890
|
return [found, OXFMT_NO_ERROR_ON_UNMATCHED, filePath];
|
|
750
|
-
|
|
891
|
+
// #2413: neither node_modules/.bin nor PATH has oxfmt, and `detect()` is
|
|
892
|
+
// config-only (it never probes the binary), so selection can reach here
|
|
893
|
+
// with nothing installed. The static command is bare `oxfmt` — spawning it
|
|
894
|
+
// only reproduces the reported `spawn oxfmt ENOENT`. Prove it unavailable.
|
|
895
|
+
return FORMATTER_UNAVAILABLE;
|
|
751
896
|
},
|
|
752
897
|
// Single source of truth: OXFMT_SUPPORTED_EXTENSIONS in tool-policy.ts.
|
|
753
898
|
// Do not hand-maintain a second copy of this list (#1134 — previously two
|
|
@@ -784,7 +929,12 @@ export const ruffFormatter = {
|
|
|
784
929
|
const installed = await ensureTool(toolId);
|
|
785
930
|
if (installed)
|
|
786
931
|
return [installed, ...args, filePath];
|
|
787
|
-
|
|
932
|
+
// #2413: `ensureTool` already resolves PATH, global bins and the managed
|
|
933
|
+
// dir (and attempts an install) before returning falsy, so reaching here
|
|
934
|
+
// means ruff is genuinely nowhere. `detect()`'s `hasRuffConfig` branch has
|
|
935
|
+
// no binary check, so a config-only project selects ruff with nothing
|
|
936
|
+
// installed; the static bare `ruff` would then ENOENT. Prove it unavailable.
|
|
937
|
+
return FORMATTER_UNAVAILABLE;
|
|
788
938
|
},
|
|
789
939
|
async detect(cwd) {
|
|
790
940
|
if (hasRuffConfig(cwd))
|
|
@@ -847,6 +997,18 @@ export const rustfmtFormatter = {
|
|
|
847
997
|
name: "rustfmt",
|
|
848
998
|
command: ["rustfmt", "$FILE"],
|
|
849
999
|
extensions: [".rs"],
|
|
1000
|
+
// #2466: rustfmt defaults to an OLDER edition than the file's actual Cargo
|
|
1001
|
+
// package when invoked bare, so it can reject valid newer-edition syntax
|
|
1002
|
+
// (e.g. Rust 2024). Carry the nearest package's `edition` (honoring
|
|
1003
|
+
// `edition.workspace = true` inheritance) through `--edition`; `undefined`
|
|
1004
|
+
// (unreadable/unparseable manifest) falls back to the static command
|
|
1005
|
+
// above, unchanged from pre-#2466 behavior.
|
|
1006
|
+
async resolveCommand(filePath, _cwd) {
|
|
1007
|
+
const edition = await resolveCargoPackageEdition(filePath);
|
|
1008
|
+
if (edition === undefined)
|
|
1009
|
+
return null;
|
|
1010
|
+
return ["rustfmt", "--edition", edition, filePath];
|
|
1011
|
+
},
|
|
850
1012
|
async detect(cwd) {
|
|
851
1013
|
if ((await which("rustfmt")) !== null)
|
|
852
1014
|
return true;
|
|
@@ -971,13 +1133,27 @@ export const ktfmtFormatter = {
|
|
|
971
1133
|
// ktfmt formats in place when given a file path (no flag needed).
|
|
972
1134
|
command: ["ktfmt", "$FILE"],
|
|
973
1135
|
extensions: [".kt", ".kts"],
|
|
1136
|
+
// #2468: ktfmt's CLI never reads a project's Gradle `ktfmt { googleStyle()
|
|
1137
|
+
// | kotlinLangStyle() }` selection — style is CLI-flag-only
|
|
1138
|
+
// (`--google-style`/`--kotlinlang-style`, verified against ktfmt v0.63's
|
|
1139
|
+
// own arg parser). Carry the nearest module's declared style through;
|
|
1140
|
+
// `undefined` (no declaration, unreadable/unparseable manifest, or an
|
|
1141
|
+
// unsupported style like the removed `dropboxStyle()`) falls back to the
|
|
1142
|
+
// bare invocation, unchanged from pre-#2468 behavior.
|
|
974
1143
|
async resolveCommand(filePath, _cwd) {
|
|
1144
|
+
const styleFlag = await resolveKtfmtGradleStyle(filePath);
|
|
975
1145
|
const inPath = await which("ktfmt");
|
|
976
1146
|
if (inPath)
|
|
977
|
-
return [inPath, filePath];
|
|
1147
|
+
return styleFlag ? [inPath, styleFlag, filePath] : [inPath, filePath];
|
|
978
1148
|
const { ensureTool } = await import("./installer/index.js");
|
|
979
1149
|
const installed = await ensureTool("ktfmt");
|
|
980
|
-
|
|
1150
|
+
// #2413: which() and ensureTool (PATH/global/managed + install) both
|
|
1151
|
+
// failed, and `detect()` gates only on hasKtfmtConfig — no binary probe —
|
|
1152
|
+
// so a config-only project reaches here with ktfmt absent. The static
|
|
1153
|
+
// command is bare `ktfmt`; spawning it reproduces the ENOENT class.
|
|
1154
|
+
if (!installed)
|
|
1155
|
+
return FORMATTER_UNAVAILABLE;
|
|
1156
|
+
return styleFlag ? [installed, styleFlag, filePath] : [installed, filePath];
|
|
981
1157
|
},
|
|
982
1158
|
async detect(cwd) {
|
|
983
1159
|
// Opt-in only: ktfmt becomes the formatter when the project elects it,
|
|
@@ -1063,18 +1239,49 @@ export const phpCsFixerFormatter = {
|
|
|
1063
1239
|
name: "php-cs-fixer",
|
|
1064
1240
|
command: ["php-cs-fixer", "fix", "$FILE"],
|
|
1065
1241
|
extensions: [".php"],
|
|
1242
|
+
// #2472: php-cs-fixer does NOT walk up parent directories looking for its
|
|
1243
|
+
// own config the way prettier/biome/eslint do (verified against upstream
|
|
1244
|
+
// `computeConfigFiles()` — see `resolvePhpCsFixerConfig`'s doc comment),
|
|
1245
|
+
// and `formatFile` spawns with cwd = the FILE's own directory, which is
|
|
1246
|
+
// not necessarily where the ancestor config `detect()` found actually
|
|
1247
|
+
// lives. Always resolve the binary explicitly here (vendor/bin first,
|
|
1248
|
+
// then global) rather than falling through to the static `command` above
|
|
1249
|
+
// — that static command can never carry `--config`, so a config found at
|
|
1250
|
+
// an ancestor would silently be dropped whenever the vendor lookup here
|
|
1251
|
+
// missed but a global binary still resolved. `--config` is attached
|
|
1252
|
+
// whenever a config resolves, even when it sits in the file's own
|
|
1253
|
+
// directory (AC3): unlike the pre-#2472 code, correctness no longer
|
|
1254
|
+
// depends on php-cs-fixer's own (nonexistent) upward search.
|
|
1066
1255
|
async resolveCommand(filePath, cwd) {
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1256
|
+
const configPath = resolvePhpCsFixerConfig(filePath);
|
|
1257
|
+
const binary = (await findInVendorBin("php-cs-fixer", cwd)) ??
|
|
1258
|
+
(await which("php-cs-fixer"));
|
|
1259
|
+
// #2413/#2472 review F4: both probes (vendor/bin, then PATH) have
|
|
1260
|
+
// PROVEN the binary is absent — returning `null` here would fall back
|
|
1261
|
+
// to the static `command` above, which is the SAME bare
|
|
1262
|
+
// `php-cs-fixer` this just failed to find, spawning it only to
|
|
1263
|
+
// re-observe the ENOENT already known. Report the proven-missing
|
|
1264
|
+
// state instead so `formatFile` skips the wasted spawn.
|
|
1265
|
+
if (!binary)
|
|
1266
|
+
return FORMATTER_UNAVAILABLE;
|
|
1267
|
+
return configPath
|
|
1268
|
+
? [binary, "fix", "--config", configPath, filePath]
|
|
1269
|
+
: [binary, "fix", filePath];
|
|
1071
1270
|
},
|
|
1072
1271
|
async detect(cwd) {
|
|
1073
1272
|
const vendorBin = await findInVendorBin("php-cs-fixer", cwd);
|
|
1074
1273
|
const globalBin = await which("php-cs-fixer");
|
|
1075
1274
|
if (!vendorBin && !globalBin)
|
|
1076
1275
|
return false;
|
|
1077
|
-
// Only run if project has explicit config
|
|
1276
|
+
// Only run if project has explicit config. This is a presence-only
|
|
1277
|
+
// climb from the project `cwd` (not necessarily the formatted file's
|
|
1278
|
+
// own directory) via this file's own `findUp` — deliberately NOT
|
|
1279
|
+
// merged with `resolvePhpCsFixerConfig` above (#2472 AC4): that
|
|
1280
|
+
// resolver climbs from the FILE's directory and needs the exact
|
|
1281
|
+
// winning path for `--config`, while this only needs a yes/no answer
|
|
1282
|
+
// from whatever `cwd` the caller passed. `rustfmtFormatter.detect`
|
|
1283
|
+
// keeps the same non-merged shape against `resolveCargoPackageEdition`
|
|
1284
|
+
// for the identical reason.
|
|
1078
1285
|
const configs = [".php-cs-fixer.php", ".php-cs-fixer.dist.php"];
|
|
1079
1286
|
const found = await findUp(configs, cwd);
|
|
1080
1287
|
return found.length > 0;
|
|
@@ -1333,9 +1540,15 @@ export const ALL_FORMATTERS = [
|
|
|
1333
1540
|
const FILENAME_FORMATTER_BASENAMES = new Set(ALL_FORMATTERS.flatMap((f) => f.filenames ?? []));
|
|
1334
1541
|
// Cache for detection results - stores array of enabled formatter names per cwd+ext
|
|
1335
1542
|
const detectionCache = new BoundedLruCache(32);
|
|
1543
|
+
// The signature is immutable for a cache generation. This memo is separate
|
|
1544
|
+
// from detectionCache because a cwd can have several extension entries, and a
|
|
1545
|
+
// warm lookup must not repeat the ancestor walk or stat matched configs.
|
|
1546
|
+
const formatterSignatureFlights = new BoundedLruCache(32);
|
|
1547
|
+
const formatterCacheGeneration = createGenerationSource("formatter-cache");
|
|
1336
1548
|
// These are the formatter configuration files consulted by the policy helpers
|
|
1337
|
-
// above. Their metadata is
|
|
1338
|
-
//
|
|
1549
|
+
// above. Their metadata is captured by the cold detection signature. The
|
|
1550
|
+
// write-result seam invalidates that signature when a config path changes, so
|
|
1551
|
+
// detection re-runs even when PATH and installed tools are unchanged. A
|
|
1339
1552
|
// filename a `has*Config` check reads but this list omits is invisible to the
|
|
1340
1553
|
// cache: the signature never moves when that file is added, so a project that
|
|
1341
1554
|
// opts in AFTER the first `getFormattersForFile` call for its cwd keeps
|
|
@@ -1436,6 +1649,18 @@ async function formatterConfigSignature(cwd) {
|
|
|
1436
1649
|
}));
|
|
1437
1650
|
return parts.join("|");
|
|
1438
1651
|
}
|
|
1652
|
+
async function getFormatterConfigSignature(cwd, normalizedCwd) {
|
|
1653
|
+
const existing = formatterSignatureFlights.get(normalizedCwd);
|
|
1654
|
+
if (existing)
|
|
1655
|
+
return existing.promise;
|
|
1656
|
+
const promise = formatterConfigSignature(cwd).finally(() => {
|
|
1657
|
+
const current = formatterSignatureFlights.get(normalizedCwd);
|
|
1658
|
+
if (current?.promise === promise)
|
|
1659
|
+
formatterSignatureFlights.delete(normalizedCwd);
|
|
1660
|
+
});
|
|
1661
|
+
formatterSignatureFlights.set(normalizedCwd, { promise });
|
|
1662
|
+
return promise;
|
|
1663
|
+
}
|
|
1439
1664
|
// --- Public API ---
|
|
1440
1665
|
export async function getFormattersForFile(filePath, cwd) {
|
|
1441
1666
|
const ext = path.extname(filePath).toLowerCase();
|
|
@@ -1449,31 +1674,19 @@ export async function getFormattersForFile(filePath, cwd) {
|
|
|
1449
1674
|
const cacheKey = FILENAME_FORMATTER_BASENAMES.has(base)
|
|
1450
1675
|
? `${normalizedCwd}:${ext}:${base}`
|
|
1451
1676
|
: `${normalizedCwd}:${ext}`;
|
|
1452
|
-
|
|
1453
|
-
//
|
|
1677
|
+
// A warm entry is authoritative until the write-result seam invalidates its
|
|
1678
|
+
// config path. This is the only way to make the warm path free of config
|
|
1679
|
+
// polling while still reacting immediately to pi-authored create/remove/
|
|
1680
|
+
// change events. External editor changes remain a documented session-boundary
|
|
1681
|
+
// limitation, like other write-result-owned freshness seams.
|
|
1454
1682
|
let cached = detectionCache.get(normalizedCwd);
|
|
1455
|
-
if (cached
|
|
1456
|
-
// An EXISTING entry whose signature moved is the user telling us the world
|
|
1457
|
-
// changed. Re-probe PATH too, so "install the tool, touch the config" still
|
|
1458
|
-
// works within one session now that a durable absence latches (#1495).
|
|
1459
|
-
// Scoped to a real change on purpose: keying this off "no entry yet" would
|
|
1460
|
-
// drop every PATH verdict on the first save in each new directory, which in
|
|
1461
|
-
// a monorepo means re-probing forever.
|
|
1462
|
-
detectionCache.delete(normalizedCwd);
|
|
1463
|
-
cached = undefined;
|
|
1464
|
-
resetWhichLatches();
|
|
1465
|
-
}
|
|
1466
|
-
if (!cached) {
|
|
1467
|
-
cached = { signature: configSignature, entries: new Map() };
|
|
1468
|
-
detectionCache.set(normalizedCwd, cached);
|
|
1469
|
-
}
|
|
1470
|
-
if (cached.entries.has(cacheKey)) {
|
|
1683
|
+
if (cached?.entries.has(cacheKey)) {
|
|
1471
1684
|
const enabledNames = cached.entries.get(cacheKey);
|
|
1472
1685
|
const selectedFormatterName = enabledNames && enabledNames.length > 0 ? enabledNames[0] : null;
|
|
1473
1686
|
logLatency({
|
|
1474
1687
|
type: "phase",
|
|
1475
1688
|
phase: "formatter_selected",
|
|
1476
|
-
filePath
|
|
1689
|
+
filePath,
|
|
1477
1690
|
durationMs: 0,
|
|
1478
1691
|
metadata: {
|
|
1479
1692
|
formatter: selectedFormatterName,
|
|
@@ -1485,9 +1698,26 @@ export async function getFormattersForFile(filePath, cwd) {
|
|
|
1485
1698
|
});
|
|
1486
1699
|
if (!enabledNames || enabledNames.length === 0)
|
|
1487
1700
|
return [];
|
|
1488
|
-
// Return cached formatters by name (preserves priority order)
|
|
1489
1701
|
return ALL_FORMATTERS.filter((f) => enabledNames.includes(f.name));
|
|
1490
1702
|
}
|
|
1703
|
+
if (!cached) {
|
|
1704
|
+
const generation = formatterCacheGeneration.capture();
|
|
1705
|
+
const configSignature = await getFormatterConfigSignature(cwd, normalizedCwd);
|
|
1706
|
+
// An invalidation can happen while the first signature walk is in flight.
|
|
1707
|
+
// Do not publish a pre-invalidation signature into the new generation.
|
|
1708
|
+
if (!generation.isCurrent()) {
|
|
1709
|
+
return getFormattersForFile(filePath, cwd);
|
|
1710
|
+
}
|
|
1711
|
+
// Another cold caller for this cwd can finish the shared signature flight
|
|
1712
|
+
// first. Re-read the LRU before installing an object so the later caller
|
|
1713
|
+
// merges its extension entry into the existing object instead of replacing
|
|
1714
|
+
// the earlier entry.
|
|
1715
|
+
cached = detectionCache.get(normalizedCwd);
|
|
1716
|
+
if (!cached) {
|
|
1717
|
+
cached = { signature: configSignature, entries: new Map() };
|
|
1718
|
+
detectionCache.set(normalizedCwd, cached);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1491
1721
|
// Detect formatters for this extension (or exact filename, e.g. terragrunt.hcl)
|
|
1492
1722
|
const matching = ALL_FORMATTERS.filter((f) => f.extensions.includes(ext) || f.filenames?.includes(base));
|
|
1493
1723
|
const formatterPolicy = getFormatterPolicyForFile(filePath);
|
|
@@ -1641,7 +1871,11 @@ export async function getFormattersForFile(filePath, cwd) {
|
|
|
1641
1871
|
return enabled;
|
|
1642
1872
|
// Store the list of enabled formatter names in cache
|
|
1643
1873
|
const enabledNames = enabled.map((f) => f.name);
|
|
1644
|
-
|
|
1874
|
+
// An invalidation may have removed or replaced this object while detection
|
|
1875
|
+
// awaited a tool probe. Never repopulate the old generation.
|
|
1876
|
+
if (detectionCache.get(normalizedCwd) === cached) {
|
|
1877
|
+
cached.entries.set(cacheKey, enabledNames);
|
|
1878
|
+
}
|
|
1645
1879
|
return enabled;
|
|
1646
1880
|
}
|
|
1647
1881
|
/**
|
|
@@ -1655,9 +1889,21 @@ export async function getFormattersForFile(filePath, cwd) {
|
|
|
1655
1889
|
* therefore re-arms every directory EXCEPT the one the user is working in.
|
|
1656
1890
|
*/
|
|
1657
1891
|
export function clearFormatterCache() {
|
|
1892
|
+
formatterCacheGeneration.bump();
|
|
1893
|
+
formatterSignatureFlights.clear();
|
|
1658
1894
|
detectionCache.clear();
|
|
1659
1895
|
resetWhichLatches();
|
|
1660
1896
|
}
|
|
1897
|
+
const formatterConfigBasenames = new Set(FORMATTER_CONFIG_FILES.map((fileName) => fileName.toLowerCase()));
|
|
1898
|
+
/** Invalidate selection when the write-result seam reports a config path. */
|
|
1899
|
+
export function invalidateFormatterCacheForPath(filePath) {
|
|
1900
|
+
const basename = filePath.includes("\\")
|
|
1901
|
+
? path.win32.basename(filePath)
|
|
1902
|
+
: path.basename(filePath);
|
|
1903
|
+
if (!formatterConfigBasenames.has(basename.toLowerCase()))
|
|
1904
|
+
return;
|
|
1905
|
+
clearFormatterCache();
|
|
1906
|
+
}
|
|
1661
1907
|
/**
|
|
1662
1908
|
* Test-only internals access for the session-state registry probe (#1895):
|
|
1663
1909
|
* the conformance suite must arm all four pieces of state this reset claims
|
|
@@ -1669,19 +1915,17 @@ export function _getFormatterResetStateForTests() {
|
|
|
1669
1915
|
whichTransientCommands,
|
|
1670
1916
|
cooldownRecordedForRetryAtMs,
|
|
1671
1917
|
detectionCache,
|
|
1918
|
+
formatterSignatureFlights,
|
|
1672
1919
|
};
|
|
1673
1920
|
}
|
|
1674
1921
|
export function clearFormatterRuntimeState() {
|
|
1675
|
-
|
|
1676
|
-
resetWhichLatches();
|
|
1922
|
+
clearFormatterCache();
|
|
1677
1923
|
// NO `resetLazyInstallAttempts()` here (#1537 review F1). This function runs
|
|
1678
1924
|
// from `resetFormatService()`, which `handleTurnEnd` calls every turn — so
|
|
1679
1925
|
// clearing the lazy-install hold here made "held for the session" mean "held
|
|
1680
1926
|
// for a turn", and a failing install re-ran every turn. The hold's only reset
|
|
1681
1927
|
// is `session_start`'s block in runtime-session.ts.
|
|
1682
1928
|
}
|
|
1683
|
-
// ESC is built via fromCharCode so no raw control byte sits in the source.
|
|
1684
|
-
const ANSI_ESCAPE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[A-Za-z]`, "g");
|
|
1685
1929
|
const BOX_DRAWING_GLOBAL = /[\u2500-\u257F]/g;
|
|
1686
1930
|
const HAS_BOX_DRAWING = /[\u2500-\u257F]/;
|
|
1687
1931
|
/**
|
|
@@ -1698,7 +1942,7 @@ const HAS_BOX_DRAWING = /[\u2500-\u257F]/;
|
|
|
1698
1942
|
*/
|
|
1699
1943
|
export function firstDiagnosticLine(text) {
|
|
1700
1944
|
for (const raw of (text ?? "").split("\n")) {
|
|
1701
|
-
const line = raw
|
|
1945
|
+
const line = stripAnsi(raw).trimEnd();
|
|
1702
1946
|
const stripped = line.replace(BOX_DRAWING_GLOBAL, "").trim();
|
|
1703
1947
|
if (!stripped)
|
|
1704
1948
|
continue;
|
|
@@ -1724,6 +1968,12 @@ async function resolveFormatterCommand(formatter, absolutePath, cwd) {
|
|
|
1724
1968
|
: null;
|
|
1725
1969
|
if (resolved === SKIP_FORMATTING)
|
|
1726
1970
|
return SKIP_FORMATTING;
|
|
1971
|
+
// The resolver proved the executable absent — DO NOT fall through to the
|
|
1972
|
+
// static bare command, which is the same binary it just probed and missing
|
|
1973
|
+
// (#2413). The caller turns this into a typed `unavailable` outcome, never a
|
|
1974
|
+
// spawn and never a failure.
|
|
1975
|
+
if (resolved === FORMATTER_UNAVAILABLE)
|
|
1976
|
+
return FORMATTER_UNAVAILABLE;
|
|
1727
1977
|
if (resolved !== null)
|
|
1728
1978
|
return resolved;
|
|
1729
1979
|
const fallback = formatter.command.map((c) => c.replace("$FILE", absolutePath));
|
|
@@ -1743,6 +1993,7 @@ export async function formatFile(filePath, formatter) {
|
|
|
1743
1993
|
try {
|
|
1744
1994
|
const absolutePath = path.resolve(filePath);
|
|
1745
1995
|
const cwd = path.dirname(absolutePath);
|
|
1996
|
+
const formatterCwd = resolveFormatterCwd(absolutePath, formatter.name);
|
|
1746
1997
|
const contentBefore = await fs.readFile(absolutePath, "utf-8");
|
|
1747
1998
|
// Resolve command: prefer local (venv/vendor/node_modules) over global.
|
|
1748
1999
|
// The shared seam must honor SKIP_FORMATTING before selecting the static
|
|
@@ -1751,13 +2002,40 @@ export async function formatFile(filePath, formatter) {
|
|
|
1751
2002
|
if (cmd === SKIP_FORMATTING) {
|
|
1752
2003
|
// Style-preserving refusal (#1144): no repo config and no detectable
|
|
1753
2004
|
// indentation to pin — formatting would impose the tool's stock style.
|
|
1754
|
-
return { success: true, changed: false };
|
|
2005
|
+
return { success: true, changed: false, outcome: "skipped" };
|
|
2006
|
+
}
|
|
2007
|
+
if (cmd === FORMATTER_UNAVAILABLE) {
|
|
2008
|
+
// The resolver proved the executable absent (#2413): the oxfmt ENOENT
|
|
2009
|
+
// trap and its siblings. This is unavailable infrastructure, not a code
|
|
2010
|
+
// failure — success stays true so it is never counted as a failed file
|
|
2011
|
+
// or requeued, and the typed outcome keeps it out of the failure bucket.
|
|
2012
|
+
return {
|
|
2013
|
+
success: true,
|
|
2014
|
+
changed: false,
|
|
2015
|
+
outcome: "unavailable",
|
|
2016
|
+
error: `${formatter.name}: formatter executable not found`,
|
|
2017
|
+
};
|
|
1755
2018
|
}
|
|
1756
2019
|
// Run formatter without blocking the event loop.
|
|
1757
2020
|
const result = await safeSpawnAsync(cmd[0], cmd.slice(1), {
|
|
1758
2021
|
timeout: 15000,
|
|
1759
|
-
cwd,
|
|
2022
|
+
cwd: formatterCwd,
|
|
1760
2023
|
});
|
|
2024
|
+
// A resolver that could NOT prove absence (it never probed PATH — e.g.
|
|
2025
|
+
// black/sqlfluff, which only look in a venv) legitimately falls back to the
|
|
2026
|
+
// bare static command. When that bare command is missing, the spawn boundary
|
|
2027
|
+
// reports a typed `tool-not-found` failure — never a nonzero exit. Treat it
|
|
2028
|
+
// as unavailable, not a formatting failure, closing the same #2413 class for
|
|
2029
|
+
// the formatters whose `null` correctly means "static untried" (no
|
|
2030
|
+
// oxfmt-specific string matching — this reads the typed spawn-failure kind).
|
|
2031
|
+
if (result.spawnFailure?.kind === "tool-not-found") {
|
|
2032
|
+
return {
|
|
2033
|
+
success: true,
|
|
2034
|
+
changed: false,
|
|
2035
|
+
outcome: "unavailable",
|
|
2036
|
+
error: result.spawnFailure.message,
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
1761
2039
|
// Strict by default (#1337): only a formatter with a documented
|
|
1762
2040
|
// benign-nonzero mode (`lenientExitCode`) may exit nonzero and still be
|
|
1763
2041
|
// read as a successful run. Everything else that exits nonzero never
|
|
@@ -1770,6 +2048,7 @@ export async function formatFile(filePath, formatter) {
|
|
|
1770
2048
|
return {
|
|
1771
2049
|
success: false,
|
|
1772
2050
|
changed: false,
|
|
2051
|
+
outcome: "failed",
|
|
1773
2052
|
error: result.error?.message ||
|
|
1774
2053
|
firstDiagnosticLine(result.stderr) ||
|
|
1775
2054
|
// biome, ktlint and `mix format` report on STDOUT; without this
|
|
@@ -1785,12 +2064,14 @@ export async function formatFile(filePath, formatter) {
|
|
|
1785
2064
|
return {
|
|
1786
2065
|
success: true,
|
|
1787
2066
|
changed,
|
|
2067
|
+
outcome: changed ? "formatted" : "unchanged",
|
|
1788
2068
|
};
|
|
1789
2069
|
}
|
|
1790
2070
|
catch (err) {
|
|
1791
2071
|
return {
|
|
1792
2072
|
success: false,
|
|
1793
2073
|
changed: false,
|
|
2074
|
+
outcome: "failed",
|
|
1794
2075
|
error: err instanceof Error ? err.message : String(err),
|
|
1795
2076
|
};
|
|
1796
2077
|
}
|