@herbertgao/pi-extensions 2026.10.0 → 2026.10.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/README.md +16 -12
- package/THIRD_PARTY_NOTICES.md +3 -3
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-help.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/index.js +4 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail-frontmatter.cjs +43 -3
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail.mjs +142 -59
- package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +21 -21
- package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
- package/node_modules/@dietrichgebert/ponytail/AGENTS.md +21 -21
- package/node_modules/@dietrichgebert/ponytail/README.md +52 -242
- package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +3 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/copilot-hooks.json +4 -4
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +70 -16
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-config.js +8 -7
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +30 -30
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-map.js +103 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +46 -23
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +39 -7
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-subagent.js +20 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/qoder-hooks.json +3 -3
- package/node_modules/@dietrichgebert/ponytail/package.json +3 -3
- package/node_modules/@dietrichgebert/ponytail/pi-extension/index.js +47 -17
- package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -7
- package/node_modules/@dietrichgebert/ponytail/skills/plugin.json +5 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +33 -101
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +89 -27
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +13 -12
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +21 -18
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +12 -13
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +96 -52
- package/node_modules/@herbertgao/pi-agency/LICENSE +21 -0
- package/node_modules/@herbertgao/pi-agency/README.md +40 -0
- package/node_modules/@herbertgao/pi-agency/package.json +53 -0
- package/node_modules/@herbertgao/pi-agency/src/index.ts +441 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +12 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +30 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +15 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +27 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +4 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +2 -2
- package/node_modules/@narumitw/pi-caffeinate/package.json +4 -4
- package/node_modules/@tifan/pi-mermaid-open/package.json +5 -1
- package/node_modules/@tifan/pi-mermaid-open/src/index.ts +26 -9
- package/node_modules/@tifan/pi-recap/package.json +1 -1
- package/node_modules/@tifan/pi-recap/src/index.ts +104 -20
- package/node_modules/@tifan/pi-recap/src/models.ts +46 -4
- package/node_modules/pi-antigravity/README.md +23 -2
- package/node_modules/pi-antigravity/package.json +5 -3
- package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +3 -2
- package/node_modules/pi-antigravity/src/features/generate-image.ts +72 -0
- package/node_modules/pi-antigravity/src/features/google-search.ts +71 -0
- package/node_modules/pi-antigravity/src/image/image.ts +86 -47
- package/node_modules/pi-antigravity/src/image/native.ts +69 -0
- package/node_modules/pi-antigravity/src/index.ts +52 -137
- package/node_modules/pi-antigravity/src/search/search.ts +130 -31
- package/node_modules/pi-antigravity/src/stream/stream.ts +162 -113
- package/node_modules/pi-antigravity/src/types/enums.ts +1 -0
- package/node_modules/pi-antigravity/src/types/types.ts +1 -1
- package/node_modules/pi-antigravity/src/utils/security.ts +47 -1
- package/node_modules/pi-antigravity/src/utils/util.ts +38 -6
- package/node_modules/pi-lens/CHANGELOG.md +1064 -0
- package/node_modules/pi-lens/README.md +115 -25
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +6 -2
- package/node_modules/pi-lens/dist/chunk-22AVIGXS.js +560 -0
- package/node_modules/pi-lens/dist/chunk-27BBHSZC.js +106 -0
- package/node_modules/pi-lens/dist/chunk-2BEMIWEW.js +51 -0
- package/node_modules/pi-lens/dist/chunk-2EECBNC5.js +89 -0
- package/node_modules/pi-lens/dist/chunk-34U7PLGE.js +23 -0
- package/node_modules/pi-lens/dist/chunk-3I5QFT3N.js +181 -0
- package/node_modules/pi-lens/dist/chunk-3MED75HT.js +37 -0
- package/node_modules/pi-lens/dist/chunk-47D2BO4S.js +87 -0
- package/node_modules/pi-lens/dist/chunk-4RERDEXR.js +348 -0
- package/node_modules/pi-lens/dist/chunk-4W2PLC4J.js +27 -0
- package/node_modules/pi-lens/dist/chunk-52H5B5TC.js +202 -0
- package/node_modules/pi-lens/dist/chunk-54B42YRX.js +27 -0
- package/node_modules/pi-lens/dist/chunk-5RXBNSW2.js +266 -0
- package/node_modules/pi-lens/dist/chunk-5THXD6X5.js +201 -0
- package/node_modules/pi-lens/dist/chunk-5W3R6LFT.js +24 -0
- package/node_modules/pi-lens/dist/chunk-5ZEJHV35.js +341 -0
- package/node_modules/pi-lens/dist/chunk-6P62QVEA.js +118 -0
- package/node_modules/pi-lens/dist/chunk-6QIUX4EP.js +459 -0
- package/node_modules/pi-lens/dist/chunk-6VCD4VKZ.js +78 -0
- package/node_modules/pi-lens/dist/chunk-7F72PXY2.js +326 -0
- package/node_modules/pi-lens/dist/chunk-7FBF7NPR.js +998 -0
- package/node_modules/pi-lens/dist/chunk-7U2IDNXN.js +37 -0
- package/node_modules/pi-lens/dist/chunk-ABBOA7UD.js +157 -0
- package/node_modules/pi-lens/dist/chunk-AHDW3GPE.js +50 -0
- package/node_modules/pi-lens/dist/chunk-AJ3QXTRQ.js +756 -0
- package/node_modules/pi-lens/dist/chunk-AJVLWNFN.js +1610 -0
- package/node_modules/pi-lens/dist/chunk-AJXODZY6.js +112 -0
- package/node_modules/pi-lens/dist/chunk-ARFCXG7S.js +84 -0
- package/node_modules/pi-lens/dist/chunk-AS44CFQ4.js +708 -0
- package/node_modules/pi-lens/dist/chunk-AWE5TMPO.js +38 -0
- package/node_modules/pi-lens/dist/chunk-B3I2UUT4.js +4692 -0
- package/node_modules/pi-lens/dist/chunk-BRJKOBZP.js +331 -0
- package/node_modules/pi-lens/dist/chunk-BW5KFIQC.js +59 -0
- package/node_modules/pi-lens/dist/chunk-C2AUYXQH.js +149 -0
- package/node_modules/pi-lens/dist/chunk-CGGAIP7J.js +29 -0
- package/node_modules/pi-lens/dist/chunk-D6QS3U6M.js +30 -0
- package/node_modules/pi-lens/dist/chunk-DFVMIKMQ.js +349 -0
- package/node_modules/pi-lens/dist/chunk-DIULIWKU.js +169 -0
- package/node_modules/pi-lens/dist/chunk-DQ2NZ7C4.js +2317 -0
- package/node_modules/pi-lens/dist/chunk-E2WFBRHM.js +213 -0
- package/node_modules/pi-lens/dist/chunk-E4MAEDPM.js +50 -0
- package/node_modules/pi-lens/dist/chunk-EOKRQ3SA.js +30 -0
- package/node_modules/pi-lens/dist/chunk-F3AU2TMM.js +2626 -0
- package/node_modules/pi-lens/dist/chunk-F3P7VQ7H.js +170 -0
- package/node_modules/pi-lens/dist/chunk-FC4RP4I2.js +12 -0
- package/node_modules/pi-lens/dist/chunk-FJNKANOS.js +32 -0
- package/node_modules/pi-lens/dist/chunk-GUOGVTCQ.js +146 -0
- package/node_modules/pi-lens/dist/chunk-GVXA6CCE.js +18 -0
- package/node_modules/pi-lens/dist/chunk-HHYAELLJ.js +2334 -0
- package/node_modules/pi-lens/dist/chunk-HMFXILPV.js +83 -0
- package/node_modules/pi-lens/dist/chunk-HVUZXSWK.js +421 -0
- package/node_modules/pi-lens/dist/chunk-HWBZIGMJ.js +62 -0
- package/node_modules/pi-lens/dist/chunk-I3R7GKDR.js +17 -0
- package/node_modules/pi-lens/dist/chunk-I7SPGOWI.js +55 -0
- package/node_modules/pi-lens/dist/chunk-IIXOAGHR.js +38 -0
- package/node_modules/pi-lens/dist/chunk-IVP5KAB7.js +2250 -0
- package/node_modules/pi-lens/dist/chunk-IXEBJSUE.js +22 -0
- package/node_modules/pi-lens/dist/chunk-JOLITGCC.js +27 -0
- package/node_modules/pi-lens/dist/chunk-K6VYLB2U.js +145 -0
- package/node_modules/pi-lens/dist/chunk-LJDODBBZ.js +78 -0
- package/node_modules/pi-lens/dist/chunk-LNAKEO63.js +78 -0
- package/node_modules/pi-lens/dist/chunk-LSMJ6KBE.js +4139 -0
- package/node_modules/pi-lens/dist/chunk-LW6XKDZQ.js +5565 -0
- package/node_modules/pi-lens/dist/chunk-N3YQJI6O.js +899 -0
- package/node_modules/pi-lens/dist/chunk-NABLC7W3.js +148 -0
- package/node_modules/pi-lens/dist/chunk-NKOUFPQF.js +659 -0
- package/node_modules/pi-lens/dist/chunk-NL5GLMFI.js +211 -0
- package/node_modules/pi-lens/dist/chunk-NMABWBZN.js +327 -0
- package/node_modules/pi-lens/dist/chunk-NTHH2J74.js +27 -0
- package/node_modules/pi-lens/dist/chunk-NTUJEGDZ.js +59 -0
- package/node_modules/pi-lens/dist/chunk-NXL4FFQQ.js +47 -0
- package/node_modules/pi-lens/dist/chunk-NXPS2L6U.js +466 -0
- package/node_modules/pi-lens/dist/chunk-O5WILGMI.js +42 -0
- package/node_modules/pi-lens/dist/chunk-O6TQT6RI.js +48 -0
- package/node_modules/pi-lens/dist/chunk-OAXNDZJ7.js +323 -0
- package/node_modules/pi-lens/dist/chunk-ODEEOXPH.js +44 -0
- package/node_modules/pi-lens/dist/chunk-OQGFCZJ4.js +4533 -0
- package/node_modules/pi-lens/dist/chunk-ORTMY4Q4.js +282 -0
- package/node_modules/pi-lens/dist/chunk-OS32RNR2.js +442 -0
- package/node_modules/pi-lens/dist/chunk-PAK6IDME.js +189 -0
- package/node_modules/pi-lens/dist/chunk-PWFHBTSF.js +406 -0
- package/node_modules/pi-lens/dist/chunk-Q5U6FMDI.js +2352 -0
- package/node_modules/pi-lens/dist/chunk-QOZXJ4NA.js +3364 -0
- package/node_modules/pi-lens/dist/chunk-QYF5S6P6.js +45 -0
- package/node_modules/pi-lens/dist/chunk-RIISMHKN.js +36 -0
- package/node_modules/pi-lens/dist/chunk-RPD4ZSB7.js +25 -0
- package/node_modules/pi-lens/dist/chunk-RPD5K5NC.js +79 -0
- package/node_modules/pi-lens/dist/chunk-RUOG5D2C.js +262 -0
- package/node_modules/pi-lens/dist/chunk-T4ERT73Y.js +599 -0
- package/node_modules/pi-lens/dist/chunk-T6D4Q4AQ.js +54 -0
- package/node_modules/pi-lens/dist/chunk-TGPXLY2J.js +391 -0
- package/node_modules/pi-lens/dist/chunk-TLGFBM3F.js +12116 -0
- package/node_modules/pi-lens/dist/chunk-TO6WL3GM.js +88 -0
- package/node_modules/pi-lens/dist/chunk-TT22N74J.js +517 -0
- package/node_modules/pi-lens/dist/chunk-TVN5N6GY.js +21216 -0
- package/node_modules/pi-lens/dist/chunk-U3VGR4VU.js +308 -0
- package/node_modules/pi-lens/dist/chunk-UK3CMEAL.js +1250 -0
- package/node_modules/pi-lens/dist/chunk-UOPVJQ4N.js +103 -0
- package/node_modules/pi-lens/dist/chunk-UXVNL3D7.js +23 -0
- package/node_modules/pi-lens/dist/chunk-UY752QSN.js +45 -0
- package/node_modules/pi-lens/dist/chunk-V5PXFSZK.js +188 -0
- package/node_modules/pi-lens/dist/chunk-V7R425XG.js +398 -0
- package/node_modules/pi-lens/dist/chunk-VCSRSYLF.js +81 -0
- package/node_modules/pi-lens/dist/chunk-VD4JQCQC.js +162 -0
- package/node_modules/pi-lens/dist/chunk-VDBPVIKD.js +62 -0
- package/node_modules/pi-lens/dist/chunk-VHQLPIRJ.js +114 -0
- package/node_modules/pi-lens/dist/chunk-VN2AXLEX.js +5414 -0
- package/node_modules/pi-lens/dist/chunk-VNMT7C64.js +478 -0
- package/node_modules/pi-lens/dist/chunk-WONERJTP.js +309 -0
- package/node_modules/pi-lens/dist/chunk-XECLWQZN.js +85 -0
- package/node_modules/pi-lens/dist/chunk-XIMYZ3JA.js +27 -0
- package/node_modules/pi-lens/dist/chunk-XK4CIILI.js +144 -0
- package/node_modules/pi-lens/dist/chunk-Y4T27COV.js +562 -0
- package/node_modules/pi-lens/dist/chunk-YGVYKNSN.js +1860 -0
- package/node_modules/pi-lens/dist/chunk-YLF3PPT5.js +308 -0
- package/node_modules/pi-lens/dist/chunk-YSNFMJKN.js +14 -0
- package/node_modules/pi-lens/dist/chunk-Z3CCVDUB.js +76 -0
- package/node_modules/pi-lens/dist/chunk-Z54ZSOBZ.js +61 -0
- package/node_modules/pi-lens/dist/chunk-ZAWW3AF6.js +97 -0
- package/node_modules/pi-lens/dist/chunk-ZMOL6RQC.js +167 -0
- package/node_modules/pi-lens/dist/index.js +16456 -8335
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +186 -232
- package/node_modules/pi-lens/dist/mcp/cli.js +137 -98
- package/node_modules/pi-lens/dist/mcp/server.js +22589 -1573
- package/node_modules/pi-lens/dist/mcp/worker.js +97 -38
- package/node_modules/pi-lens/dist/probes/installer.js +135 -0
- package/node_modules/pi-lens/dist/workers/project-snapshot-persist-worker.js +22 -0
- package/node_modules/pi-lens/dist/workers/review-graph-persist-worker.js +15 -0
- package/node_modules/pi-lens/docs/agent-guide.md +13 -4
- package/node_modules/pi-lens/docs/agent-tools.md +42 -0
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +3 -2
- package/node_modules/pi-lens/docs/configuration.md +20 -1
- package/node_modules/pi-lens/docs/environment-variables.md +52 -2
- package/node_modules/pi-lens/docs/features.md +8 -4
- package/node_modules/pi-lens/docs/globalconfig.md +1 -0
- package/node_modules/pi-lens/docs/io-bridge-v2-migration.md +94 -0
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +36 -4
- package/node_modules/pi-lens/docs/lsp-idle-eviction.md +124 -0
- package/node_modules/pi-lens/docs/mcp.md +21 -5
- package/node_modules/pi-lens/docs/merge-queue-rollout.md +77 -0
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +156 -88
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +74 -26
- package/node_modules/pi-lens/docs/pi-lens-merge-policy.md +694 -0
- package/node_modules/pi-lens/docs/pi-lens-monitor.md +73 -7
- package/node_modules/pi-lens/docs/pi-lens-retro.md +6 -4
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +188 -48
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +246 -45
- package/node_modules/pi-lens/docs/pi-lens-warden.md +18 -7
- package/node_modules/pi-lens/docs/public-api-stability.md +55 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +84 -17
- package/node_modules/pi-lens/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +53 -49
- package/node_modules/pi-lens/docs/settings.md +1 -0
- package/node_modules/pi-lens/docs/usage.md +5 -1
- package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm +0 -0
- package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm.json +3 -3
- package/node_modules/pi-lens/package.json +18 -6
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +1275 -62
- package/node_modules/pi-lens/scripts/download-grammars.js +5 -0
- package/node_modules/pi-lens/scripts/grammars.lock.json +6 -1
- package/node_modules/pi-lens/scripts/install-selftest.mjs +33 -34
- package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +13 -4
- package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +18 -0
- package/node_modules/pi-typesafe/README.md +1 -1
- package/node_modules/pi-typesafe/dist/backends.d.ts +15 -5
- package/node_modules/pi-typesafe/dist/backends.js +18 -0
- package/node_modules/pi-typesafe/dist/client.d.ts +4 -3
- package/node_modules/pi-typesafe/dist/client.js +22 -7
- package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
- package/node_modules/pi-typesafe/dist/index.js +1 -1
- package/node_modules/pi-typesafe/dist/usage.d.ts +14 -4
- package/node_modules/pi-typesafe/dist/usage.js +147 -42
- package/node_modules/pi-typesafe/package.json +1 -1
- package/node_modules/pi-web-access/CHANGELOG.md +48 -0
- package/node_modules/pi-web-access/README.md +57 -11
- package/node_modules/pi-web-access/brave-rate-limit.ts +8 -2
- package/node_modules/pi-web-access/curator-page.ts +5 -1
- package/node_modules/pi-web-access/curator-server.ts +2 -0
- package/node_modules/pi-web-access/declared-web-links.ts +2 -2
- package/node_modules/pi-web-access/degoog.ts +323 -0
- package/node_modules/pi-web-access/dist/index.js +3483 -2745
- package/node_modules/pi-web-access/dist/mcp-cli.js +27443 -0
- package/node_modules/pi-web-access/exa.ts +5 -1
- package/node_modules/pi-web-access/extract.ts +41 -3
- package/node_modules/pi-web-access/firecrawl.ts +35 -4
- package/node_modules/pi-web-access/gemini-search.ts +34 -11
- package/node_modules/pi-web-access/gemini-web.ts +14 -9
- package/node_modules/pi-web-access/index.ts +153 -1202
- package/node_modules/pi-web-access/keenable.ts +221 -0
- package/node_modules/pi-web-access/mcp-cli.ts +40 -0
- package/node_modules/pi-web-access/mcp-server.ts +81 -0
- package/node_modules/pi-web-access/mcp-tool-definitions.ts +117 -0
- package/node_modules/pi-web-access/openai-search.ts +34 -1
- package/node_modules/pi-web-access/package.json +23 -5
- package/node_modules/pi-web-access/perplexity.ts +64 -56
- package/node_modules/pi-web-access/tool-arguments.ts +20 -0
- package/node_modules/pi-web-access/video-extract.ts +1 -1
- package/node_modules/pi-web-access/web-tool-contract.ts +86 -0
- package/node_modules/pi-web-access/web-tool-core.ts +1502 -0
- package/node_modules/pi-web-access/youtube-extract.ts +2 -5
- package/package.json +17 -12
- package/node_modules/@dietrichgebert/ponytail/README.es.md +0 -329
- package/node_modules/@dietrichgebert/ponytail/README.ko.md +0 -346
- package/node_modules/@dietrichgebert/ponytail/assets/benchmark-3model.svg +0 -21
- package/node_modules/@dietrichgebert/ponytail/assets/benchmark-agentic.svg +0 -62
- package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.svg +0 -115
- package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt-dark.svg +0 -27
- package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt.svg +0 -27
- package/node_modules/@dietrichgebert/ponytail/assets/logo.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +0 -4
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +0 -4
- package/node_modules/@dietrichgebert/ponytail/assets/social-preview.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-es.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-ko.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner.png +0 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -35
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +0 -1610
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +0 -593
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +0 -147
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +0 -325
- package/node_modules/pi-lens/dist/clients/analysed-root.js +0 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +0 -731
- package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +0 -146
- package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +0 -118
- package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -10
- package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +0 -154
- package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +0 -75
- package/node_modules/pi-lens/dist/clients/atomic-write.js +0 -189
- package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +0 -12
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +0 -1056
- package/node_modules/pi-lens/dist/clients/biome-client.js +0 -357
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -837
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +0 -129
- package/node_modules/pi-lens/dist/clients/bootstrap.js +0 -569
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +0 -190
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +0 -202
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +0 -295
- package/node_modules/pi-lens/dist/clients/build-identity.js +0 -95
- package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +0 -113
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +0 -105
- package/node_modules/pi-lens/dist/clients/bus-publish.js +0 -171
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +0 -190
- package/node_modules/pi-lens/dist/clients/cache-manager.js +0 -524
- package/node_modules/pi-lens/dist/clients/cache-observability.js +0 -1313
- package/node_modules/pi-lens/dist/clients/call-graph.js +0 -852
- package/node_modules/pi-lens/dist/clients/cargo-manifest.js +0 -422
- package/node_modules/pi-lens/dist/clients/cascade-budget.js +0 -196
- package/node_modules/pi-lens/dist/clients/cascade-format.js +0 -315
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -31
- package/node_modules/pi-lens/dist/clients/cascade-types.js +0 -9
- package/node_modules/pi-lens/dist/clients/child-unref.js +0 -124
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +0 -286
- package/node_modules/pi-lens/dist/clients/codebase-model.js +0 -173
- package/node_modules/pi-lens/dist/clients/collateral-test-role.js +0 -40
- package/node_modules/pi-lens/dist/clients/complexity-client.js +0 -462
- package/node_modules/pi-lens/dist/clients/config-core/deny.js +0 -221
- package/node_modules/pi-lens/dist/clients/config-core/index.js +0 -50
- package/node_modules/pi-lens/dist/clients/config-core/merge.js +0 -357
- package/node_modules/pi-lens/dist/clients/config-core/normalize.js +0 -340
- package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +0 -248
- package/node_modules/pi-lens/dist/clients/config-core/provenance.js +0 -164
- package/node_modules/pi-lens/dist/clients/config-core/records.js +0 -218
- package/node_modules/pi-lens/dist/clients/config-core/resolve.js +0 -125
- package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +0 -78
- package/node_modules/pi-lens/dist/clients/config-core/schema.js +0 -165
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +0 -285
- package/node_modules/pi-lens/dist/clients/config-locations.js +0 -388
- package/node_modules/pi-lens/dist/clients/config-resolve.js +0 -798
- package/node_modules/pi-lens/dist/clients/config-schema.js +0 -251
- package/node_modules/pi-lens/dist/clients/config-warn.js +0 -407
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +0 -26
- package/node_modules/pi-lens/dist/clients/cooperative-budget.js +0 -42
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +0 -510
- package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -30
- package/node_modules/pi-lens/dist/clients/deadline-utils.js +0 -280
- package/node_modules/pi-lens/dist/clients/debug-handles.js +0 -211
- package/node_modules/pi-lens/dist/clients/debug-heap.js +0 -174
- package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +0 -106
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +0 -426
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +0 -169
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +0 -885
- package/node_modules/pi-lens/dist/clients/deps/ast-grep-napi.js +0 -22
- package/node_modules/pi-lens/dist/clients/deps/js-yaml.js +0 -9
- package/node_modules/pi-lens/dist/clients/deps/minimatch.js +0 -4
- package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +0 -14
- package/node_modules/pi-lens/dist/clients/deps/typebox.js +0 -14
- package/node_modules/pi-lens/dist/clients/deps/vscode-jsonrpc.js +0 -1
- package/node_modules/pi-lens/dist/clients/deps/web-tree-sitter.js +0 -24
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +0 -630
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +0 -305
- package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +0 -73
- package/node_modules/pi-lens/dist/clients/diagnostic-tracker.js +0 -105
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +0 -230
- package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +0 -65
- package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +0 -343
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -35
- package/node_modules/pi-lens/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +0 -1269
- package/node_modules/pi-lens/dist/clients/dispatch/fact-provider-types.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/fact-rule-runner.js +0 -17
- package/node_modules/pi-lens/dist/clients/dispatch/fact-runner.js +0 -30
- package/node_modules/pi-lens/dist/clients/dispatch/fact-scheduler.js +0 -68
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +0 -377
- package/node_modules/pi-lens/dist/clients/dispatch/facts/comment-facts.js +0 -22
- package/node_modules/pi-lens/dist/clients/dispatch/facts/file-content.js +0 -19
- package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +0 -348
- package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +0 -204
- package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
- package/node_modules/pi-lens/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
- package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +0 -339
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +0 -417
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +0 -89
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +0 -2377
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -11
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +0 -87
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -278
- package/node_modules/pi-lens/dist/clients/dispatch/priorities.js +0 -20
- package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +0 -50
- package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +0 -69
- package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +0 -135
- package/node_modules/pi-lens/dist/clients/dispatch/rules/async-noise.js +0 -40
- package/node_modules/pi-lens/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
- package/node_modules/pi-lens/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
- package/node_modules/pi-lens/dist/clients/dispatch/rules/error-obscuring.js +0 -32
- package/node_modules/pi-lens/dist/clients/dispatch/rules/error-swallowing.js +0 -31
- package/node_modules/pi-lens/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +0 -72
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +0 -74
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
- package/node_modules/pi-lens/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
- package/node_modules/pi-lens/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
- package/node_modules/pi-lens/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
- package/node_modules/pi-lens/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
- package/node_modules/pi-lens/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
- package/node_modules/pi-lens/dist/clients/dispatch/runner-context.js +0 -52
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -111
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -296
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -219
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -82
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -416
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -183
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -176
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -164
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -195
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -137
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -29
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -69
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -167
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +0 -104
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -135
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -91
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -171
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -940
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -117
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +0 -108
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -99
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -110
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -379
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -167
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -102
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +0 -414
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -85
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -120
- package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -62
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +0 -169
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -99
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -88
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +0 -256
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -173
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -102
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -128
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -259
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -183
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -166
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -163
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -174
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -168
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -125
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -643
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -253
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -128
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -95
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -79
- package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +0 -79
- package/node_modules/pi-lens/dist/clients/dispatch/tool-profile.js +0 -30
- package/node_modules/pi-lens/dist/clients/dispatch/types.js +0 -21
- package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +0 -45
- package/node_modules/pi-lens/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
- package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -32
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +0 -124
- package/node_modules/pi-lens/dist/clients/durable-store.js +0 -97
- package/node_modules/pi-lens/dist/clients/effective-config.js +0 -415
- package/node_modules/pi-lens/dist/clients/env-utils.js +0 -83
- package/node_modules/pi-lens/dist/clients/error-class.js +0 -23
- package/node_modules/pi-lens/dist/clients/eval-timestamp.js +0 -17
- package/node_modules/pi-lens/dist/clients/event-loop-hold.js +0 -274
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +0 -250
- package/node_modules/pi-lens/dist/clients/extension-log.js +0 -504
- package/node_modules/pi-lens/dist/clients/extension-mode.js +0 -73
- package/node_modules/pi-lens/dist/clients/feature-hints.js +0 -103
- package/node_modules/pi-lens/dist/clients/file-kinds.js +0 -412
- package/node_modules/pi-lens/dist/clients/file-role.js +0 -134
- package/node_modules/pi-lens/dist/clients/file-time.js +0 -160
- package/node_modules/pi-lens/dist/clients/file-utils.js +0 -1032
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +0 -494
- package/node_modules/pi-lens/dist/clients/finding-identity.js +0 -88
- package/node_modules/pi-lens/dist/clients/fix-worklog.js +0 -103
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +0 -343
- package/node_modules/pi-lens/dist/clients/format-service.js +0 -222
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +0 -9
- package/node_modules/pi-lens/dist/clients/formatters.js +0 -2032
- package/node_modules/pi-lens/dist/clients/freshness-cadence.js +0 -17
- package/node_modules/pi-lens/dist/clients/freshness.js +0 -38
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +0 -394
- package/node_modules/pi-lens/dist/clients/generation-guard.js +0 -215
- package/node_modules/pi-lens/dist/clients/git-guard.js +0 -1136
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +0 -243
- package/node_modules/pi-lens/dist/clients/gitleaks-client.js +0 -509
- package/node_modules/pi-lens/dist/clients/go-client.js +0 -104
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +0 -605
- package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +0 -348
- package/node_modules/pi-lens/dist/clients/grammar-source.js +0 -465
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +0 -135
- package/node_modules/pi-lens/dist/clients/hashline-anchor.js +0 -424
- package/node_modules/pi-lens/dist/clients/hook-budgets.js +0 -76
- package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +0 -140
- package/node_modules/pi-lens/dist/clients/host-ports.js +0 -39
- package/node_modules/pi-lens/dist/clients/indent-retarget.js +0 -154
- package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +0 -186
- package/node_modules/pi-lens/dist/clients/install-diagnostics.js +0 -156
- package/node_modules/pi-lens/dist/clients/installer/index.js +0 -5416
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +0 -58
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +0 -783
- package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +0 -7
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +0 -1137
- package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +0 -145
- package/node_modules/pi-lens/dist/clients/instance-registry.js +0 -773
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +0 -378
- package/node_modules/pi-lens/dist/clients/json-cache-read.js +0 -65
- package/node_modules/pi-lens/dist/clients/knip-client.js +0 -788
- package/node_modules/pi-lens/dist/clients/language-policy.js +0 -252
- package/node_modules/pi-lens/dist/clients/language-profile.js +0 -361
- package/node_modules/pi-lens/dist/clients/language-registry.js +0 -603
- package/node_modules/pi-lens/dist/clients/latency-logger.js +0 -727
- package/node_modules/pi-lens/dist/clients/lazy-import.js +0 -21
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +0 -54
- package/node_modules/pi-lens/dist/clients/lens-config.js +0 -404
- package/node_modules/pi-lens/dist/clients/lens-engine.js +0 -358
- package/node_modules/pi-lens/dist/clients/lens-events.js +0 -125
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +0 -467
- package/node_modules/pi-lens/dist/clients/lens-map.js +0 -1156
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +0 -74
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -222
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +0 -178
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +0 -421
- package/node_modules/pi-lens/dist/clients/lsp/client.js +0 -4593
- package/node_modules/pi-lens/dist/clients/lsp/config.js +0 -691
- package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +0 -41
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +0 -225
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +0 -467
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +0 -1219
- package/node_modules/pi-lens/dist/clients/lsp/index.js +0 -8305
- package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +0 -216
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +0 -298
- package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -63
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +0 -746
- package/node_modules/pi-lens/dist/clients/lsp/lombok.js +0 -158
- package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +0 -5
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -251
- package/node_modules/pi-lens/dist/clients/lsp/position-encoding.js +0 -61
- package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +0 -83
- package/node_modules/pi-lens/dist/clients/lsp/server.js +0 -3290
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +0 -192
- package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +0 -17
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -56
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +0 -332
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +0 -85
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/index.js +0 -10
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +0 -392
- package/node_modules/pi-lens/dist/clients/lsp/watch-queue.js +0 -61
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +0 -184
- package/node_modules/pi-lens/dist/clients/lsp-budget.js +0 -186
- package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +0 -110
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +0 -9
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +0 -396
- package/node_modules/pi-lens/dist/clients/lsp-status.js +0 -59
- package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +0 -40
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +0 -420
- package/node_modules/pi-lens/dist/clients/mcp/host-shim.js +0 -31
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +0 -523
- package/node_modules/pi-lens/dist/clients/mcp/review.js +0 -197
- package/node_modules/pi-lens/dist/clients/mcp/session.js +0 -412
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +0 -208
- package/node_modules/pi-lens/dist/clients/message-end-attribution.js +0 -30
- package/node_modules/pi-lens/dist/clients/metrics-client.js +0 -107
- package/node_modules/pi-lens/dist/clients/metrics-history.js +0 -327
- package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +0 -263
- package/node_modules/pi-lens/dist/clients/model-provider.js +0 -53
- package/node_modules/pi-lens/dist/clients/module-report-lsp.js +0 -189
- package/node_modules/pi-lens/dist/clients/module-report.js +0 -2150
- package/node_modules/pi-lens/dist/clients/mutating-tool.js +0 -651
- package/node_modules/pi-lens/dist/clients/mutation-attribution.js +0 -368
- package/node_modules/pi-lens/dist/clients/mutation-bridge.js +0 -240
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -734
- package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +0 -101
- package/node_modules/pi-lens/dist/clients/observed-mutation.js +0 -1253
- package/node_modules/pi-lens/dist/clients/oldtext-autopatch.js +0 -128
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +0 -496
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +0 -333
- package/node_modules/pi-lens/dist/clients/opengrep-config.js +0 -76
- package/node_modules/pi-lens/dist/clients/package-manager.js +0 -607
- package/node_modules/pi-lens/dist/clients/package-root.js +0 -38
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +0 -439
- package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +0 -43
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +0 -108
- package/node_modules/pi-lens/dist/clients/path-utils.js +0 -1244
- package/node_modules/pi-lens/dist/clients/performance-report.js +0 -331
- package/node_modules/pi-lens/dist/clients/persist-debounce.js +0 -70
- package/node_modules/pi-lens/dist/clients/persistent-reverify.js +0 -271
- package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +0 -119
- package/node_modules/pi-lens/dist/clients/pipeline.js +0 -1480
- package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +0 -37
- package/node_modules/pi-lens/dist/clients/print-mode.js +0 -21
- package/node_modules/pi-lens/dist/clients/probe-home-state.js +0 -228
- package/node_modules/pi-lens/dist/clients/process-bridge.js +0 -66
- package/node_modules/pi-lens/dist/clients/process-singletons.js +0 -210
- package/node_modules/pi-lens/dist/clients/process-snapshot.js +0 -68
- package/node_modules/pi-lens/dist/clients/project-changes.js +0 -181
- package/node_modules/pi-lens/dist/clients/project-conventions.js +0 -171
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +0 -137
- package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +0 -109
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +0 -520
- package/node_modules/pi-lens/dist/clients/project-diagnostics/types.js +0 -1
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +0 -807
- package/node_modules/pi-lens/dist/clients/project-report.js +0 -701
- package/node_modules/pi-lens/dist/clients/project-scale.js +0 -237
- package/node_modules/pi-lens/dist/clients/project-scan-policy.js +0 -11
- package/node_modules/pi-lens/dist/clients/project-snapshot-fingerprint.js +0 -48
- package/node_modules/pi-lens/dist/clients/project-snapshot-persist-worker.js +0 -20
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +0 -1653
- package/node_modules/pi-lens/dist/clients/project-trust.js +0 -181
- package/node_modules/pi-lens/dist/clients/python-environment.js +0 -217
- package/node_modules/pi-lens/dist/clients/python-provenance.js +0 -639
- package/node_modules/pi-lens/dist/clients/quiet-window-config.js +0 -47
- package/node_modules/pi-lens/dist/clients/quiet-window.js +0 -236
- package/node_modules/pi-lens/dist/clients/read-bridge.js +0 -139
- package/node_modules/pi-lens/dist/clients/read-expansion.js +0 -266
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +0 -251
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +0 -1306
- package/node_modules/pi-lens/dist/clients/read-guard.js +0 -1561
- package/node_modules/pi-lens/dist/clients/recent-touches.js +0 -240
- package/node_modules/pi-lens/dist/clients/redact/secrets.js +0 -449
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +0 -628
- package/node_modules/pi-lens/dist/clients/reverse-deps.js +0 -270
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +0 -5126
- package/node_modules/pi-lens/dist/clients/review-graph/format.js +0 -33
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +0 -163
- package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +0 -481
- package/node_modules/pi-lens/dist/clients/review-graph/persist-worker.js +0 -10
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +0 -244
- package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +0 -21
- package/node_modules/pi-lens/dist/clients/review-graph/service.js +0 -96
- package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +0 -82
- package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +0 -103
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +0 -299
- package/node_modules/pi-lens/dist/clients/review-graph/types.js +0 -1
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +0 -449
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +0 -81
- package/node_modules/pi-lens/dist/clients/ruff-client.js +0 -179
- package/node_modules/pi-lens/dist/clients/rules-scanner.js +0 -131
- package/node_modules/pi-lens/dist/clients/run-duration.js +0 -55
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +0 -746
- package/node_modules/pi-lens/dist/clients/runtime-config.js +0 -77
- package/node_modules/pi-lens/dist/clients/runtime-context.js +0 -211
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +0 -1324
- package/node_modules/pi-lens/dist/clients/runtime-session.js +0 -2485
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +0 -1295
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +0 -2049
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +0 -3767
- package/node_modules/pi-lens/dist/clients/rust-client.js +0 -97
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +0 -2357
- package/node_modules/pi-lens/dist/clients/sanitize.js +0 -286
- package/node_modules/pi-lens/dist/clients/scan-utils.js +0 -25
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +0 -165
- package/node_modules/pi-lens/dist/clients/search-read-registration.js +0 -94
- package/node_modules/pi-lens/dist/clients/secret-findings.js +0 -114
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +0 -352
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +0 -297
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +0 -484
- package/node_modules/pi-lens/dist/clients/session-start-observability.js +0 -94
- package/node_modules/pi-lens/dist/clients/session-state-store.js +0 -101
- package/node_modules/pi-lens/dist/clients/session-summary.js +0 -15
- package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +0 -27
- package/node_modules/pi-lens/dist/clients/sg-runner.js +0 -876
- package/node_modules/pi-lens/dist/clients/sgconfig.js +0 -347
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +0 -338
- package/node_modules/pi-lens/dist/clients/single-flight.js +0 -158
- package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +0 -108
- package/node_modules/pi-lens/dist/clients/skills-resolver.js +0 -141
- package/node_modules/pi-lens/dist/clients/slow-fs.js +0 -138
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +0 -231
- package/node_modules/pi-lens/dist/clients/source-filter.js +0 -596
- package/node_modules/pi-lens/dist/clients/source-walker.js +0 -211
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +0 -120
- package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +0 -110
- package/node_modules/pi-lens/dist/clients/stale-marker.js +0 -17
- package/node_modules/pi-lens/dist/clients/startup-scan.js +0 -400
- package/node_modules/pi-lens/dist/clients/startup-timing.js +0 -130
- package/node_modules/pi-lens/dist/clients/string-utils.js +0 -33
- package/node_modules/pi-lens/dist/clients/subagent-mode.js +0 -111
- package/node_modules/pi-lens/dist/clients/symbol-containment.js +0 -24
- package/node_modules/pi-lens/dist/clients/symbol-types.js +0 -5
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +0 -2507
- package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +0 -287
- package/node_modules/pi-lens/dist/clients/todo-scanner.js +0 -229
- package/node_modules/pi-lens/dist/clients/tool-agreement.js +0 -423
- package/node_modules/pi-lens/dist/clients/tool-config.js +0 -249
- package/node_modules/pi-lens/dist/clients/tool-cwd.js +0 -270
- package/node_modules/pi-lens/dist/clients/tool-definition.js +0 -47
- package/node_modules/pi-lens/dist/clients/tool-event.js +0 -105
- package/node_modules/pi-lens/dist/clients/tool-policy.js +0 -2755
- package/node_modules/pi-lens/dist/clients/tool-probe.js +0 -53
- package/node_modules/pi-lens/dist/clients/tool-render.js +0 -156
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +0 -118
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +0 -364
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +0 -4195
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +0 -100
- package/node_modules/pi-lens/dist/clients/tree-sitter-navigator.js +0 -269
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +0 -569
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +0 -158
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
- package/node_modules/pi-lens/dist/clients/trivy-client.js +0 -467
- package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -50
- package/node_modules/pi-lens/dist/clients/turn-context.js +0 -52
- package/node_modules/pi-lens/dist/clients/turn-end/lane.js +0 -51
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +0 -112
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +0 -204
- package/node_modules/pi-lens/dist/clients/turn-summary-render.js +0 -72
- package/node_modules/pi-lens/dist/clients/turn-summary.js +0 -132
- package/node_modules/pi-lens/dist/clients/typos-config.js +0 -29
- package/node_modules/pi-lens/dist/clients/user-notify.js +0 -51
- package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +0 -57
- package/node_modules/pi-lens/dist/clients/warm-attach.js +0 -305
- package/node_modules/pi-lens/dist/clients/widget-state.js +0 -1808
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -49
- package/node_modules/pi-lens/dist/clients/word-index-store.js +0 -459
- package/node_modules/pi-lens/dist/clients/word-index.js +0 -2080
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +0 -443
- package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +0 -65
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +0 -340
- package/node_modules/pi-lens/dist/i18n.js +0 -69
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +0 -124
- package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +0 -583
- package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +0 -129
- package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
- package/node_modules/pi-lens/dist/tools/activate-tools.js +0 -87
- package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +0 -137
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +0 -244
- package/node_modules/pi-lens/dist/tools/ast-grep-search.js +0 -762
- package/node_modules/pi-lens/dist/tools/effective-config.js +0 -90
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +0 -375
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +0 -2571
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +0 -1745
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +0 -1367
- package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +0 -294
- package/node_modules/pi-lens/dist/tools/module-report.js +0 -382
- package/node_modules/pi-lens/dist/tools/project-report.js +0 -86
- package/node_modules/pi-lens/dist/tools/render-compact.js +0 -374
- package/node_modules/pi-lens/dist/tools/scan-progress.js +0 -53
- package/node_modules/pi-lens/dist/tools/shared.js +0 -29
- package/node_modules/pi-lens/dist/tools/symbol-search.js +0 -155
|
@@ -1,2032 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Formatter Definitions for pi-lens
|
|
3
|
-
*
|
|
4
|
-
* Auto-detects formatters based on:
|
|
5
|
-
* - Config files (biome.json, .prettierrc, etc.)
|
|
6
|
-
* - Dependencies (package.json, requirements.txt, etc.)
|
|
7
|
-
* - Binary availability (which/where)
|
|
8
|
-
*
|
|
9
|
-
* Inspired by OpenCode's formatter.ts pattern
|
|
10
|
-
*/
|
|
11
|
-
import { logExtension } from "./extension-log.js";
|
|
12
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
|
-
import * as fs from "node:fs/promises";
|
|
14
|
-
import * as os from "node:os";
|
|
15
|
-
import * as path from "node:path";
|
|
16
|
-
import { BoundedLruCache } from "./bounded-cache.js";
|
|
17
|
-
import { createGenerationSource } from "./generation-guard.js";
|
|
18
|
-
import { findNearestContaining, normalizeMapKey } from "./path-utils.js";
|
|
19
|
-
import { resolveToolCwd } from "./tool-cwd.js";
|
|
20
|
-
import { resolveCargoPackageEdition } from "./cargo-manifest.js";
|
|
21
|
-
import { resolveKtfmtGradleStyle } from "./gradle-ktfmt-style.js";
|
|
22
|
-
import { resolvePhpCsFixerConfig } from "./php-cs-fixer-config.js";
|
|
23
|
-
import { TERRAGRUNT_FILENAMES } from "./file-kinds.js";
|
|
24
|
-
import { stripAnsi } from "./sanitize.js";
|
|
25
|
-
import { detectIndentation, hasDetectableIndentation, } from "./dispatch/indent-detect.js";
|
|
26
|
-
import { logLatency } from "./latency-logger.js";
|
|
27
|
-
import { compareOrdinal } from "./string-utils.js";
|
|
28
|
-
import { classifyProbeFailure, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
|
|
29
|
-
import { findGlobalBinary, findLocalBinUpwards, VENDOR_BIN_DIRS, VENV_BIN_DIRS, } from "./package-manager.js";
|
|
30
|
-
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
31
|
-
import { establishToolAgreement } from "./tool-agreement.js";
|
|
32
|
-
import { recordDegradationOnce } from "./degradation-ledger.js";
|
|
33
|
-
import { probeToolAsync } from "./tool-probe.js";
|
|
34
|
-
import { assertInstallAllowed } from "./project-trust.js";
|
|
35
|
-
import { tryLazyInstallForFormatter } from "./dispatch/runners/utils/lazy-installer.js";
|
|
36
|
-
import { getToolPath } from "./installer/index.js";
|
|
37
|
-
import { findPSScriptAnalyzerConfigPath, getAutoInstallToolIdForFormatter, getFormatterPolicyForFile, getSmartDefaultFormatterName, hasBiomeConfig, hasBlackConfig, hasClangFormatConfig, hasCljfmtConfig, hasCmakeFormatConfig, hasCsharpierConfig, hasFantomasConfig, hasGoogleJavaFormatConfig, hasKtfmtConfig, hasKtlintConfig, hasMixFormatConfig, hasNearestPackageJsonDependency, hasNearestPackageJsonField, hasOcamlformatConfig, hasOrmoluConfig, hasOxfmtConfig, hasOxfmtSvelteConfig, hasPhpCsFixerConfig, hasPrettierConfig, hasPSScriptAnalyzerConfig, hasRubocopConfig, hasRuffConfig, hasSqlfluffConfig, hasStandardrbConfig, hasStyluaConfig, hasSwiftformatConfig, hasTaploConfig, hasTerraformConfig, hasVitePlusConfig, OXFMT_SUPPORTED_EXTENSIONS, } from "./tool-policy.js";
|
|
38
|
-
/**
|
|
39
|
-
* Lazy-install a formatter's tool, through the shared seam (#1537).
|
|
40
|
-
*
|
|
41
|
-
* This used to own a second copy of the attempt guard — `_lazyInstallAttempts`,
|
|
42
|
-
* a Set keyed before the install ran and never cleared — so a `gem install
|
|
43
|
-
* rubocop` that died on a network blip was never retried for the session. The
|
|
44
|
-
* state, the transient/durable classification and the retry ladder now live in
|
|
45
|
-
* `lazy-installer.ts`; what stays here is the one thing that is formatter
|
|
46
|
-
* business: a fresh binary on PATH invalidates every "not found" verdict (#1495).
|
|
47
|
-
*/
|
|
48
|
-
export async function tryLazyInstallFormatterTool(tool, cwd) {
|
|
49
|
-
const ok = await tryLazyInstallForFormatter(tool, cwd);
|
|
50
|
-
if (ok)
|
|
51
|
-
resetWhichLatches();
|
|
52
|
-
return ok;
|
|
53
|
-
}
|
|
54
|
-
// --- Types ---
|
|
55
|
-
/**
|
|
56
|
-
* Sentinel a `resolveCommand` returns to mean "do not format this file at
|
|
57
|
-
* all" — distinct from `null`, which means "fall back to the static
|
|
58
|
-
* `command`". #1144's style-preserving resolvers return this when the repo
|
|
59
|
-
* has no config AND the file's indentation is undetectable: running the
|
|
60
|
-
* stock command there is exactly the stock-style imposition the fix bans.
|
|
61
|
-
*/
|
|
62
|
-
export const SKIP_FORMATTING = "skip-formatting";
|
|
63
|
-
/**
|
|
64
|
-
* Sentinel a `resolveCommand` returns when it has PROVEN the selected
|
|
65
|
-
* formatter has no executable — the binary is absent from every place the
|
|
66
|
-
* resolver probed (node_modules/.bin, a venv, a managed install dir, PATH) and
|
|
67
|
-
* no alternative resolved (#2413). Distinct from `null` ("no override, fall
|
|
68
|
-
* back to the static `command`") and from SKIP_FORMATTING ("style-preserving
|
|
69
|
-
* refusal"): the static fallback for these formatters is the SAME bare binary
|
|
70
|
-
* the resolver just proved missing, so spawning it only re-triggers the ENOENT
|
|
71
|
-
* the resolver already observed — and reporting that as a formatting failure is
|
|
72
|
-
* the exact defect this fixes (recurring defect shape 10: an unavailable
|
|
73
|
-
* producer collapsed into an ordinary failed result).
|
|
74
|
-
*/
|
|
75
|
-
export const FORMATTER_UNAVAILABLE = "formatter-unavailable";
|
|
76
|
-
export const FORMATTER_AGREEMENT_UNAVAILABLE = "formatter-agreement-unavailable";
|
|
77
|
-
// --- Utility Functions ---
|
|
78
|
-
async function fileExists(filePath) {
|
|
79
|
-
try {
|
|
80
|
-
await fs.access(filePath);
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
async function findUp(targets, startDir, stopDir = path.parse(startDir).root) {
|
|
88
|
-
const found = [];
|
|
89
|
-
let currentDir = startDir;
|
|
90
|
-
while (currentDir !== stopDir) {
|
|
91
|
-
// One `readdir` per directory instead of one `access` per target: the
|
|
92
|
-
// per-target probe made this loop's cost O(targets), so a long target
|
|
93
|
-
// list (e.g. `FORMATTER_CONFIG_FILES`) paid for every entry at every
|
|
94
|
-
// ancestor directory even when almost none of them exist (#1603).
|
|
95
|
-
let entries;
|
|
96
|
-
try {
|
|
97
|
-
entries = await fs.readdir(currentDir);
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
entries = [];
|
|
101
|
-
}
|
|
102
|
-
if (entries.length > 0) {
|
|
103
|
-
// On default-case-insensitive platforms the old `fs.access` probe
|
|
104
|
-
// matched a config file regardless of on-disk casing; fold so the
|
|
105
|
-
// readdir membership check keeps that behavior.
|
|
106
|
-
const foldCase = process.platform === "win32" || process.platform === "darwin";
|
|
107
|
-
const entrySet = new Set(foldCase ? entries.map((e) => e.toLowerCase()) : entries);
|
|
108
|
-
for (const target of targets) {
|
|
109
|
-
// Nested candidates (for example node_modules/.bin/biome) do
|
|
110
|
-
// not appear as direct readdir members. They retain the old
|
|
111
|
-
// bounded candidate probe; direct names use the O(depth + matches)
|
|
112
|
-
// directory-membership path above.
|
|
113
|
-
if (target.includes("/") || target.includes("\\"))
|
|
114
|
-
continue;
|
|
115
|
-
if (entrySet.has(foldCase ? target.toLowerCase() : target)) {
|
|
116
|
-
const candidate = path.join(currentDir, target);
|
|
117
|
-
// Directory membership is only a cheap candidate filter. Keep the
|
|
118
|
-
// old access check for matched entries so dangling links and entries
|
|
119
|
-
// that cannot be read do not become config evidence (#1603 R2).
|
|
120
|
-
if (await fileExists(candidate))
|
|
121
|
-
found.push(candidate);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
for (const target of targets) {
|
|
126
|
-
if (!target.includes("/") && !target.includes("\\"))
|
|
127
|
-
continue;
|
|
128
|
-
const candidate = path.join(currentDir, target);
|
|
129
|
-
if (await fileExists(candidate))
|
|
130
|
-
found.push(candidate);
|
|
131
|
-
}
|
|
132
|
-
const parent = path.dirname(currentDir);
|
|
133
|
-
if (parent === currentDir)
|
|
134
|
-
break;
|
|
135
|
-
currentDir = parent;
|
|
136
|
-
}
|
|
137
|
-
return found;
|
|
138
|
-
}
|
|
139
|
-
/** Test-only access to the real filesystem walker and its candidate filter. */
|
|
140
|
-
export function _findUpForTests(targets, startDir, stopDir) {
|
|
141
|
-
return findUp(targets, startDir, stopDir);
|
|
142
|
-
}
|
|
143
|
-
const WHICH_BUDGET_MS = 5000;
|
|
144
|
-
/**
|
|
145
|
-
* PATH lookups, governed by the shared availability policy (#1495).
|
|
146
|
-
*
|
|
147
|
-
* `which()` is a spawn on a 5 s budget, not a filesystem check, and around a
|
|
148
|
-
* dozen `detect*` implementations gate on it. A transient timeout used to drop
|
|
149
|
-
* the formatter AND get written to `detectionCache` as an empty enabled-list —
|
|
150
|
-
* a cache invalidated only by a config file's mtime or size, never by time. One
|
|
151
|
-
* stalled `which rustfmt` therefore disabled Rust formatting for the session
|
|
152
|
-
* unless the user happened to edit a config file.
|
|
153
|
-
*
|
|
154
|
-
* Now: only a genuine absence latches, a stall expires on a cooldown, and the
|
|
155
|
-
* detection pass refuses to cache an empty result a stall caused.
|
|
156
|
-
*/
|
|
157
|
-
const whichLatchByCommand = new Map();
|
|
158
|
-
/**
|
|
159
|
-
* Commands whose CURRENT `which` verdict is transient — a probe that never got a
|
|
160
|
-
* fair hearing, still inside its cooldown.
|
|
161
|
-
*
|
|
162
|
-
* Scoped per command on purpose (#1495 review). The first cut counted transient
|
|
163
|
-
* verdicts process-wide and compared the count across a detection pass, which
|
|
164
|
-
* over-suppressed: one stalled `which rustfmt` stopped an unrelated Python or
|
|
165
|
-
* shell detection from caching, and stamped `reason: "probe-timeout"` on
|
|
166
|
-
* selections where nothing had timed out. A command leaves the set as soon as
|
|
167
|
-
* its verdict becomes trustworthy — a resolved path or a genuine absence.
|
|
168
|
-
*/
|
|
169
|
-
const whichTransientCommands = new Set();
|
|
170
|
-
/**
|
|
171
|
-
* The binaries `which()` was asked about inside the current `detect()` call
|
|
172
|
-
* (#1539).
|
|
173
|
-
*
|
|
174
|
-
* This replaces the `command[0]` approximation the poison guard used to run on.
|
|
175
|
-
* `command[0]` is the formatter's own binary, which is exact for most
|
|
176
|
-
* `detect()`s and wrong for the ones that consult a second binary — and the
|
|
177
|
-
* "extras are harmless" argument does not hold: `rustfmt` answering a GENUINE
|
|
178
|
-
* absence while `which rustup` stalls skips the lazy install and leaves nothing
|
|
179
|
-
* in the transient set for `command[0]` to match. Recording the actual probes
|
|
180
|
-
* makes the guard exact in both directions: it also stops a leftover transient
|
|
181
|
-
* verdict for some other pass's binary from being blamed on this decision.
|
|
182
|
-
*
|
|
183
|
-
* `AsyncLocalStorage` because `detect()` is async and two selection passes can
|
|
184
|
-
* be in flight at once (`tests/clients/formatters-which-latch.test.ts` runs
|
|
185
|
-
* exactly that); a module-level set would let one pass's probes leak into the
|
|
186
|
-
* other's verdict. Built eagerly — unlike `extension-log.ts`'s lazy console
|
|
187
|
-
* capture, this store is entered on every detection pass, so there is no window
|
|
188
|
-
* where paying for it is avoidable.
|
|
189
|
-
*/
|
|
190
|
-
const probedCommandsStorage = new AsyncLocalStorage();
|
|
191
|
-
/**
|
|
192
|
-
* Run one candidate's `detect()` and read the per-candidate verdict back out of
|
|
193
|
-
* the PATH latch (#1539). `detected` is the plain boolean the caller still needs
|
|
194
|
-
* for selection; `verdict` is why.
|
|
195
|
-
*/
|
|
196
|
-
async function detectCandidate(formatter, cwd) {
|
|
197
|
-
const probed = new Set();
|
|
198
|
-
let detected = false;
|
|
199
|
-
let error;
|
|
200
|
-
try {
|
|
201
|
-
detected = await probedCommandsStorage.run(probed, () => formatter.detect(cwd));
|
|
202
|
-
}
|
|
203
|
-
catch (err) {
|
|
204
|
-
// A `detect()` that threw still probed whatever it probed first, and those
|
|
205
|
-
// probes are the fact the guard needs. Swallowing them here would let a
|
|
206
|
-
// stall inside a throwing detection cache as a clean "not this project".
|
|
207
|
-
error = err;
|
|
208
|
-
}
|
|
209
|
-
const stalledCommands = [...probed].filter((command) => whichTransientCommands.has(command));
|
|
210
|
-
return {
|
|
211
|
-
detected,
|
|
212
|
-
...(error !== undefined && { error }),
|
|
213
|
-
outcome: {
|
|
214
|
-
name: formatter.name,
|
|
215
|
-
verdict: detected
|
|
216
|
-
? "available"
|
|
217
|
-
: stalledCommands.length > 0
|
|
218
|
-
? "unreachable"
|
|
219
|
-
: "missing",
|
|
220
|
-
stalledCommands,
|
|
221
|
-
},
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Drop the PATH verdicts, so a newly installed binary is visible at once.
|
|
226
|
-
*
|
|
227
|
-
* Module-private on purpose. Clearing the latches alone does NOT re-arm
|
|
228
|
-
* formatter availability: `getFormattersForFile` answers from `detectionCache`
|
|
229
|
-
* before it ever probes PATH, so a caller that wants a genuine re-probe must
|
|
230
|
-
* drop both. `clearFormatterCache` is that pair, and it is the only reset a
|
|
231
|
-
* session boundary should call (#1895 review round).
|
|
232
|
-
*/
|
|
233
|
-
function resetWhichLatches() {
|
|
234
|
-
whichLatchByCommand.clear();
|
|
235
|
-
whichTransientCommands.clear();
|
|
236
|
-
cooldownRecordedForRetryAtMs.clear();
|
|
237
|
-
}
|
|
238
|
-
/**
|
|
239
|
-
* Last `retryAtMs` a cooldown-served record was emitted for, per command.
|
|
240
|
-
*
|
|
241
|
-
* The bound (#1539): the memo branch used to return before
|
|
242
|
-
* `logAvailabilityDecision`, so a formatter held off by a transient cooldown
|
|
243
|
-
* produced ONE record for arbitrarily many decisions — a reader counting
|
|
244
|
-
* `availability_decision` rows undercounted how long it had been off. Logging
|
|
245
|
-
* every cache hit is not an option either: this seam is consulted per save.
|
|
246
|
-
* One extra row per cooldown WINDOW is the compromise, and `retryAtMs` is the
|
|
247
|
-
* window's identity, so the ladder itself does the rate limiting.
|
|
248
|
-
*/
|
|
249
|
-
const cooldownRecordedForRetryAtMs = new Map();
|
|
250
|
-
/**
|
|
251
|
-
* Record that a still-cooling verdict was served from the latch, at most once
|
|
252
|
-
* per cooldown window (#1539's second defect).
|
|
253
|
-
*/
|
|
254
|
-
function noteCooldownServedVerdict(command, latch) {
|
|
255
|
-
if (latch.getOutcome() !== "transient")
|
|
256
|
-
return;
|
|
257
|
-
const retryAtMs = latch.getRetryAtMs();
|
|
258
|
-
if (retryAtMs <= 0)
|
|
259
|
-
return;
|
|
260
|
-
if (cooldownRecordedForRetryAtMs.get(command) === retryAtMs)
|
|
261
|
-
return;
|
|
262
|
-
cooldownRecordedForRetryAtMs.set(command, retryAtMs);
|
|
263
|
-
const cause = latch.getCause();
|
|
264
|
-
if (cause === null) {
|
|
265
|
-
// `getOutcome() === "transient"` is only ever set by `noteUnavailable` in
|
|
266
|
-
// the same call that sets `cause`, so this is unreachable today. It is
|
|
267
|
-
// still not a throw (#1539 review F2): this runs inside `which()`, which
|
|
268
|
-
// runs inside `detect()`, and the smart-default branch of the selection
|
|
269
|
-
// pass rethrows a detection failure — so an invariant break in a LOGGING
|
|
270
|
-
// helper could take down formatting for the file. Nothing is fabricated
|
|
271
|
-
// either (#1535's rule: a made-up cause would mislabel WHY a formatter is
|
|
272
|
-
// off, in the record this fix exists to make honest). The row is dropped
|
|
273
|
-
// and the anomaly is reported, bounded by the same once-per-window gate
|
|
274
|
-
// this function already passed.
|
|
275
|
-
logExtension({
|
|
276
|
-
subsystem: "format",
|
|
277
|
-
message: `which latch: transient outcome with no cause for ${command}; cooldown-served record dropped`,
|
|
278
|
-
metadata: { tool: command, retryAtMs },
|
|
279
|
-
});
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
logAvailabilityDecision({
|
|
283
|
-
tool: command,
|
|
284
|
-
verdict: "unavailable",
|
|
285
|
-
outcome: "transient",
|
|
286
|
-
cause,
|
|
287
|
-
elapsedMs: 0,
|
|
288
|
-
latched: false,
|
|
289
|
-
retryAfterMs: Math.max(1, retryAtMs - Date.now()),
|
|
290
|
-
budgetMs: WHICH_BUDGET_MS,
|
|
291
|
-
servedFromCooldown: true,
|
|
292
|
-
// No probe ran here: the latch's own remembered cause is replayed
|
|
293
|
-
// as-is, so the call site is the one asserting it (#2209).
|
|
294
|
-
classifiedBy: "caller",
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
async function which(command) {
|
|
298
|
-
probedCommandsStorage.getStore()?.add(command);
|
|
299
|
-
let entry = whichLatchByCommand.get(command);
|
|
300
|
-
if (!entry) {
|
|
301
|
-
entry = { latch: createAvailabilityLatch(), resolved: null };
|
|
302
|
-
whichLatchByCommand.set(command, entry);
|
|
303
|
-
}
|
|
304
|
-
const memo = entry.latch.read();
|
|
305
|
-
if (memo !== null) {
|
|
306
|
-
if (!memo)
|
|
307
|
-
noteCooldownServedVerdict(command, entry.latch);
|
|
308
|
-
return memo ? entry.resolved : null;
|
|
309
|
-
}
|
|
310
|
-
const stallSampler = startHostStallSampler();
|
|
311
|
-
const startedAt = Date.now();
|
|
312
|
-
const result = await safeSpawnAsync(process.platform === "win32" ? "where" : "which", [command], { timeout: WHICH_BUDGET_MS });
|
|
313
|
-
const hostStallMs = stallSampler.stop();
|
|
314
|
-
const elapsedMs = Date.now() - startedAt;
|
|
315
|
-
const resolved = !result.error && result.status === 0
|
|
316
|
-
? (result.stdout?.trim().split(/\r?\n/)[0] ?? null)
|
|
317
|
-
: null;
|
|
318
|
-
if (resolved) {
|
|
319
|
-
entry.resolved = resolved;
|
|
320
|
-
entry.latch.noteAvailable();
|
|
321
|
-
whichTransientCommands.delete(command);
|
|
322
|
-
cooldownRecordedForRetryAtMs.delete(command);
|
|
323
|
-
logAvailabilityDecision({
|
|
324
|
-
tool: command,
|
|
325
|
-
verdict: "available",
|
|
326
|
-
outcome: "success",
|
|
327
|
-
cause: "ok",
|
|
328
|
-
elapsedMs,
|
|
329
|
-
latched: true,
|
|
330
|
-
hostStallMs,
|
|
331
|
-
budgetMs: WHICH_BUDGET_MS,
|
|
332
|
-
classifiedBy: "probe",
|
|
333
|
-
});
|
|
334
|
-
return resolved;
|
|
335
|
-
}
|
|
336
|
-
entry.resolved = null;
|
|
337
|
-
// A `which`/`where` that RAN and found nothing exits nonzero with NO spawn
|
|
338
|
-
// error, and only that is a genuine absence an install would fix.
|
|
339
|
-
//
|
|
340
|
-
// An UNSPAWNABLE prober is the opposite (#1495 review): EACCES, `spawn
|
|
341
|
-
// UNKNOWN`, EMFILE, an unresolvable cwd, or `where`/`which` itself missing
|
|
342
|
-
// says nothing about the tool that was asked for. Claiming `missing` there
|
|
343
|
-
// was the worst version of this bug, because the prober is shared by every
|
|
344
|
-
// which-gated formatter — one EACCES would have latched a dozen of them off
|
|
345
|
-
// for the session. So the `missing` override is granted only to a prober that
|
|
346
|
-
// ran, and anything that stopped it from running stays transient.
|
|
347
|
-
const proberRan = !result.error;
|
|
348
|
-
const classified = classifyProbeFailure(result, {
|
|
349
|
-
hostStallMs,
|
|
350
|
-
...(proberRan && { unclassifiedFailureOutcome: "missing" }),
|
|
351
|
-
});
|
|
352
|
-
let { outcome, cause } = classified;
|
|
353
|
-
// This call site OVERRIDES classifyProbeFailure's own answer below, so a
|
|
354
|
-
// row it forced is a caller assertion, not a probe classification — the
|
|
355
|
-
// scrutiny availability-policy.ts:253-258 reserves for "caller" rows must
|
|
356
|
-
// not be laundered away by mislabeling this one "probe" (#2226 review F1).
|
|
357
|
-
let outcomeForced = false;
|
|
358
|
-
if (!proberRan && outcome !== "transient") {
|
|
359
|
-
outcome = "transient";
|
|
360
|
-
cause = "probe-rejected";
|
|
361
|
-
outcomeForced = true;
|
|
362
|
-
}
|
|
363
|
-
const retryAfterMs = entry.latch.noteUnavailable(outcome, cause);
|
|
364
|
-
if (outcome === "transient")
|
|
365
|
-
whichTransientCommands.add(command);
|
|
366
|
-
else
|
|
367
|
-
whichTransientCommands.delete(command);
|
|
368
|
-
// A fresh probe opens a new cooldown window (or ends the cooldown), so the
|
|
369
|
-
// next cache hit is entitled to its own record.
|
|
370
|
-
cooldownRecordedForRetryAtMs.delete(command);
|
|
371
|
-
logAvailabilityDecision({
|
|
372
|
-
tool: command,
|
|
373
|
-
verdict: "unavailable",
|
|
374
|
-
outcome,
|
|
375
|
-
cause,
|
|
376
|
-
elapsedMs,
|
|
377
|
-
latched: retryAfterMs === 0,
|
|
378
|
-
hostStallMs,
|
|
379
|
-
...(retryAfterMs > 0 && { retryAfterMs }),
|
|
380
|
-
budgetMs: WHICH_BUDGET_MS,
|
|
381
|
-
classifiedBy: outcomeForced ? "caller" : "probe",
|
|
382
|
-
});
|
|
383
|
-
return null;
|
|
384
|
-
}
|
|
385
|
-
async function resolveGoFmtBinary() {
|
|
386
|
-
const inPath = await which("gofmt");
|
|
387
|
-
if (inPath)
|
|
388
|
-
return inPath;
|
|
389
|
-
const goCheck = await probeToolAsync("go", ["env", "GOROOT"], {
|
|
390
|
-
timeout: 5000,
|
|
391
|
-
});
|
|
392
|
-
if (goCheck.error || goCheck.status !== 0)
|
|
393
|
-
return null;
|
|
394
|
-
const goroot = (goCheck.stdout ?? "").trim();
|
|
395
|
-
if (!goroot)
|
|
396
|
-
return null;
|
|
397
|
-
const binary = path.join(goroot, "bin", process.platform === "win32" ? "gofmt.exe" : "gofmt");
|
|
398
|
-
return (await fileExists(binary)) ? binary : null;
|
|
399
|
-
}
|
|
400
|
-
// --- Venv / Local Binary Helpers ---
|
|
401
|
-
/**
|
|
402
|
-
* Walk up from cwd looking for a binary in .venv or venv.
|
|
403
|
-
* Returns the absolute path if found, null otherwise.
|
|
404
|
-
*
|
|
405
|
-
* Tool-resolution walker, not a config lookup (#2514/#2517 policy): escaping
|
|
406
|
-
* the project upward past HOME means STOP, not keep reading — a `.venv`/`venv`
|
|
407
|
-
* bin found at or above HOME can never be THIS project's own virtualenv. The
|
|
408
|
-
* ceiling lives in `findLocalBinUpwards` and is default-on there; the tests
|
|
409
|
-
* that pin a fake HOME drive that walker directly rather than threading a
|
|
410
|
-
* `homeDir` no production caller ever passed (#2544 review F3).
|
|
411
|
-
*
|
|
412
|
-
* On Windows the candidate order is EXT-OUTER, DIR-INNER — `.venv/Scripts/x.exe`,
|
|
413
|
-
* `venv/Scripts/x.exe`, then the bare names — which is what the pre-fold copy
|
|
414
|
-
* did and what `findLocalBinUpwards` preserves.
|
|
415
|
-
*/
|
|
416
|
-
async function findInVenv(binary, cwd) {
|
|
417
|
-
return (findLocalBinUpwards(binary, cwd, {
|
|
418
|
-
windowsExt: ".exe",
|
|
419
|
-
binDirs: VENV_BIN_DIRS,
|
|
420
|
-
}) ?? null);
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* Check vendor/bin for PHP Composer-managed tools.
|
|
424
|
-
* Walks up from cwd to find vendor/bin/<binary>.
|
|
425
|
-
*
|
|
426
|
-
* Tool-resolution walker, not a config lookup (#2514/#2517 policy): escaping
|
|
427
|
-
* the project upward past HOME means STOP, not keep reading — a `vendor/bin`
|
|
428
|
-
* match found at or above HOME can never be THIS project's own Composer
|
|
429
|
-
* install. Same single walker as `findInVenv` above; only `binDirs` and the
|
|
430
|
-
* Windows extension differ.
|
|
431
|
-
*/
|
|
432
|
-
async function findInVendorBin(binary, cwd) {
|
|
433
|
-
return (findLocalBinUpwards(binary, cwd, {
|
|
434
|
-
windowsExt: ".bat",
|
|
435
|
-
binDirs: VENDOR_BIN_DIRS,
|
|
436
|
-
}) ?? null);
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* Check node_modules/.bin for locally installed Node tools.
|
|
440
|
-
* Walks up from cwd to find node_modules/.bin/<binary>.
|
|
441
|
-
*
|
|
442
|
-
* #2514: this WAS a private, byte-for-byte duplicate of
|
|
443
|
-
* `findLocalBinUpwards` (`package-manager.ts`) — the same walk `stylua`
|
|
444
|
-
* below, `taplo.ts`, `knip-client.ts`, and (via `findNodeToolBinary`)
|
|
445
|
-
* `dependency-checker.ts`/`jscpd-client.ts` already reuse. A stray
|
|
446
|
-
* `~/node_modules/.bin/oxfmt.cmd` (the home-level pi-extensions manifest
|
|
447
|
-
* installs its own bins) was picked up as the project's formatter because
|
|
448
|
-
* this copy had no HOME ceiling; fixing only this copy would have left every
|
|
449
|
-
* other caller of `findLocalBinUpwards` with the same defect. Delegating
|
|
450
|
-
* here instead of hand-rolling a second ceiling keeps there being exactly one
|
|
451
|
-
* `node_modules/.bin` walker — see that function for the tool-resolution
|
|
452
|
-
* "escaping the project means STOP" policy (#2517).
|
|
453
|
-
*/
|
|
454
|
-
async function findInNodeModules(binary, cwd) {
|
|
455
|
-
return findLocalBinUpwards(binary, cwd) ?? null;
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* Returns true if `bundle exec <gem>` should be used:
|
|
459
|
-
* bundle binary is available AND Gemfile.lock exists in the tree.
|
|
460
|
-
*/
|
|
461
|
-
async function canUseBundleExec(cwd) {
|
|
462
|
-
if ((await which("bundle")) === null)
|
|
463
|
-
return false;
|
|
464
|
-
const lockfiles = await findUp(["Gemfile.lock"], cwd);
|
|
465
|
-
return lockfiles.length > 0;
|
|
466
|
-
}
|
|
467
|
-
async function resolveManagedSmartDefaultCommand(formatterName, filePath, args) {
|
|
468
|
-
const toolId = getAutoInstallToolIdForFormatter(formatterName);
|
|
469
|
-
if (!toolId)
|
|
470
|
-
return FORMATTER_UNAVAILABLE;
|
|
471
|
-
const { ensureTool } = await import("./installer/index.js");
|
|
472
|
-
const installed = await ensureTool(toolId);
|
|
473
|
-
if (!installed) {
|
|
474
|
-
recordDegradationOnce({
|
|
475
|
-
kind: "formatter-unavailable",
|
|
476
|
-
subject: formatterName,
|
|
477
|
-
reason: "managed formatter lookup and installation returned no executable",
|
|
478
|
-
});
|
|
479
|
-
return FORMATTER_UNAVAILABLE;
|
|
480
|
-
}
|
|
481
|
-
return [installed, ...args, filePath];
|
|
482
|
-
}
|
|
483
|
-
/** Resolve an explicitly selected formatter from the shared managed-tool seam. */
|
|
484
|
-
async function resolveManagedFormatterCommand(toolId, filePath, args, findLocal, cwd) {
|
|
485
|
-
const local = findLocal && cwd ? await findLocal(toolId, cwd) : null;
|
|
486
|
-
const installed = local ?? (await which(toolId)) ?? (await getToolPath(toolId));
|
|
487
|
-
return installed ? [installed, ...args, filePath] : FORMATTER_UNAVAILABLE;
|
|
488
|
-
}
|
|
489
|
-
function managedFormatterResolver(toolId, args, findLocal) {
|
|
490
|
-
return (filePath, cwd) => resolveManagedFormatterCommand(toolId, filePath, args, findLocal, cwd);
|
|
491
|
-
}
|
|
492
|
-
function managedToolDetect(toolId, configDetect, nativeDetect) {
|
|
493
|
-
return async (cwd) => {
|
|
494
|
-
const available = (nativeDetect && (await nativeDetect(cwd))) ||
|
|
495
|
-
Boolean(await getToolPath(toolId));
|
|
496
|
-
return available && (configDetect ? await configDetect(cwd) : true);
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
/**
|
|
500
|
-
* One entry per formatter that can be selected via explicit project config
|
|
501
|
-
* (the `formatterPolicy` "explicit-config" branch of `getFormattersForFile`).
|
|
502
|
-
* A formatter with NO entry here can never win that branch — the switch this
|
|
503
|
-
* table replaced had exactly that failure mode for `psscriptanalyzer-format`
|
|
504
|
-
* (#1572: `.ps1`'s policy sets `defaultWhenUnconfigured: false` AND the
|
|
505
|
-
* formatter had no config check, so neither selection branch could ever pick
|
|
506
|
-
* it).
|
|
507
|
-
*
|
|
508
|
-
* This table IS the source of truth for "which formatters have an explicit-
|
|
509
|
-
* config check" — `FORMATTERS_WITH_EXPLICIT_CONFIG_CHECK` below derives its
|
|
510
|
-
* membership from these keys rather than hand-listing them a second time, so
|
|
511
|
-
* the coverage guard in formatter-policy-consistency.test.ts (#1572) can never
|
|
512
|
-
* drift from what this function actually does.
|
|
513
|
-
*/
|
|
514
|
-
// A `Map`, not an object literal: an object literal's lookup inherits
|
|
515
|
-
// `Object.prototype`, so `checks["toString"]` or `checks["constructor"]`
|
|
516
|
-
// resolves to a real (non-formatter) function instead of `undefined` —
|
|
517
|
-
// a formatter genuinely NAMED one of those prototype properties would read
|
|
518
|
-
// as "has an explicit-config check" when it has none (review finding, #1572).
|
|
519
|
-
// `Map.prototype.get` has no such inherited-key hazard.
|
|
520
|
-
const EXPLICIT_FORMATTER_CONFIG_CHECKS = new Map([
|
|
521
|
-
["biome", (cwd) => hasBiomeConfig(cwd)],
|
|
522
|
-
[
|
|
523
|
-
"prettier",
|
|
524
|
-
(cwd) => hasPrettierConfig(cwd) || hasNearestPackageJsonField(cwd, "prettier"),
|
|
525
|
-
],
|
|
526
|
-
// .svelte is conditional beyond "an oxfmt config exists": oxfmt requires the
|
|
527
|
-
// `svelte` package installed AND the config's `svelte` flag enabled
|
|
528
|
-
// (verified empirically — see hasOxfmtSvelteConfig). The generic checks
|
|
529
|
-
// below are NOT sufficient for .svelte — an oxfmt.toml with no svelte flag,
|
|
530
|
-
// or an oxfmt dependency alone, both fail at runtime for .svelte
|
|
531
|
-
// specifically (other extensions are unaffected by this stricter gate).
|
|
532
|
-
[
|
|
533
|
-
"oxfmt",
|
|
534
|
-
(cwd, ext) => ext === ".svelte"
|
|
535
|
-
? hasOxfmtSvelteConfig(cwd)
|
|
536
|
-
: hasOxfmtConfig(cwd) ||
|
|
537
|
-
hasVitePlusConfig(cwd) ||
|
|
538
|
-
// The published package is `oxfmt`; `@oxc-project/oxfmt` does not
|
|
539
|
-
// exist on npm. Accept both (scoped kept for forward-compat).
|
|
540
|
-
hasNearestPackageJsonDependency(cwd, "oxfmt") ||
|
|
541
|
-
hasNearestPackageJsonDependency(cwd, "@oxc-project/oxfmt"),
|
|
542
|
-
],
|
|
543
|
-
["ruff", (cwd) => hasRuffConfig(cwd)],
|
|
544
|
-
["black", (cwd) => hasBlackConfig(cwd)],
|
|
545
|
-
["sqlfluff", (cwd) => hasSqlfluffConfig(cwd)],
|
|
546
|
-
["rubocop", (cwd) => hasRubocopConfig(cwd)],
|
|
547
|
-
["standardrb", (cwd) => hasStandardrbConfig(cwd)],
|
|
548
|
-
["clang-format", (cwd) => hasClangFormatConfig(cwd)],
|
|
549
|
-
["php-cs-fixer", (cwd) => hasPhpCsFixerConfig(cwd)],
|
|
550
|
-
["stylua", (cwd) => hasStyluaConfig(cwd)],
|
|
551
|
-
["ocamlformat", (cwd) => hasOcamlformatConfig(cwd)],
|
|
552
|
-
["google-java-format", (cwd) => hasGoogleJavaFormatConfig(cwd)],
|
|
553
|
-
["ktfmt", (cwd) => hasKtfmtConfig(cwd)],
|
|
554
|
-
["ktlint", (cwd) => hasKtlintConfig(cwd)],
|
|
555
|
-
["cljfmt", (cwd) => hasCljfmtConfig(cwd)],
|
|
556
|
-
["cmake-format", (cwd) => hasCmakeFormatConfig(cwd)],
|
|
557
|
-
["psscriptanalyzer-format", (cwd) => hasPSScriptAnalyzerConfig(cwd)],
|
|
558
|
-
// #1595 sweep — see the comment above hasCsharpierConfig et al. in
|
|
559
|
-
// tool-policy.ts for why nixfmt (the 8th formatter #1572 flagged) is NOT
|
|
560
|
-
// wired here: it has no config-file convention and no manifest-marker
|
|
561
|
-
// equivalent to `.terraform.lock.hcl`, so there is no honest opt-in signal.
|
|
562
|
-
["csharpier", (cwd) => hasCsharpierConfig(cwd)],
|
|
563
|
-
["ormolu", (cwd) => hasOrmoluConfig(cwd)],
|
|
564
|
-
["taplo", (cwd) => hasTaploConfig(cwd)],
|
|
565
|
-
["terraform", (cwd) => hasTerraformConfig(cwd)],
|
|
566
|
-
["swiftformat", (cwd) => hasSwiftformatConfig(cwd)],
|
|
567
|
-
["fantomas", (cwd) => hasFantomasConfig(cwd)],
|
|
568
|
-
["mix", (cwd) => hasMixFormatConfig(cwd)],
|
|
569
|
-
]);
|
|
570
|
-
function hasExplicitFormatterConfig(formatterName, cwd, ext) {
|
|
571
|
-
return (EXPLICIT_FORMATTER_CONFIG_CHECKS.get(formatterName)?.(cwd, ext) ?? false);
|
|
572
|
-
}
|
|
573
|
-
// Exported for the "every registered formatter is selectable" coverage guard
|
|
574
|
-
// (formatter-policy-consistency.test.ts, #1572): derived from the table above,
|
|
575
|
-
// not hand-listed, so it cannot drift from what `hasExplicitFormatterConfig`
|
|
576
|
-
// actually checks.
|
|
577
|
-
export const FORMATTERS_WITH_EXPLICIT_CONFIG_CHECK = new Set(EXPLICIT_FORMATTER_CONFIG_CHECKS.keys());
|
|
578
|
-
// --- Formatter Definitions ---
|
|
579
|
-
async function hasEditorConfig(cwd) {
|
|
580
|
-
return findNearestContaining(cwd, [".editorconfig"]) !== undefined;
|
|
581
|
-
}
|
|
582
|
-
async function indentationArgs(filePath, tool, cwd) {
|
|
583
|
-
if ((await hasEditorConfig(cwd)) ||
|
|
584
|
-
hasExplicitFormatterConfig(tool, cwd, path.extname(filePath))) {
|
|
585
|
-
return [];
|
|
586
|
-
}
|
|
587
|
-
let content;
|
|
588
|
-
try {
|
|
589
|
-
content = await fs.readFile(filePath, "utf8");
|
|
590
|
-
}
|
|
591
|
-
catch {
|
|
592
|
-
// Resolution tests and callers may probe a path before it exists.
|
|
593
|
-
return [];
|
|
594
|
-
}
|
|
595
|
-
if (!hasDetectableIndentation(content))
|
|
596
|
-
return null;
|
|
597
|
-
const indentation = detectIndentation(content);
|
|
598
|
-
if (!indentation) {
|
|
599
|
-
// #3038: the file HAS indentation, but only nested runs, so no unit can be
|
|
600
|
-
// proven. The caller turns this into SKIP_FORMATTING — a file the user
|
|
601
|
-
// expected to be formatted silently is not — so the refusal gets one
|
|
602
|
-
// bounded row. Subject is the tool (four possible values, recorded once
|
|
603
|
-
// per session each) rather than the path: a session that edits many such
|
|
604
|
-
// files must not write one ledger key per file.
|
|
605
|
-
recordDegradationOnce({
|
|
606
|
-
kind: "formatter-skip",
|
|
607
|
-
subject: tool,
|
|
608
|
-
reason: "indentation evidence is ambiguous; formatter style not pinned",
|
|
609
|
-
});
|
|
610
|
-
return null;
|
|
611
|
-
}
|
|
612
|
-
if (tool === "shfmt")
|
|
613
|
-
return indentation.style === "tab"
|
|
614
|
-
? ["-i", "0"]
|
|
615
|
-
: ["-i", String(indentation.width)];
|
|
616
|
-
if (tool === "prettier") {
|
|
617
|
-
return [
|
|
618
|
-
indentation.style === "tab" ? "--use-tabs" : "--no-use-tabs",
|
|
619
|
-
"--tab-width",
|
|
620
|
-
String(indentation.width),
|
|
621
|
-
];
|
|
622
|
-
}
|
|
623
|
-
if (tool === "ruff") {
|
|
624
|
-
// `ruff format` has NO --indent-style/--indent-width flags (it errors with
|
|
625
|
-
// "unexpected argument" and exits 2, which formatFile reported as a silent
|
|
626
|
-
// clean no-op back when exit-code strictness was opt-in). Style is pinned
|
|
627
|
-
// through inline TOML overrides instead (#1144 follow-up).
|
|
628
|
-
return [
|
|
629
|
-
"--config",
|
|
630
|
-
`indent-width=${indentation.width}`,
|
|
631
|
-
"--config",
|
|
632
|
-
`format.indent-style='${indentation.style}'`,
|
|
633
|
-
];
|
|
634
|
-
}
|
|
635
|
-
return [
|
|
636
|
-
"--indent-style",
|
|
637
|
-
indentation.style,
|
|
638
|
-
"--indent-width",
|
|
639
|
-
String(indentation.width),
|
|
640
|
-
];
|
|
641
|
-
}
|
|
642
|
-
/**
|
|
643
|
-
* Resolve the cwd for the formatter child, capped at the user's home
|
|
644
|
-
* directory. Files outside a project retain the historical file-directory
|
|
645
|
-
* cwd. The optional homeDir is only for the ceiling regression test.
|
|
646
|
-
*/
|
|
647
|
-
export function resolveFormatterCwd(absolutePath, formatterName, homeDir = os.homedir()) {
|
|
648
|
-
return resolveToolCwd("formatter", formatterName ?? "unknown", absolutePath, {
|
|
649
|
-
// Formatter discovery historically walks above the file directory. The
|
|
650
|
-
// filesystem root is the neutral dispatch boundary; the seam's `$HOME`
|
|
651
|
-
// ceiling prevents it from escaping the user's workspace.
|
|
652
|
-
cwd: path.parse(path.resolve(absolutePath)).root,
|
|
653
|
-
homeDir,
|
|
654
|
-
}).cwd;
|
|
655
|
-
}
|
|
656
|
-
/**
|
|
657
|
-
* Every biome invocation must carry this. Biome exits 1 with "No files were
|
|
658
|
-
* processed in the specified paths" when the path is ignored by the repo's own
|
|
659
|
-
* biome.json or carries an extension biome does not handle — a benign outcome
|
|
660
|
-
* that, under #1337's strict default, would otherwise report a formatting
|
|
661
|
-
* failure on every edit under an ignored directory. Verified against biome
|
|
662
|
-
* 2.4.12: ignored path → exit 1, +flag → exit 0; unsupported extension → exit
|
|
663
|
-
* 1, +flag → exit 0; and a real syntax error still exits 1 WITH the flag, so
|
|
664
|
-
* strictness is preserved for actual failures.
|
|
665
|
-
*
|
|
666
|
-
* `clients/dispatch/runners/biome-check.ts:108` already passes this on the lint
|
|
667
|
-
* path; the formatter path did not, which is what made biome the one
|
|
668
|
-
* misclassified formatter in the #1337 audit.
|
|
669
|
-
*/
|
|
670
|
-
const BIOME_UNMATCHED_FLAG = "--no-errors-on-unmatched";
|
|
671
|
-
export const biomeFormatter = {
|
|
672
|
-
name: "biome",
|
|
673
|
-
command: [
|
|
674
|
-
"npx",
|
|
675
|
-
"@biomejs/biome",
|
|
676
|
-
"format",
|
|
677
|
-
"--write",
|
|
678
|
-
BIOME_UNMATCHED_FLAG,
|
|
679
|
-
"$FILE",
|
|
680
|
-
],
|
|
681
|
-
async resolveCommand(filePath, cwd) {
|
|
682
|
-
const editorConfigFlag = (await hasEditorConfig(cwd))
|
|
683
|
-
? ["--use-editorconfig=true"]
|
|
684
|
-
: [];
|
|
685
|
-
const styleArgs = await indentationArgs(filePath, "biome", cwd);
|
|
686
|
-
if (styleArgs === null)
|
|
687
|
-
return SKIP_FORMATTING;
|
|
688
|
-
const args = [
|
|
689
|
-
"format",
|
|
690
|
-
"--write",
|
|
691
|
-
BIOME_UNMATCHED_FLAG,
|
|
692
|
-
...editorConfigFlag,
|
|
693
|
-
...styleArgs,
|
|
694
|
-
];
|
|
695
|
-
const local = await findInNodeModules("biome", cwd);
|
|
696
|
-
if (local)
|
|
697
|
-
return [local, ...args, filePath];
|
|
698
|
-
// Any package manager's global bin dir (npm/pnpm/yarn/bun) before we
|
|
699
|
-
// auto-install — catches a `pnpm add -g @biomejs/biome` PATH misses (#375).
|
|
700
|
-
const global = await findGlobalBinary("biome");
|
|
701
|
-
if (global)
|
|
702
|
-
return [global, ...args, filePath];
|
|
703
|
-
const toolId = getAutoInstallToolIdForFormatter("biome");
|
|
704
|
-
if (!toolId)
|
|
705
|
-
return null;
|
|
706
|
-
const { ensureTool } = await import("./installer/index.js");
|
|
707
|
-
const installed = await ensureTool(toolId);
|
|
708
|
-
if (installed)
|
|
709
|
-
return [installed, ...args, filePath];
|
|
710
|
-
return null;
|
|
711
|
-
},
|
|
712
|
-
extensions: [
|
|
713
|
-
".js",
|
|
714
|
-
".jsx",
|
|
715
|
-
".mjs",
|
|
716
|
-
".cjs",
|
|
717
|
-
".ts",
|
|
718
|
-
".tsx",
|
|
719
|
-
".mts",
|
|
720
|
-
".cts",
|
|
721
|
-
".json",
|
|
722
|
-
".jsonc",
|
|
723
|
-
".css",
|
|
724
|
-
".scss",
|
|
725
|
-
".sass",
|
|
726
|
-
".vue",
|
|
727
|
-
".svelte",
|
|
728
|
-
".html",
|
|
729
|
-
".htm",
|
|
730
|
-
],
|
|
731
|
-
async detect(cwd) {
|
|
732
|
-
return (hasBiomeConfig(cwd) ||
|
|
733
|
-
hasNearestPackageJsonDependency(cwd, "@biomejs/biome"));
|
|
734
|
-
},
|
|
735
|
-
};
|
|
736
|
-
export const prettierFormatter = {
|
|
737
|
-
name: "prettier",
|
|
738
|
-
command: ["npx", "prettier", "--write", "$FILE"],
|
|
739
|
-
async resolveCommand(filePath, cwd) {
|
|
740
|
-
const styleArgs = await indentationArgs(filePath, "prettier", cwd);
|
|
741
|
-
if (styleArgs === null)
|
|
742
|
-
return SKIP_FORMATTING;
|
|
743
|
-
const args = ["--write", ...styleArgs];
|
|
744
|
-
const local = await findInNodeModules("prettier", cwd);
|
|
745
|
-
if (local)
|
|
746
|
-
return [local, ...args, filePath];
|
|
747
|
-
// Global bin of any manager (npm/pnpm/yarn/bun) before auto-install (#375).
|
|
748
|
-
const global = await findGlobalBinary("prettier");
|
|
749
|
-
if (global)
|
|
750
|
-
return [global, ...args, filePath];
|
|
751
|
-
return resolveManagedSmartDefaultCommand("prettier", filePath, args);
|
|
752
|
-
},
|
|
753
|
-
extensions: [
|
|
754
|
-
".js",
|
|
755
|
-
".jsx",
|
|
756
|
-
".mjs",
|
|
757
|
-
".cjs",
|
|
758
|
-
".ts",
|
|
759
|
-
".tsx",
|
|
760
|
-
".mts",
|
|
761
|
-
".cts",
|
|
762
|
-
".json",
|
|
763
|
-
".jsonc",
|
|
764
|
-
".css",
|
|
765
|
-
".scss",
|
|
766
|
-
".sass",
|
|
767
|
-
".less",
|
|
768
|
-
".vue",
|
|
769
|
-
".svelte",
|
|
770
|
-
".html",
|
|
771
|
-
".htm",
|
|
772
|
-
".md",
|
|
773
|
-
".mdx",
|
|
774
|
-
".yaml",
|
|
775
|
-
".yml",
|
|
776
|
-
".graphql",
|
|
777
|
-
".gql",
|
|
778
|
-
],
|
|
779
|
-
async detect(cwd) {
|
|
780
|
-
return (hasPrettierConfig(cwd) ||
|
|
781
|
-
hasNearestPackageJsonDependency(cwd, "prettier") ||
|
|
782
|
-
hasNearestPackageJsonField(cwd, "prettier"));
|
|
783
|
-
},
|
|
784
|
-
};
|
|
785
|
-
/**
|
|
786
|
-
* Turns "no target files left after ignore rules" into a clean exit 0.
|
|
787
|
-
*
|
|
788
|
-
* oxfmt is offered for every extension in `OXFMT_SUPPORTED_EXTENSIONS` as soon
|
|
789
|
-
* as a config exists, but a config's `ignorePatterns` can exclude any of them.
|
|
790
|
-
* Handed a path it has been told to ignore, oxfmt 0.64.0 exits 2 with
|
|
791
|
-
* "Expected at least one target file. All matched files may have been excluded
|
|
792
|
-
* by ignore rules." Under the #1337 strict default that reads as a formatting
|
|
793
|
-
* failure, so a user whose oxfmt config ignores (say) Markdown sees an error on
|
|
794
|
-
* every Markdown edit.
|
|
795
|
-
*
|
|
796
|
-
* Same class as biome's "No files were processed in the specified paths", and
|
|
797
|
-
* the same fix: ask the tool not to treat an empty target set as an error,
|
|
798
|
-
* rather than teaching `formatFile` to forgive an exit code. This is the
|
|
799
|
-
* narrower repair — a flag cannot mask anything else, whereas status- or
|
|
800
|
-
* message-keyed leniency would have to be trusted to stay tight across oxfmt
|
|
801
|
-
* releases. Verified against oxfmt 0.64.0: with the flag, an ignored path exits
|
|
802
|
-
* 0 untouched, a normal file is still rewritten, and an unparseable file still
|
|
803
|
-
* exits 2. `tests/clients/dispatch/oxfmt-ignored-file-noop.test.ts` pins all
|
|
804
|
-
* three against the real binary.
|
|
805
|
-
*
|
|
806
|
-
* The flag also silences a genuinely missing file, which `formatFile` never
|
|
807
|
-
* reaches: it reads the file before it spawns anything, so a missing path
|
|
808
|
-
* fails there.
|
|
809
|
-
*/
|
|
810
|
-
const OXFMT_NO_ERROR_ON_UNMATCHED = "--no-error-on-unmatched-pattern";
|
|
811
|
-
export const oxfmtFormatter = {
|
|
812
|
-
name: "oxfmt",
|
|
813
|
-
command: ["oxfmt", OXFMT_NO_ERROR_ON_UNMATCHED, "$FILE"],
|
|
814
|
-
// #1337 audit: oxfmt (and the `vp fmt --write` path) publish no exit-code
|
|
815
|
-
// table. `--write` is the default and `--check` is the separate verification
|
|
816
|
-
// mode, so there is no documented nonzero-on-reformat. Absent a documented
|
|
817
|
-
// benign-nonzero mode, it stays strict — the safe direction, since the failure
|
|
818
|
-
// mode of guessing wrong the other way is a silent no-op (#1336).
|
|
819
|
-
async resolveCommand(filePath, cwd) {
|
|
820
|
-
if (hasVitePlusConfig(cwd)) {
|
|
821
|
-
// No unmatched-pattern flag here: this is `vp`, a different CLI with
|
|
822
|
-
// its own arguments. Whether `vp fmt` has the same empty-target
|
|
823
|
-
// behavior is unverified, so nothing is claimed about it.
|
|
824
|
-
const localVp = await findInNodeModules("vp", cwd);
|
|
825
|
-
if (localVp)
|
|
826
|
-
return [localVp, "fmt", filePath, "--write"];
|
|
827
|
-
const globalVp = await which("vp");
|
|
828
|
-
if (globalVp)
|
|
829
|
-
return [globalVp, "fmt", filePath, "--write"];
|
|
830
|
-
}
|
|
831
|
-
const local = await findInNodeModules("oxfmt", cwd);
|
|
832
|
-
if (local)
|
|
833
|
-
return [local, OXFMT_NO_ERROR_ON_UNMATCHED, filePath];
|
|
834
|
-
const found = await which("oxfmt");
|
|
835
|
-
if (found)
|
|
836
|
-
return [found, OXFMT_NO_ERROR_ON_UNMATCHED, filePath];
|
|
837
|
-
return ((await resolveManagedFormatterCommand("oxfmt", filePath, [OXFMT_NO_ERROR_ON_UNMATCHED], findInNodeModules, cwd)) ?? FORMATTER_UNAVAILABLE);
|
|
838
|
-
},
|
|
839
|
-
// Single source of truth: OXFMT_SUPPORTED_EXTENSIONS in tool-policy.ts.
|
|
840
|
-
// Do not hand-maintain a second copy of this list (#1134 — previously two
|
|
841
|
-
// parallel hand-maintained lists, the #883 single-source-of-truth class).
|
|
842
|
-
extensions: [...OXFMT_SUPPORTED_EXTENSIONS],
|
|
843
|
-
async detect(cwd) {
|
|
844
|
-
return (hasOxfmtConfig(cwd) ||
|
|
845
|
-
hasVitePlusConfig(cwd) ||
|
|
846
|
-
// Published package is `oxfmt` (the scoped name does not exist on npm).
|
|
847
|
-
hasNearestPackageJsonDependency(cwd, "oxfmt") ||
|
|
848
|
-
hasNearestPackageJsonDependency(cwd, "@oxc-project/oxfmt"));
|
|
849
|
-
},
|
|
850
|
-
};
|
|
851
|
-
export const ruffFormatter = {
|
|
852
|
-
name: "ruff",
|
|
853
|
-
command: ["ruff", "format", "$FILE"],
|
|
854
|
-
extensions: [".py", ".pyi"],
|
|
855
|
-
// Strict (the #1337 default): `ruff format` exits 0 on a successful in-place
|
|
856
|
-
// rewrite and 2 on argument rejection / syntax error (verified, ruff 0.x:
|
|
857
|
-
// well-formed → 0, reformatted → 0, unparseable → 2). The exit-2 no-op is
|
|
858
|
-
// exactly what hid the bad --indent-style flags for a full release cycle.
|
|
859
|
-
async resolveCommand(filePath, cwd) {
|
|
860
|
-
const styleArgs = await indentationArgs(filePath, "ruff", cwd);
|
|
861
|
-
if (styleArgs === null)
|
|
862
|
-
return SKIP_FORMATTING;
|
|
863
|
-
const args = ["format", ...styleArgs];
|
|
864
|
-
const venv = await findInVenv("ruff", cwd);
|
|
865
|
-
if (venv)
|
|
866
|
-
return [venv, ...args, filePath];
|
|
867
|
-
const toolId = getAutoInstallToolIdForFormatter("ruff");
|
|
868
|
-
if (!toolId)
|
|
869
|
-
return null;
|
|
870
|
-
const { ensureTool } = await import("./installer/index.js");
|
|
871
|
-
const installed = await ensureTool(toolId);
|
|
872
|
-
if (installed)
|
|
873
|
-
return [installed, ...args, filePath];
|
|
874
|
-
// #2413: `ensureTool` already resolves PATH, global bins and the managed
|
|
875
|
-
// dir (and attempts an install) before returning falsy, so reaching here
|
|
876
|
-
// means ruff is genuinely nowhere. `detect()`'s `hasRuffConfig` branch has
|
|
877
|
-
// no binary check, so a config-only project selects ruff with nothing
|
|
878
|
-
// installed; the static bare `ruff` would then ENOENT. Prove it unavailable.
|
|
879
|
-
return FORMATTER_UNAVAILABLE;
|
|
880
|
-
},
|
|
881
|
-
async detect(cwd) {
|
|
882
|
-
if (hasRuffConfig(cwd))
|
|
883
|
-
return true;
|
|
884
|
-
// No-config fallback: if Ruff is already available, allow formatter usage.
|
|
885
|
-
// This keeps Python default behavior consistent with startup defaults.
|
|
886
|
-
return Boolean(await getToolPath("ruff"));
|
|
887
|
-
},
|
|
888
|
-
};
|
|
889
|
-
export const blackFormatter = {
|
|
890
|
-
name: "black",
|
|
891
|
-
command: ["black", "$FILE"],
|
|
892
|
-
extensions: [".py", ".pyi"],
|
|
893
|
-
resolveCommand: managedFormatterResolver("black", [], findInVenv),
|
|
894
|
-
async detect(cwd) {
|
|
895
|
-
return hasBlackConfig(cwd);
|
|
896
|
-
},
|
|
897
|
-
};
|
|
898
|
-
export const sqlfluffFormatter = {
|
|
899
|
-
name: "sqlfluff",
|
|
900
|
-
command: ["sqlfluff", "fix", "--force", "$FILE"],
|
|
901
|
-
extensions: [".sql"],
|
|
902
|
-
lenientExitCode: "lint-autofix: `sqlfluff fix` writes the corrected file and then exits 1 " +
|
|
903
|
-
"when unfixable violations remain (its documented exit codes are 0 = all " +
|
|
904
|
-
"clean, 1 = violations remain, 2 = command/config failure), so a nonzero " +
|
|
905
|
-
"exit routinely accompanies a successful rewrite.",
|
|
906
|
-
lenientStatuses: [1],
|
|
907
|
-
async resolveCommand(filePath, cwd) {
|
|
908
|
-
const venv = await findInVenv("sqlfluff", cwd);
|
|
909
|
-
if (venv)
|
|
910
|
-
return [venv, "fix", "--force", filePath];
|
|
911
|
-
return null;
|
|
912
|
-
},
|
|
913
|
-
async detect(cwd) {
|
|
914
|
-
return hasSqlfluffConfig(cwd);
|
|
915
|
-
},
|
|
916
|
-
};
|
|
917
|
-
export const gofmtFormatter = {
|
|
918
|
-
name: "gofmt",
|
|
919
|
-
command: ["gofmt", "-w", "$FILE"],
|
|
920
|
-
extensions: [".go"],
|
|
921
|
-
async resolveCommand(filePath, _cwd) {
|
|
922
|
-
const gofmtBinary = await resolveGoFmtBinary();
|
|
923
|
-
if (!gofmtBinary)
|
|
924
|
-
return null;
|
|
925
|
-
return [gofmtBinary, "-w", filePath];
|
|
926
|
-
},
|
|
927
|
-
async detect(_cwd) {
|
|
928
|
-
return (await resolveGoFmtBinary()) !== null;
|
|
929
|
-
},
|
|
930
|
-
};
|
|
931
|
-
export const rustfmtFormatter = {
|
|
932
|
-
name: "rustfmt",
|
|
933
|
-
command: ["rustfmt", "$FILE"],
|
|
934
|
-
extensions: [".rs"],
|
|
935
|
-
// #2466: rustfmt defaults to an OLDER edition than the file's actual Cargo
|
|
936
|
-
// package when invoked bare, so it can reject valid newer-edition syntax
|
|
937
|
-
// (e.g. Rust 2024). Carry the nearest package's `edition` (honoring
|
|
938
|
-
// `edition.workspace = true` inheritance) through `--edition`; `undefined`
|
|
939
|
-
// (unreadable/unparseable manifest) falls back to the static command
|
|
940
|
-
// above, unchanged from pre-#2466 behavior.
|
|
941
|
-
async resolveCommand(filePath, _cwd) {
|
|
942
|
-
const edition = await resolveCargoPackageEdition(filePath);
|
|
943
|
-
if (edition === undefined)
|
|
944
|
-
return null;
|
|
945
|
-
return ["rustfmt", "--edition", edition, filePath];
|
|
946
|
-
},
|
|
947
|
-
async detect(cwd) {
|
|
948
|
-
if ((await which("rustfmt")) !== null)
|
|
949
|
-
return true;
|
|
950
|
-
// If we're in a Rust project, attempt one lazy install of rustfmt component.
|
|
951
|
-
const rustProject = (await findUp(["Cargo.toml"], cwd)).length > 0;
|
|
952
|
-
if (!rustProject)
|
|
953
|
-
return false;
|
|
954
|
-
if ((await which("rustup")) === null)
|
|
955
|
-
return false;
|
|
956
|
-
await tryLazyInstallFormatterTool("rustfmt", cwd);
|
|
957
|
-
return (await which("rustfmt")) !== null;
|
|
958
|
-
},
|
|
959
|
-
};
|
|
960
|
-
export const zigFormatter = {
|
|
961
|
-
name: "zig",
|
|
962
|
-
command: ["zig", "fmt", "$FILE"],
|
|
963
|
-
extensions: [".zig", ".zon"],
|
|
964
|
-
async detect(_cwd) {
|
|
965
|
-
return (await which("zig")) !== null;
|
|
966
|
-
},
|
|
967
|
-
};
|
|
968
|
-
export const dartFormatter = {
|
|
969
|
-
name: "dart",
|
|
970
|
-
command: ["dart", "format", "$FILE"],
|
|
971
|
-
extensions: [".dart"],
|
|
972
|
-
async detect(_cwd) {
|
|
973
|
-
return (await which("dart")) !== null;
|
|
974
|
-
},
|
|
975
|
-
};
|
|
976
|
-
export const shfmtFormatter = {
|
|
977
|
-
name: "shfmt",
|
|
978
|
-
command: ["shfmt", "-w", "$FILE"],
|
|
979
|
-
extensions: [".sh", ".bash"],
|
|
980
|
-
async resolveCommand(filePath, cwd) {
|
|
981
|
-
const styleArgs = await indentationArgs(filePath, "shfmt", cwd);
|
|
982
|
-
if (styleArgs === null)
|
|
983
|
-
return SKIP_FORMATTING;
|
|
984
|
-
const inPath = await which("shfmt");
|
|
985
|
-
if (inPath)
|
|
986
|
-
return [inPath, "-w", ...styleArgs, filePath];
|
|
987
|
-
return resolveManagedSmartDefaultCommand("shfmt", filePath, [
|
|
988
|
-
"-w",
|
|
989
|
-
...styleArgs,
|
|
990
|
-
]);
|
|
991
|
-
},
|
|
992
|
-
detect: managedToolDetect("shfmt", undefined, async () => (await which("shfmt")) !== null),
|
|
993
|
-
};
|
|
994
|
-
export const nixfmtFormatter = {
|
|
995
|
-
name: "nixfmt",
|
|
996
|
-
command: ["nixfmt", "$FILE"],
|
|
997
|
-
// #1337 audit: nixfmt's README documents in-place formatting but has no
|
|
998
|
-
// exit-code section and no change-detection mode, so nothing documents a
|
|
999
|
-
// benign nonzero. Strict by default (see oxfmt above for the rationale).
|
|
1000
|
-
extensions: [".nix"],
|
|
1001
|
-
async detect(_cwd) {
|
|
1002
|
-
return (await which("nixfmt")) !== null;
|
|
1003
|
-
},
|
|
1004
|
-
};
|
|
1005
|
-
export const mixFormatter = {
|
|
1006
|
-
name: "mix",
|
|
1007
|
-
command: ["mix", "format", "$FILE"],
|
|
1008
|
-
extensions: [".ex", ".exs", ".eex", ".heex", ".leex"],
|
|
1009
|
-
async detect(_cwd) {
|
|
1010
|
-
return (await which("mix")) !== null;
|
|
1011
|
-
},
|
|
1012
|
-
};
|
|
1013
|
-
export const ocamlformatFormatter = {
|
|
1014
|
-
name: "ocamlformat",
|
|
1015
|
-
command: ["ocamlformat", "-i", "$FILE"],
|
|
1016
|
-
extensions: [".ml", ".mli"],
|
|
1017
|
-
async detect(cwd) {
|
|
1018
|
-
const hasBinary = (await which("ocamlformat")) !== null;
|
|
1019
|
-
if (!hasBinary)
|
|
1020
|
-
return false;
|
|
1021
|
-
const configs = [".ocamlformat"];
|
|
1022
|
-
const found = await findUp(configs, cwd);
|
|
1023
|
-
return found.length > 0;
|
|
1024
|
-
},
|
|
1025
|
-
};
|
|
1026
|
-
export const clangFormatFormatter = {
|
|
1027
|
-
name: "clang-format",
|
|
1028
|
-
command: ["clang-format", "-i", "$FILE"],
|
|
1029
|
-
extensions: [".c", ".cc", ".cpp", ".cxx", ".h", ".hpp", ".ino"],
|
|
1030
|
-
async detect(cwd) {
|
|
1031
|
-
const hasBinary = (await which("clang-format")) !== null;
|
|
1032
|
-
if (!hasBinary)
|
|
1033
|
-
return false;
|
|
1034
|
-
const configs = [".clang-format", "_clang-format"];
|
|
1035
|
-
const found = await findUp(configs, cwd);
|
|
1036
|
-
return found.length > 0;
|
|
1037
|
-
},
|
|
1038
|
-
};
|
|
1039
|
-
export const ktlintFormatter = {
|
|
1040
|
-
name: "ktlint",
|
|
1041
|
-
command: ["ktlint", "-F", "$FILE"],
|
|
1042
|
-
extensions: [".kt", ".kts"],
|
|
1043
|
-
lenientExitCode: "lint-autofix: `ktlint -F` autocorrects what it can and then exits 1 if " +
|
|
1044
|
-
"any lint error remains unfixed — the documented CLI contract (ktlint " +
|
|
1045
|
-
"exits nonzero whenever violations are reported, and -F does not suppress " +
|
|
1046
|
-
"the ones it cannot correct).",
|
|
1047
|
-
lenientStatuses: [1],
|
|
1048
|
-
async resolveCommand(filePath, _cwd) {
|
|
1049
|
-
const inPath = await which("ktlint");
|
|
1050
|
-
if (inPath)
|
|
1051
|
-
return [inPath, "-F", filePath];
|
|
1052
|
-
return resolveManagedSmartDefaultCommand("ktlint", filePath, ["-F"]);
|
|
1053
|
-
},
|
|
1054
|
-
detect: managedToolDetect("ktlint", undefined, async () => (await which("ktlint")) !== null),
|
|
1055
|
-
};
|
|
1056
|
-
export const ktfmtFormatter = {
|
|
1057
|
-
name: "ktfmt",
|
|
1058
|
-
// ktfmt formats in place when given a file path (no flag needed).
|
|
1059
|
-
command: ["ktfmt", "$FILE"],
|
|
1060
|
-
extensions: [".kt", ".kts"],
|
|
1061
|
-
// #2468: ktfmt's CLI never reads a project's Gradle `ktfmt { googleStyle()
|
|
1062
|
-
// | kotlinLangStyle() }` selection — style is CLI-flag-only
|
|
1063
|
-
// (`--google-style`/`--kotlinlang-style`, verified against ktfmt v0.63's
|
|
1064
|
-
// own arg parser). Carry the nearest module's declared style through;
|
|
1065
|
-
// `undefined` (no declaration, unreadable/unparseable manifest, or an
|
|
1066
|
-
// unsupported style like the removed `dropboxStyle()`) falls back to the
|
|
1067
|
-
// bare invocation, unchanged from pre-#2468 behavior.
|
|
1068
|
-
async resolveCommand(filePath, _cwd) {
|
|
1069
|
-
const styleFlag = await resolveKtfmtGradleStyle(filePath);
|
|
1070
|
-
const inPath = await which("ktfmt");
|
|
1071
|
-
if (inPath)
|
|
1072
|
-
return styleFlag ? [inPath, styleFlag, filePath] : [inPath, filePath];
|
|
1073
|
-
const { ensureTool } = await import("./installer/index.js");
|
|
1074
|
-
const installed = await ensureTool("ktfmt");
|
|
1075
|
-
// #2413: which() and ensureTool (PATH/global/managed + install) both
|
|
1076
|
-
// failed, and `detect()` gates only on hasKtfmtConfig — no binary probe —
|
|
1077
|
-
// so a config-only project reaches here with ktfmt absent. The static
|
|
1078
|
-
// command is bare `ktfmt`; spawning it reproduces the ENOENT class.
|
|
1079
|
-
if (!installed)
|
|
1080
|
-
return FORMATTER_UNAVAILABLE;
|
|
1081
|
-
return styleFlag ? [installed, styleFlag, filePath] : [installed, filePath];
|
|
1082
|
-
},
|
|
1083
|
-
async detect(cwd) {
|
|
1084
|
-
// Opt-in only: ktfmt becomes the formatter when the project elects it,
|
|
1085
|
-
// otherwise ktlint stays the Kotlin smart-default (#129).
|
|
1086
|
-
return hasKtfmtConfig(cwd);
|
|
1087
|
-
},
|
|
1088
|
-
};
|
|
1089
|
-
export const rubocopFormatter = {
|
|
1090
|
-
name: "rubocop",
|
|
1091
|
-
command: ["rubocop", "-a", "--no-color", "$FILE"],
|
|
1092
|
-
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
|
1093
|
-
lenientExitCode: "lint-autofix: `rubocop -a` exits 1 whenever ANY offense remains after it " +
|
|
1094
|
-
"has already rewritten the file. Verified locally (rubocop on Ruby 3.4): a " +
|
|
1095
|
-
"file with an unfixable Lint/UselessAssignment exits 1, and even a " +
|
|
1096
|
-
"tidy file exits 1 on an unfixable Style/Documentation offense — nonzero " +
|
|
1097
|
-
"is the normal outcome, not a failure.",
|
|
1098
|
-
lenientStatuses: [1],
|
|
1099
|
-
async resolveCommand(filePath, cwd) {
|
|
1100
|
-
if (await canUseBundleExec(cwd))
|
|
1101
|
-
return ["bundle", "exec", "rubocop", "-a", "--no-color", filePath];
|
|
1102
|
-
return null;
|
|
1103
|
-
},
|
|
1104
|
-
async detect(cwd) {
|
|
1105
|
-
if (!hasRubocopConfig(cwd))
|
|
1106
|
-
return false;
|
|
1107
|
-
if ((await which("rubocop")) !== null)
|
|
1108
|
-
return true;
|
|
1109
|
-
await tryLazyInstallFormatterTool("rubocop", cwd);
|
|
1110
|
-
return (await which("rubocop")) !== null;
|
|
1111
|
-
},
|
|
1112
|
-
};
|
|
1113
|
-
export const standardrbFormatter = {
|
|
1114
|
-
name: "standardrb",
|
|
1115
|
-
command: ["standardrb", "--fix", "$FILE"],
|
|
1116
|
-
extensions: [".rb", ".rake"],
|
|
1117
|
-
lenientExitCode: "lint-autofix: standardrb is a RuboCop wrapper and inherits its exit " +
|
|
1118
|
-
"contract — `--fix` exits 1 when offenses remain after the rewrite.",
|
|
1119
|
-
lenientStatuses: [1],
|
|
1120
|
-
async resolveCommand(filePath, cwd) {
|
|
1121
|
-
if (await canUseBundleExec(cwd))
|
|
1122
|
-
return ["bundle", "exec", "standardrb", "--fix", filePath];
|
|
1123
|
-
return null;
|
|
1124
|
-
},
|
|
1125
|
-
async detect(cwd) {
|
|
1126
|
-
if (!hasStandardrbConfig(cwd))
|
|
1127
|
-
return false;
|
|
1128
|
-
return (await which("standardrb")) !== null;
|
|
1129
|
-
},
|
|
1130
|
-
};
|
|
1131
|
-
export const gleamFormatter = {
|
|
1132
|
-
name: "gleam",
|
|
1133
|
-
command: ["gleam", "format", "$FILE"],
|
|
1134
|
-
extensions: [".gleam"],
|
|
1135
|
-
async detect(cwd) {
|
|
1136
|
-
// Present if gleam.toml exists (any Gleam project)
|
|
1137
|
-
const found = await findUp(["gleam.toml"], cwd);
|
|
1138
|
-
if (found.length > 0)
|
|
1139
|
-
return (await which("gleam")) !== null;
|
|
1140
|
-
return false;
|
|
1141
|
-
},
|
|
1142
|
-
};
|
|
1143
|
-
export const typstyleFormatter = {
|
|
1144
|
-
name: "typstyle",
|
|
1145
|
-
command: ["typstyle", "-i", "$FILE"],
|
|
1146
|
-
extensions: [".typ", ".typc"],
|
|
1147
|
-
async resolveCommand(filePath) {
|
|
1148
|
-
const inPath = await which("typstyle");
|
|
1149
|
-
if (inPath)
|
|
1150
|
-
return [inPath, "-i", filePath];
|
|
1151
|
-
return resolveManagedSmartDefaultCommand("typstyle", filePath, ["-i"]);
|
|
1152
|
-
},
|
|
1153
|
-
detect: managedToolDetect("typstyle", undefined, async () => (await which("typstyle")) !== null),
|
|
1154
|
-
};
|
|
1155
|
-
export const terraformFormatter = {
|
|
1156
|
-
name: "terraform",
|
|
1157
|
-
command: ["terraform", "fmt", "$FILE"],
|
|
1158
|
-
extensions: [".tf", ".tfvars"],
|
|
1159
|
-
async detect(_cwd) {
|
|
1160
|
-
return (await which("terraform")) !== null;
|
|
1161
|
-
},
|
|
1162
|
-
};
|
|
1163
|
-
export const terragruntHclFormatter = {
|
|
1164
|
-
name: "terragrunt-hcl",
|
|
1165
|
-
command: ["terragrunt", "hcl", "fmt", "--file", "$FILE"],
|
|
1166
|
-
extensions: [],
|
|
1167
|
-
filenames: TERRAGRUNT_FILENAMES,
|
|
1168
|
-
// Strict (the #1337 default): verified exit 0 on a successful in-place format
|
|
1169
|
-
// against terragrunt v1.1.2. A binary predating the `hcl` command group exits
|
|
1170
|
-
// nonzero and touches nothing, which unguarded reads as "already formatted".
|
|
1171
|
-
async detect(_cwd) {
|
|
1172
|
-
return (await which("terragrunt")) !== null;
|
|
1173
|
-
},
|
|
1174
|
-
};
|
|
1175
|
-
export const phpCsFixerFormatter = {
|
|
1176
|
-
name: "php-cs-fixer",
|
|
1177
|
-
command: ["php-cs-fixer", "fix", "$FILE"],
|
|
1178
|
-
extensions: [".php"],
|
|
1179
|
-
// #2472: php-cs-fixer does NOT walk up parent directories looking for its
|
|
1180
|
-
// own config the way prettier/biome/eslint do (verified against upstream
|
|
1181
|
-
// `computeConfigFiles()` — see `resolvePhpCsFixerConfig`'s doc comment),
|
|
1182
|
-
// and `formatFile` spawns with cwd = the FILE's own directory, which is
|
|
1183
|
-
// not necessarily where the ancestor config `detect()` found actually
|
|
1184
|
-
// lives. Always resolve the binary explicitly here (vendor/bin first,
|
|
1185
|
-
// then global) rather than falling through to the static `command` above
|
|
1186
|
-
// — that static command can never carry `--config`, so a config found at
|
|
1187
|
-
// an ancestor would silently be dropped whenever the vendor lookup here
|
|
1188
|
-
// missed but a global binary still resolved. `--config` is attached
|
|
1189
|
-
// whenever a config resolves, even when it sits in the file's own
|
|
1190
|
-
// directory (AC3): unlike the pre-#2472 code, correctness no longer
|
|
1191
|
-
// depends on php-cs-fixer's own (nonexistent) upward search.
|
|
1192
|
-
async resolveCommand(filePath, cwd) {
|
|
1193
|
-
const configPath = resolvePhpCsFixerConfig(filePath);
|
|
1194
|
-
const binary = (await findInVendorBin("php-cs-fixer", cwd)) ??
|
|
1195
|
-
(await which("php-cs-fixer"));
|
|
1196
|
-
const managed = await resolveManagedFormatterCommand("php-cs-fixer", filePath, [], findInVendorBin, cwd);
|
|
1197
|
-
if (managed === FORMATTER_UNAVAILABLE)
|
|
1198
|
-
return FORMATTER_UNAVAILABLE;
|
|
1199
|
-
const resolved = binary ?? managed[0];
|
|
1200
|
-
// #2413/#2472 review F4: both probes (vendor/bin, then PATH) have
|
|
1201
|
-
// PROVEN the binary is absent — returning `null` here would fall back
|
|
1202
|
-
// to the static `command` above, which is the SAME bare
|
|
1203
|
-
// `php-cs-fixer` this just failed to find, spawning it only to
|
|
1204
|
-
// re-observe the ENOENT already known. Report the proven-missing
|
|
1205
|
-
// state instead so `formatFile` skips the wasted spawn.
|
|
1206
|
-
if (!resolved)
|
|
1207
|
-
return FORMATTER_UNAVAILABLE;
|
|
1208
|
-
return configPath
|
|
1209
|
-
? [resolved, "fix", "--config", configPath, filePath]
|
|
1210
|
-
: [resolved, "fix", filePath];
|
|
1211
|
-
},
|
|
1212
|
-
detect: managedToolDetect("php-cs-fixer", async (cwd) => (await findUp([".php-cs-fixer.php", ".php-cs-fixer.dist.php"], cwd))
|
|
1213
|
-
.length > 0, async (cwd) => Boolean((await findInVendorBin("php-cs-fixer", cwd)) ||
|
|
1214
|
-
(await which("php-cs-fixer")))),
|
|
1215
|
-
};
|
|
1216
|
-
export const csharpierFormatter = {
|
|
1217
|
-
name: "csharpier",
|
|
1218
|
-
// CSharpier ≥1.0 is a standalone `csharpier format <file>`; the `dotnet
|
|
1219
|
-
// csharpier <file>` form was removed (a bare `dotnet csharpier` now errors
|
|
1220
|
-
// "a dotnet-prefixed executable with this name could not be found"). Keep the
|
|
1221
|
-
// legacy form as a fallback for CSharpier 0.x via resolveCommand.
|
|
1222
|
-
command: ["csharpier", "format", "$FILE"],
|
|
1223
|
-
extensions: [".cs"],
|
|
1224
|
-
async resolveCommand(filePath, _cwd) {
|
|
1225
|
-
if ((await which("csharpier")) !== null) {
|
|
1226
|
-
return ["csharpier", "format", filePath];
|
|
1227
|
-
}
|
|
1228
|
-
// CSharpier 0.x: invoked through the dotnet driver.
|
|
1229
|
-
if ((await which("dotnet")) !== null) {
|
|
1230
|
-
const legacy = await probeToolAsync("dotnet", ["csharpier", "--version"], { timeout: 5000 });
|
|
1231
|
-
if (!legacy.error && legacy.status === 0) {
|
|
1232
|
-
return ["dotnet", "csharpier", filePath];
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
return null;
|
|
1236
|
-
},
|
|
1237
|
-
async detect(_cwd) {
|
|
1238
|
-
// CSharpier ≥1.0 standalone binary …
|
|
1239
|
-
if ((await which("csharpier")) !== null)
|
|
1240
|
-
return true;
|
|
1241
|
-
// … or the legacy dotnet-driver form (CSharpier 0.x).
|
|
1242
|
-
if ((await which("dotnet")) === null)
|
|
1243
|
-
return false;
|
|
1244
|
-
const result = await probeToolAsync("dotnet", ["csharpier", "--version"], {
|
|
1245
|
-
timeout: 5000,
|
|
1246
|
-
});
|
|
1247
|
-
return !result.error && result.status === 0;
|
|
1248
|
-
},
|
|
1249
|
-
};
|
|
1250
|
-
export const fantomasFormatter = {
|
|
1251
|
-
name: "fantomas",
|
|
1252
|
-
command: ["fantomas", "$FILE"],
|
|
1253
|
-
extensions: [".fs", ".fsi", ".fsx"],
|
|
1254
|
-
async detect(_cwd) {
|
|
1255
|
-
return (await which("fantomas")) !== null;
|
|
1256
|
-
},
|
|
1257
|
-
};
|
|
1258
|
-
export const swiftformatFormatter = {
|
|
1259
|
-
name: "swiftformat",
|
|
1260
|
-
command: ["swiftformat", "$FILE"],
|
|
1261
|
-
extensions: [".swift"],
|
|
1262
|
-
async detect(_cwd) {
|
|
1263
|
-
return (await which("swiftformat")) !== null;
|
|
1264
|
-
},
|
|
1265
|
-
};
|
|
1266
|
-
export const styluaFormatter = {
|
|
1267
|
-
name: "stylua",
|
|
1268
|
-
command: ["stylua", "$FILE"],
|
|
1269
|
-
extensions: [".lua"],
|
|
1270
|
-
resolveCommand: managedFormatterResolver("stylua", [], findLocalBinUpwards),
|
|
1271
|
-
detect: managedToolDetect("stylua", async (cwd) => (await findUp(["stylua.toml", ".stylua.toml"], cwd)).length > 0, async (cwd) => Boolean(findLocalBinUpwards("stylua", cwd) || (await which("stylua")))),
|
|
1272
|
-
};
|
|
1273
|
-
export const ormoluFormatter = {
|
|
1274
|
-
name: "ormolu",
|
|
1275
|
-
command: ["ormolu", "--mode", "inplace", "$FILE"],
|
|
1276
|
-
extensions: [".hs", ".lhs"],
|
|
1277
|
-
async detect(_cwd) {
|
|
1278
|
-
return (await which("ormolu")) !== null;
|
|
1279
|
-
},
|
|
1280
|
-
};
|
|
1281
|
-
export const taploFormatter = {
|
|
1282
|
-
name: "taplo",
|
|
1283
|
-
command: ["taplo", "fmt", "$FILE"],
|
|
1284
|
-
extensions: [".toml"],
|
|
1285
|
-
async resolveCommand(filePath, _cwd) {
|
|
1286
|
-
const inPath = await which("taplo");
|
|
1287
|
-
if (inPath)
|
|
1288
|
-
return [inPath, "fmt", filePath];
|
|
1289
|
-
return resolveManagedSmartDefaultCommand("taplo", filePath, ["fmt"]);
|
|
1290
|
-
},
|
|
1291
|
-
detect: managedToolDetect("taplo", undefined, async () => (await which("taplo")) !== null),
|
|
1292
|
-
};
|
|
1293
|
-
export const googleJavaFormatFormatter = {
|
|
1294
|
-
name: "google-java-format",
|
|
1295
|
-
command: ["google-java-format", "--replace", "$FILE"],
|
|
1296
|
-
extensions: [".java"],
|
|
1297
|
-
resolveCommand: managedFormatterResolver("google-java-format", ["--replace"]),
|
|
1298
|
-
detect: managedToolDetect("google-java-format", hasGoogleJavaFormatConfig, async () => (await which("google-java-format")) !== null),
|
|
1299
|
-
};
|
|
1300
|
-
export const cljfmtFormatter = {
|
|
1301
|
-
name: "cljfmt",
|
|
1302
|
-
command: ["cljfmt", "fix", "$FILE"],
|
|
1303
|
-
extensions: [".clj", ".cljc", ".cljs"],
|
|
1304
|
-
resolveCommand: managedFormatterResolver("cljfmt", ["fix"]),
|
|
1305
|
-
detect: managedToolDetect("cljfmt", hasCljfmtConfig, async () => (await which("cljfmt")) !== null),
|
|
1306
|
-
};
|
|
1307
|
-
export const cmakeFormatFormatter = {
|
|
1308
|
-
name: "cmake-format",
|
|
1309
|
-
command: ["cmake-format", "-i", "$FILE"],
|
|
1310
|
-
extensions: [".cmake"],
|
|
1311
|
-
resolveCommand: managedFormatterResolver("cmake-format", ["-i"]),
|
|
1312
|
-
detect: managedToolDetect("cmake-format", hasCmakeFormatConfig, async () => (await which("cmake-format")) !== null),
|
|
1313
|
-
};
|
|
1314
|
-
export const cueFormatter = {
|
|
1315
|
-
name: "cue",
|
|
1316
|
-
// `cue fmt <file>` rewrites in place already. There is no `-w`: the only
|
|
1317
|
-
// flags are --check, -d/--diff and --files, so `cue fmt -w` aborts with
|
|
1318
|
-
// "unknown shorthand flag: 'w'" on every .cue write (verified on cue
|
|
1319
|
-
// v0.17.1).
|
|
1320
|
-
command: ["cue", "fmt", "$FILE"],
|
|
1321
|
-
extensions: [".cue"],
|
|
1322
|
-
async detect(_cwd) {
|
|
1323
|
-
return (await which("cue")) !== null;
|
|
1324
|
-
},
|
|
1325
|
-
};
|
|
1326
|
-
/**
|
|
1327
|
-
* The PowerShell one-liner behind `psscriptanalyzer-format`.
|
|
1328
|
-
*
|
|
1329
|
-
* #1337 audit finding: without `$ErrorActionPreference = 'Stop'`, a failing
|
|
1330
|
-
* `Invoke-Formatter` is a NON-TERMINATING error — pwsh still exits 0, so this
|
|
1331
|
-
* formatter could never report a failure through the strict seam and the #1336
|
|
1332
|
-
* silent-no-op class survived intact for `.ps1/.psm1/.psd1`. Verified: an
|
|
1333
|
-
* `Invoke-Formatter` argument-validation failure exits 0 under the old script.
|
|
1334
|
-
*
|
|
1335
|
-
* Two more defects fixed here rather than left as landmines:
|
|
1336
|
-
* - the old script ran `Set-Content -Value $formatted` even when `$formatted`
|
|
1337
|
-
* was `$null`, which TRUNCATES the file it was asked to format;
|
|
1338
|
-
* - single-quoted interpolation broke on any path containing an apostrophe.
|
|
1339
|
-
* An empty/whitespace-only file exits 0 without touching anything, so "nothing
|
|
1340
|
-
* to format" stays a clean no-op rather than a reported failure.
|
|
1341
|
-
*
|
|
1342
|
-
* `settingsPath`, when the project has one (#1572 review F2), is passed
|
|
1343
|
-
* straight to `Invoke-Formatter -Settings`. Gating selection on the file's
|
|
1344
|
-
* presence without ever reading its rules would run the project through the
|
|
1345
|
-
* stock `CodeFormatting` ruleset regardless of what it declared — the same
|
|
1346
|
-
* stock-style imposition #1144 banned for the other config-first formatters.
|
|
1347
|
-
*/
|
|
1348
|
-
function psScriptAnalyzerCommand(filePath, settingsPath) {
|
|
1349
|
-
// PowerShell single-quoted strings escape an apostrophe by doubling it.
|
|
1350
|
-
const quoted = filePath.replace(/'/g, "''");
|
|
1351
|
-
const settingsArg = settingsPath
|
|
1352
|
-
? ` -Settings '${settingsPath.replace(/'/g, "''")}'`
|
|
1353
|
-
: "";
|
|
1354
|
-
return [
|
|
1355
|
-
"$ErrorActionPreference = 'Stop'",
|
|
1356
|
-
`$p = '${quoted}'`,
|
|
1357
|
-
"$content = Get-Content -Raw -LiteralPath $p",
|
|
1358
|
-
"if ([string]::IsNullOrWhiteSpace($content)) { exit 0 }",
|
|
1359
|
-
`$formatted = Invoke-Formatter -ScriptDefinition $content${settingsArg}`,
|
|
1360
|
-
"if ($null -eq $formatted) { throw 'Invoke-Formatter returned no output' }",
|
|
1361
|
-
"Set-Content -LiteralPath $p -Value $formatted",
|
|
1362
|
-
].join("; ");
|
|
1363
|
-
}
|
|
1364
|
-
export const psscriptanalyzerFormatFormatter = {
|
|
1365
|
-
name: "psscriptanalyzer-format",
|
|
1366
|
-
command: ["pwsh", "-NoProfile", "-Command", psScriptAnalyzerCommand("$FILE")],
|
|
1367
|
-
extensions: [".ps1", ".psm1", ".psd1"],
|
|
1368
|
-
async resolveCommand(filePath, cwd) {
|
|
1369
|
-
const pwsh = (await which("pwsh")) ?? (await which("powershell"));
|
|
1370
|
-
if (!pwsh)
|
|
1371
|
-
return null;
|
|
1372
|
-
const settingsPath = findPSScriptAnalyzerConfigPath(cwd);
|
|
1373
|
-
return [
|
|
1374
|
-
pwsh,
|
|
1375
|
-
"-NoProfile",
|
|
1376
|
-
"-Command",
|
|
1377
|
-
psScriptAnalyzerCommand(filePath, settingsPath),
|
|
1378
|
-
];
|
|
1379
|
-
},
|
|
1380
|
-
async detect(_cwd) {
|
|
1381
|
-
const pwsh = (await which("pwsh")) ?? (await which("powershell"));
|
|
1382
|
-
if (!pwsh)
|
|
1383
|
-
return false;
|
|
1384
|
-
// Check PSScriptAnalyzer module is available
|
|
1385
|
-
const result = await probeToolAsync(pwsh, [
|
|
1386
|
-
"-NoProfile",
|
|
1387
|
-
"-Command",
|
|
1388
|
-
"Get-Module -ListAvailable PSScriptAnalyzer | Select-Object -First 1 -ExpandProperty Name",
|
|
1389
|
-
], { timeout: 5_000 });
|
|
1390
|
-
return (result.stdout ?? "").includes("PSScriptAnalyzer");
|
|
1391
|
-
},
|
|
1392
|
-
};
|
|
1393
|
-
// --- Registry ---
|
|
1394
|
-
// Exported for the formatter/policy drift guard (tests/clients/
|
|
1395
|
-
// formatter-policy-consistency.test.ts, #1135): the test cross-checks these
|
|
1396
|
-
// definitions against tool-policy.ts's FORMATTER_POLICY_BY_EXTENSION so the two
|
|
1397
|
-
// hand-maintained inverse mappings can never silently diverge.
|
|
1398
|
-
export const ALL_FORMATTERS = [
|
|
1399
|
-
biomeFormatter,
|
|
1400
|
-
prettierFormatter,
|
|
1401
|
-
oxfmtFormatter,
|
|
1402
|
-
ruffFormatter,
|
|
1403
|
-
blackFormatter,
|
|
1404
|
-
sqlfluffFormatter,
|
|
1405
|
-
gofmtFormatter,
|
|
1406
|
-
rustfmtFormatter,
|
|
1407
|
-
zigFormatter,
|
|
1408
|
-
dartFormatter,
|
|
1409
|
-
shfmtFormatter,
|
|
1410
|
-
nixfmtFormatter,
|
|
1411
|
-
mixFormatter,
|
|
1412
|
-
ocamlformatFormatter,
|
|
1413
|
-
clangFormatFormatter,
|
|
1414
|
-
ktlintFormatter,
|
|
1415
|
-
ktfmtFormatter,
|
|
1416
|
-
terraformFormatter,
|
|
1417
|
-
terragruntHclFormatter,
|
|
1418
|
-
phpCsFixerFormatter,
|
|
1419
|
-
csharpierFormatter,
|
|
1420
|
-
fantomasFormatter,
|
|
1421
|
-
swiftformatFormatter,
|
|
1422
|
-
styluaFormatter,
|
|
1423
|
-
ormoluFormatter,
|
|
1424
|
-
rubocopFormatter,
|
|
1425
|
-
standardrbFormatter,
|
|
1426
|
-
gleamFormatter,
|
|
1427
|
-
typstyleFormatter,
|
|
1428
|
-
taploFormatter,
|
|
1429
|
-
googleJavaFormatFormatter,
|
|
1430
|
-
cljfmtFormatter,
|
|
1431
|
-
cmakeFormatFormatter,
|
|
1432
|
-
cueFormatter,
|
|
1433
|
-
psscriptanalyzerFormatFormatter,
|
|
1434
|
-
];
|
|
1435
|
-
// Basenames claimed by a filename-keyed formatter, e.g. terragrunt.hcl.
|
|
1436
|
-
const FILENAME_FORMATTER_BASENAMES = new Set(ALL_FORMATTERS.flatMap((f) => f.filenames ?? []));
|
|
1437
|
-
// Cache for detection results - stores array of enabled formatter names per cwd+ext
|
|
1438
|
-
const detectionCache = new BoundedLruCache(32);
|
|
1439
|
-
// The signature is immutable for a cache generation. This memo is separate
|
|
1440
|
-
// from detectionCache because a cwd can have several extension entries, and a
|
|
1441
|
-
// warm lookup must not repeat the ancestor walk or stat matched configs.
|
|
1442
|
-
const formatterSignatureFlights = new BoundedLruCache(32);
|
|
1443
|
-
const formatterCacheGeneration = createGenerationSource("formatter-cache");
|
|
1444
|
-
// These are the formatter configuration files consulted by the policy helpers
|
|
1445
|
-
// above. Their metadata is captured by the cold detection signature. The
|
|
1446
|
-
// write-result seam invalidates that signature when a config path changes, so
|
|
1447
|
-
// detection re-runs even when PATH and installed tools are unchanged. A
|
|
1448
|
-
// filename a `has*Config` check reads but this list omits is invisible to the
|
|
1449
|
-
// cache: the signature never moves when that file is added, so a project that
|
|
1450
|
-
// opts in AFTER the first `getFormattersForFile` call for its cwd keeps
|
|
1451
|
-
// getting the stale (pre-opt-in) cached answer for the rest of the session
|
|
1452
|
-
// (#1572 review F1 — proved for psscriptanalyzer-format's settings file;
|
|
1453
|
-
// swept against every `EXPLICIT_FORMATTER_CONFIG_CHECKS` entry below, which
|
|
1454
|
-
// turned up the same gap for google-java-format, cljfmt, cmake-format, the
|
|
1455
|
-
// Kotlin/Spotless gradle files, sqlfluff's setup.cfg, and oxfmt's
|
|
1456
|
-
// vite-plus.json / additional vite.config extensions).
|
|
1457
|
-
const FORMATTER_CONFIG_FILES = [
|
|
1458
|
-
"package.json",
|
|
1459
|
-
"biome.json",
|
|
1460
|
-
"biome.jsonc",
|
|
1461
|
-
".prettierrc",
|
|
1462
|
-
".prettierrc.json",
|
|
1463
|
-
".prettierrc.yml",
|
|
1464
|
-
".prettierrc.yaml",
|
|
1465
|
-
".prettierrc.js",
|
|
1466
|
-
".prettierrc.cjs",
|
|
1467
|
-
".prettierrc.mjs",
|
|
1468
|
-
"prettier.config.js",
|
|
1469
|
-
"prettier.config.cjs",
|
|
1470
|
-
"prettier.config.mjs",
|
|
1471
|
-
"prettier.config.ts",
|
|
1472
|
-
"pyproject.toml",
|
|
1473
|
-
"ruff.toml",
|
|
1474
|
-
".ruff.toml",
|
|
1475
|
-
"black.toml",
|
|
1476
|
-
".black",
|
|
1477
|
-
"tox.ini",
|
|
1478
|
-
"setup.cfg",
|
|
1479
|
-
"requirements.txt",
|
|
1480
|
-
"Pipfile",
|
|
1481
|
-
".sqlfluff",
|
|
1482
|
-
".rubocop.yml",
|
|
1483
|
-
".rubocop.yaml",
|
|
1484
|
-
"Gemfile",
|
|
1485
|
-
".clang-format",
|
|
1486
|
-
"_clang-format",
|
|
1487
|
-
".php-cs-fixer.php",
|
|
1488
|
-
".php-cs-fixer.dist.php",
|
|
1489
|
-
"stylua.toml",
|
|
1490
|
-
".stylua.toml",
|
|
1491
|
-
".ocamlformat",
|
|
1492
|
-
".editorconfig",
|
|
1493
|
-
".google-java-format",
|
|
1494
|
-
".ktfmt",
|
|
1495
|
-
".ktfmt.kts",
|
|
1496
|
-
"build.gradle.kts",
|
|
1497
|
-
"build.gradle",
|
|
1498
|
-
"settings.gradle.kts",
|
|
1499
|
-
"settings.gradle",
|
|
1500
|
-
".cljfmt.edn",
|
|
1501
|
-
"cljfmt.edn",
|
|
1502
|
-
".cljfmt",
|
|
1503
|
-
".cmake-format",
|
|
1504
|
-
".cmake-format.yaml",
|
|
1505
|
-
".cmake-format.yml",
|
|
1506
|
-
".cmake-format.json",
|
|
1507
|
-
".cmake-format.py",
|
|
1508
|
-
"cmake-format.py",
|
|
1509
|
-
"cmake-format.yaml",
|
|
1510
|
-
"cmake-format.yml",
|
|
1511
|
-
"oxfmt.toml",
|
|
1512
|
-
".oxfmtrc.json",
|
|
1513
|
-
"vite-plus.json",
|
|
1514
|
-
"vite.config.ts",
|
|
1515
|
-
"vite.config.mts",
|
|
1516
|
-
"vite.config.cts",
|
|
1517
|
-
"vite.config.js",
|
|
1518
|
-
"vite.config.mjs",
|
|
1519
|
-
"vite.config.cjs",
|
|
1520
|
-
"PSScriptAnalyzerSettings.psd1",
|
|
1521
|
-
"ScriptAnalyzerSettings.psd1",
|
|
1522
|
-
// #1595 sweep additions.
|
|
1523
|
-
".csharpierrc",
|
|
1524
|
-
".csharpierrc.json",
|
|
1525
|
-
".csharpierrc.yaml",
|
|
1526
|
-
".csharpierrc.yml",
|
|
1527
|
-
".ormolu",
|
|
1528
|
-
"taplo.toml",
|
|
1529
|
-
".taplo.toml",
|
|
1530
|
-
".terraform.lock.hcl",
|
|
1531
|
-
".swiftformat",
|
|
1532
|
-
".fantomasignore",
|
|
1533
|
-
".formatter.exs",
|
|
1534
|
-
];
|
|
1535
|
-
async function formatterConfigSignature(cwd) {
|
|
1536
|
-
const paths = await findUp(FORMATTER_CONFIG_FILES, cwd);
|
|
1537
|
-
const parts = await Promise.all(paths.sort(compareOrdinal).map(async (filePath) => {
|
|
1538
|
-
try {
|
|
1539
|
-
const stat = await fs.stat(filePath);
|
|
1540
|
-
return `${filePath}:${stat.mtimeMs}:${stat.size}`;
|
|
1541
|
-
}
|
|
1542
|
-
catch {
|
|
1543
|
-
return `${filePath}:missing`;
|
|
1544
|
-
}
|
|
1545
|
-
}));
|
|
1546
|
-
return parts.join("|");
|
|
1547
|
-
}
|
|
1548
|
-
async function getFormatterConfigSignature(cwd, normalizedCwd) {
|
|
1549
|
-
const existing = formatterSignatureFlights.get(normalizedCwd);
|
|
1550
|
-
if (existing)
|
|
1551
|
-
return existing.promise;
|
|
1552
|
-
const promise = formatterConfigSignature(cwd).finally(() => {
|
|
1553
|
-
const current = formatterSignatureFlights.get(normalizedCwd);
|
|
1554
|
-
if (current?.promise === promise)
|
|
1555
|
-
formatterSignatureFlights.delete(normalizedCwd);
|
|
1556
|
-
});
|
|
1557
|
-
formatterSignatureFlights.set(normalizedCwd, { promise });
|
|
1558
|
-
return promise;
|
|
1559
|
-
}
|
|
1560
|
-
// --- Public API ---
|
|
1561
|
-
export async function getFormattersForFile(filePath, cwd) {
|
|
1562
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
1563
|
-
const base = path.basename(filePath).toLowerCase();
|
|
1564
|
-
// Filename-keyed formatters (e.g. terragrunt.hcl) can share an extension
|
|
1565
|
-
// with unrelated files in the same dir (.terraform.lock.hcl next to
|
|
1566
|
-
// terragrunt.hcl). Fold the basename into the cache key only when a
|
|
1567
|
-
// filename-based formatter actually applies, so a plain .hcl file cached
|
|
1568
|
-
// first doesn't poison the cache for terragrunt.hcl/root.hcl, or vice versa.
|
|
1569
|
-
const normalizedCwd = normalizeMapKey(cwd);
|
|
1570
|
-
const cacheKey = FILENAME_FORMATTER_BASENAMES.has(base)
|
|
1571
|
-
? `${normalizedCwd}:${ext}:${base}`
|
|
1572
|
-
: `${normalizedCwd}:${ext}`;
|
|
1573
|
-
// A warm entry is authoritative until the write-result seam invalidates its
|
|
1574
|
-
// config path. This is the only way to make the warm path free of config
|
|
1575
|
-
// polling while still reacting immediately to pi-authored create/remove/
|
|
1576
|
-
// change events. External editor changes remain a documented session-boundary
|
|
1577
|
-
// limitation, like other write-result-owned freshness seams.
|
|
1578
|
-
let cached = detectionCache.get(normalizedCwd);
|
|
1579
|
-
if (cached?.entries.has(cacheKey)) {
|
|
1580
|
-
const enabledNames = cached.entries.get(cacheKey);
|
|
1581
|
-
const selectedFormatterName = enabledNames && enabledNames.length > 0 ? enabledNames[0] : null;
|
|
1582
|
-
logLatency({
|
|
1583
|
-
type: "phase",
|
|
1584
|
-
phase: "formatter_selected",
|
|
1585
|
-
filePath,
|
|
1586
|
-
durationMs: 0,
|
|
1587
|
-
metadata: {
|
|
1588
|
-
formatter: selectedFormatterName,
|
|
1589
|
-
reason: "cache",
|
|
1590
|
-
cached: true,
|
|
1591
|
-
outcome: "hit",
|
|
1592
|
-
cwd,
|
|
1593
|
-
},
|
|
1594
|
-
});
|
|
1595
|
-
if (!enabledNames || enabledNames.length === 0)
|
|
1596
|
-
return [];
|
|
1597
|
-
return ALL_FORMATTERS.filter((f) => enabledNames.includes(f.name));
|
|
1598
|
-
}
|
|
1599
|
-
if (!cached) {
|
|
1600
|
-
const generation = formatterCacheGeneration.capture();
|
|
1601
|
-
const configSignature = await getFormatterConfigSignature(cwd, normalizedCwd);
|
|
1602
|
-
// An invalidation can happen while the first signature walk is in flight.
|
|
1603
|
-
// Do not publish a pre-invalidation signature into the new generation.
|
|
1604
|
-
if (!generation.isCurrent()) {
|
|
1605
|
-
return getFormattersForFile(filePath, cwd);
|
|
1606
|
-
}
|
|
1607
|
-
// Another cold caller for this cwd can finish the shared signature flight
|
|
1608
|
-
// first. Re-read the LRU before installing an object so the later caller
|
|
1609
|
-
// merges its extension entry into the existing object instead of replacing
|
|
1610
|
-
// the earlier entry.
|
|
1611
|
-
cached = detectionCache.get(normalizedCwd);
|
|
1612
|
-
if (!cached) {
|
|
1613
|
-
cached = { signature: configSignature, entries: new Map() };
|
|
1614
|
-
detectionCache.set(normalizedCwd, cached);
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
// Detect formatters for this extension (or exact filename, e.g. terragrunt.hcl)
|
|
1618
|
-
const matching = ALL_FORMATTERS.filter((f) => f.extensions.includes(ext) || f.filenames?.includes(base));
|
|
1619
|
-
const formatterPolicy = getFormatterPolicyForFile(filePath);
|
|
1620
|
-
const smartDefaultFormatterName = getSmartDefaultFormatterName(filePath);
|
|
1621
|
-
const candidateFormatters = formatterPolicy?.formatterNames?.length
|
|
1622
|
-
? matching.filter((f) => formatterPolicy.formatterNames.includes(f.name))
|
|
1623
|
-
: matching;
|
|
1624
|
-
let selected;
|
|
1625
|
-
/**
|
|
1626
|
-
* One entry per candidate this pass actually ASKED, in the order it asked
|
|
1627
|
-
* them (#1539). Candidates eliminated without a probe — an explicit-config
|
|
1628
|
-
* decision is pure filesystem — contribute nothing, which is the point: a
|
|
1629
|
-
* decision that never probed cannot have been degraded by a probe.
|
|
1630
|
-
*/
|
|
1631
|
-
const candidateOutcomes = [];
|
|
1632
|
-
if (formatterPolicy) {
|
|
1633
|
-
const explicitlyConfigured = candidateFormatters.filter((formatter) => hasExplicitFormatterConfig(formatter.name, cwd, ext));
|
|
1634
|
-
if (explicitlyConfigured.length > 0) {
|
|
1635
|
-
// A formatter with explicit project config was found — use it.
|
|
1636
|
-
// Prefer the policy's defaultFormatter only if it has explicit config,
|
|
1637
|
-
// otherwise pick the first explicitly-configured formatter.
|
|
1638
|
-
selected = formatterPolicy.defaultFormatter
|
|
1639
|
-
? (explicitlyConfigured.find((f) => f.name === formatterPolicy.defaultFormatter) ?? explicitlyConfigured[0])
|
|
1640
|
-
: explicitlyConfigured[0];
|
|
1641
|
-
}
|
|
1642
|
-
else if (smartDefaultFormatterName) {
|
|
1643
|
-
// Reached only when explicitlyConfigured is empty, so no candidate
|
|
1644
|
-
// has explicit config. Safe to activate the smart-default.
|
|
1645
|
-
const smartDefaultFormatter = candidateFormatters.find((f) => f.name === smartDefaultFormatterName);
|
|
1646
|
-
if (smartDefaultFormatter) {
|
|
1647
|
-
const autoInstallToolId = getAutoInstallToolIdForFormatter(smartDefaultFormatter.name);
|
|
1648
|
-
if (autoInstallToolId) {
|
|
1649
|
-
selected = smartDefaultFormatter;
|
|
1650
|
-
}
|
|
1651
|
-
else {
|
|
1652
|
-
const { detected, error, outcome } = await detectCandidate(smartDefaultFormatter, cwd);
|
|
1653
|
-
candidateOutcomes.push(outcome);
|
|
1654
|
-
// This branch never caught a throwing detection, and still
|
|
1655
|
-
// does not: `detectCandidate` catches only so the probes survive.
|
|
1656
|
-
if (error !== undefined)
|
|
1657
|
-
throw error;
|
|
1658
|
-
if (detected)
|
|
1659
|
-
selected = smartDefaultFormatter;
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
else {
|
|
1665
|
-
for (const formatter of candidateFormatters) {
|
|
1666
|
-
const { detected, error, outcome } = await detectCandidate(formatter, cwd);
|
|
1667
|
-
candidateOutcomes.push(outcome);
|
|
1668
|
-
if (error !== undefined) {
|
|
1669
|
-
// pi-lens-ignore: missing-error-propagation — optional formatter detection, skip on failure
|
|
1670
|
-
logExtension({
|
|
1671
|
-
subsystem: "format",
|
|
1672
|
-
message: `Detection failed for ${formatter.name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1673
|
-
metadata: { formatter: formatter.name, cwd },
|
|
1674
|
-
});
|
|
1675
|
-
continue;
|
|
1676
|
-
}
|
|
1677
|
-
if (detected) {
|
|
1678
|
-
selected = formatter;
|
|
1679
|
-
break;
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
const enabled = selected ? [selected] : [];
|
|
1684
|
-
// The #925/#1467 `wouldPoisonCache` shape: an empty result produced while a
|
|
1685
|
-
// PATH probe was timing out is not a finding about this project. Caching it
|
|
1686
|
-
// would survive until a config file's mtime or size changed, so leave the
|
|
1687
|
-
// cache untouched and let the next turn re-detect.
|
|
1688
|
-
// Which of THIS file's candidates are currently un-answered? Read off the
|
|
1689
|
-
// binaries the candidates' own `detect()`s actually probed (#1539), so a
|
|
1690
|
-
// stalled `which rustfmt` cannot stop a shell or Python detection from
|
|
1691
|
-
// caching (#1495 review) AND a leftover transient verdict for a binary this
|
|
1692
|
-
// pass never consulted cannot be blamed on this decision.
|
|
1693
|
-
//
|
|
1694
|
-
// This replaces the `command[0]` approximation, which missed both shapes of
|
|
1695
|
-
// extra probe: a co-equal ALTERNATIVE (`pwsh` ?? `powershell`) and — the
|
|
1696
|
-
// case the old comment wrongly called harmless — an install FALLBACK reached
|
|
1697
|
-
// after the primary answered a GENUINE absence. `rustfmt` missing plus a
|
|
1698
|
-
// stalled `which rustup` skips the lazy install, and nothing named `rustfmt`
|
|
1699
|
-
// is transient, so the empty result used to cache for the session.
|
|
1700
|
-
//
|
|
1701
|
-
// Residual: the record covers this module's own `which()` only. A detection
|
|
1702
|
-
// that reaches PATH some other way is invisible to it — ktlint's `detect()`
|
|
1703
|
-
// falls back to the installer's `getToolPath("ktlint")`, which has its own
|
|
1704
|
-
// probe and its own 24-hour cache. Widening the record to those is deferred to
|
|
1705
|
-
// the installer-side follow-up rather than bolted on here.
|
|
1706
|
-
const stalledCandidateCommands = [
|
|
1707
|
-
...new Set(candidateOutcomes.flatMap((outcome) => outcome.stalledCommands)),
|
|
1708
|
-
];
|
|
1709
|
-
const poisonedByTransientProbe = enabled.length === 0 && stalledCandidateCommands.length > 0;
|
|
1710
|
-
// A NON-empty result the poison guard cannot see (#1539): the winner won only
|
|
1711
|
-
// because a candidate ahead of it was never asked. `candidateOutcomes` is in
|
|
1712
|
-
// ask order and the loop stops at the winner, so every entry before the
|
|
1713
|
-
// winner's is a candidate that lost — and an `unreachable` one did not lose
|
|
1714
|
-
// on the merits. Caching this would hand the session to the runner-up until
|
|
1715
|
-
// a config file's mtime or size changed.
|
|
1716
|
-
const winnerIndex = selected
|
|
1717
|
-
? candidateOutcomes.findIndex((outcome) => outcome.name === selected.name)
|
|
1718
|
-
: -1;
|
|
1719
|
-
const unreachablePreferred = candidateOutcomes
|
|
1720
|
-
.slice(0, winnerIndex === -1 ? 0 : winnerIndex)
|
|
1721
|
-
.filter((outcome) => outcome.verdict === "unreachable")
|
|
1722
|
-
.map((outcome) => outcome.name);
|
|
1723
|
-
const degradedSelection = unreachablePreferred.length > 0;
|
|
1724
|
-
let selectionReason;
|
|
1725
|
-
if (poisonedByTransientProbe) {
|
|
1726
|
-
selectionReason = "probe-timeout";
|
|
1727
|
-
}
|
|
1728
|
-
else if (degradedSelection) {
|
|
1729
|
-
// The reason now describes how the winner WON, not just what the config
|
|
1730
|
-
// looks like. "explicit-config" on a selection whose preferred candidate
|
|
1731
|
-
// was never reachable was the most misleading record in this seam.
|
|
1732
|
-
selectionReason = "preferred-unreachable";
|
|
1733
|
-
}
|
|
1734
|
-
else if (!selected) {
|
|
1735
|
-
selectionReason = "none";
|
|
1736
|
-
}
|
|
1737
|
-
else if (!formatterPolicy) {
|
|
1738
|
-
selectionReason = "detect";
|
|
1739
|
-
}
|
|
1740
|
-
else {
|
|
1741
|
-
selectionReason = candidateFormatters.some((f) => hasExplicitFormatterConfig(f.name, cwd, ext))
|
|
1742
|
-
? "explicit-config"
|
|
1743
|
-
: "smart-default";
|
|
1744
|
-
}
|
|
1745
|
-
const provisional = poisonedByTransientProbe || degradedSelection;
|
|
1746
|
-
logLatency({
|
|
1747
|
-
type: "phase",
|
|
1748
|
-
phase: "formatter_selected",
|
|
1749
|
-
filePath: filePath,
|
|
1750
|
-
durationMs: 0,
|
|
1751
|
-
metadata: {
|
|
1752
|
-
formatter: selected?.name ?? null,
|
|
1753
|
-
reason: selectionReason,
|
|
1754
|
-
outcome: "miss",
|
|
1755
|
-
cwd,
|
|
1756
|
-
...(provisional && {
|
|
1757
|
-
cached: false,
|
|
1758
|
-
stalledProbes: stalledCandidateCommands,
|
|
1759
|
-
}),
|
|
1760
|
-
...(degradedSelection && { unreachablePreferred }),
|
|
1761
|
-
},
|
|
1762
|
-
});
|
|
1763
|
-
// Provisional either way: not cached, so the next pass re-detects once the
|
|
1764
|
-
// cooldown expires and the preferred formatter's recovery takes effect
|
|
1765
|
-
// without waiting on a config-file edit.
|
|
1766
|
-
if (provisional)
|
|
1767
|
-
return enabled;
|
|
1768
|
-
// Store the list of enabled formatter names in cache
|
|
1769
|
-
const enabledNames = enabled.map((f) => f.name);
|
|
1770
|
-
// An invalidation may have removed or replaced this object while detection
|
|
1771
|
-
// awaited a tool probe. Never repopulate the old generation.
|
|
1772
|
-
if (detectionCache.get(normalizedCwd) === cached) {
|
|
1773
|
-
cached.entries.set(cacheKey, enabledNames);
|
|
1774
|
-
}
|
|
1775
|
-
return enabled;
|
|
1776
|
-
}
|
|
1777
|
-
/**
|
|
1778
|
-
* Re-arm formatter availability: drop both the per-cwd selection cache and the
|
|
1779
|
-
* PATH latches.
|
|
1780
|
-
*
|
|
1781
|
-
* #1895: this is the session-boundary reset. Both halves are required. The
|
|
1782
|
-
* latches decide whether `which` runs at all, but `detectionCache` short-
|
|
1783
|
-
* circuits ahead of them — a same-cwd lookup returns the previous verdict's
|
|
1784
|
-
* formatter names without reaching a probe. Clearing only the latches
|
|
1785
|
-
* therefore re-arms every directory EXCEPT the one the user is working in.
|
|
1786
|
-
*/
|
|
1787
|
-
export function clearFormatterCache() {
|
|
1788
|
-
formatterCacheGeneration.bump();
|
|
1789
|
-
formatterSignatureFlights.clear();
|
|
1790
|
-
detectionCache.clear();
|
|
1791
|
-
resetWhichLatches();
|
|
1792
|
-
}
|
|
1793
|
-
const formatterConfigBasenames = new Set(FORMATTER_CONFIG_FILES.map((fileName) => fileName.toLowerCase()));
|
|
1794
|
-
/** Invalidate selection when the write-result seam reports a config path. */
|
|
1795
|
-
export function invalidateFormatterCacheForPath(filePath) {
|
|
1796
|
-
const basename = filePath.includes("\\")
|
|
1797
|
-
? path.win32.basename(filePath)
|
|
1798
|
-
: path.basename(filePath);
|
|
1799
|
-
if (!formatterConfigBasenames.has(basename.toLowerCase()))
|
|
1800
|
-
return;
|
|
1801
|
-
clearFormatterCache();
|
|
1802
|
-
}
|
|
1803
|
-
/**
|
|
1804
|
-
* Test-only internals access for the session-state registry probe (#1895):
|
|
1805
|
-
* the conformance suite must arm all four pieces of state this reset claims
|
|
1806
|
-
* to cover and prove none survives.
|
|
1807
|
-
*/
|
|
1808
|
-
export function _getFormatterResetStateForTests() {
|
|
1809
|
-
return {
|
|
1810
|
-
whichLatchByCommand,
|
|
1811
|
-
whichTransientCommands,
|
|
1812
|
-
cooldownRecordedForRetryAtMs,
|
|
1813
|
-
detectionCache,
|
|
1814
|
-
formatterSignatureFlights,
|
|
1815
|
-
};
|
|
1816
|
-
}
|
|
1817
|
-
export function clearFormatterRuntimeState() {
|
|
1818
|
-
clearFormatterCache();
|
|
1819
|
-
// NO `resetLazyInstallAttempts()` here (#1537 review F1). This function runs
|
|
1820
|
-
// from `resetFormatService()`, which `handleTurnEnd` calls every turn — so
|
|
1821
|
-
// clearing the lazy-install hold here made "held for the session" mean "held
|
|
1822
|
-
// for a turn", and a failing install re-ran every turn. The hold's only reset
|
|
1823
|
-
// is `session_start`'s block in runtime-session.ts.
|
|
1824
|
-
}
|
|
1825
|
-
const BOX_DRAWING_GLOBAL = /[\u2500-\u257F]/g;
|
|
1826
|
-
/** Blank, or a rule made of box-drawing characters and spacing alone. */
|
|
1827
|
-
const DECORATION_RULE = /^[\u2500-\u257F\s]*$/;
|
|
1828
|
-
/** The renderer's section heading: one word, then box-drawing to end of line. */
|
|
1829
|
-
const DECORATION_HEADING = /^\S+[ \t]+[\u2500-\u257F][\u2500-\u257F\s]*$/;
|
|
1830
|
-
/**
|
|
1831
|
-
* First line of `text` that actually carries a diagnostic.
|
|
1832
|
-
*
|
|
1833
|
-
* #1337 made nonzero exits user-visible, which put whatever this returns in
|
|
1834
|
-
* front of the agent (`clients/pipeline.ts`) and in the end-of-turn summary
|
|
1835
|
-
* (`clients/runtime-agent-end.ts`) — so "first line of stderr" is no longer
|
|
1836
|
-
* good enough. Biome opens stderr with a decorated section rule
|
|
1837
|
-
* (`format ━━━━━━━━━━━`), which as an error message is pure noise.
|
|
1838
|
-
*
|
|
1839
|
-
* Skips blank lines, pure box-drawing rules, and short banner headings; strips
|
|
1840
|
-
* ANSI colour so the message stays readable in a plain-text surface.
|
|
1841
|
-
*/
|
|
1842
|
-
export function firstDiagnosticLine(text) {
|
|
1843
|
-
for (const raw of (text ?? "").split("\n")) {
|
|
1844
|
-
const line = stripAnsi(raw).trimEnd();
|
|
1845
|
-
if (isDecorativeLine(line))
|
|
1846
|
-
continue;
|
|
1847
|
-
return line.replace(BOX_DRAWING_GLOBAL, "").trim().slice(0, 300);
|
|
1848
|
-
}
|
|
1849
|
-
return undefined;
|
|
1850
|
-
}
|
|
1851
|
-
/**
|
|
1852
|
-
* The two decoration SHAPES a renderer emits, and nothing else: a rule made of
|
|
1853
|
-
* box-drawing characters alone (blank lines included), and a `<word> ━━━━`
|
|
1854
|
-
* section heading whose remainder after the word is box-drawing only.
|
|
1855
|
-
*
|
|
1856
|
-
* The single normalization both readers above and below share, so the bounded
|
|
1857
|
-
* tail cannot surface what the first-line reader already rejects (#3312 review
|
|
1858
|
-
* R3-4: the tail put biome's `format ━━━━` banner back in front of the strict
|
|
1859
|
-
* #1337 seam). Matching SHAPES rather than "contains a box character and is
|
|
1860
|
-
* short" is #3312 review R4-1: the length heuristic deleted
|
|
1861
|
-
* `━ traceback source excerpt`, a diagnostic whose own text opens with a box
|
|
1862
|
-
* character. A line carrying any non-decorative text is never decoration.
|
|
1863
|
-
*/
|
|
1864
|
-
function isDecorativeLine(line) {
|
|
1865
|
-
return DECORATION_RULE.test(line) || DECORATION_HEADING.test(line);
|
|
1866
|
-
}
|
|
1867
|
-
const FORMATTER_ERROR_TAIL_LINES = 20;
|
|
1868
|
-
/**
|
|
1869
|
-
* Preserve a bounded tail of a formatter's diagnostic output.
|
|
1870
|
-
*
|
|
1871
|
-
* A traceback's first line is not actionable on its own (#3312). Keep the
|
|
1872
|
-
* final lines, which include the exception and its message, while bounding
|
|
1873
|
-
* the model-facing error when a formatter emits an unexpectedly large log.
|
|
1874
|
-
*
|
|
1875
|
-
* Decorative lines are dropped FIRST, by the same predicate `firstDiagnosticLine`
|
|
1876
|
-
* uses: a tail that kept them would hand the strict #1337 seam the banner this
|
|
1877
|
-
* module already decided is not a diagnostic, and every banner line it kept
|
|
1878
|
-
* would evict a real traceback line from the bound.
|
|
1879
|
-
*/
|
|
1880
|
-
export function diagnosticTail(text, maxLines = FORMATTER_ERROR_TAIL_LINES) {
|
|
1881
|
-
const lines = [];
|
|
1882
|
-
for (const raw of (text ?? "").split("\n")) {
|
|
1883
|
-
const line = stripAnsi(raw).trimEnd();
|
|
1884
|
-
if (isDecorativeLine(line))
|
|
1885
|
-
continue;
|
|
1886
|
-
lines.push(line.slice(0, 300));
|
|
1887
|
-
}
|
|
1888
|
-
if (lines.length === 0)
|
|
1889
|
-
return undefined;
|
|
1890
|
-
return lines.slice(-maxLines).join("\n");
|
|
1891
|
-
}
|
|
1892
|
-
/**
|
|
1893
|
-
* Resolve a formatter command without allowing the static command fallback to
|
|
1894
|
-
* bypass a resolver's style-preservation refusal (#1345). `null` means the
|
|
1895
|
-
* primary command is unavailable; the explicit sentinel means formatting is
|
|
1896
|
-
* forbidden for this file and must be returned before the static command is
|
|
1897
|
-
* materialized.
|
|
1898
|
-
*/
|
|
1899
|
-
async function resolveFormatterCommand(formatter, absolutePath, cwd) {
|
|
1900
|
-
const agreement = establishToolAgreement(formatter.name, cwd);
|
|
1901
|
-
if (agreement.decision === "decline") {
|
|
1902
|
-
recordDegradationOnce({
|
|
1903
|
-
kind: "formatter-agreement-unavailable",
|
|
1904
|
-
subject: agreement.subject,
|
|
1905
|
-
reason: agreement.reason,
|
|
1906
|
-
});
|
|
1907
|
-
return FORMATTER_AGREEMENT_UNAVAILABLE;
|
|
1908
|
-
}
|
|
1909
|
-
const resolved = formatter.resolveCommand
|
|
1910
|
-
? await formatter.resolveCommand(absolutePath, cwd)
|
|
1911
|
-
: null;
|
|
1912
|
-
if (resolved === SKIP_FORMATTING)
|
|
1913
|
-
return SKIP_FORMATTING;
|
|
1914
|
-
// The resolver proved the executable absent — DO NOT fall through to the
|
|
1915
|
-
// static bare command, which is the same binary it just probed and missing
|
|
1916
|
-
// (#2413). The caller turns this into a typed `unavailable` outcome, never a
|
|
1917
|
-
// spawn and never a failure.
|
|
1918
|
-
if (resolved === FORMATTER_UNAVAILABLE)
|
|
1919
|
-
return FORMATTER_UNAVAILABLE;
|
|
1920
|
-
if (resolved !== null)
|
|
1921
|
-
return resolved;
|
|
1922
|
-
const fallback = formatter.command.map((c) => c.replace("$FILE", absolutePath));
|
|
1923
|
-
// Trust gate on the install-capable static fallback (#1334 S5): npx can
|
|
1924
|
-
// DOWNLOAD packages, so an untrusted project treats the fallback as
|
|
1925
|
-
// unavailable -- a skip, not a formatter failure; may converge next turn.
|
|
1926
|
-
if (fallback[0] === "npx" &&
|
|
1927
|
-
!assertInstallAllowed(`formatter npx fallback: ${formatter.name}`)) {
|
|
1928
|
-
// No second ledger entry here (#1366 review): assertInstallAllowed just
|
|
1929
|
-
// recorded the trust-refusal with this formatter's context — recording
|
|
1930
|
-
// formatter-skip too would count one user-visible degradation twice.
|
|
1931
|
-
return SKIP_FORMATTING;
|
|
1932
|
-
}
|
|
1933
|
-
return fallback;
|
|
1934
|
-
}
|
|
1935
|
-
export async function formatFile(filePath, formatter) {
|
|
1936
|
-
try {
|
|
1937
|
-
const absolutePath = path.resolve(filePath);
|
|
1938
|
-
const cwd = path.dirname(absolutePath);
|
|
1939
|
-
const formatterCwd = resolveToolCwd("formatter", formatter.name, absolutePath, { cwd: path.parse(absolutePath).root }).cwd;
|
|
1940
|
-
const contentBefore = await fs.readFile(absolutePath, "utf-8");
|
|
1941
|
-
// Resolve command: prefer local (venv/vendor/node_modules) over global.
|
|
1942
|
-
// The shared seam must honor SKIP_FORMATTING before selecting the static
|
|
1943
|
-
// command, including its npx fallback (#1345).
|
|
1944
|
-
const cmd = await resolveFormatterCommand(formatter, absolutePath, cwd);
|
|
1945
|
-
if (cmd === SKIP_FORMATTING) {
|
|
1946
|
-
// Style-preserving refusal (#1144): no repo config and no detectable
|
|
1947
|
-
// indentation to pin — formatting would impose the tool's stock style.
|
|
1948
|
-
return { success: true, changed: false, outcome: "skipped" };
|
|
1949
|
-
}
|
|
1950
|
-
if (cmd === FORMATTER_UNAVAILABLE) {
|
|
1951
|
-
// The resolver proved the executable absent (#2413): the oxfmt ENOENT
|
|
1952
|
-
// trap and its siblings. This is unavailable infrastructure, not a code
|
|
1953
|
-
// failure — success stays true so it is never counted as a failed file
|
|
1954
|
-
// or requeued, and the typed outcome keeps it out of the failure bucket.
|
|
1955
|
-
return {
|
|
1956
|
-
success: true,
|
|
1957
|
-
changed: false,
|
|
1958
|
-
outcome: "unavailable",
|
|
1959
|
-
error: `${formatter.name}: formatter executable not found`,
|
|
1960
|
-
};
|
|
1961
|
-
}
|
|
1962
|
-
if (cmd === FORMATTER_AGREEMENT_UNAVAILABLE) {
|
|
1963
|
-
return {
|
|
1964
|
-
success: true,
|
|
1965
|
-
changed: false,
|
|
1966
|
-
outcome: "unavailable",
|
|
1967
|
-
error: `${formatter.name}: project tool agreement could not be established`,
|
|
1968
|
-
};
|
|
1969
|
-
}
|
|
1970
|
-
// Run formatter without blocking the event loop.
|
|
1971
|
-
const result = await safeSpawnAsync(cmd[0], cmd.slice(1), {
|
|
1972
|
-
timeout: 15000,
|
|
1973
|
-
cwd: formatterCwd,
|
|
1974
|
-
});
|
|
1975
|
-
// A resolver that could NOT prove absence (it never probed PATH — e.g.
|
|
1976
|
-
// black/sqlfluff, which only look in a venv) legitimately falls back to the
|
|
1977
|
-
// bare static command. When that bare command is missing, the spawn boundary
|
|
1978
|
-
// reports a typed `tool-not-found` failure — never a nonzero exit. Treat it
|
|
1979
|
-
// as unavailable, not a formatting failure, closing the same #2413 class for
|
|
1980
|
-
// the formatters whose `null` correctly means "static untried" (no
|
|
1981
|
-
// oxfmt-specific string matching — this reads the typed spawn-failure kind).
|
|
1982
|
-
if (result.spawnFailure?.kind === "tool-not-found") {
|
|
1983
|
-
return {
|
|
1984
|
-
success: true,
|
|
1985
|
-
changed: false,
|
|
1986
|
-
outcome: "unavailable",
|
|
1987
|
-
error: result.spawnFailure.message,
|
|
1988
|
-
};
|
|
1989
|
-
}
|
|
1990
|
-
// Strict by default (#1337): only a formatter with a documented
|
|
1991
|
-
// benign-nonzero mode (`lenientExitCode`) may exit nonzero and still be
|
|
1992
|
-
// read as a successful run. Everything else that exits nonzero never
|
|
1993
|
-
// rewrote the file, and reporting {success: true, changed: false} there is
|
|
1994
|
-
// indistinguishable from "already formatted" — the #1336 silent no-op.
|
|
1995
|
-
const lenientOk = formatter.lenientExitCode !== undefined &&
|
|
1996
|
-
result.status !== null &&
|
|
1997
|
-
(formatter.lenientStatuses ?? []).includes(result.status);
|
|
1998
|
-
if (result.error || (result.status !== 0 && !lenientOk)) {
|
|
1999
|
-
return {
|
|
2000
|
-
success: false,
|
|
2001
|
-
changed: false,
|
|
2002
|
-
outcome: "failed",
|
|
2003
|
-
error: result.error?.message ||
|
|
2004
|
-
diagnosticTail(result.stderr) ||
|
|
2005
|
-
// biome, ktlint and `mix format` report on STDOUT; without this
|
|
2006
|
-
// their diagnostic is discarded and the user is told only that
|
|
2007
|
-
// the tool "exited with status 1".
|
|
2008
|
-
diagnosticTail(result.stdout) ||
|
|
2009
|
-
`${formatter.name} exited with status ${result.status}`,
|
|
2010
|
-
};
|
|
2011
|
-
}
|
|
2012
|
-
// Check if content changed
|
|
2013
|
-
const contentAfter = await fs.readFile(absolutePath, "utf-8");
|
|
2014
|
-
const changed = contentBefore !== contentAfter;
|
|
2015
|
-
return {
|
|
2016
|
-
success: true,
|
|
2017
|
-
changed,
|
|
2018
|
-
outcome: changed ? "formatted" : "unchanged",
|
|
2019
|
-
};
|
|
2020
|
-
}
|
|
2021
|
-
catch (err) {
|
|
2022
|
-
return {
|
|
2023
|
-
success: false,
|
|
2024
|
-
changed: false,
|
|
2025
|
-
outcome: "failed",
|
|
2026
|
-
error: err instanceof Error ? err.message : String(err),
|
|
2027
|
-
};
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
export function listAllFormatters() {
|
|
2031
|
-
return ALL_FORMATTERS.map((f) => f.name);
|
|
2032
|
-
}
|