@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.
Files changed (769) hide show
  1. package/README.md +16 -12
  2. package/THIRD_PARTY_NOTICES.md +3 -3
  3. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +2 -2
  4. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +2 -2
  5. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +1 -1
  6. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-help.md +1 -1
  7. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +2 -2
  8. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +1 -1
  9. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/index.js +4 -0
  10. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail-frontmatter.cjs +43 -3
  11. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail.mjs +142 -59
  12. package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +21 -21
  13. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
  14. package/node_modules/@dietrichgebert/ponytail/AGENTS.md +21 -21
  15. package/node_modules/@dietrichgebert/ponytail/README.md +52 -242
  16. package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +3 -3
  17. package/node_modules/@dietrichgebert/ponytail/hooks/copilot-hooks.json +4 -4
  18. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +70 -16
  19. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-config.js +8 -7
  20. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +30 -30
  21. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-map.js +103 -0
  22. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +46 -23
  23. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +39 -7
  24. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-subagent.js +20 -3
  25. package/node_modules/@dietrichgebert/ponytail/hooks/qoder-hooks.json +3 -3
  26. package/node_modules/@dietrichgebert/ponytail/package.json +3 -3
  27. package/node_modules/@dietrichgebert/ponytail/pi-extension/index.js +47 -17
  28. package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -7
  29. package/node_modules/@dietrichgebert/ponytail/skills/plugin.json +5 -0
  30. package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +33 -101
  31. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +89 -27
  32. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +13 -12
  33. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +21 -18
  34. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +12 -13
  35. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +96 -52
  36. package/node_modules/@herbertgao/pi-agency/LICENSE +21 -0
  37. package/node_modules/@herbertgao/pi-agency/README.md +40 -0
  38. package/node_modules/@herbertgao/pi-agency/package.json +53 -0
  39. package/node_modules/@herbertgao/pi-agency/src/index.ts +441 -0
  40. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +12 -12
  41. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +30 -6
  42. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +15 -9
  43. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +27 -9
  44. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +4 -1
  45. package/node_modules/@herbertgao/pi-cc-extensions/package.json +2 -2
  46. package/node_modules/@narumitw/pi-caffeinate/package.json +4 -4
  47. package/node_modules/@tifan/pi-mermaid-open/package.json +5 -1
  48. package/node_modules/@tifan/pi-mermaid-open/src/index.ts +26 -9
  49. package/node_modules/@tifan/pi-recap/package.json +1 -1
  50. package/node_modules/@tifan/pi-recap/src/index.ts +104 -20
  51. package/node_modules/@tifan/pi-recap/src/models.ts +46 -4
  52. package/node_modules/pi-antigravity/README.md +23 -2
  53. package/node_modules/pi-antigravity/package.json +5 -3
  54. package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +3 -2
  55. package/node_modules/pi-antigravity/src/features/generate-image.ts +72 -0
  56. package/node_modules/pi-antigravity/src/features/google-search.ts +71 -0
  57. package/node_modules/pi-antigravity/src/image/image.ts +86 -47
  58. package/node_modules/pi-antigravity/src/image/native.ts +69 -0
  59. package/node_modules/pi-antigravity/src/index.ts +52 -137
  60. package/node_modules/pi-antigravity/src/search/search.ts +130 -31
  61. package/node_modules/pi-antigravity/src/stream/stream.ts +162 -113
  62. package/node_modules/pi-antigravity/src/types/enums.ts +1 -0
  63. package/node_modules/pi-antigravity/src/types/types.ts +1 -1
  64. package/node_modules/pi-antigravity/src/utils/security.ts +47 -1
  65. package/node_modules/pi-antigravity/src/utils/util.ts +38 -6
  66. package/node_modules/pi-lens/CHANGELOG.md +1064 -0
  67. package/node_modules/pi-lens/README.md +115 -25
  68. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +6 -2
  69. package/node_modules/pi-lens/dist/chunk-22AVIGXS.js +560 -0
  70. package/node_modules/pi-lens/dist/chunk-27BBHSZC.js +106 -0
  71. package/node_modules/pi-lens/dist/chunk-2BEMIWEW.js +51 -0
  72. package/node_modules/pi-lens/dist/chunk-2EECBNC5.js +89 -0
  73. package/node_modules/pi-lens/dist/chunk-34U7PLGE.js +23 -0
  74. package/node_modules/pi-lens/dist/chunk-3I5QFT3N.js +181 -0
  75. package/node_modules/pi-lens/dist/chunk-3MED75HT.js +37 -0
  76. package/node_modules/pi-lens/dist/chunk-47D2BO4S.js +87 -0
  77. package/node_modules/pi-lens/dist/chunk-4RERDEXR.js +348 -0
  78. package/node_modules/pi-lens/dist/chunk-4W2PLC4J.js +27 -0
  79. package/node_modules/pi-lens/dist/chunk-52H5B5TC.js +202 -0
  80. package/node_modules/pi-lens/dist/chunk-54B42YRX.js +27 -0
  81. package/node_modules/pi-lens/dist/chunk-5RXBNSW2.js +266 -0
  82. package/node_modules/pi-lens/dist/chunk-5THXD6X5.js +201 -0
  83. package/node_modules/pi-lens/dist/chunk-5W3R6LFT.js +24 -0
  84. package/node_modules/pi-lens/dist/chunk-5ZEJHV35.js +341 -0
  85. package/node_modules/pi-lens/dist/chunk-6P62QVEA.js +118 -0
  86. package/node_modules/pi-lens/dist/chunk-6QIUX4EP.js +459 -0
  87. package/node_modules/pi-lens/dist/chunk-6VCD4VKZ.js +78 -0
  88. package/node_modules/pi-lens/dist/chunk-7F72PXY2.js +326 -0
  89. package/node_modules/pi-lens/dist/chunk-7FBF7NPR.js +998 -0
  90. package/node_modules/pi-lens/dist/chunk-7U2IDNXN.js +37 -0
  91. package/node_modules/pi-lens/dist/chunk-ABBOA7UD.js +157 -0
  92. package/node_modules/pi-lens/dist/chunk-AHDW3GPE.js +50 -0
  93. package/node_modules/pi-lens/dist/chunk-AJ3QXTRQ.js +756 -0
  94. package/node_modules/pi-lens/dist/chunk-AJVLWNFN.js +1610 -0
  95. package/node_modules/pi-lens/dist/chunk-AJXODZY6.js +112 -0
  96. package/node_modules/pi-lens/dist/chunk-ARFCXG7S.js +84 -0
  97. package/node_modules/pi-lens/dist/chunk-AS44CFQ4.js +708 -0
  98. package/node_modules/pi-lens/dist/chunk-AWE5TMPO.js +38 -0
  99. package/node_modules/pi-lens/dist/chunk-B3I2UUT4.js +4692 -0
  100. package/node_modules/pi-lens/dist/chunk-BRJKOBZP.js +331 -0
  101. package/node_modules/pi-lens/dist/chunk-BW5KFIQC.js +59 -0
  102. package/node_modules/pi-lens/dist/chunk-C2AUYXQH.js +149 -0
  103. package/node_modules/pi-lens/dist/chunk-CGGAIP7J.js +29 -0
  104. package/node_modules/pi-lens/dist/chunk-D6QS3U6M.js +30 -0
  105. package/node_modules/pi-lens/dist/chunk-DFVMIKMQ.js +349 -0
  106. package/node_modules/pi-lens/dist/chunk-DIULIWKU.js +169 -0
  107. package/node_modules/pi-lens/dist/chunk-DQ2NZ7C4.js +2317 -0
  108. package/node_modules/pi-lens/dist/chunk-E2WFBRHM.js +213 -0
  109. package/node_modules/pi-lens/dist/chunk-E4MAEDPM.js +50 -0
  110. package/node_modules/pi-lens/dist/chunk-EOKRQ3SA.js +30 -0
  111. package/node_modules/pi-lens/dist/chunk-F3AU2TMM.js +2626 -0
  112. package/node_modules/pi-lens/dist/chunk-F3P7VQ7H.js +170 -0
  113. package/node_modules/pi-lens/dist/chunk-FC4RP4I2.js +12 -0
  114. package/node_modules/pi-lens/dist/chunk-FJNKANOS.js +32 -0
  115. package/node_modules/pi-lens/dist/chunk-GUOGVTCQ.js +146 -0
  116. package/node_modules/pi-lens/dist/chunk-GVXA6CCE.js +18 -0
  117. package/node_modules/pi-lens/dist/chunk-HHYAELLJ.js +2334 -0
  118. package/node_modules/pi-lens/dist/chunk-HMFXILPV.js +83 -0
  119. package/node_modules/pi-lens/dist/chunk-HVUZXSWK.js +421 -0
  120. package/node_modules/pi-lens/dist/chunk-HWBZIGMJ.js +62 -0
  121. package/node_modules/pi-lens/dist/chunk-I3R7GKDR.js +17 -0
  122. package/node_modules/pi-lens/dist/chunk-I7SPGOWI.js +55 -0
  123. package/node_modules/pi-lens/dist/chunk-IIXOAGHR.js +38 -0
  124. package/node_modules/pi-lens/dist/chunk-IVP5KAB7.js +2250 -0
  125. package/node_modules/pi-lens/dist/chunk-IXEBJSUE.js +22 -0
  126. package/node_modules/pi-lens/dist/chunk-JOLITGCC.js +27 -0
  127. package/node_modules/pi-lens/dist/chunk-K6VYLB2U.js +145 -0
  128. package/node_modules/pi-lens/dist/chunk-LJDODBBZ.js +78 -0
  129. package/node_modules/pi-lens/dist/chunk-LNAKEO63.js +78 -0
  130. package/node_modules/pi-lens/dist/chunk-LSMJ6KBE.js +4139 -0
  131. package/node_modules/pi-lens/dist/chunk-LW6XKDZQ.js +5565 -0
  132. package/node_modules/pi-lens/dist/chunk-N3YQJI6O.js +899 -0
  133. package/node_modules/pi-lens/dist/chunk-NABLC7W3.js +148 -0
  134. package/node_modules/pi-lens/dist/chunk-NKOUFPQF.js +659 -0
  135. package/node_modules/pi-lens/dist/chunk-NL5GLMFI.js +211 -0
  136. package/node_modules/pi-lens/dist/chunk-NMABWBZN.js +327 -0
  137. package/node_modules/pi-lens/dist/chunk-NTHH2J74.js +27 -0
  138. package/node_modules/pi-lens/dist/chunk-NTUJEGDZ.js +59 -0
  139. package/node_modules/pi-lens/dist/chunk-NXL4FFQQ.js +47 -0
  140. package/node_modules/pi-lens/dist/chunk-NXPS2L6U.js +466 -0
  141. package/node_modules/pi-lens/dist/chunk-O5WILGMI.js +42 -0
  142. package/node_modules/pi-lens/dist/chunk-O6TQT6RI.js +48 -0
  143. package/node_modules/pi-lens/dist/chunk-OAXNDZJ7.js +323 -0
  144. package/node_modules/pi-lens/dist/chunk-ODEEOXPH.js +44 -0
  145. package/node_modules/pi-lens/dist/chunk-OQGFCZJ4.js +4533 -0
  146. package/node_modules/pi-lens/dist/chunk-ORTMY4Q4.js +282 -0
  147. package/node_modules/pi-lens/dist/chunk-OS32RNR2.js +442 -0
  148. package/node_modules/pi-lens/dist/chunk-PAK6IDME.js +189 -0
  149. package/node_modules/pi-lens/dist/chunk-PWFHBTSF.js +406 -0
  150. package/node_modules/pi-lens/dist/chunk-Q5U6FMDI.js +2352 -0
  151. package/node_modules/pi-lens/dist/chunk-QOZXJ4NA.js +3364 -0
  152. package/node_modules/pi-lens/dist/chunk-QYF5S6P6.js +45 -0
  153. package/node_modules/pi-lens/dist/chunk-RIISMHKN.js +36 -0
  154. package/node_modules/pi-lens/dist/chunk-RPD4ZSB7.js +25 -0
  155. package/node_modules/pi-lens/dist/chunk-RPD5K5NC.js +79 -0
  156. package/node_modules/pi-lens/dist/chunk-RUOG5D2C.js +262 -0
  157. package/node_modules/pi-lens/dist/chunk-T4ERT73Y.js +599 -0
  158. package/node_modules/pi-lens/dist/chunk-T6D4Q4AQ.js +54 -0
  159. package/node_modules/pi-lens/dist/chunk-TGPXLY2J.js +391 -0
  160. package/node_modules/pi-lens/dist/chunk-TLGFBM3F.js +12116 -0
  161. package/node_modules/pi-lens/dist/chunk-TO6WL3GM.js +88 -0
  162. package/node_modules/pi-lens/dist/chunk-TT22N74J.js +517 -0
  163. package/node_modules/pi-lens/dist/chunk-TVN5N6GY.js +21216 -0
  164. package/node_modules/pi-lens/dist/chunk-U3VGR4VU.js +308 -0
  165. package/node_modules/pi-lens/dist/chunk-UK3CMEAL.js +1250 -0
  166. package/node_modules/pi-lens/dist/chunk-UOPVJQ4N.js +103 -0
  167. package/node_modules/pi-lens/dist/chunk-UXVNL3D7.js +23 -0
  168. package/node_modules/pi-lens/dist/chunk-UY752QSN.js +45 -0
  169. package/node_modules/pi-lens/dist/chunk-V5PXFSZK.js +188 -0
  170. package/node_modules/pi-lens/dist/chunk-V7R425XG.js +398 -0
  171. package/node_modules/pi-lens/dist/chunk-VCSRSYLF.js +81 -0
  172. package/node_modules/pi-lens/dist/chunk-VD4JQCQC.js +162 -0
  173. package/node_modules/pi-lens/dist/chunk-VDBPVIKD.js +62 -0
  174. package/node_modules/pi-lens/dist/chunk-VHQLPIRJ.js +114 -0
  175. package/node_modules/pi-lens/dist/chunk-VN2AXLEX.js +5414 -0
  176. package/node_modules/pi-lens/dist/chunk-VNMT7C64.js +478 -0
  177. package/node_modules/pi-lens/dist/chunk-WONERJTP.js +309 -0
  178. package/node_modules/pi-lens/dist/chunk-XECLWQZN.js +85 -0
  179. package/node_modules/pi-lens/dist/chunk-XIMYZ3JA.js +27 -0
  180. package/node_modules/pi-lens/dist/chunk-XK4CIILI.js +144 -0
  181. package/node_modules/pi-lens/dist/chunk-Y4T27COV.js +562 -0
  182. package/node_modules/pi-lens/dist/chunk-YGVYKNSN.js +1860 -0
  183. package/node_modules/pi-lens/dist/chunk-YLF3PPT5.js +308 -0
  184. package/node_modules/pi-lens/dist/chunk-YSNFMJKN.js +14 -0
  185. package/node_modules/pi-lens/dist/chunk-Z3CCVDUB.js +76 -0
  186. package/node_modules/pi-lens/dist/chunk-Z54ZSOBZ.js +61 -0
  187. package/node_modules/pi-lens/dist/chunk-ZAWW3AF6.js +97 -0
  188. package/node_modules/pi-lens/dist/chunk-ZMOL6RQC.js +167 -0
  189. package/node_modules/pi-lens/dist/index.js +16456 -8335
  190. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +186 -232
  191. package/node_modules/pi-lens/dist/mcp/cli.js +137 -98
  192. package/node_modules/pi-lens/dist/mcp/server.js +22589 -1573
  193. package/node_modules/pi-lens/dist/mcp/worker.js +97 -38
  194. package/node_modules/pi-lens/dist/probes/installer.js +135 -0
  195. package/node_modules/pi-lens/dist/workers/project-snapshot-persist-worker.js +22 -0
  196. package/node_modules/pi-lens/dist/workers/review-graph-persist-worker.js +15 -0
  197. package/node_modules/pi-lens/docs/agent-guide.md +13 -4
  198. package/node_modules/pi-lens/docs/agent-tools.md +42 -0
  199. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +3 -2
  200. package/node_modules/pi-lens/docs/configuration.md +20 -1
  201. package/node_modules/pi-lens/docs/environment-variables.md +52 -2
  202. package/node_modules/pi-lens/docs/features.md +8 -4
  203. package/node_modules/pi-lens/docs/globalconfig.md +1 -0
  204. package/node_modules/pi-lens/docs/io-bridge-v2-migration.md +94 -0
  205. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +36 -4
  206. package/node_modules/pi-lens/docs/lsp-idle-eviction.md +124 -0
  207. package/node_modules/pi-lens/docs/mcp.md +21 -5
  208. package/node_modules/pi-lens/docs/merge-queue-rollout.md +77 -0
  209. package/node_modules/pi-lens/docs/pi-lens-fixer.md +156 -88
  210. package/node_modules/pi-lens/docs/pi-lens-investigator.md +74 -26
  211. package/node_modules/pi-lens/docs/pi-lens-merge-policy.md +694 -0
  212. package/node_modules/pi-lens/docs/pi-lens-monitor.md +73 -7
  213. package/node_modules/pi-lens/docs/pi-lens-retro.md +6 -4
  214. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +188 -48
  215. package/node_modules/pi-lens/docs/pi-lens-subagent.md +246 -45
  216. package/node_modules/pi-lens/docs/pi-lens-warden.md +18 -7
  217. package/node_modules/pi-lens/docs/public-api-stability.md +55 -0
  218. package/node_modules/pi-lens/docs/release-qa-baseline.md +84 -17
  219. package/node_modules/pi-lens/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
  220. package/node_modules/pi-lens/docs/servercapabilities.md +53 -49
  221. package/node_modules/pi-lens/docs/settings.md +1 -0
  222. package/node_modules/pi-lens/docs/usage.md +5 -1
  223. package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm +0 -0
  224. package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm.json +3 -3
  225. package/node_modules/pi-lens/package.json +18 -6
  226. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
  227. package/node_modules/pi-lens/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
  228. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +1275 -62
  229. package/node_modules/pi-lens/scripts/download-grammars.js +5 -0
  230. package/node_modules/pi-lens/scripts/grammars.lock.json +6 -1
  231. package/node_modules/pi-lens/scripts/install-selftest.mjs +33 -34
  232. package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +13 -4
  233. package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +18 -0
  234. package/node_modules/pi-typesafe/README.md +1 -1
  235. package/node_modules/pi-typesafe/dist/backends.d.ts +15 -5
  236. package/node_modules/pi-typesafe/dist/backends.js +18 -0
  237. package/node_modules/pi-typesafe/dist/client.d.ts +4 -3
  238. package/node_modules/pi-typesafe/dist/client.js +22 -7
  239. package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
  240. package/node_modules/pi-typesafe/dist/index.js +1 -1
  241. package/node_modules/pi-typesafe/dist/usage.d.ts +14 -4
  242. package/node_modules/pi-typesafe/dist/usage.js +147 -42
  243. package/node_modules/pi-typesafe/package.json +1 -1
  244. package/node_modules/pi-web-access/CHANGELOG.md +48 -0
  245. package/node_modules/pi-web-access/README.md +57 -11
  246. package/node_modules/pi-web-access/brave-rate-limit.ts +8 -2
  247. package/node_modules/pi-web-access/curator-page.ts +5 -1
  248. package/node_modules/pi-web-access/curator-server.ts +2 -0
  249. package/node_modules/pi-web-access/declared-web-links.ts +2 -2
  250. package/node_modules/pi-web-access/degoog.ts +323 -0
  251. package/node_modules/pi-web-access/dist/index.js +3483 -2745
  252. package/node_modules/pi-web-access/dist/mcp-cli.js +27443 -0
  253. package/node_modules/pi-web-access/exa.ts +5 -1
  254. package/node_modules/pi-web-access/extract.ts +41 -3
  255. package/node_modules/pi-web-access/firecrawl.ts +35 -4
  256. package/node_modules/pi-web-access/gemini-search.ts +34 -11
  257. package/node_modules/pi-web-access/gemini-web.ts +14 -9
  258. package/node_modules/pi-web-access/index.ts +153 -1202
  259. package/node_modules/pi-web-access/keenable.ts +221 -0
  260. package/node_modules/pi-web-access/mcp-cli.ts +40 -0
  261. package/node_modules/pi-web-access/mcp-server.ts +81 -0
  262. package/node_modules/pi-web-access/mcp-tool-definitions.ts +117 -0
  263. package/node_modules/pi-web-access/openai-search.ts +34 -1
  264. package/node_modules/pi-web-access/package.json +23 -5
  265. package/node_modules/pi-web-access/perplexity.ts +64 -56
  266. package/node_modules/pi-web-access/tool-arguments.ts +20 -0
  267. package/node_modules/pi-web-access/video-extract.ts +1 -1
  268. package/node_modules/pi-web-access/web-tool-contract.ts +86 -0
  269. package/node_modules/pi-web-access/web-tool-core.ts +1502 -0
  270. package/node_modules/pi-web-access/youtube-extract.ts +2 -5
  271. package/package.json +17 -12
  272. package/node_modules/@dietrichgebert/ponytail/README.es.md +0 -329
  273. package/node_modules/@dietrichgebert/ponytail/README.ko.md +0 -346
  274. package/node_modules/@dietrichgebert/ponytail/assets/benchmark-3model.svg +0 -21
  275. package/node_modules/@dietrichgebert/ponytail/assets/benchmark-agentic.svg +0 -62
  276. package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.png +0 -0
  277. package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.svg +0 -115
  278. package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt-dark.svg +0 -27
  279. package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt.svg +0 -27
  280. package/node_modules/@dietrichgebert/ponytail/assets/logo.png +0 -0
  281. package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
  282. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +0 -4
  283. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +0 -4
  284. package/node_modules/@dietrichgebert/ponytail/assets/social-preview.png +0 -0
  285. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-es.png +0 -0
  286. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-ko.png +0 -0
  287. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner.png +0 -0
  288. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -35
  289. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +0 -1610
  290. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +0 -593
  291. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +0 -147
  292. package/node_modules/pi-lens/dist/clients/agent-nudge.js +0 -325
  293. package/node_modules/pi-lens/dist/clients/analysed-root.js +0 -1
  294. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +0 -731
  295. package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +0 -146
  296. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +0 -118
  297. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -10
  298. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +0 -154
  299. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +0 -75
  300. package/node_modules/pi-lens/dist/clients/atomic-write.js +0 -189
  301. package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +0 -12
  302. package/node_modules/pi-lens/dist/clients/bash-file-access.js +0 -1056
  303. package/node_modules/pi-lens/dist/clients/biome-client.js +0 -357
  304. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -837
  305. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +0 -129
  306. package/node_modules/pi-lens/dist/clients/bootstrap.js +0 -569
  307. package/node_modules/pi-lens/dist/clients/bounded-cache.js +0 -190
  308. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +0 -202
  309. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +0 -295
  310. package/node_modules/pi-lens/dist/clients/build-identity.js +0 -95
  311. package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +0 -113
  312. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +0 -105
  313. package/node_modules/pi-lens/dist/clients/bus-publish.js +0 -171
  314. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +0 -190
  315. package/node_modules/pi-lens/dist/clients/cache-manager.js +0 -524
  316. package/node_modules/pi-lens/dist/clients/cache-observability.js +0 -1313
  317. package/node_modules/pi-lens/dist/clients/call-graph.js +0 -852
  318. package/node_modules/pi-lens/dist/clients/cargo-manifest.js +0 -422
  319. package/node_modules/pi-lens/dist/clients/cascade-budget.js +0 -196
  320. package/node_modules/pi-lens/dist/clients/cascade-format.js +0 -315
  321. package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -31
  322. package/node_modules/pi-lens/dist/clients/cascade-types.js +0 -9
  323. package/node_modules/pi-lens/dist/clients/child-unref.js +0 -124
  324. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +0 -286
  325. package/node_modules/pi-lens/dist/clients/codebase-model.js +0 -173
  326. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +0 -40
  327. package/node_modules/pi-lens/dist/clients/complexity-client.js +0 -462
  328. package/node_modules/pi-lens/dist/clients/config-core/deny.js +0 -221
  329. package/node_modules/pi-lens/dist/clients/config-core/index.js +0 -50
  330. package/node_modules/pi-lens/dist/clients/config-core/merge.js +0 -357
  331. package/node_modules/pi-lens/dist/clients/config-core/normalize.js +0 -340
  332. package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +0 -248
  333. package/node_modules/pi-lens/dist/clients/config-core/provenance.js +0 -164
  334. package/node_modules/pi-lens/dist/clients/config-core/records.js +0 -218
  335. package/node_modules/pi-lens/dist/clients/config-core/resolve.js +0 -125
  336. package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +0 -78
  337. package/node_modules/pi-lens/dist/clients/config-core/schema.js +0 -165
  338. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +0 -285
  339. package/node_modules/pi-lens/dist/clients/config-locations.js +0 -388
  340. package/node_modules/pi-lens/dist/clients/config-resolve.js +0 -798
  341. package/node_modules/pi-lens/dist/clients/config-schema.js +0 -251
  342. package/node_modules/pi-lens/dist/clients/config-warn.js +0 -407
  343. package/node_modules/pi-lens/dist/clients/console-guard-install.js +0 -26
  344. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +0 -42
  345. package/node_modules/pi-lens/dist/clients/dead-code-client.js +0 -510
  346. package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -30
  347. package/node_modules/pi-lens/dist/clients/deadline-utils.js +0 -280
  348. package/node_modules/pi-lens/dist/clients/debug-handles.js +0 -211
  349. package/node_modules/pi-lens/dist/clients/debug-heap.js +0 -174
  350. package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +0 -106
  351. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +0 -426
  352. package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +0 -169
  353. package/node_modules/pi-lens/dist/clients/dependency-checker.js +0 -885
  354. package/node_modules/pi-lens/dist/clients/deps/ast-grep-napi.js +0 -22
  355. package/node_modules/pi-lens/dist/clients/deps/js-yaml.js +0 -9
  356. package/node_modules/pi-lens/dist/clients/deps/minimatch.js +0 -4
  357. package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +0 -14
  358. package/node_modules/pi-lens/dist/clients/deps/typebox.js +0 -14
  359. package/node_modules/pi-lens/dist/clients/deps/vscode-jsonrpc.js +0 -1
  360. package/node_modules/pi-lens/dist/clients/deps/web-tree-sitter.js +0 -24
  361. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +0 -630
  362. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +0 -305
  363. package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +0 -73
  364. package/node_modules/pi-lens/dist/clients/diagnostic-tracker.js +0 -105
  365. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +0 -230
  366. package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +0 -65
  367. package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +0 -343
  368. package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -35
  369. package/node_modules/pi-lens/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
  370. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +0 -1269
  371. package/node_modules/pi-lens/dist/clients/dispatch/fact-provider-types.js +0 -1
  372. package/node_modules/pi-lens/dist/clients/dispatch/fact-rule-runner.js +0 -17
  373. package/node_modules/pi-lens/dist/clients/dispatch/fact-runner.js +0 -30
  374. package/node_modules/pi-lens/dist/clients/dispatch/fact-scheduler.js +0 -68
  375. package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +0 -377
  376. package/node_modules/pi-lens/dist/clients/dispatch/facts/comment-facts.js +0 -22
  377. package/node_modules/pi-lens/dist/clients/dispatch/facts/file-content.js +0 -19
  378. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +0 -348
  379. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +0 -204
  380. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
  381. package/node_modules/pi-lens/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
  382. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +0 -339
  383. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +0 -417
  384. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +0 -89
  385. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +0 -2377
  386. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -11
  387. package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +0 -87
  388. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -278
  389. package/node_modules/pi-lens/dist/clients/dispatch/priorities.js +0 -20
  390. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +0 -50
  391. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +0 -69
  392. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +0 -135
  393. package/node_modules/pi-lens/dist/clients/dispatch/rules/async-noise.js +0 -40
  394. package/node_modules/pi-lens/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
  395. package/node_modules/pi-lens/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
  396. package/node_modules/pi-lens/dist/clients/dispatch/rules/error-obscuring.js +0 -32
  397. package/node_modules/pi-lens/dist/clients/dispatch/rules/error-swallowing.js +0 -31
  398. package/node_modules/pi-lens/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
  399. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +0 -72
  400. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +0 -74
  401. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
  402. package/node_modules/pi-lens/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
  403. package/node_modules/pi-lens/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
  404. package/node_modules/pi-lens/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
  405. package/node_modules/pi-lens/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
  406. package/node_modules/pi-lens/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
  407. package/node_modules/pi-lens/dist/clients/dispatch/runner-context.js +0 -52
  408. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -111
  409. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
  410. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -296
  411. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -219
  412. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -82
  413. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -416
  414. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -183
  415. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -176
  416. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -164
  417. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -195
  418. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -137
  419. package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -29
  420. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -69
  421. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -167
  422. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +0 -104
  423. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -135
  424. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -91
  425. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -171
  426. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -940
  427. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -117
  428. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +0 -108
  429. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -99
  430. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -110
  431. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -379
  432. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -167
  433. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -102
  434. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +0 -414
  435. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -85
  436. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -120
  437. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -62
  438. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
  439. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +0 -169
  440. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -99
  441. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -88
  442. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +0 -256
  443. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -173
  444. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -102
  445. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -128
  446. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -259
  447. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -183
  448. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -166
  449. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -163
  450. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -174
  451. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -168
  452. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -125
  453. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -643
  454. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -253
  455. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
  456. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
  457. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
  458. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
  459. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
  460. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
  461. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
  462. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
  463. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -15
  464. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -128
  465. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
  466. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -95
  467. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -79
  468. package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +0 -79
  469. package/node_modules/pi-lens/dist/clients/dispatch/tool-profile.js +0 -30
  470. package/node_modules/pi-lens/dist/clients/dispatch/types.js +0 -21
  471. package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +0 -45
  472. package/node_modules/pi-lens/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
  473. package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -32
  474. package/node_modules/pi-lens/dist/clients/disposition-publish.js +0 -124
  475. package/node_modules/pi-lens/dist/clients/durable-store.js +0 -97
  476. package/node_modules/pi-lens/dist/clients/effective-config.js +0 -415
  477. package/node_modules/pi-lens/dist/clients/env-utils.js +0 -83
  478. package/node_modules/pi-lens/dist/clients/error-class.js +0 -23
  479. package/node_modules/pi-lens/dist/clients/eval-timestamp.js +0 -17
  480. package/node_modules/pi-lens/dist/clients/event-loop-hold.js +0 -274
  481. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +0 -250
  482. package/node_modules/pi-lens/dist/clients/extension-log.js +0 -504
  483. package/node_modules/pi-lens/dist/clients/extension-mode.js +0 -73
  484. package/node_modules/pi-lens/dist/clients/feature-hints.js +0 -103
  485. package/node_modules/pi-lens/dist/clients/file-kinds.js +0 -412
  486. package/node_modules/pi-lens/dist/clients/file-role.js +0 -134
  487. package/node_modules/pi-lens/dist/clients/file-time.js +0 -160
  488. package/node_modules/pi-lens/dist/clients/file-utils.js +0 -1032
  489. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +0 -494
  490. package/node_modules/pi-lens/dist/clients/finding-identity.js +0 -88
  491. package/node_modules/pi-lens/dist/clients/fix-worklog.js +0 -103
  492. package/node_modules/pi-lens/dist/clients/format-events-publish.js +0 -343
  493. package/node_modules/pi-lens/dist/clients/format-service.js +0 -222
  494. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +0 -9
  495. package/node_modules/pi-lens/dist/clients/formatters.js +0 -2032
  496. package/node_modules/pi-lens/dist/clients/freshness-cadence.js +0 -17
  497. package/node_modules/pi-lens/dist/clients/freshness.js +0 -38
  498. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +0 -394
  499. package/node_modules/pi-lens/dist/clients/generation-guard.js +0 -215
  500. package/node_modules/pi-lens/dist/clients/git-guard.js +0 -1136
  501. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +0 -243
  502. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +0 -509
  503. package/node_modules/pi-lens/dist/clients/go-client.js +0 -104
  504. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +0 -605
  505. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +0 -348
  506. package/node_modules/pi-lens/dist/clients/grammar-source.js +0 -465
  507. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +0 -135
  508. package/node_modules/pi-lens/dist/clients/hashline-anchor.js +0 -424
  509. package/node_modules/pi-lens/dist/clients/hook-budgets.js +0 -76
  510. package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +0 -140
  511. package/node_modules/pi-lens/dist/clients/host-ports.js +0 -39
  512. package/node_modules/pi-lens/dist/clients/indent-retarget.js +0 -154
  513. package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +0 -186
  514. package/node_modules/pi-lens/dist/clients/install-diagnostics.js +0 -156
  515. package/node_modules/pi-lens/dist/clients/installer/index.js +0 -5416
  516. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +0 -58
  517. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +0 -783
  518. package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +0 -7
  519. package/node_modules/pi-lens/dist/clients/instance-reaper.js +0 -1137
  520. package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +0 -145
  521. package/node_modules/pi-lens/dist/clients/instance-registry.js +0 -773
  522. package/node_modules/pi-lens/dist/clients/jscpd-client.js +0 -378
  523. package/node_modules/pi-lens/dist/clients/json-cache-read.js +0 -65
  524. package/node_modules/pi-lens/dist/clients/knip-client.js +0 -788
  525. package/node_modules/pi-lens/dist/clients/language-policy.js +0 -252
  526. package/node_modules/pi-lens/dist/clients/language-profile.js +0 -361
  527. package/node_modules/pi-lens/dist/clients/language-registry.js +0 -603
  528. package/node_modules/pi-lens/dist/clients/latency-logger.js +0 -727
  529. package/node_modules/pi-lens/dist/clients/lazy-import.js +0 -21
  530. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +0 -54
  531. package/node_modules/pi-lens/dist/clients/lens-config.js +0 -404
  532. package/node_modules/pi-lens/dist/clients/lens-engine.js +0 -358
  533. package/node_modules/pi-lens/dist/clients/lens-events.js +0 -125
  534. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +0 -467
  535. package/node_modules/pi-lens/dist/clients/lens-map.js +0 -1156
  536. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +0 -74
  537. package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -222
  538. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +0 -178
  539. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +0 -421
  540. package/node_modules/pi-lens/dist/clients/lsp/client.js +0 -4593
  541. package/node_modules/pi-lens/dist/clients/lsp/config.js +0 -691
  542. package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +0 -41
  543. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +0 -225
  544. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +0 -467
  545. package/node_modules/pi-lens/dist/clients/lsp/edits.js +0 -1219
  546. package/node_modules/pi-lens/dist/clients/lsp/index.js +0 -8305
  547. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +0 -216
  548. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +0 -298
  549. package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -63
  550. package/node_modules/pi-lens/dist/clients/lsp/launch.js +0 -746
  551. package/node_modules/pi-lens/dist/clients/lsp/lombok.js +0 -158
  552. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +0 -5
  553. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -251
  554. package/node_modules/pi-lens/dist/clients/lsp/position-encoding.js +0 -61
  555. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +0 -83
  556. package/node_modules/pi-lens/dist/clients/lsp/server.js +0 -3290
  557. package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +0 -192
  558. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +0 -17
  559. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -56
  560. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +0 -332
  561. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
  562. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +0 -85
  563. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/index.js +0 -10
  564. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +0 -392
  565. package/node_modules/pi-lens/dist/clients/lsp/watch-queue.js +0 -61
  566. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
  567. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
  568. package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +0 -184
  569. package/node_modules/pi-lens/dist/clients/lsp-budget.js +0 -186
  570. package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +0 -110
  571. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +0 -9
  572. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +0 -396
  573. package/node_modules/pi-lens/dist/clients/lsp-status.js +0 -59
  574. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +0 -40
  575. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +0 -420
  576. package/node_modules/pi-lens/dist/clients/mcp/host-shim.js +0 -31
  577. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +0 -523
  578. package/node_modules/pi-lens/dist/clients/mcp/review.js +0 -197
  579. package/node_modules/pi-lens/dist/clients/mcp/session.js +0 -412
  580. package/node_modules/pi-lens/dist/clients/memory-sampler.js +0 -208
  581. package/node_modules/pi-lens/dist/clients/message-end-attribution.js +0 -30
  582. package/node_modules/pi-lens/dist/clients/metrics-client.js +0 -107
  583. package/node_modules/pi-lens/dist/clients/metrics-history.js +0 -327
  584. package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +0 -263
  585. package/node_modules/pi-lens/dist/clients/model-provider.js +0 -53
  586. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +0 -189
  587. package/node_modules/pi-lens/dist/clients/module-report.js +0 -2150
  588. package/node_modules/pi-lens/dist/clients/mutating-tool.js +0 -651
  589. package/node_modules/pi-lens/dist/clients/mutation-attribution.js +0 -368
  590. package/node_modules/pi-lens/dist/clients/mutation-bridge.js +0 -240
  591. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -734
  592. package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +0 -101
  593. package/node_modules/pi-lens/dist/clients/observed-mutation.js +0 -1253
  594. package/node_modules/pi-lens/dist/clients/oldtext-autopatch.js +0 -128
  595. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +0 -496
  596. package/node_modules/pi-lens/dist/clients/opengrep-client.js +0 -333
  597. package/node_modules/pi-lens/dist/clients/opengrep-config.js +0 -76
  598. package/node_modules/pi-lens/dist/clients/package-manager.js +0 -607
  599. package/node_modules/pi-lens/dist/clients/package-root.js +0 -38
  600. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +0 -439
  601. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +0 -43
  602. package/node_modules/pi-lens/dist/clients/path-keyed-map.js +0 -108
  603. package/node_modules/pi-lens/dist/clients/path-utils.js +0 -1244
  604. package/node_modules/pi-lens/dist/clients/performance-report.js +0 -331
  605. package/node_modules/pi-lens/dist/clients/persist-debounce.js +0 -70
  606. package/node_modules/pi-lens/dist/clients/persistent-reverify.js +0 -271
  607. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +0 -119
  608. package/node_modules/pi-lens/dist/clients/pipeline.js +0 -1480
  609. package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +0 -37
  610. package/node_modules/pi-lens/dist/clients/print-mode.js +0 -21
  611. package/node_modules/pi-lens/dist/clients/probe-home-state.js +0 -228
  612. package/node_modules/pi-lens/dist/clients/process-bridge.js +0 -66
  613. package/node_modules/pi-lens/dist/clients/process-singletons.js +0 -210
  614. package/node_modules/pi-lens/dist/clients/process-snapshot.js +0 -68
  615. package/node_modules/pi-lens/dist/clients/project-changes.js +0 -181
  616. package/node_modules/pi-lens/dist/clients/project-conventions.js +0 -171
  617. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +0 -137
  618. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +0 -109
  619. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
  620. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
  621. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
  622. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
  623. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
  624. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
  625. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
  626. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
  627. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
  628. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
  629. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
  630. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +0 -520
  631. package/node_modules/pi-lens/dist/clients/project-diagnostics/types.js +0 -1
  632. package/node_modules/pi-lens/dist/clients/project-lens-config.js +0 -807
  633. package/node_modules/pi-lens/dist/clients/project-report.js +0 -701
  634. package/node_modules/pi-lens/dist/clients/project-scale.js +0 -237
  635. package/node_modules/pi-lens/dist/clients/project-scan-policy.js +0 -11
  636. package/node_modules/pi-lens/dist/clients/project-snapshot-fingerprint.js +0 -48
  637. package/node_modules/pi-lens/dist/clients/project-snapshot-persist-worker.js +0 -20
  638. package/node_modules/pi-lens/dist/clients/project-snapshot.js +0 -1653
  639. package/node_modules/pi-lens/dist/clients/project-trust.js +0 -181
  640. package/node_modules/pi-lens/dist/clients/python-environment.js +0 -217
  641. package/node_modules/pi-lens/dist/clients/python-provenance.js +0 -639
  642. package/node_modules/pi-lens/dist/clients/quiet-window-config.js +0 -47
  643. package/node_modules/pi-lens/dist/clients/quiet-window.js +0 -236
  644. package/node_modules/pi-lens/dist/clients/read-bridge.js +0 -139
  645. package/node_modules/pi-lens/dist/clients/read-expansion.js +0 -266
  646. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +0 -251
  647. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +0 -1306
  648. package/node_modules/pi-lens/dist/clients/read-guard.js +0 -1561
  649. package/node_modules/pi-lens/dist/clients/recent-touches.js +0 -240
  650. package/node_modules/pi-lens/dist/clients/redact/secrets.js +0 -449
  651. package/node_modules/pi-lens/dist/clients/resource-sampler.js +0 -628
  652. package/node_modules/pi-lens/dist/clients/reverse-deps.js +0 -270
  653. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +0 -5126
  654. package/node_modules/pi-lens/dist/clients/review-graph/format.js +0 -33
  655. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +0 -163
  656. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +0 -481
  657. package/node_modules/pi-lens/dist/clients/review-graph/persist-worker.js +0 -10
  658. package/node_modules/pi-lens/dist/clients/review-graph/query.js +0 -244
  659. package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +0 -21
  660. package/node_modules/pi-lens/dist/clients/review-graph/service.js +0 -96
  661. package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +0 -82
  662. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +0 -103
  663. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +0 -299
  664. package/node_modules/pi-lens/dist/clients/review-graph/types.js +0 -1
  665. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +0 -449
  666. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +0 -81
  667. package/node_modules/pi-lens/dist/clients/ruff-client.js +0 -179
  668. package/node_modules/pi-lens/dist/clients/rules-scanner.js +0 -131
  669. package/node_modules/pi-lens/dist/clients/run-duration.js +0 -55
  670. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +0 -746
  671. package/node_modules/pi-lens/dist/clients/runtime-config.js +0 -77
  672. package/node_modules/pi-lens/dist/clients/runtime-context.js +0 -211
  673. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +0 -1324
  674. package/node_modules/pi-lens/dist/clients/runtime-session.js +0 -2485
  675. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +0 -1295
  676. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +0 -2049
  677. package/node_modules/pi-lens/dist/clients/runtime-turn.js +0 -3767
  678. package/node_modules/pi-lens/dist/clients/rust-client.js +0 -97
  679. package/node_modules/pi-lens/dist/clients/safe-spawn.js +0 -2357
  680. package/node_modules/pi-lens/dist/clients/sanitize.js +0 -286
  681. package/node_modules/pi-lens/dist/clients/scan-utils.js +0 -25
  682. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +0 -165
  683. package/node_modules/pi-lens/dist/clients/search-read-registration.js +0 -94
  684. package/node_modules/pi-lens/dist/clients/secret-findings.js +0 -114
  685. package/node_modules/pi-lens/dist/clients/security-scan-client.js +0 -352
  686. package/node_modules/pi-lens/dist/clients/session-event-guard.js +0 -297
  687. package/node_modules/pi-lens/dist/clients/session-lifecycle.js +0 -484
  688. package/node_modules/pi-lens/dist/clients/session-start-observability.js +0 -94
  689. package/node_modules/pi-lens/dist/clients/session-state-store.js +0 -101
  690. package/node_modules/pi-lens/dist/clients/session-summary.js +0 -15
  691. package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +0 -27
  692. package/node_modules/pi-lens/dist/clients/sg-runner.js +0 -876
  693. package/node_modules/pi-lens/dist/clients/sgconfig.js +0 -347
  694. package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +0 -338
  695. package/node_modules/pi-lens/dist/clients/single-flight.js +0 -158
  696. package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +0 -108
  697. package/node_modules/pi-lens/dist/clients/skills-resolver.js +0 -141
  698. package/node_modules/pi-lens/dist/clients/slow-fs.js +0 -138
  699. package/node_modules/pi-lens/dist/clients/smells-rollup.js +0 -231
  700. package/node_modules/pi-lens/dist/clients/source-filter.js +0 -596
  701. package/node_modules/pi-lens/dist/clients/source-walker.js +0 -211
  702. package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +0 -120
  703. package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +0 -110
  704. package/node_modules/pi-lens/dist/clients/stale-marker.js +0 -17
  705. package/node_modules/pi-lens/dist/clients/startup-scan.js +0 -400
  706. package/node_modules/pi-lens/dist/clients/startup-timing.js +0 -130
  707. package/node_modules/pi-lens/dist/clients/string-utils.js +0 -33
  708. package/node_modules/pi-lens/dist/clients/subagent-mode.js +0 -111
  709. package/node_modules/pi-lens/dist/clients/symbol-containment.js +0 -24
  710. package/node_modules/pi-lens/dist/clients/symbol-types.js +0 -5
  711. package/node_modules/pi-lens/dist/clients/test-runner-client.js +0 -2507
  712. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +0 -287
  713. package/node_modules/pi-lens/dist/clients/todo-scanner.js +0 -229
  714. package/node_modules/pi-lens/dist/clients/tool-agreement.js +0 -423
  715. package/node_modules/pi-lens/dist/clients/tool-config.js +0 -249
  716. package/node_modules/pi-lens/dist/clients/tool-cwd.js +0 -270
  717. package/node_modules/pi-lens/dist/clients/tool-definition.js +0 -47
  718. package/node_modules/pi-lens/dist/clients/tool-event.js +0 -105
  719. package/node_modules/pi-lens/dist/clients/tool-policy.js +0 -2755
  720. package/node_modules/pi-lens/dist/clients/tool-probe.js +0 -53
  721. package/node_modules/pi-lens/dist/clients/tool-render.js +0 -156
  722. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +0 -118
  723. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +0 -364
  724. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +0 -4195
  725. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +0 -100
  726. package/node_modules/pi-lens/dist/clients/tree-sitter-navigator.js +0 -269
  727. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +0 -569
  728. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +0 -158
  729. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
  730. package/node_modules/pi-lens/dist/clients/trivy-client.js +0 -467
  731. package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -50
  732. package/node_modules/pi-lens/dist/clients/turn-context.js +0 -52
  733. package/node_modules/pi-lens/dist/clients/turn-end/lane.js +0 -51
  734. package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +0 -112
  735. package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +0 -204
  736. package/node_modules/pi-lens/dist/clients/turn-summary-render.js +0 -72
  737. package/node_modules/pi-lens/dist/clients/turn-summary.js +0 -132
  738. package/node_modules/pi-lens/dist/clients/typos-config.js +0 -29
  739. package/node_modules/pi-lens/dist/clients/user-notify.js +0 -51
  740. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +0 -57
  741. package/node_modules/pi-lens/dist/clients/warm-attach.js +0 -305
  742. package/node_modules/pi-lens/dist/clients/widget-state.js +0 -1808
  743. package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -49
  744. package/node_modules/pi-lens/dist/clients/word-index-store.js +0 -459
  745. package/node_modules/pi-lens/dist/clients/word-index.js +0 -2080
  746. package/node_modules/pi-lens/dist/clients/workspace-topology.js +0 -443
  747. package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +0 -65
  748. package/node_modules/pi-lens/dist/clients/zizmor-config.js +0 -340
  749. package/node_modules/pi-lens/dist/i18n.js +0 -69
  750. package/node_modules/pi-lens/dist/mcp/build-staleness.js +0 -124
  751. package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +0 -583
  752. package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +0 -129
  753. package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
  754. package/node_modules/pi-lens/dist/tools/activate-tools.js +0 -87
  755. package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +0 -137
  756. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +0 -244
  757. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +0 -762
  758. package/node_modules/pi-lens/dist/tools/effective-config.js +0 -90
  759. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +0 -375
  760. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +0 -2571
  761. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +0 -1745
  762. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +0 -1367
  763. package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +0 -294
  764. package/node_modules/pi-lens/dist/tools/module-report.js +0 -382
  765. package/node_modules/pi-lens/dist/tools/project-report.js +0 -86
  766. package/node_modules/pi-lens/dist/tools/render-compact.js +0 -374
  767. package/node_modules/pi-lens/dist/tools/scan-progress.js +0 -53
  768. package/node_modules/pi-lens/dist/tools/shared.js +0 -29
  769. 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
- }