@herbertgao/pi-extensions 2026.8.3 → 2026.8.5
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 +45 -4
- package/examples/pi-footer.json +275 -0
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +10 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +10 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +7 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +5 -8
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
- package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
- package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +15 -2
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
- package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
- package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
- package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
- package/node_modules/@narumitw/pi-btw/README.md +8 -7
- package/node_modules/@narumitw/pi-btw/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/src/menu.ts +1 -0
- package/node_modules/pi-footer/CHANGELOG.md +145 -0
- package/node_modules/pi-footer/LICENSE +21 -0
- package/node_modules/pi-footer/README.md +354 -0
- package/node_modules/pi-footer/package.json +68 -0
- package/node_modules/pi-footer/src/cache.ts +106 -0
- package/node_modules/pi-footer/src/colors.ts +237 -0
- package/node_modules/pi-footer/src/config.ts +202 -0
- package/node_modules/pi-footer/src/event-widgets.ts +41 -0
- package/node_modules/pi-footer/src/extension-statuses.ts +102 -0
- package/node_modules/pi-footer/src/git.ts +135 -0
- package/node_modules/pi-footer/src/index.ts +268 -0
- package/node_modules/pi-footer/src/metrics.ts +141 -0
- package/node_modules/pi-footer/src/presets.ts +383 -0
- package/node_modules/pi-footer/src/render.ts +114 -0
- package/node_modules/pi-footer/src/separators.ts +52 -0
- package/node_modules/pi-footer/src/types.ts +122 -0
- package/node_modules/pi-footer/src/ui/color-level-confirm.ts +22 -0
- package/node_modules/pi-footer/src/ui/color-options.ts +195 -0
- package/node_modules/pi-footer/src/ui/config-lifecycle.ts +61 -0
- package/node_modules/pi-footer/src/ui/edit-colors.ts +13 -0
- package/node_modules/pi-footer/src/ui/events.ts +35 -0
- package/node_modules/pi-footer/src/ui/extension-status-picker.ts +60 -0
- package/node_modules/pi-footer/src/ui/extension-statuses.ts +73 -0
- package/node_modules/pi-footer/src/ui/fields.ts +252 -0
- package/node_modules/pi-footer/src/ui/global-menu.ts +126 -0
- package/node_modules/pi-footer/src/ui/helpers.ts +44 -0
- package/node_modules/pi-footer/src/ui/layout.ts +17 -0
- package/node_modules/pi-footer/src/ui/line-list.ts +19 -0
- package/node_modules/pi-footer/src/ui/model.ts +64 -0
- package/node_modules/pi-footer/src/ui/navigation.ts +29 -0
- package/node_modules/pi-footer/src/ui/option-edit.ts +93 -0
- package/node_modules/pi-footer/src/ui/overlay-render.ts +103 -0
- package/node_modules/pi-footer/src/ui/screen-context.ts +18 -0
- package/node_modules/pi-footer/src/ui/screen-controller.ts +27 -0
- package/node_modules/pi-footer/src/ui/screen-render.ts +32 -0
- package/node_modules/pi-footer/src/ui/screen-state.ts +20 -0
- package/node_modules/pi-footer/src/ui/screen.ts +243 -0
- package/node_modules/pi-footer/src/ui/screens/add-widget.ts +92 -0
- package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +52 -0
- package/node_modules/pi-footer/src/ui/screens/controller.ts +12 -0
- package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +68 -0
- package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +115 -0
- package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +61 -0
- package/node_modules/pi-footer/src/ui/screens/global.ts +75 -0
- package/node_modules/pi-footer/src/ui/screens/line-list.ts +108 -0
- package/node_modules/pi-footer/src/ui/screens/main.ts +99 -0
- package/node_modules/pi-footer/src/ui/screens/terminal.ts +121 -0
- package/node_modules/pi-footer/src/ui/screens/widget-list.ts +154 -0
- package/node_modules/pi-footer/src/ui/terminal-menu.ts +32 -0
- package/node_modules/pi-footer/src/ui/theme.ts +48 -0
- package/node_modules/pi-footer/src/ui/title-bar.ts +100 -0
- package/node_modules/pi-footer/src/ui/widget-actions.ts +76 -0
- package/node_modules/pi-footer/src/ui.ts +66 -0
- package/node_modules/pi-footer/src/widgets/context.ts +28 -0
- package/node_modules/pi-footer/src/widgets/core/active-tools.ts +17 -0
- package/node_modules/pi-footer/src/widgets/core/context-window.ts +24 -0
- package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +19 -0
- package/node_modules/pi-footer/src/widgets/core/cwd.ts +108 -0
- package/node_modules/pi-footer/src/widgets/core/event.ts +34 -0
- package/node_modules/pi-footer/src/widgets/core/external-status.ts +102 -0
- package/node_modules/pi-footer/src/widgets/core/model-provider.ts +20 -0
- package/node_modules/pi-footer/src/widgets/core/model.ts +35 -0
- package/node_modules/pi-footer/src/widgets/core/provider.ts +17 -0
- package/node_modules/pi-footer/src/widgets/core/session-name.ts +17 -0
- package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +17 -0
- package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/branch.ts +81 -0
- package/node_modules/pi-footer/src/widgets/git/clean.ts +19 -0
- package/node_modules/pi-footer/src/widgets/git/deletions.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/diff.ts +38 -0
- package/node_modules/pi-footer/src/widgets/git/insertions.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/remote.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/root.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/sha.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/staged.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/status.ts +19 -0
- package/node_modules/pi-footer/src/widgets/git/unstaged.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/untracked.ts +17 -0
- package/node_modules/pi-footer/src/widgets/instance.ts +118 -0
- package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +18 -0
- package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +32 -0
- package/node_modules/pi-footer/src/widgets/layout/separator.ts +57 -0
- package/node_modules/pi-footer/src/widgets/layout/spacer.ts +31 -0
- package/node_modules/pi-footer/src/widgets/options.ts +122 -0
- package/node_modules/pi-footer/src/widgets/project/runtime.ts +361 -0
- package/node_modules/pi-footer/src/widgets/registry.ts +223 -0
- package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +17 -0
- package/node_modules/pi-footer/src/widgets/session/compactions.ts +17 -0
- package/node_modules/pi-footer/src/widgets/session/elapsed.ts +18 -0
- package/node_modules/pi-footer/src/widgets/session/last-activity.ts +18 -0
- package/node_modules/pi-footer/src/widgets/session/messages.ts +19 -0
- package/node_modules/pi-footer/src/widgets/session/session-id.ts +17 -0
- package/node_modules/pi-footer/src/widgets/session/session-start.ts +18 -0
- package/node_modules/pi-footer/src/widgets/session/tool-results.ts +17 -0
- package/node_modules/pi-footer/src/widgets/session/total-messages.ts +19 -0
- package/node_modules/pi-footer/src/widgets/session/total-time.ts +18 -0
- package/node_modules/pi-footer/src/widgets/session/user-messages.ts +17 -0
- package/node_modules/pi-footer/src/widgets/store.ts +26 -0
- package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +55 -0
- package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +18 -0
- package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +18 -0
- package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +108 -0
- package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +26 -0
- package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +29 -0
- package/node_modules/pi-footer/src/widgets/tokens/context.ts +29 -0
- package/node_modules/pi-footer/src/widgets/tokens/cost.ts +51 -0
- package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +25 -0
- package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +18 -0
- package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +25 -0
- package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +18 -0
- package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +20 -0
- package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +25 -0
- package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +18 -0
- package/node_modules/pi-footer/src/widgets/types.ts +181 -0
- package/node_modules/pi-footer/src/widgets/utils/colors.ts +11 -0
- package/node_modules/pi-footer/src/widgets/utils/context.ts +112 -0
- package/node_modules/pi-footer/src/widgets/utils/session.ts +19 -0
- package/node_modules/pi-footer/src/widgets/utils/token-format.ts +78 -0
- package/node_modules/pi-lens/CHANGELOG.md +2440 -954
- package/node_modules/pi-lens/README.md +13 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
- package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
- package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
- package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
- package/node_modules/pi-lens/dist/clients/biome-client.js +29 -25
- package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
- package/node_modules/pi-lens/dist/clients/bus-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
- package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
- package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
- package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
- package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
- package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
- package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
- package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +14 -0
- package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
- package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +347 -75
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +61 -11
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +63 -20
- package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
- package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
- package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1085 -681
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
- package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
- package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +14 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +277 -26
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
- package/node_modules/pi-lens/dist/clients/extension-log.js +139 -0
- package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
- package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
- package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
- package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +16 -3
- package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
- package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
- package/node_modules/pi-lens/dist/clients/git-guard.js +681 -11
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
- package/node_modules/pi-lens/dist/clients/go-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +8 -0
- package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
- package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +390 -99
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +31 -54
- package/node_modules/pi-lens/dist/clients/knip-client.js +15 -36
- package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
- package/node_modules/pi-lens/dist/clients/latency-logger.js +23 -1
- package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
- package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
- package/node_modules/pi-lens/dist/clients/lens-events.js +27 -4
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +30 -0
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/client.js +493 -48
- package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +86 -0
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
- package/node_modules/pi-lens/dist/clients/lsp/index.js +1807 -780
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
- package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
- package/node_modules/pi-lens/dist/clients/lsp/server.js +196 -32
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +123 -32
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
- package/node_modules/pi-lens/dist/clients/mcp/session.js +216 -8
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
- package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
- package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
- package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
- package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
- package/node_modules/pi-lens/dist/clients/pipeline.js +47 -13
- package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
- package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
- package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
- package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
- package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
- package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
- package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
- package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
- package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
- package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +107 -73
- package/node_modules/pi-lens/dist/clients/runtime-context.js +36 -12
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +74 -6
- package/node_modules/pi-lens/dist/clients/runtime-session.js +405 -117
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +218 -23
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +237 -64
- package/node_modules/pi-lens/dist/clients/rust-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
- package/node_modules/pi-lens/dist/clients/sg-runner.js +284 -174
- package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +224 -0
- package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
- package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
- package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
- package/node_modules/pi-lens/dist/clients/startup-timing.js +1 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +38 -8
- package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
- package/node_modules/pi-lens/dist/clients/tool-policy.js +388 -39
- package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
- package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
- package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
- package/node_modules/pi-lens/dist/clients/warm-attach.js +33 -13
- package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
- package/node_modules/pi-lens/dist/clients/word-index.js +391 -64
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
- package/node_modules/pi-lens/dist/index.js +49361 -36888
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
- package/node_modules/pi-lens/dist/mcp/server.js +211 -44
- package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +200 -36
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +153 -40
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
- package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
- package/node_modules/pi-lens/docs/agent-guide.md +2 -2
- package/node_modules/pi-lens/docs/agent-tools.md +21 -10
- package/node_modules/pi-lens/docs/analysisall.md +192 -0
- package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +5 -2
- package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
- package/node_modules/pi-lens/docs/environment-variables.md +44 -0
- package/node_modules/pi-lens/docs/fable.md +27 -0
- package/node_modules/pi-lens/docs/features.md +31 -2
- package/node_modules/pi-lens/docs/globalconfig.md +45 -1
- package/node_modules/pi-lens/docs/language-coverage.md +4 -3
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
- package/node_modules/pi-lens/docs/mcp.md +37 -4
- package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
- package/node_modules/pi-lens/docs/settings.md +12 -1
- package/node_modules/pi-lens/docs/tools.md +4 -1
- package/node_modules/pi-lens/docs/usage.md +18 -0
- package/node_modules/pi-lens/package.json +10 -10
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
- package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
- package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +55 -2
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +25 -0
- package/node_modules/pi-mcp-adapter/README.md +34 -3
- package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
- package/node_modules/pi-mcp-adapter/commands.ts +15 -7
- package/node_modules/pi-mcp-adapter/direct-tools.ts +54 -7
- package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
- package/node_modules/pi-mcp-adapter/index.ts +8 -3
- package/node_modules/pi-mcp-adapter/init.ts +29 -5
- package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +27 -4
- package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
- package/node_modules/pi-mcp-adapter/package.json +3 -3
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
- package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
- package/node_modules/pi-mcp-adapter/server-manager.ts +36 -7
- package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
- package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +42 -4
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
- package/node_modules/pi-mcp-adapter/types.ts +93 -27
- package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
- package/package.json +17 -13
- package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
- package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
- package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
- package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
- package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
- package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
- package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
- package/node_modules/pi-lens/dist/clients/types.js +0 -11
|
@@ -8,14 +8,22 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Inspired by OpenCode's formatter.ts pattern
|
|
10
10
|
*/
|
|
11
|
+
import { logExtension } from "./extension-log.js";
|
|
11
12
|
import * as fs from "node:fs/promises";
|
|
12
13
|
import * as path from "node:path";
|
|
14
|
+
import { BoundedLruCache } from "./bounded-cache.js";
|
|
15
|
+
import { normalizeMapKey } from "./path-utils.js";
|
|
16
|
+
import { TERRAGRUNT_FILENAMES } from "./file-kinds.js";
|
|
17
|
+
import { detectIndentation, hasDetectableIndentation, } from "./dispatch/indent-detect.js";
|
|
13
18
|
import { logLatency } from "./latency-logger.js";
|
|
14
19
|
import { findGlobalBinary } from "./package-manager.js";
|
|
15
20
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
16
|
-
import {
|
|
21
|
+
import { assertInstallAllowed } from "./project-trust.js";
|
|
22
|
+
import { getAutoInstallToolIdForFormatter, getFormatterPolicyForFile, getSmartDefaultFormatterName, hasBiomeConfig, hasBlackConfig, hasClangFormatConfig, hasCljfmtConfig, hasCmakeFormatConfig, hasGoogleJavaFormatConfig, hasKtfmtConfig, hasKtlintConfig, hasNearestPackageJsonDependency, hasNearestPackageJsonField, hasOcamlformatConfig, hasOxfmtConfig, hasOxfmtSvelteConfig, hasPhpCsFixerConfig, hasPrettierConfig, hasRubocopConfig, hasRuffConfig, hasSqlfluffConfig, hasStandardrbConfig, hasStyluaConfig, hasVitePlusConfig, OXFMT_SUPPORTED_EXTENSIONS, } from "./tool-policy.js";
|
|
17
23
|
const _lazyInstallAttempts = new Set();
|
|
18
24
|
export async function tryLazyInstallFormatterTool(tool, cwd) {
|
|
25
|
+
if (!assertInstallAllowed(`formatter lazy install: ${tool}`))
|
|
26
|
+
return false;
|
|
19
27
|
const attemptKey = `${tool}:${cwd}`;
|
|
20
28
|
if (_lazyInstallAttempts.has(attemptKey))
|
|
21
29
|
return false;
|
|
@@ -28,7 +36,11 @@ export async function tryLazyInstallFormatterTool(tool, cwd) {
|
|
|
28
36
|
});
|
|
29
37
|
const ok = !res.error && res.status === 0;
|
|
30
38
|
if (!ok) {
|
|
31
|
-
|
|
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
|
+
});
|
|
32
44
|
}
|
|
33
45
|
return ok;
|
|
34
46
|
}
|
|
@@ -39,10 +51,23 @@ export async function tryLazyInstallFormatterTool(tool, cwd) {
|
|
|
39
51
|
});
|
|
40
52
|
const ok = !res.error && res.status === 0;
|
|
41
53
|
if (!ok) {
|
|
42
|
-
|
|
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
|
+
});
|
|
43
59
|
}
|
|
44
60
|
return ok;
|
|
45
61
|
}
|
|
62
|
+
// --- Types ---
|
|
63
|
+
/**
|
|
64
|
+
* Sentinel a `resolveCommand` returns to mean "do not format this file at
|
|
65
|
+
* all" — distinct from `null`, which means "fall back to the static
|
|
66
|
+
* `command`". #1144's style-preserving resolvers return this when the repo
|
|
67
|
+
* has no config AND the file's indentation is undetectable: running the
|
|
68
|
+
* stock command there is exactly the stock-style imposition the fix bans.
|
|
69
|
+
*/
|
|
70
|
+
export const SKIP_FORMATTING = "skip-formatting";
|
|
46
71
|
// --- Utility Functions ---
|
|
47
72
|
async function fileExists(filePath) {
|
|
48
73
|
try {
|
|
@@ -189,13 +214,23 @@ async function resolveManagedSmartDefaultCommand(formatterName, filePath, args)
|
|
|
189
214
|
return null;
|
|
190
215
|
return [installed, ...args, filePath];
|
|
191
216
|
}
|
|
192
|
-
function hasExplicitFormatterConfig(formatterName, cwd) {
|
|
217
|
+
function hasExplicitFormatterConfig(formatterName, cwd, ext) {
|
|
193
218
|
switch (formatterName) {
|
|
194
219
|
case "biome":
|
|
195
220
|
return hasBiomeConfig(cwd);
|
|
196
221
|
case "prettier":
|
|
197
222
|
return (hasPrettierConfig(cwd) || hasNearestPackageJsonField(cwd, "prettier"));
|
|
198
223
|
case "oxfmt":
|
|
224
|
+
// .svelte is conditional beyond "an oxfmt config exists": oxfmt
|
|
225
|
+
// requires the `svelte` package installed AND the config's `svelte`
|
|
226
|
+
// flag enabled (verified empirically — see hasOxfmtSvelteConfig).
|
|
227
|
+
// The generic checks below are NOT sufficient for .svelte — an
|
|
228
|
+
// oxfmt.toml with no svelte flag, or an oxfmt dependency alone,
|
|
229
|
+
// both fail at runtime for .svelte specifically (other extensions
|
|
230
|
+
// are unaffected by this stricter gate).
|
|
231
|
+
if (ext === ".svelte") {
|
|
232
|
+
return hasOxfmtSvelteConfig(cwd);
|
|
233
|
+
}
|
|
199
234
|
return (hasOxfmtConfig(cwd) ||
|
|
200
235
|
hasVitePlusConfig(cwd) ||
|
|
201
236
|
// The published package is `oxfmt`; `@oxc-project/oxfmt` does not
|
|
@@ -224,6 +259,8 @@ function hasExplicitFormatterConfig(formatterName, cwd) {
|
|
|
224
259
|
return hasGoogleJavaFormatConfig(cwd);
|
|
225
260
|
case "ktfmt":
|
|
226
261
|
return hasKtfmtConfig(cwd);
|
|
262
|
+
case "ktlint":
|
|
263
|
+
return hasKtlintConfig(cwd);
|
|
227
264
|
case "cljfmt":
|
|
228
265
|
return hasCljfmtConfig(cwd);
|
|
229
266
|
case "cmake-format":
|
|
@@ -242,28 +279,94 @@ async function hasEditorConfig(cwd) {
|
|
|
242
279
|
return false;
|
|
243
280
|
}
|
|
244
281
|
}
|
|
282
|
+
async function indentationArgs(filePath, tool, cwd) {
|
|
283
|
+
if (await hasEditorConfig(cwd) || hasExplicitFormatterConfig(tool, cwd, path.extname(filePath))) {
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
let content;
|
|
287
|
+
try {
|
|
288
|
+
content = await fs.readFile(filePath, "utf8");
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
// Resolution tests and callers may probe a path before it exists.
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
if (!hasDetectableIndentation(content))
|
|
295
|
+
return null;
|
|
296
|
+
const indentation = detectIndentation(content);
|
|
297
|
+
if (tool === "shfmt")
|
|
298
|
+
return indentation.style === "tab" ? ["-i", "0"] : ["-i", String(indentation.width)];
|
|
299
|
+
if (tool === "prettier") {
|
|
300
|
+
return [indentation.style === "tab" ? "--use-tabs" : "--no-use-tabs", "--tab-width", String(indentation.width)];
|
|
301
|
+
}
|
|
302
|
+
if (tool === "ruff") {
|
|
303
|
+
// `ruff format` has NO --indent-style/--indent-width flags (it errors with
|
|
304
|
+
// "unexpected argument" and exits 2, which formatFile reported as a silent
|
|
305
|
+
// clean no-op back when exit-code strictness was opt-in). Style is pinned
|
|
306
|
+
// through inline TOML overrides instead (#1144 follow-up).
|
|
307
|
+
return [
|
|
308
|
+
"--config",
|
|
309
|
+
`indent-width=${indentation.width}`,
|
|
310
|
+
"--config",
|
|
311
|
+
`format.indent-style='${indentation.style}'`,
|
|
312
|
+
];
|
|
313
|
+
}
|
|
314
|
+
return ["--indent-style", indentation.style, "--indent-width", String(indentation.width)];
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Every biome invocation must carry this. Biome exits 1 with "No files were
|
|
318
|
+
* processed in the specified paths" when the path is ignored by the repo's own
|
|
319
|
+
* biome.json or carries an extension biome does not handle — a benign outcome
|
|
320
|
+
* that, under #1337's strict default, would otherwise report a formatting
|
|
321
|
+
* failure on every edit under an ignored directory. Verified against biome
|
|
322
|
+
* 2.4.12: ignored path → exit 1, +flag → exit 0; unsupported extension → exit
|
|
323
|
+
* 1, +flag → exit 0; and a real syntax error still exits 1 WITH the flag, so
|
|
324
|
+
* strictness is preserved for actual failures.
|
|
325
|
+
*
|
|
326
|
+
* `clients/dispatch/runners/biome-check.ts:108` already passes this on the lint
|
|
327
|
+
* path; the formatter path did not, which is what made biome the one
|
|
328
|
+
* misclassified formatter in the #1337 audit.
|
|
329
|
+
*/
|
|
330
|
+
const BIOME_UNMATCHED_FLAG = "--no-errors-on-unmatched";
|
|
245
331
|
export const biomeFormatter = {
|
|
246
332
|
name: "biome",
|
|
247
|
-
command: [
|
|
333
|
+
command: [
|
|
334
|
+
"npx",
|
|
335
|
+
"@biomejs/biome",
|
|
336
|
+
"format",
|
|
337
|
+
"--write",
|
|
338
|
+
BIOME_UNMATCHED_FLAG,
|
|
339
|
+
"$FILE",
|
|
340
|
+
],
|
|
248
341
|
async resolveCommand(filePath, cwd) {
|
|
249
342
|
const editorConfigFlag = (await hasEditorConfig(cwd))
|
|
250
343
|
? ["--use-editorconfig=true"]
|
|
251
344
|
: [];
|
|
345
|
+
const styleArgs = await indentationArgs(filePath, "biome", cwd);
|
|
346
|
+
if (styleArgs === null)
|
|
347
|
+
return SKIP_FORMATTING;
|
|
348
|
+
const args = [
|
|
349
|
+
"format",
|
|
350
|
+
"--write",
|
|
351
|
+
BIOME_UNMATCHED_FLAG,
|
|
352
|
+
...editorConfigFlag,
|
|
353
|
+
...styleArgs,
|
|
354
|
+
];
|
|
252
355
|
const local = await findInNodeModules("biome", cwd);
|
|
253
356
|
if (local)
|
|
254
|
-
return [local,
|
|
357
|
+
return [local, ...args, filePath];
|
|
255
358
|
// Any package manager's global bin dir (npm/pnpm/yarn/bun) before we
|
|
256
359
|
// auto-install — catches a `pnpm add -g @biomejs/biome` PATH misses (#375).
|
|
257
360
|
const global = await findGlobalBinary("biome");
|
|
258
361
|
if (global)
|
|
259
|
-
return [global,
|
|
362
|
+
return [global, ...args, filePath];
|
|
260
363
|
const toolId = getAutoInstallToolIdForFormatter("biome");
|
|
261
364
|
if (!toolId)
|
|
262
365
|
return null;
|
|
263
366
|
const { ensureTool } = await import("./installer/index.js");
|
|
264
367
|
const installed = await ensureTool(toolId);
|
|
265
368
|
if (installed)
|
|
266
|
-
return [installed,
|
|
369
|
+
return [installed, ...args, filePath];
|
|
267
370
|
return null;
|
|
268
371
|
},
|
|
269
372
|
extensions: [
|
|
@@ -294,14 +397,18 @@ export const prettierFormatter = {
|
|
|
294
397
|
name: "prettier",
|
|
295
398
|
command: ["npx", "prettier", "--write", "$FILE"],
|
|
296
399
|
async resolveCommand(filePath, cwd) {
|
|
400
|
+
const styleArgs = await indentationArgs(filePath, "prettier", cwd);
|
|
401
|
+
if (styleArgs === null)
|
|
402
|
+
return SKIP_FORMATTING;
|
|
403
|
+
const args = ["--write", ...styleArgs];
|
|
297
404
|
const local = await findInNodeModules("prettier", cwd);
|
|
298
405
|
if (local)
|
|
299
|
-
return [local,
|
|
406
|
+
return [local, ...args, filePath];
|
|
300
407
|
// Global bin of any manager (npm/pnpm/yarn/bun) before auto-install (#375).
|
|
301
408
|
const global = await findGlobalBinary("prettier");
|
|
302
409
|
if (global)
|
|
303
|
-
return [global,
|
|
304
|
-
return resolveManagedSmartDefaultCommand("prettier", filePath,
|
|
410
|
+
return [global, ...args, filePath];
|
|
411
|
+
return resolveManagedSmartDefaultCommand("prettier", filePath, args);
|
|
305
412
|
},
|
|
306
413
|
extensions: [
|
|
307
414
|
".js",
|
|
@@ -338,6 +445,11 @@ export const prettierFormatter = {
|
|
|
338
445
|
export const oxfmtFormatter = {
|
|
339
446
|
name: "oxfmt",
|
|
340
447
|
command: ["oxfmt", "$FILE"],
|
|
448
|
+
// #1337 audit: oxfmt (and the `vp fmt --write` path) publish no exit-code
|
|
449
|
+
// table. `--write` is the default and `--check` is the separate verification
|
|
450
|
+
// mode, so there is no documented nonzero-on-reformat. Absent a documented
|
|
451
|
+
// benign-nonzero mode, it stays strict — the safe direction, since the failure
|
|
452
|
+
// mode of guessing wrong the other way is a silent no-op (#1336).
|
|
341
453
|
async resolveCommand(filePath, cwd) {
|
|
342
454
|
if (hasVitePlusConfig(cwd)) {
|
|
343
455
|
const localVp = await findInNodeModules("vp", cwd);
|
|
@@ -355,18 +467,10 @@ export const oxfmtFormatter = {
|
|
|
355
467
|
return [found, filePath];
|
|
356
468
|
return null;
|
|
357
469
|
},
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
".css", ".scss", ".less",
|
|
363
|
-
".html", ".htm",
|
|
364
|
-
".json", ".jsonc",
|
|
365
|
-
".yaml", ".yml",
|
|
366
|
-
".md", ".mdx",
|
|
367
|
-
".graphql", ".gql",
|
|
368
|
-
".toml",
|
|
369
|
-
],
|
|
470
|
+
// Single source of truth: OXFMT_SUPPORTED_EXTENSIONS in tool-policy.ts.
|
|
471
|
+
// Do not hand-maintain a second copy of this list (#1134 — previously two
|
|
472
|
+
// parallel hand-maintained lists, the #883 single-source-of-truth class).
|
|
473
|
+
extensions: [...OXFMT_SUPPORTED_EXTENSIONS],
|
|
370
474
|
async detect(cwd) {
|
|
371
475
|
return (hasOxfmtConfig(cwd) ||
|
|
372
476
|
hasVitePlusConfig(cwd) ||
|
|
@@ -379,17 +483,25 @@ export const ruffFormatter = {
|
|
|
379
483
|
name: "ruff",
|
|
380
484
|
command: ["ruff", "format", "$FILE"],
|
|
381
485
|
extensions: [".py", ".pyi"],
|
|
486
|
+
// Strict (the #1337 default): `ruff format` exits 0 on a successful in-place
|
|
487
|
+
// rewrite and 2 on argument rejection / syntax error (verified, ruff 0.x:
|
|
488
|
+
// well-formed → 0, reformatted → 0, unparseable → 2). The exit-2 no-op is
|
|
489
|
+
// exactly what hid the bad --indent-style flags for a full release cycle.
|
|
382
490
|
async resolveCommand(filePath, cwd) {
|
|
491
|
+
const styleArgs = await indentationArgs(filePath, "ruff", cwd);
|
|
492
|
+
if (styleArgs === null)
|
|
493
|
+
return SKIP_FORMATTING;
|
|
494
|
+
const args = ["format", ...styleArgs];
|
|
383
495
|
const venv = await findInVenv("ruff", cwd);
|
|
384
496
|
if (venv)
|
|
385
|
-
return [venv,
|
|
497
|
+
return [venv, ...args, filePath];
|
|
386
498
|
const toolId = getAutoInstallToolIdForFormatter("ruff");
|
|
387
499
|
if (!toolId)
|
|
388
500
|
return null;
|
|
389
501
|
const { ensureTool } = await import("./installer/index.js");
|
|
390
502
|
const installed = await ensureTool(toolId);
|
|
391
503
|
if (installed)
|
|
392
|
-
return [installed,
|
|
504
|
+
return [installed, ...args, filePath];
|
|
393
505
|
return null;
|
|
394
506
|
},
|
|
395
507
|
async detect(cwd) {
|
|
@@ -420,6 +532,11 @@ export const sqlfluffFormatter = {
|
|
|
420
532
|
name: "sqlfluff",
|
|
421
533
|
command: ["sqlfluff", "fix", "--force", "$FILE"],
|
|
422
534
|
extensions: [".sql"],
|
|
535
|
+
lenientExitCode: "lint-autofix: `sqlfluff fix` writes the corrected file and then exits 1 " +
|
|
536
|
+
"when unfixable violations remain (its documented exit codes are 0 = all " +
|
|
537
|
+
"clean, 1 = violations remain, 2 = command/config failure), so a nonzero " +
|
|
538
|
+
"exit routinely accompanies a successful rewrite.",
|
|
539
|
+
lenientStatuses: [1],
|
|
423
540
|
async resolveCommand(filePath, cwd) {
|
|
424
541
|
const venv = await findInVenv("sqlfluff", cwd);
|
|
425
542
|
if (venv)
|
|
@@ -481,11 +598,14 @@ export const shfmtFormatter = {
|
|
|
481
598
|
name: "shfmt",
|
|
482
599
|
command: ["shfmt", "-w", "$FILE"],
|
|
483
600
|
extensions: [".sh", ".bash"],
|
|
484
|
-
async resolveCommand(filePath,
|
|
601
|
+
async resolveCommand(filePath, cwd) {
|
|
602
|
+
const styleArgs = await indentationArgs(filePath, "shfmt", cwd);
|
|
603
|
+
if (styleArgs === null)
|
|
604
|
+
return SKIP_FORMATTING;
|
|
485
605
|
const inPath = await which("shfmt");
|
|
486
606
|
if (inPath)
|
|
487
|
-
return [inPath, "-w", filePath];
|
|
488
|
-
return resolveManagedSmartDefaultCommand("shfmt", filePath, ["-w"]);
|
|
607
|
+
return [inPath, "-w", ...styleArgs, filePath];
|
|
608
|
+
return resolveManagedSmartDefaultCommand("shfmt", filePath, ["-w", ...styleArgs]);
|
|
489
609
|
},
|
|
490
610
|
async detect(_cwd) {
|
|
491
611
|
if ((await which("shfmt")) !== null)
|
|
@@ -497,6 +617,9 @@ export const shfmtFormatter = {
|
|
|
497
617
|
export const nixfmtFormatter = {
|
|
498
618
|
name: "nixfmt",
|
|
499
619
|
command: ["nixfmt", "$FILE"],
|
|
620
|
+
// #1337 audit: nixfmt's README documents in-place formatting but has no
|
|
621
|
+
// exit-code section and no change-detection mode, so nothing documents a
|
|
622
|
+
// benign nonzero. Strict by default (see oxfmt above for the rationale).
|
|
500
623
|
extensions: [".nix"],
|
|
501
624
|
async detect(_cwd) {
|
|
502
625
|
return (await which("nixfmt")) !== null;
|
|
@@ -540,6 +663,11 @@ export const ktlintFormatter = {
|
|
|
540
663
|
name: "ktlint",
|
|
541
664
|
command: ["ktlint", "-F", "$FILE"],
|
|
542
665
|
extensions: [".kt", ".kts"],
|
|
666
|
+
lenientExitCode: "lint-autofix: `ktlint -F` autocorrects what it can and then exits 1 if " +
|
|
667
|
+
"any lint error remains unfixed — the documented CLI contract (ktlint " +
|
|
668
|
+
"exits nonzero whenever violations are reported, and -F does not suppress " +
|
|
669
|
+
"the ones it cannot correct).",
|
|
670
|
+
lenientStatuses: [1],
|
|
543
671
|
async resolveCommand(filePath, _cwd) {
|
|
544
672
|
const inPath = await which("ktlint");
|
|
545
673
|
if (inPath)
|
|
@@ -576,6 +704,12 @@ export const rubocopFormatter = {
|
|
|
576
704
|
name: "rubocop",
|
|
577
705
|
command: ["rubocop", "-a", "--no-color", "$FILE"],
|
|
578
706
|
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
|
707
|
+
lenientExitCode: "lint-autofix: `rubocop -a` exits 1 whenever ANY offense remains after it " +
|
|
708
|
+
"has already rewritten the file. Verified locally (rubocop on Ruby 3.4): a " +
|
|
709
|
+
"file with an unfixable Lint/UselessAssignment exits 1, and even a " +
|
|
710
|
+
"tidy file exits 1 on an unfixable Style/Documentation offense — nonzero " +
|
|
711
|
+
"is the normal outcome, not a failure.",
|
|
712
|
+
lenientStatuses: [1],
|
|
579
713
|
async resolveCommand(filePath, cwd) {
|
|
580
714
|
if (await canUseBundleExec(cwd))
|
|
581
715
|
return ["bundle", "exec", "rubocop", "-a", "--no-color", filePath];
|
|
@@ -594,6 +728,9 @@ export const standardrbFormatter = {
|
|
|
594
728
|
name: "standardrb",
|
|
595
729
|
command: ["standardrb", "--fix", "$FILE"],
|
|
596
730
|
extensions: [".rb", ".rake"],
|
|
731
|
+
lenientExitCode: "lint-autofix: standardrb is a RuboCop wrapper and inherits its exit " +
|
|
732
|
+
"contract — `--fix` exits 1 when offenses remain after the rewrite.",
|
|
733
|
+
lenientStatuses: [1],
|
|
597
734
|
async resolveCommand(filePath, cwd) {
|
|
598
735
|
if (await canUseBundleExec(cwd))
|
|
599
736
|
return ["bundle", "exec", "standardrb", "--fix", filePath];
|
|
@@ -625,6 +762,18 @@ export const terraformFormatter = {
|
|
|
625
762
|
return (await which("terraform")) !== null;
|
|
626
763
|
},
|
|
627
764
|
};
|
|
765
|
+
export const terragruntHclFormatter = {
|
|
766
|
+
name: "terragrunt-hcl",
|
|
767
|
+
command: ["terragrunt", "hcl", "fmt", "--file", "$FILE"],
|
|
768
|
+
extensions: [],
|
|
769
|
+
filenames: TERRAGRUNT_FILENAMES,
|
|
770
|
+
// Strict (the #1337 default): verified exit 0 on a successful in-place format
|
|
771
|
+
// against terragrunt v1.1.2. A binary predating the `hcl` command group exits
|
|
772
|
+
// nonzero and touches nothing, which unguarded reads as "already formatted".
|
|
773
|
+
async detect(_cwd) {
|
|
774
|
+
return (await which("terragrunt")) !== null;
|
|
775
|
+
},
|
|
776
|
+
};
|
|
628
777
|
export const phpCsFixerFormatter = {
|
|
629
778
|
name: "php-cs-fixer",
|
|
630
779
|
command: ["php-cs-fixer", "fix", "$FILE"],
|
|
@@ -766,24 +915,44 @@ export const cmakeFormatFormatter = {
|
|
|
766
915
|
return hasCmakeFormatConfig(cwd);
|
|
767
916
|
},
|
|
768
917
|
};
|
|
918
|
+
/**
|
|
919
|
+
* The PowerShell one-liner behind `psscriptanalyzer-format`.
|
|
920
|
+
*
|
|
921
|
+
* #1337 audit finding: without `$ErrorActionPreference = 'Stop'`, a failing
|
|
922
|
+
* `Invoke-Formatter` is a NON-TERMINATING error — pwsh still exits 0, so this
|
|
923
|
+
* formatter could never report a failure through the strict seam and the #1336
|
|
924
|
+
* silent-no-op class survived intact for `.ps1/.psm1/.psd1`. Verified: an
|
|
925
|
+
* `Invoke-Formatter` argument-validation failure exits 0 under the old script.
|
|
926
|
+
*
|
|
927
|
+
* Two more defects fixed here rather than left as landmines:
|
|
928
|
+
* - the old script ran `Set-Content -Value $formatted` even when `$formatted`
|
|
929
|
+
* was `$null`, which TRUNCATES the file it was asked to format;
|
|
930
|
+
* - single-quoted interpolation broke on any path containing an apostrophe.
|
|
931
|
+
* An empty/whitespace-only file exits 0 without touching anything, so "nothing
|
|
932
|
+
* to format" stays a clean no-op rather than a reported failure.
|
|
933
|
+
*/
|
|
934
|
+
function psScriptAnalyzerCommand(filePath) {
|
|
935
|
+
// PowerShell single-quoted strings escape an apostrophe by doubling it.
|
|
936
|
+
const quoted = filePath.replace(/'/g, "''");
|
|
937
|
+
return [
|
|
938
|
+
"$ErrorActionPreference = 'Stop'",
|
|
939
|
+
`$p = '${quoted}'`,
|
|
940
|
+
"$content = Get-Content -Raw -LiteralPath $p",
|
|
941
|
+
"if ([string]::IsNullOrWhiteSpace($content)) { exit 0 }",
|
|
942
|
+
"$formatted = Invoke-Formatter -ScriptDefinition $content",
|
|
943
|
+
"if ($null -eq $formatted) { throw 'Invoke-Formatter returned no output' }",
|
|
944
|
+
"Set-Content -LiteralPath $p -Value $formatted",
|
|
945
|
+
].join("; ");
|
|
946
|
+
}
|
|
769
947
|
export const psscriptanalyzerFormatFormatter = {
|
|
770
948
|
name: "psscriptanalyzer-format",
|
|
771
|
-
command: [
|
|
772
|
-
"pwsh",
|
|
773
|
-
"-Command",
|
|
774
|
-
"Invoke-Formatter -ScriptDefinition (Get-Content -Raw '$FILE') | Set-Content '$FILE'",
|
|
775
|
-
],
|
|
949
|
+
command: ["pwsh", "-NoProfile", "-Command", psScriptAnalyzerCommand("$FILE")],
|
|
776
950
|
extensions: [".ps1", ".psm1", ".psd1"],
|
|
777
951
|
async resolveCommand(filePath, _cwd) {
|
|
778
952
|
const pwsh = (await which("pwsh")) ?? (await which("powershell"));
|
|
779
953
|
if (!pwsh)
|
|
780
954
|
return null;
|
|
781
|
-
return [
|
|
782
|
-
pwsh,
|
|
783
|
-
"-NoProfile",
|
|
784
|
-
"-Command",
|
|
785
|
-
`$content = Get-Content -Raw '${filePath}'; $formatted = Invoke-Formatter -ScriptDefinition $content; Set-Content -Path '${filePath}' -Value $formatted`,
|
|
786
|
-
];
|
|
955
|
+
return [pwsh, "-NoProfile", "-Command", psScriptAnalyzerCommand(filePath)];
|
|
787
956
|
},
|
|
788
957
|
async detect(_cwd) {
|
|
789
958
|
const pwsh = (await which("pwsh")) ?? (await which("powershell"));
|
|
@@ -799,7 +968,11 @@ export const psscriptanalyzerFormatFormatter = {
|
|
|
799
968
|
},
|
|
800
969
|
};
|
|
801
970
|
// --- Registry ---
|
|
802
|
-
|
|
971
|
+
// Exported for the formatter/policy drift guard (tests/clients/
|
|
972
|
+
// formatter-policy-consistency.test.ts, #1135): the test cross-checks these
|
|
973
|
+
// definitions against tool-policy.ts's FORMATTER_POLICY_BY_EXTENSION so the two
|
|
974
|
+
// hand-maintained inverse mappings can never silently diverge.
|
|
975
|
+
export const ALL_FORMATTERS = [
|
|
803
976
|
biomeFormatter,
|
|
804
977
|
prettierFormatter,
|
|
805
978
|
oxfmtFormatter,
|
|
@@ -818,6 +991,7 @@ const ALL_FORMATTERS = [
|
|
|
818
991
|
ktlintFormatter,
|
|
819
992
|
ktfmtFormatter,
|
|
820
993
|
terraformFormatter,
|
|
994
|
+
terragruntHclFormatter,
|
|
821
995
|
phpCsFixerFormatter,
|
|
822
996
|
csharpierFormatter,
|
|
823
997
|
fantomasFormatter,
|
|
@@ -833,27 +1007,70 @@ const ALL_FORMATTERS = [
|
|
|
833
1007
|
cmakeFormatFormatter,
|
|
834
1008
|
psscriptanalyzerFormatFormatter,
|
|
835
1009
|
];
|
|
1010
|
+
// Basenames claimed by a filename-keyed formatter, e.g. terragrunt.hcl.
|
|
1011
|
+
const FILENAME_FORMATTER_BASENAMES = new Set(ALL_FORMATTERS.flatMap((f) => f.filenames ?? []));
|
|
836
1012
|
// Cache for detection results - stores array of enabled formatter names per cwd+ext
|
|
837
|
-
const detectionCache = new
|
|
1013
|
+
const detectionCache = new BoundedLruCache(32);
|
|
1014
|
+
// These are the formatter configuration files consulted by the policy helpers
|
|
1015
|
+
// 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.
|
|
1017
|
+
const FORMATTER_CONFIG_FILES = [
|
|
1018
|
+
"package.json", "biome.json", "biome.jsonc", ".prettierrc", ".prettierrc.json",
|
|
1019
|
+
".prettierrc.yml", ".prettierrc.yaml", ".prettierrc.js", ".prettierrc.cjs",
|
|
1020
|
+
".prettierrc.mjs", "prettier.config.js", "prettier.config.cjs", "prettier.config.mjs",
|
|
1021
|
+
"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",
|
|
1023
|
+
"_clang-format", ".php-cs-fixer.php", ".php-cs-fixer.dist.php", "stylua.toml",
|
|
1024
|
+
".stylua.toml", ".ocamlformat", ".editorconfig", ".ktfmt", ".ktfmt.kts",
|
|
1025
|
+
".cljfmt.edn", "cmake-format.py", ".cmake-format.yaml", ".cmake-format.json",
|
|
1026
|
+
"oxfmt.toml", ".oxfmtrc.json", "vite.config.ts", "vite.config.js", "vite.config.mjs",
|
|
1027
|
+
];
|
|
1028
|
+
async function formatterConfigSignature(cwd) {
|
|
1029
|
+
const paths = await findUp(FORMATTER_CONFIG_FILES, cwd);
|
|
1030
|
+
const parts = await Promise.all(paths.sort((a, b) => a.localeCompare(b)).map(async (filePath) => {
|
|
1031
|
+
try {
|
|
1032
|
+
const stat = await fs.stat(filePath);
|
|
1033
|
+
return `${filePath}:${stat.mtimeMs}:${stat.size}`;
|
|
1034
|
+
}
|
|
1035
|
+
catch {
|
|
1036
|
+
return `${filePath}:missing`;
|
|
1037
|
+
}
|
|
1038
|
+
}));
|
|
1039
|
+
return parts.join("|");
|
|
1040
|
+
}
|
|
838
1041
|
// --- Public API ---
|
|
839
1042
|
export async function getFormattersForFile(filePath, cwd) {
|
|
840
1043
|
const ext = path.extname(filePath).toLowerCase();
|
|
841
|
-
const
|
|
1044
|
+
const base = path.basename(filePath).toLowerCase();
|
|
1045
|
+
// Filename-keyed formatters (e.g. terragrunt.hcl) can share an extension
|
|
1046
|
+
// with unrelated files in the same dir (.terraform.lock.hcl next to
|
|
1047
|
+
// terragrunt.hcl). Fold the basename into the cache key only when a
|
|
1048
|
+
// filename-based formatter actually applies, so a plain .hcl file cached
|
|
1049
|
+
// first doesn't poison the cache for terragrunt.hcl/root.hcl, or vice versa.
|
|
1050
|
+
const normalizedCwd = normalizeMapKey(cwd);
|
|
1051
|
+
const cacheKey = FILENAME_FORMATTER_BASENAMES.has(base)
|
|
1052
|
+
? `${normalizedCwd}:${ext}:${base}`
|
|
1053
|
+
: `${normalizedCwd}:${ext}`;
|
|
1054
|
+
const configSignature = await formatterConfigSignature(cwd);
|
|
842
1055
|
// Check cache
|
|
843
|
-
let cached = detectionCache.get(
|
|
1056
|
+
let cached = detectionCache.get(normalizedCwd);
|
|
1057
|
+
if (cached?.signature !== configSignature) {
|
|
1058
|
+
cached = undefined;
|
|
1059
|
+
detectionCache.delete(normalizedCwd);
|
|
1060
|
+
}
|
|
844
1061
|
if (!cached) {
|
|
845
|
-
cached = new Map();
|
|
846
|
-
detectionCache.set(
|
|
1062
|
+
cached = { signature: configSignature, entries: new Map() };
|
|
1063
|
+
detectionCache.set(normalizedCwd, cached);
|
|
847
1064
|
}
|
|
848
|
-
if (cached.has(cacheKey)) {
|
|
849
|
-
const enabledNames = cached.get(cacheKey);
|
|
1065
|
+
if (cached.entries.has(cacheKey)) {
|
|
1066
|
+
const enabledNames = cached.entries.get(cacheKey);
|
|
850
1067
|
if (!enabledNames || enabledNames.length === 0)
|
|
851
1068
|
return [];
|
|
852
1069
|
// Return cached formatters by name (preserves priority order)
|
|
853
1070
|
return ALL_FORMATTERS.filter((f) => enabledNames.includes(f.name));
|
|
854
1071
|
}
|
|
855
|
-
// Detect formatters for this extension
|
|
856
|
-
const matching = ALL_FORMATTERS.filter((f) => f.extensions.includes(ext));
|
|
1072
|
+
// Detect formatters for this extension (or exact filename, e.g. terragrunt.hcl)
|
|
1073
|
+
const matching = ALL_FORMATTERS.filter((f) => f.extensions.includes(ext) || f.filenames?.includes(base));
|
|
857
1074
|
const formatterPolicy = getFormatterPolicyForFile(filePath);
|
|
858
1075
|
const smartDefaultFormatterName = getSmartDefaultFormatterName(filePath);
|
|
859
1076
|
const candidateFormatters = formatterPolicy?.formatterNames?.length
|
|
@@ -861,7 +1078,7 @@ export async function getFormattersForFile(filePath, cwd) {
|
|
|
861
1078
|
: matching;
|
|
862
1079
|
let selected;
|
|
863
1080
|
if (formatterPolicy) {
|
|
864
|
-
const explicitlyConfigured = candidateFormatters.filter((formatter) => hasExplicitFormatterConfig(formatter.name, cwd));
|
|
1081
|
+
const explicitlyConfigured = candidateFormatters.filter((formatter) => hasExplicitFormatterConfig(formatter.name, cwd, ext));
|
|
865
1082
|
if (explicitlyConfigured.length > 0) {
|
|
866
1083
|
// A formatter with explicit project config was found — use it.
|
|
867
1084
|
// Prefer the policy's defaultFormatter only if it has explicit config,
|
|
@@ -892,7 +1109,11 @@ export async function getFormattersForFile(filePath, cwd) {
|
|
|
892
1109
|
}
|
|
893
1110
|
catch (err) {
|
|
894
1111
|
// pi-lens-ignore: missing-error-propagation — optional formatter detection, skip on failure
|
|
895
|
-
|
|
1112
|
+
logExtension({
|
|
1113
|
+
subsystem: "format",
|
|
1114
|
+
message: `Detection failed for ${formatter.name}: ${err instanceof Error ? err.message : String(err)}`,
|
|
1115
|
+
metadata: { formatter: formatter.name, cwd },
|
|
1116
|
+
});
|
|
896
1117
|
}
|
|
897
1118
|
}
|
|
898
1119
|
}
|
|
@@ -905,7 +1126,7 @@ export async function getFormattersForFile(filePath, cwd) {
|
|
|
905
1126
|
selectionReason = "detect";
|
|
906
1127
|
}
|
|
907
1128
|
else {
|
|
908
|
-
selectionReason = candidateFormatters.some((f) => hasExplicitFormatterConfig(f.name, cwd))
|
|
1129
|
+
selectionReason = candidateFormatters.some((f) => hasExplicitFormatterConfig(f.name, cwd, ext))
|
|
909
1130
|
? "explicit-config"
|
|
910
1131
|
: "smart-default";
|
|
911
1132
|
}
|
|
@@ -922,7 +1143,7 @@ export async function getFormattersForFile(filePath, cwd) {
|
|
|
922
1143
|
});
|
|
923
1144
|
// Store the list of enabled formatter names in cache
|
|
924
1145
|
const enabledNames = enabled.map((f) => f.name);
|
|
925
|
-
cached.set(cacheKey, enabledNames);
|
|
1146
|
+
cached.entries.set(cacheKey, enabledNames);
|
|
926
1147
|
return enabled;
|
|
927
1148
|
}
|
|
928
1149
|
export function clearFormatterCache() {
|
|
@@ -932,27 +1153,103 @@ export function clearFormatterRuntimeState() {
|
|
|
932
1153
|
detectionCache.clear();
|
|
933
1154
|
_lazyInstallAttempts.clear();
|
|
934
1155
|
}
|
|
1156
|
+
// ESC is built via fromCharCode so no raw control byte sits in the source.
|
|
1157
|
+
const ANSI_ESCAPE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[A-Za-z]`, "g");
|
|
1158
|
+
const BOX_DRAWING_GLOBAL = /[\u2500-\u257F]/g;
|
|
1159
|
+
const HAS_BOX_DRAWING = /[\u2500-\u257F]/;
|
|
1160
|
+
/**
|
|
1161
|
+
* First line of `text` that actually carries a diagnostic.
|
|
1162
|
+
*
|
|
1163
|
+
* #1337 made nonzero exits user-visible, which put whatever this returns in
|
|
1164
|
+
* front of the agent (`clients/pipeline.ts`) and in the end-of-turn summary
|
|
1165
|
+
* (`clients/runtime-agent-end.ts`) — so "first line of stderr" is no longer
|
|
1166
|
+
* good enough. Biome opens stderr with a decorated section rule
|
|
1167
|
+
* (`format ━━━━━━━━━━━`), which as an error message is pure noise.
|
|
1168
|
+
*
|
|
1169
|
+
* Skips blank lines, pure box-drawing rules, and short banner headings; strips
|
|
1170
|
+
* ANSI colour so the message stays readable in a plain-text surface.
|
|
1171
|
+
*/
|
|
1172
|
+
export function firstDiagnosticLine(text) {
|
|
1173
|
+
for (const raw of (text ?? "").split("\n")) {
|
|
1174
|
+
const line = raw.replace(ANSI_ESCAPE, "").trimEnd();
|
|
1175
|
+
const stripped = line.replace(BOX_DRAWING_GLOBAL, "").trim();
|
|
1176
|
+
if (!stripped)
|
|
1177
|
+
continue;
|
|
1178
|
+
// "format ━━━━━━━━" is a section banner, not a diagnostic. Require a rule
|
|
1179
|
+
// AND a short remainder so a real one-line error containing a box
|
|
1180
|
+
// character is not discarded.
|
|
1181
|
+
if (HAS_BOX_DRAWING.test(line) && stripped.length <= 24)
|
|
1182
|
+
continue;
|
|
1183
|
+
return stripped.slice(0, 300);
|
|
1184
|
+
}
|
|
1185
|
+
return undefined;
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Resolve a formatter command without allowing the static command fallback to
|
|
1189
|
+
* bypass a resolver's style-preservation refusal (#1345). `null` means the
|
|
1190
|
+
* primary command is unavailable; the explicit sentinel means formatting is
|
|
1191
|
+
* forbidden for this file and must be returned before the static command is
|
|
1192
|
+
* materialized.
|
|
1193
|
+
*/
|
|
1194
|
+
async function resolveFormatterCommand(formatter, absolutePath, cwd) {
|
|
1195
|
+
const resolved = formatter.resolveCommand
|
|
1196
|
+
? await formatter.resolveCommand(absolutePath, cwd)
|
|
1197
|
+
: null;
|
|
1198
|
+
if (resolved === SKIP_FORMATTING)
|
|
1199
|
+
return SKIP_FORMATTING;
|
|
1200
|
+
if (resolved !== null)
|
|
1201
|
+
return resolved;
|
|
1202
|
+
const fallback = formatter.command.map((c) => c.replace("$FILE", absolutePath));
|
|
1203
|
+
// Trust gate on the install-capable static fallback (#1334 S5): npx can
|
|
1204
|
+
// DOWNLOAD packages, so an untrusted project treats the fallback as
|
|
1205
|
+
// unavailable -- a skip, not a formatter failure; may converge next turn.
|
|
1206
|
+
if (fallback[0] === "npx" &&
|
|
1207
|
+
!assertInstallAllowed(`formatter npx fallback: ${formatter.name}`)) {
|
|
1208
|
+
// No second ledger entry here (#1366 review): assertInstallAllowed just
|
|
1209
|
+
// recorded the trust-refusal with this formatter's context — recording
|
|
1210
|
+
// formatter-skip too would count one user-visible degradation twice.
|
|
1211
|
+
return SKIP_FORMATTING;
|
|
1212
|
+
}
|
|
1213
|
+
return fallback;
|
|
1214
|
+
}
|
|
935
1215
|
export async function formatFile(filePath, formatter) {
|
|
936
1216
|
try {
|
|
937
1217
|
const absolutePath = path.resolve(filePath);
|
|
938
1218
|
const cwd = path.dirname(absolutePath);
|
|
939
1219
|
const contentBefore = await fs.readFile(absolutePath, "utf-8");
|
|
940
|
-
// Resolve command: prefer local (venv/vendor/node_modules) over global
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1220
|
+
// Resolve command: prefer local (venv/vendor/node_modules) over global.
|
|
1221
|
+
// The shared seam must honor SKIP_FORMATTING before selecting the static
|
|
1222
|
+
// command, including its npx fallback (#1345).
|
|
1223
|
+
const cmd = await resolveFormatterCommand(formatter, absolutePath, cwd);
|
|
1224
|
+
if (cmd === SKIP_FORMATTING) {
|
|
1225
|
+
// Style-preserving refusal (#1144): no repo config and no detectable
|
|
1226
|
+
// indentation to pin — formatting would impose the tool's stock style.
|
|
1227
|
+
return { success: true, changed: false };
|
|
1228
|
+
}
|
|
946
1229
|
// Run formatter without blocking the event loop.
|
|
947
1230
|
const result = await safeSpawnAsync(cmd[0], cmd.slice(1), {
|
|
948
1231
|
timeout: 15000,
|
|
949
1232
|
cwd,
|
|
950
1233
|
});
|
|
951
|
-
|
|
1234
|
+
// Strict by default (#1337): only a formatter with a documented
|
|
1235
|
+
// benign-nonzero mode (`lenientExitCode`) may exit nonzero and still be
|
|
1236
|
+
// read as a successful run. Everything else that exits nonzero never
|
|
1237
|
+
// rewrote the file, and reporting {success: true, changed: false} there is
|
|
1238
|
+
// indistinguishable from "already formatted" — the #1336 silent no-op.
|
|
1239
|
+
const lenientOk = formatter.lenientExitCode !== undefined &&
|
|
1240
|
+
result.status !== null &&
|
|
1241
|
+
(formatter.lenientStatuses ?? []).includes(result.status);
|
|
1242
|
+
if (result.error || (result.status !== 0 && !lenientOk)) {
|
|
952
1243
|
return {
|
|
953
1244
|
success: false,
|
|
954
1245
|
changed: false,
|
|
955
|
-
error: result.error
|
|
1246
|
+
error: result.error?.message ||
|
|
1247
|
+
firstDiagnosticLine(result.stderr) ||
|
|
1248
|
+
// biome, ktlint and `mix format` report on STDOUT; without this
|
|
1249
|
+
// their diagnostic is discarded and the user is told only that
|
|
1250
|
+
// the tool "exited with status 1".
|
|
1251
|
+
firstDiagnosticLine(result.stdout) ||
|
|
1252
|
+
`${formatter.name} exited with status ${result.status}`,
|
|
956
1253
|
};
|
|
957
1254
|
}
|
|
958
1255
|
// Check if content changed
|