@herbertgao/pi-extensions 2026.10.1 → 2026.10.3

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 (934) hide show
  1. package/README.md +14 -17
  2. package/THIRD_PARTY_NOTICES.md +2 -2
  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/.qoder/rules/ponytail.md +21 -21
  10. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
  11. package/node_modules/@dietrichgebert/ponytail/AGENTS.md +21 -21
  12. package/node_modules/@dietrichgebert/ponytail/README.md +68 -72
  13. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +7 -0
  14. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +30 -30
  15. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-map.js +103 -0
  16. package/node_modules/@dietrichgebert/ponytail/package.json +2 -2
  17. package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +33 -101
  18. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +89 -32
  19. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +13 -12
  20. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +17 -16
  21. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +9 -10
  22. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +96 -57
  23. package/node_modules/{remote-pi → @herbertgao/pi-agency}/LICENSE +1 -1
  24. package/node_modules/@herbertgao/pi-agency/README.md +40 -0
  25. package/node_modules/@herbertgao/pi-agency/package.json +53 -0
  26. package/node_modules/@herbertgao/pi-agency/src/index.ts +441 -0
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +12 -12
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +30 -6
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +15 -9
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +27 -9
  31. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +4 -1
  32. package/node_modules/@herbertgao/pi-cc-extensions/package.json +2 -2
  33. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  34. package/node_modules/@herbertgao/pi-subagents/README.md +1 -1
  35. package/node_modules/@herbertgao/pi-subagents/package.json +4 -5
  36. package/node_modules/@herbertgao/pi-subagents/src/index.ts +1 -1
  37. package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +1 -1
  38. package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +48 -72
  39. package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +1 -1
  40. package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +15 -6
  41. package/node_modules/@herbertgao/pi-subagents/src/workflow/collisions.ts +4 -1
  42. package/node_modules/@herbertgao/pi-subagents/src/workflow/json-schema.ts +9 -8
  43. package/node_modules/@narumitw/pi-caffeinate/dist/index.ts +6 -1
  44. package/node_modules/@narumitw/pi-caffeinate/dist/index.ts.map +2 -2
  45. package/node_modules/@narumitw/pi-caffeinate/package.json +3 -3
  46. package/node_modules/@narumitw/pi-caffeinate/src/dbus-inhibit.ts +13 -1
  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 +1159 -0
  67. package/node_modules/pi-lens/README.md +116 -25
  68. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +6 -2
  69. package/node_modules/pi-lens/dist/chunk-2BEMIWEW.js +51 -0
  70. package/node_modules/pi-lens/dist/chunk-2EECBNC5.js +89 -0
  71. package/node_modules/pi-lens/dist/chunk-2FQ62SPM.js +83 -0
  72. package/node_modules/pi-lens/dist/chunk-2UMYMRI3.js +118 -0
  73. package/node_modules/pi-lens/dist/chunk-34OCDWOS.js +48 -0
  74. package/node_modules/pi-lens/dist/chunk-3FERWMCH.js +144 -0
  75. package/node_modules/pi-lens/dist/chunk-3JL6IFD7.js +97 -0
  76. package/node_modules/pi-lens/dist/chunk-3JSEWUWI.js +327 -0
  77. package/node_modules/pi-lens/dist/chunk-3MED75HT.js +37 -0
  78. package/node_modules/pi-lens/dist/chunk-3MRXIJ6I.js +79 -0
  79. package/node_modules/pi-lens/dist/chunk-3P4PF2BX.js +2250 -0
  80. package/node_modules/pi-lens/dist/chunk-3PNFWVHT.js +201 -0
  81. package/node_modules/pi-lens/dist/chunk-3TTPKMEF.js +162 -0
  82. package/node_modules/pi-lens/dist/chunk-5JHXUETW.js +326 -0
  83. package/node_modules/pi-lens/dist/chunk-5W3R6LFT.js +24 -0
  84. package/node_modules/pi-lens/dist/chunk-64LU2OVH.js +341 -0
  85. package/node_modules/pi-lens/dist/chunk-6B5I6ICN.js +44 -0
  86. package/node_modules/pi-lens/dist/chunk-6P54TMWL.js +274 -0
  87. package/node_modules/pi-lens/dist/chunk-6UYA642J.js +145 -0
  88. package/node_modules/pi-lens/dist/chunk-6XX5HCLK.js +398 -0
  89. package/node_modules/pi-lens/dist/chunk-7AXO3ZDF.js +30 -0
  90. package/node_modules/pi-lens/dist/chunk-7BX5DWSA.js +1907 -0
  91. package/node_modules/pi-lens/dist/chunk-7ELY2CSM.js +902 -0
  92. package/node_modules/pi-lens/dist/chunk-7M4T5N3V.js +38 -0
  93. package/node_modules/pi-lens/dist/chunk-7U2IDNXN.js +37 -0
  94. package/node_modules/pi-lens/dist/chunk-ABBOA7UD.js +157 -0
  95. package/node_modules/pi-lens/dist/chunk-AFZYMAIV.js +756 -0
  96. package/node_modules/pi-lens/dist/chunk-ALQRYH6O.js +112 -0
  97. package/node_modules/pi-lens/dist/chunk-AXKPQ6ZI.js +1610 -0
  98. package/node_modules/pi-lens/dist/chunk-BHWHXMQ2.js +62 -0
  99. package/node_modules/pi-lens/dist/chunk-BJF25SUG.js +22 -0
  100. package/node_modules/pi-lens/dist/chunk-BNKLYOOO.js +18 -0
  101. package/node_modules/pi-lens/dist/chunk-BW5KFIQC.js +59 -0
  102. package/node_modules/pi-lens/dist/chunk-CBD2B5CP.js +599 -0
  103. package/node_modules/pi-lens/dist/chunk-CFYISHUW.js +42 -0
  104. package/node_modules/pi-lens/dist/chunk-CMU4RMJB.js +30 -0
  105. package/node_modules/pi-lens/dist/chunk-CULQYFLU.js +14 -0
  106. package/node_modules/pi-lens/dist/chunk-D65GOCJB.js +289 -0
  107. package/node_modules/pi-lens/dist/chunk-D6YF5HKX.js +59 -0
  108. package/node_modules/pi-lens/dist/chunk-DMS52BEP.js +342 -0
  109. package/node_modules/pi-lens/dist/chunk-ELVWJYRV.js +211 -0
  110. package/node_modules/pi-lens/dist/chunk-EUT3B22Y.js +27 -0
  111. package/node_modules/pi-lens/dist/chunk-F5VLZHUW.js +146 -0
  112. package/node_modules/pi-lens/dist/chunk-FC4RP4I2.js +12 -0
  113. package/node_modules/pi-lens/dist/chunk-FGALODG3.js +55 -0
  114. package/node_modules/pi-lens/dist/chunk-G3RLJYA2.js +2358 -0
  115. package/node_modules/pi-lens/dist/chunk-GAYGGICG.js +32 -0
  116. package/node_modules/pi-lens/dist/chunk-H2W2AXD3.js +419 -0
  117. package/node_modules/pi-lens/dist/chunk-H5EV3VMF.js +114 -0
  118. package/node_modules/pi-lens/dist/chunk-HREZQCT3.js +106 -0
  119. package/node_modules/pi-lens/dist/chunk-HSIRMK2D.js +517 -0
  120. package/node_modules/pi-lens/dist/chunk-IMSQR4WO.js +184 -0
  121. package/node_modules/pi-lens/dist/chunk-IQZXOZ7S.js +4816 -0
  122. package/node_modules/pi-lens/dist/chunk-IR6XFA5P.js +37 -0
  123. package/node_modules/pi-lens/dist/chunk-IUKPRZER.js +78 -0
  124. package/node_modules/pi-lens/dist/chunk-IUR76K6A.js +262 -0
  125. package/node_modules/pi-lens/dist/chunk-J3FZR3YY.js +98 -0
  126. package/node_modules/pi-lens/dist/chunk-J6DFZDFA.js +562 -0
  127. package/node_modules/pi-lens/dist/chunk-J7WJZ6W7.js +50 -0
  128. package/node_modules/pi-lens/dist/chunk-JONGANYH.js +202 -0
  129. package/node_modules/pi-lens/dist/chunk-KJCWXUUS.js +425 -0
  130. package/node_modules/pi-lens/dist/chunk-KJN4IOEJ.js +17 -0
  131. package/node_modules/pi-lens/dist/chunk-KMGPUKFD.js +25 -0
  132. package/node_modules/pi-lens/dist/chunk-LD7WFTRX.js +4533 -0
  133. package/node_modules/pi-lens/dist/chunk-LJDODBBZ.js +78 -0
  134. package/node_modules/pi-lens/dist/chunk-LNOMA3MP.js +169 -0
  135. package/node_modules/pi-lens/dist/chunk-MGIWSROG.js +2334 -0
  136. package/node_modules/pi-lens/dist/chunk-MQA2YBAH.js +308 -0
  137. package/node_modules/pi-lens/dist/chunk-MTDRLSI7.js +21720 -0
  138. package/node_modules/pi-lens/dist/chunk-N2IRDBZI.js +2355 -0
  139. package/node_modules/pi-lens/dist/chunk-N77WFUL7.js +50 -0
  140. package/node_modules/pi-lens/dist/chunk-N7YNJE5R.js +466 -0
  141. package/node_modules/pi-lens/dist/chunk-NABLC7W3.js +148 -0
  142. package/node_modules/pi-lens/dist/chunk-NFXP47RV.js +999 -0
  143. package/node_modules/pi-lens/dist/chunk-NJF2X5XF.js +45 -0
  144. package/node_modules/pi-lens/dist/chunk-NXL4FFQQ.js +47 -0
  145. package/node_modules/pi-lens/dist/chunk-O7HZX7CA.js +309 -0
  146. package/node_modules/pi-lens/dist/chunk-OC7LYAU5.js +708 -0
  147. package/node_modules/pi-lens/dist/chunk-OEX24ECI.js +348 -0
  148. package/node_modules/pi-lens/dist/chunk-OYOB3ZFG.js +78 -0
  149. package/node_modules/pi-lens/dist/chunk-OYTMZXRF.js +41 -0
  150. package/node_modules/pi-lens/dist/chunk-OZJINN4O.js +84 -0
  151. package/node_modules/pi-lens/dist/chunk-PCHDGUKT.js +23 -0
  152. package/node_modules/pi-lens/dist/chunk-QOZXJ4NA.js +3364 -0
  153. package/node_modules/pi-lens/dist/chunk-QYF5S6P6.js +45 -0
  154. package/node_modules/pi-lens/dist/chunk-RJFED73B.js +79 -0
  155. package/node_modules/pi-lens/dist/chunk-RTQNRM5X.js +27 -0
  156. package/node_modules/pi-lens/dist/chunk-S6AMWKPC.js +189 -0
  157. package/node_modules/pi-lens/dist/chunk-T4QZ6FEH.js +308 -0
  158. package/node_modules/pi-lens/dist/chunk-T54VJRAD.js +5456 -0
  159. package/node_modules/pi-lens/dist/chunk-TA5L5KWQ.js +167 -0
  160. package/node_modules/pi-lens/dist/chunk-TQFBZVCG.js +27 -0
  161. package/node_modules/pi-lens/dist/chunk-U52SAGRM.js +62 -0
  162. package/node_modules/pi-lens/dist/chunk-UEAHAMEI.js +5814 -0
  163. package/node_modules/pi-lens/dist/chunk-UGSMCQ4D.js +327 -0
  164. package/node_modules/pi-lens/dist/chunk-UOPVJQ4N.js +103 -0
  165. package/node_modules/pi-lens/dist/chunk-USHG4DLH.js +459 -0
  166. package/node_modules/pi-lens/dist/chunk-UXVNL3D7.js +23 -0
  167. package/node_modules/pi-lens/dist/chunk-V5MEBST3.js +4139 -0
  168. package/node_modules/pi-lens/dist/chunk-VGEVL5EQ.js +29 -0
  169. package/node_modules/pi-lens/dist/chunk-VPWKF3GN.js +87 -0
  170. package/node_modules/pi-lens/dist/chunk-VSMJK3ZJ.js +1250 -0
  171. package/node_modules/pi-lens/dist/chunk-VYKKMYVT.js +560 -0
  172. package/node_modules/pi-lens/dist/chunk-VZHJS6DR.js +442 -0
  173. package/node_modules/pi-lens/dist/chunk-WATC3IQR.js +213 -0
  174. package/node_modules/pi-lens/dist/chunk-WPE5FPGR.js +54 -0
  175. package/node_modules/pi-lens/dist/chunk-WSEWKCXU.js +188 -0
  176. package/node_modules/pi-lens/dist/chunk-WW5BLB74.js +659 -0
  177. package/node_modules/pi-lens/dist/chunk-XGUL4SLQ.js +406 -0
  178. package/node_modules/pi-lens/dist/chunk-XHSCVSOL.js +170 -0
  179. package/node_modules/pi-lens/dist/chunk-XIMYZ3JA.js +27 -0
  180. package/node_modules/pi-lens/dist/chunk-YLN4K6WD.js +385 -0
  181. package/node_modules/pi-lens/dist/chunk-Z3CCVDUB.js +76 -0
  182. package/node_modules/pi-lens/dist/chunk-Z54ZSOBZ.js +61 -0
  183. package/node_modules/pi-lens/dist/chunk-Z6BB4JLQ.js +478 -0
  184. package/node_modules/pi-lens/dist/chunk-Z7QQA2KT.js +27 -0
  185. package/node_modules/pi-lens/dist/chunk-ZETYB3XG.js +2626 -0
  186. package/node_modules/pi-lens/dist/chunk-ZMOMPX2Z.js +87 -0
  187. package/node_modules/pi-lens/dist/chunk-ZOEYVXSV.js +149 -0
  188. package/node_modules/pi-lens/dist/chunk-ZZUFZUQZ.js +12120 -0
  189. package/node_modules/pi-lens/dist/index.js +39712 -29876
  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 +22688 -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 +5 -4
  200. package/node_modules/pi-lens/docs/configuration.md +28 -2
  201. package/node_modules/pi-lens/docs/custom-rules.md +25 -2
  202. package/node_modules/pi-lens/docs/environment-variables.md +86 -8
  203. package/node_modules/pi-lens/docs/features.md +45 -5
  204. package/node_modules/pi-lens/docs/globalconfig.md +2 -1
  205. package/node_modules/pi-lens/docs/io-bridge-v2-migration.md +94 -0
  206. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +37 -4
  207. package/node_modules/pi-lens/docs/lsp-idle-eviction.md +124 -0
  208. package/node_modules/pi-lens/docs/mcp.md +21 -5
  209. package/node_modules/pi-lens/docs/merge-queue-rollout.md +77 -0
  210. package/node_modules/pi-lens/docs/pi-lens-fixer.md +160 -88
  211. package/node_modules/pi-lens/docs/pi-lens-investigator.md +74 -26
  212. package/node_modules/pi-lens/docs/pi-lens-merge-policy.md +736 -0
  213. package/node_modules/pi-lens/docs/pi-lens-monitor.md +75 -7
  214. package/node_modules/pi-lens/docs/pi-lens-retro.md +6 -4
  215. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +189 -48
  216. package/node_modules/pi-lens/docs/pi-lens-subagent.md +246 -45
  217. package/node_modules/pi-lens/docs/pi-lens-warden.md +18 -7
  218. package/node_modules/pi-lens/docs/public-api-stability.md +55 -0
  219. package/node_modules/pi-lens/docs/release-qa-baseline.md +84 -17
  220. package/node_modules/pi-lens/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
  221. package/node_modules/pi-lens/docs/schema/pi-lens-config-v1.json +530 -0
  222. package/node_modules/pi-lens/docs/servercapabilities.md +56 -49
  223. package/node_modules/pi-lens/docs/settings.md +1 -0
  224. package/node_modules/pi-lens/docs/usage.md +5 -1
  225. package/node_modules/pi-lens/docs/word-index.md +6 -0
  226. package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm +0 -0
  227. package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm.json +3 -3
  228. package/node_modules/pi-lens/package.json +18 -6
  229. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
  230. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-identity-operator-on-literals-test.yml +12 -2
  231. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/throw-new-error-js-test.yml +2 -0
  232. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/throw-new-error-test.yml +2 -0
  233. package/node_modules/pi-lens/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
  234. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-identity-operator-on-literals.yml +20 -12
  235. package/node_modules/pi-lens/rules/ast-grep-rules/rules/throw-new-error-js.yml +2 -2
  236. package/node_modules/pi-lens/rules/ast-grep-rules/rules/throw-new-error.yml +2 -2
  237. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +1275 -62
  238. package/node_modules/pi-lens/scripts/download-grammars.js +5 -0
  239. package/node_modules/pi-lens/scripts/grammars.lock.json +6 -1
  240. package/node_modules/pi-lens/scripts/install-selftest.mjs +33 -34
  241. package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +13 -4
  242. package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +18 -0
  243. package/node_modules/pi-web-access/CHANGELOG.md +28 -0
  244. package/node_modules/pi-web-access/README.md +10 -6
  245. package/node_modules/pi-web-access/ceramic.ts +219 -0
  246. package/node_modules/pi-web-access/curator-page.ts +3 -1
  247. package/node_modules/pi-web-access/curator-server.ts +1 -0
  248. package/node_modules/pi-web-access/dist/index.js +846 -588
  249. package/node_modules/pi-web-access/dist/mcp-cli.js +725 -516
  250. package/node_modules/pi-web-access/gemini-search.ts +8 -4
  251. package/node_modules/pi-web-access/github-extract.ts +34 -2
  252. package/node_modules/pi-web-access/index.ts +72 -30
  253. package/node_modules/pi-web-access/openai-search.ts +3 -3
  254. package/node_modules/pi-web-access/package.json +2 -2
  255. package/node_modules/pi-web-access/page-query.ts +6 -9
  256. package/node_modules/pi-web-access/parallel-mcp.ts +2 -0
  257. package/node_modules/pi-web-access/query-rewrite.ts +2 -2
  258. package/node_modules/pi-web-access/storage.ts +49 -0
  259. package/node_modules/pi-web-access/summary-review.ts +7 -6
  260. package/node_modules/pi-web-access/utils.ts +21 -6
  261. package/node_modules/pi-web-access/web-tool-core.ts +1 -0
  262. package/package.json +19 -19
  263. package/node_modules/@dietrichgebert/ponytail/README.es.md +0 -212
  264. package/node_modules/@dietrichgebert/ponytail/README.ja.md +0 -212
  265. package/node_modules/@dietrichgebert/ponytail/README.ko.md +0 -212
  266. package/node_modules/@dietrichgebert/ponytail/README.zh-CN.md +0 -212
  267. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -35
  268. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +0 -1610
  269. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +0 -593
  270. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +0 -147
  271. package/node_modules/pi-lens/dist/clients/agent-nudge.js +0 -325
  272. package/node_modules/pi-lens/dist/clients/analysed-root.js +0 -1
  273. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +0 -731
  274. package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +0 -146
  275. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +0 -118
  276. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -10
  277. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +0 -154
  278. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +0 -75
  279. package/node_modules/pi-lens/dist/clients/atomic-write.js +0 -189
  280. package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +0 -12
  281. package/node_modules/pi-lens/dist/clients/bash-file-access.js +0 -1056
  282. package/node_modules/pi-lens/dist/clients/biome-client.js +0 -357
  283. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -837
  284. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +0 -129
  285. package/node_modules/pi-lens/dist/clients/bootstrap.js +0 -569
  286. package/node_modules/pi-lens/dist/clients/bounded-cache.js +0 -190
  287. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +0 -202
  288. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +0 -295
  289. package/node_modules/pi-lens/dist/clients/build-identity.js +0 -95
  290. package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +0 -113
  291. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +0 -105
  292. package/node_modules/pi-lens/dist/clients/bus-publish.js +0 -171
  293. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +0 -190
  294. package/node_modules/pi-lens/dist/clients/cache-manager.js +0 -524
  295. package/node_modules/pi-lens/dist/clients/cache-observability.js +0 -1313
  296. package/node_modules/pi-lens/dist/clients/call-graph.js +0 -852
  297. package/node_modules/pi-lens/dist/clients/cargo-manifest.js +0 -422
  298. package/node_modules/pi-lens/dist/clients/cascade-budget.js +0 -196
  299. package/node_modules/pi-lens/dist/clients/cascade-format.js +0 -315
  300. package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -31
  301. package/node_modules/pi-lens/dist/clients/cascade-types.js +0 -9
  302. package/node_modules/pi-lens/dist/clients/child-unref.js +0 -124
  303. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +0 -286
  304. package/node_modules/pi-lens/dist/clients/codebase-model.js +0 -173
  305. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +0 -40
  306. package/node_modules/pi-lens/dist/clients/complexity-client.js +0 -462
  307. package/node_modules/pi-lens/dist/clients/config-core/deny.js +0 -221
  308. package/node_modules/pi-lens/dist/clients/config-core/index.js +0 -50
  309. package/node_modules/pi-lens/dist/clients/config-core/merge.js +0 -357
  310. package/node_modules/pi-lens/dist/clients/config-core/normalize.js +0 -340
  311. package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +0 -248
  312. package/node_modules/pi-lens/dist/clients/config-core/provenance.js +0 -164
  313. package/node_modules/pi-lens/dist/clients/config-core/records.js +0 -218
  314. package/node_modules/pi-lens/dist/clients/config-core/resolve.js +0 -125
  315. package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +0 -78
  316. package/node_modules/pi-lens/dist/clients/config-core/schema.js +0 -165
  317. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +0 -285
  318. package/node_modules/pi-lens/dist/clients/config-locations.js +0 -388
  319. package/node_modules/pi-lens/dist/clients/config-resolve.js +0 -798
  320. package/node_modules/pi-lens/dist/clients/config-schema.js +0 -251
  321. package/node_modules/pi-lens/dist/clients/config-warn.js +0 -407
  322. package/node_modules/pi-lens/dist/clients/console-guard-install.js +0 -26
  323. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +0 -42
  324. package/node_modules/pi-lens/dist/clients/dead-code-client.js +0 -510
  325. package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -30
  326. package/node_modules/pi-lens/dist/clients/deadline-utils.js +0 -280
  327. package/node_modules/pi-lens/dist/clients/debug-handles.js +0 -211
  328. package/node_modules/pi-lens/dist/clients/debug-heap.js +0 -174
  329. package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +0 -106
  330. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +0 -426
  331. package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +0 -169
  332. package/node_modules/pi-lens/dist/clients/dependency-checker.js +0 -885
  333. package/node_modules/pi-lens/dist/clients/deps/ast-grep-napi.js +0 -22
  334. package/node_modules/pi-lens/dist/clients/deps/js-yaml.js +0 -9
  335. package/node_modules/pi-lens/dist/clients/deps/minimatch.js +0 -4
  336. package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +0 -14
  337. package/node_modules/pi-lens/dist/clients/deps/typebox.js +0 -14
  338. package/node_modules/pi-lens/dist/clients/deps/vscode-jsonrpc.js +0 -1
  339. package/node_modules/pi-lens/dist/clients/deps/web-tree-sitter.js +0 -24
  340. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +0 -630
  341. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +0 -305
  342. package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +0 -73
  343. package/node_modules/pi-lens/dist/clients/diagnostic-tracker.js +0 -105
  344. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +0 -230
  345. package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +0 -65
  346. package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +0 -343
  347. package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -35
  348. package/node_modules/pi-lens/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
  349. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +0 -1269
  350. package/node_modules/pi-lens/dist/clients/dispatch/fact-provider-types.js +0 -1
  351. package/node_modules/pi-lens/dist/clients/dispatch/fact-rule-runner.js +0 -17
  352. package/node_modules/pi-lens/dist/clients/dispatch/fact-runner.js +0 -30
  353. package/node_modules/pi-lens/dist/clients/dispatch/fact-scheduler.js +0 -68
  354. package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +0 -377
  355. package/node_modules/pi-lens/dist/clients/dispatch/facts/comment-facts.js +0 -22
  356. package/node_modules/pi-lens/dist/clients/dispatch/facts/file-content.js +0 -19
  357. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +0 -348
  358. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +0 -204
  359. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
  360. package/node_modules/pi-lens/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
  361. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +0 -339
  362. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +0 -417
  363. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +0 -89
  364. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +0 -2377
  365. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -11
  366. package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +0 -87
  367. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -278
  368. package/node_modules/pi-lens/dist/clients/dispatch/priorities.js +0 -20
  369. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +0 -50
  370. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +0 -69
  371. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +0 -135
  372. package/node_modules/pi-lens/dist/clients/dispatch/rules/async-noise.js +0 -40
  373. package/node_modules/pi-lens/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
  374. package/node_modules/pi-lens/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
  375. package/node_modules/pi-lens/dist/clients/dispatch/rules/error-obscuring.js +0 -32
  376. package/node_modules/pi-lens/dist/clients/dispatch/rules/error-swallowing.js +0 -31
  377. package/node_modules/pi-lens/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
  378. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +0 -72
  379. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +0 -74
  380. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
  381. package/node_modules/pi-lens/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
  382. package/node_modules/pi-lens/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
  383. package/node_modules/pi-lens/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
  384. package/node_modules/pi-lens/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
  385. package/node_modules/pi-lens/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
  386. package/node_modules/pi-lens/dist/clients/dispatch/runner-context.js +0 -52
  387. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -111
  388. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
  389. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -296
  390. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -219
  391. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -82
  392. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -416
  393. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -183
  394. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -176
  395. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -164
  396. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -195
  397. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -137
  398. package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -29
  399. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -69
  400. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -167
  401. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +0 -104
  402. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -135
  403. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -91
  404. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -171
  405. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -940
  406. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -117
  407. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +0 -108
  408. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -99
  409. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -110
  410. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -379
  411. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -167
  412. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -102
  413. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +0 -414
  414. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -85
  415. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -120
  416. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -62
  417. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
  418. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +0 -169
  419. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -99
  420. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -88
  421. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +0 -256
  422. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -173
  423. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -102
  424. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -128
  425. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -259
  426. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -183
  427. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -166
  428. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -163
  429. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -174
  430. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -168
  431. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -125
  432. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -643
  433. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -253
  434. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
  435. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
  436. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
  437. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
  438. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
  439. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
  440. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
  441. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
  442. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -15
  443. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -128
  444. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
  445. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -95
  446. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -79
  447. package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +0 -79
  448. package/node_modules/pi-lens/dist/clients/dispatch/tool-profile.js +0 -30
  449. package/node_modules/pi-lens/dist/clients/dispatch/types.js +0 -21
  450. package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +0 -45
  451. package/node_modules/pi-lens/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
  452. package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -32
  453. package/node_modules/pi-lens/dist/clients/disposition-publish.js +0 -124
  454. package/node_modules/pi-lens/dist/clients/durable-store.js +0 -97
  455. package/node_modules/pi-lens/dist/clients/effective-config.js +0 -415
  456. package/node_modules/pi-lens/dist/clients/env-utils.js +0 -83
  457. package/node_modules/pi-lens/dist/clients/error-class.js +0 -23
  458. package/node_modules/pi-lens/dist/clients/eval-timestamp.js +0 -17
  459. package/node_modules/pi-lens/dist/clients/event-loop-hold.js +0 -274
  460. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +0 -250
  461. package/node_modules/pi-lens/dist/clients/extension-log.js +0 -504
  462. package/node_modules/pi-lens/dist/clients/extension-mode.js +0 -73
  463. package/node_modules/pi-lens/dist/clients/feature-hints.js +0 -103
  464. package/node_modules/pi-lens/dist/clients/file-kinds.js +0 -412
  465. package/node_modules/pi-lens/dist/clients/file-role.js +0 -134
  466. package/node_modules/pi-lens/dist/clients/file-time.js +0 -160
  467. package/node_modules/pi-lens/dist/clients/file-utils.js +0 -1032
  468. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +0 -494
  469. package/node_modules/pi-lens/dist/clients/finding-identity.js +0 -88
  470. package/node_modules/pi-lens/dist/clients/fix-worklog.js +0 -103
  471. package/node_modules/pi-lens/dist/clients/format-events-publish.js +0 -343
  472. package/node_modules/pi-lens/dist/clients/format-service.js +0 -222
  473. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +0 -9
  474. package/node_modules/pi-lens/dist/clients/formatters.js +0 -2032
  475. package/node_modules/pi-lens/dist/clients/freshness-cadence.js +0 -17
  476. package/node_modules/pi-lens/dist/clients/freshness.js +0 -38
  477. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +0 -394
  478. package/node_modules/pi-lens/dist/clients/generation-guard.js +0 -215
  479. package/node_modules/pi-lens/dist/clients/git-guard.js +0 -1136
  480. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +0 -243
  481. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +0 -509
  482. package/node_modules/pi-lens/dist/clients/go-client.js +0 -104
  483. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +0 -605
  484. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +0 -348
  485. package/node_modules/pi-lens/dist/clients/grammar-source.js +0 -465
  486. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +0 -135
  487. package/node_modules/pi-lens/dist/clients/hashline-anchor.js +0 -424
  488. package/node_modules/pi-lens/dist/clients/hook-budgets.js +0 -76
  489. package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +0 -140
  490. package/node_modules/pi-lens/dist/clients/host-ports.js +0 -39
  491. package/node_modules/pi-lens/dist/clients/indent-retarget.js +0 -154
  492. package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +0 -186
  493. package/node_modules/pi-lens/dist/clients/install-diagnostics.js +0 -156
  494. package/node_modules/pi-lens/dist/clients/installer/index.js +0 -5416
  495. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +0 -58
  496. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +0 -783
  497. package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +0 -7
  498. package/node_modules/pi-lens/dist/clients/instance-reaper.js +0 -1137
  499. package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +0 -145
  500. package/node_modules/pi-lens/dist/clients/instance-registry.js +0 -773
  501. package/node_modules/pi-lens/dist/clients/jscpd-client.js +0 -378
  502. package/node_modules/pi-lens/dist/clients/json-cache-read.js +0 -65
  503. package/node_modules/pi-lens/dist/clients/knip-client.js +0 -788
  504. package/node_modules/pi-lens/dist/clients/language-policy.js +0 -252
  505. package/node_modules/pi-lens/dist/clients/language-profile.js +0 -361
  506. package/node_modules/pi-lens/dist/clients/language-registry.js +0 -603
  507. package/node_modules/pi-lens/dist/clients/latency-logger.js +0 -727
  508. package/node_modules/pi-lens/dist/clients/lazy-import.js +0 -21
  509. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +0 -54
  510. package/node_modules/pi-lens/dist/clients/lens-config.js +0 -404
  511. package/node_modules/pi-lens/dist/clients/lens-engine.js +0 -358
  512. package/node_modules/pi-lens/dist/clients/lens-events.js +0 -125
  513. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +0 -467
  514. package/node_modules/pi-lens/dist/clients/lens-map.js +0 -1156
  515. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +0 -74
  516. package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -222
  517. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +0 -178
  518. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +0 -421
  519. package/node_modules/pi-lens/dist/clients/lsp/client.js +0 -4593
  520. package/node_modules/pi-lens/dist/clients/lsp/config.js +0 -691
  521. package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +0 -41
  522. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +0 -225
  523. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +0 -467
  524. package/node_modules/pi-lens/dist/clients/lsp/edits.js +0 -1219
  525. package/node_modules/pi-lens/dist/clients/lsp/index.js +0 -8305
  526. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +0 -216
  527. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +0 -298
  528. package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -63
  529. package/node_modules/pi-lens/dist/clients/lsp/launch.js +0 -746
  530. package/node_modules/pi-lens/dist/clients/lsp/lombok.js +0 -158
  531. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +0 -5
  532. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -251
  533. package/node_modules/pi-lens/dist/clients/lsp/position-encoding.js +0 -61
  534. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +0 -83
  535. package/node_modules/pi-lens/dist/clients/lsp/server.js +0 -3290
  536. package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +0 -192
  537. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +0 -17
  538. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -56
  539. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +0 -332
  540. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
  541. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +0 -85
  542. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/index.js +0 -10
  543. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +0 -392
  544. package/node_modules/pi-lens/dist/clients/lsp/watch-queue.js +0 -61
  545. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
  546. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
  547. package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +0 -184
  548. package/node_modules/pi-lens/dist/clients/lsp-budget.js +0 -186
  549. package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +0 -110
  550. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +0 -9
  551. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +0 -396
  552. package/node_modules/pi-lens/dist/clients/lsp-status.js +0 -59
  553. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +0 -40
  554. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +0 -420
  555. package/node_modules/pi-lens/dist/clients/mcp/host-shim.js +0 -31
  556. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +0 -523
  557. package/node_modules/pi-lens/dist/clients/mcp/review.js +0 -197
  558. package/node_modules/pi-lens/dist/clients/mcp/session.js +0 -412
  559. package/node_modules/pi-lens/dist/clients/memory-sampler.js +0 -208
  560. package/node_modules/pi-lens/dist/clients/message-end-attribution.js +0 -30
  561. package/node_modules/pi-lens/dist/clients/metrics-client.js +0 -107
  562. package/node_modules/pi-lens/dist/clients/metrics-history.js +0 -327
  563. package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +0 -263
  564. package/node_modules/pi-lens/dist/clients/model-provider.js +0 -53
  565. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +0 -189
  566. package/node_modules/pi-lens/dist/clients/module-report.js +0 -2150
  567. package/node_modules/pi-lens/dist/clients/mutating-tool.js +0 -651
  568. package/node_modules/pi-lens/dist/clients/mutation-attribution.js +0 -368
  569. package/node_modules/pi-lens/dist/clients/mutation-bridge.js +0 -240
  570. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -734
  571. package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +0 -101
  572. package/node_modules/pi-lens/dist/clients/observed-mutation.js +0 -1253
  573. package/node_modules/pi-lens/dist/clients/oldtext-autopatch.js +0 -128
  574. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +0 -496
  575. package/node_modules/pi-lens/dist/clients/opengrep-client.js +0 -333
  576. package/node_modules/pi-lens/dist/clients/opengrep-config.js +0 -76
  577. package/node_modules/pi-lens/dist/clients/package-manager.js +0 -607
  578. package/node_modules/pi-lens/dist/clients/package-root.js +0 -38
  579. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +0 -439
  580. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +0 -43
  581. package/node_modules/pi-lens/dist/clients/path-keyed-map.js +0 -108
  582. package/node_modules/pi-lens/dist/clients/path-utils.js +0 -1244
  583. package/node_modules/pi-lens/dist/clients/performance-report.js +0 -331
  584. package/node_modules/pi-lens/dist/clients/persist-debounce.js +0 -70
  585. package/node_modules/pi-lens/dist/clients/persistent-reverify.js +0 -271
  586. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +0 -119
  587. package/node_modules/pi-lens/dist/clients/pipeline.js +0 -1480
  588. package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +0 -37
  589. package/node_modules/pi-lens/dist/clients/print-mode.js +0 -21
  590. package/node_modules/pi-lens/dist/clients/probe-home-state.js +0 -228
  591. package/node_modules/pi-lens/dist/clients/process-bridge.js +0 -66
  592. package/node_modules/pi-lens/dist/clients/process-singletons.js +0 -210
  593. package/node_modules/pi-lens/dist/clients/process-snapshot.js +0 -68
  594. package/node_modules/pi-lens/dist/clients/project-changes.js +0 -181
  595. package/node_modules/pi-lens/dist/clients/project-conventions.js +0 -171
  596. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +0 -137
  597. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +0 -109
  598. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
  599. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
  600. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
  601. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
  602. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
  603. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
  604. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
  605. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
  606. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
  607. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
  608. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
  609. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +0 -520
  610. package/node_modules/pi-lens/dist/clients/project-diagnostics/types.js +0 -1
  611. package/node_modules/pi-lens/dist/clients/project-lens-config.js +0 -807
  612. package/node_modules/pi-lens/dist/clients/project-report.js +0 -701
  613. package/node_modules/pi-lens/dist/clients/project-scale.js +0 -237
  614. package/node_modules/pi-lens/dist/clients/project-scan-policy.js +0 -11
  615. package/node_modules/pi-lens/dist/clients/project-snapshot-fingerprint.js +0 -48
  616. package/node_modules/pi-lens/dist/clients/project-snapshot-persist-worker.js +0 -20
  617. package/node_modules/pi-lens/dist/clients/project-snapshot.js +0 -1653
  618. package/node_modules/pi-lens/dist/clients/project-trust.js +0 -181
  619. package/node_modules/pi-lens/dist/clients/python-environment.js +0 -217
  620. package/node_modules/pi-lens/dist/clients/python-provenance.js +0 -639
  621. package/node_modules/pi-lens/dist/clients/quiet-window-config.js +0 -47
  622. package/node_modules/pi-lens/dist/clients/quiet-window.js +0 -236
  623. package/node_modules/pi-lens/dist/clients/read-bridge.js +0 -139
  624. package/node_modules/pi-lens/dist/clients/read-expansion.js +0 -266
  625. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +0 -251
  626. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +0 -1306
  627. package/node_modules/pi-lens/dist/clients/read-guard.js +0 -1561
  628. package/node_modules/pi-lens/dist/clients/recent-touches.js +0 -240
  629. package/node_modules/pi-lens/dist/clients/redact/secrets.js +0 -449
  630. package/node_modules/pi-lens/dist/clients/resource-sampler.js +0 -628
  631. package/node_modules/pi-lens/dist/clients/reverse-deps.js +0 -270
  632. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +0 -5126
  633. package/node_modules/pi-lens/dist/clients/review-graph/format.js +0 -33
  634. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +0 -163
  635. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +0 -481
  636. package/node_modules/pi-lens/dist/clients/review-graph/persist-worker.js +0 -10
  637. package/node_modules/pi-lens/dist/clients/review-graph/query.js +0 -244
  638. package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +0 -21
  639. package/node_modules/pi-lens/dist/clients/review-graph/service.js +0 -96
  640. package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +0 -82
  641. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +0 -103
  642. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +0 -299
  643. package/node_modules/pi-lens/dist/clients/review-graph/types.js +0 -1
  644. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +0 -449
  645. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +0 -81
  646. package/node_modules/pi-lens/dist/clients/ruff-client.js +0 -179
  647. package/node_modules/pi-lens/dist/clients/rules-scanner.js +0 -131
  648. package/node_modules/pi-lens/dist/clients/run-duration.js +0 -55
  649. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +0 -746
  650. package/node_modules/pi-lens/dist/clients/runtime-config.js +0 -77
  651. package/node_modules/pi-lens/dist/clients/runtime-context.js +0 -211
  652. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +0 -1324
  653. package/node_modules/pi-lens/dist/clients/runtime-session.js +0 -2485
  654. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +0 -1295
  655. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +0 -2049
  656. package/node_modules/pi-lens/dist/clients/runtime-turn.js +0 -3767
  657. package/node_modules/pi-lens/dist/clients/rust-client.js +0 -97
  658. package/node_modules/pi-lens/dist/clients/safe-spawn.js +0 -2357
  659. package/node_modules/pi-lens/dist/clients/sanitize.js +0 -286
  660. package/node_modules/pi-lens/dist/clients/scan-utils.js +0 -25
  661. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +0 -165
  662. package/node_modules/pi-lens/dist/clients/search-read-registration.js +0 -94
  663. package/node_modules/pi-lens/dist/clients/secret-findings.js +0 -114
  664. package/node_modules/pi-lens/dist/clients/security-scan-client.js +0 -352
  665. package/node_modules/pi-lens/dist/clients/session-event-guard.js +0 -297
  666. package/node_modules/pi-lens/dist/clients/session-lifecycle.js +0 -484
  667. package/node_modules/pi-lens/dist/clients/session-start-observability.js +0 -94
  668. package/node_modules/pi-lens/dist/clients/session-state-store.js +0 -101
  669. package/node_modules/pi-lens/dist/clients/session-summary.js +0 -15
  670. package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +0 -27
  671. package/node_modules/pi-lens/dist/clients/sg-runner.js +0 -876
  672. package/node_modules/pi-lens/dist/clients/sgconfig.js +0 -347
  673. package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +0 -338
  674. package/node_modules/pi-lens/dist/clients/single-flight.js +0 -158
  675. package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +0 -108
  676. package/node_modules/pi-lens/dist/clients/skills-resolver.js +0 -141
  677. package/node_modules/pi-lens/dist/clients/slow-fs.js +0 -138
  678. package/node_modules/pi-lens/dist/clients/smells-rollup.js +0 -231
  679. package/node_modules/pi-lens/dist/clients/source-filter.js +0 -596
  680. package/node_modules/pi-lens/dist/clients/source-walker.js +0 -211
  681. package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +0 -120
  682. package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +0 -110
  683. package/node_modules/pi-lens/dist/clients/stale-marker.js +0 -17
  684. package/node_modules/pi-lens/dist/clients/startup-scan.js +0 -400
  685. package/node_modules/pi-lens/dist/clients/startup-timing.js +0 -130
  686. package/node_modules/pi-lens/dist/clients/string-utils.js +0 -33
  687. package/node_modules/pi-lens/dist/clients/subagent-mode.js +0 -111
  688. package/node_modules/pi-lens/dist/clients/symbol-containment.js +0 -24
  689. package/node_modules/pi-lens/dist/clients/symbol-types.js +0 -5
  690. package/node_modules/pi-lens/dist/clients/test-runner-client.js +0 -2507
  691. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +0 -287
  692. package/node_modules/pi-lens/dist/clients/todo-scanner.js +0 -229
  693. package/node_modules/pi-lens/dist/clients/tool-agreement.js +0 -423
  694. package/node_modules/pi-lens/dist/clients/tool-config.js +0 -249
  695. package/node_modules/pi-lens/dist/clients/tool-cwd.js +0 -270
  696. package/node_modules/pi-lens/dist/clients/tool-definition.js +0 -47
  697. package/node_modules/pi-lens/dist/clients/tool-event.js +0 -105
  698. package/node_modules/pi-lens/dist/clients/tool-policy.js +0 -2755
  699. package/node_modules/pi-lens/dist/clients/tool-probe.js +0 -53
  700. package/node_modules/pi-lens/dist/clients/tool-render.js +0 -156
  701. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +0 -118
  702. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +0 -364
  703. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +0 -4195
  704. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +0 -100
  705. package/node_modules/pi-lens/dist/clients/tree-sitter-navigator.js +0 -269
  706. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +0 -569
  707. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +0 -158
  708. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
  709. package/node_modules/pi-lens/dist/clients/trivy-client.js +0 -467
  710. package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -50
  711. package/node_modules/pi-lens/dist/clients/turn-context.js +0 -52
  712. package/node_modules/pi-lens/dist/clients/turn-end/lane.js +0 -51
  713. package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +0 -112
  714. package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +0 -204
  715. package/node_modules/pi-lens/dist/clients/turn-summary-render.js +0 -72
  716. package/node_modules/pi-lens/dist/clients/turn-summary.js +0 -132
  717. package/node_modules/pi-lens/dist/clients/typos-config.js +0 -29
  718. package/node_modules/pi-lens/dist/clients/user-notify.js +0 -51
  719. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +0 -57
  720. package/node_modules/pi-lens/dist/clients/warm-attach.js +0 -305
  721. package/node_modules/pi-lens/dist/clients/widget-state.js +0 -1808
  722. package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -49
  723. package/node_modules/pi-lens/dist/clients/word-index-store.js +0 -459
  724. package/node_modules/pi-lens/dist/clients/word-index.js +0 -2080
  725. package/node_modules/pi-lens/dist/clients/workspace-topology.js +0 -443
  726. package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +0 -65
  727. package/node_modules/pi-lens/dist/clients/zizmor-config.js +0 -340
  728. package/node_modules/pi-lens/dist/i18n.js +0 -69
  729. package/node_modules/pi-lens/dist/mcp/build-staleness.js +0 -124
  730. package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +0 -583
  731. package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +0 -129
  732. package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
  733. package/node_modules/pi-lens/dist/tools/activate-tools.js +0 -87
  734. package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +0 -137
  735. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +0 -244
  736. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +0 -762
  737. package/node_modules/pi-lens/dist/tools/effective-config.js +0 -90
  738. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +0 -375
  739. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +0 -2571
  740. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +0 -1745
  741. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +0 -1367
  742. package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +0 -294
  743. package/node_modules/pi-lens/dist/tools/module-report.js +0 -382
  744. package/node_modules/pi-lens/dist/tools/project-report.js +0 -86
  745. package/node_modules/pi-lens/dist/tools/render-compact.js +0 -374
  746. package/node_modules/pi-lens/dist/tools/scan-progress.js +0 -53
  747. package/node_modules/pi-lens/dist/tools/shared.js +0 -29
  748. package/node_modules/pi-lens/dist/tools/symbol-search.js +0 -155
  749. package/node_modules/pi-web-access/opencode-session-headers.ts +0 -24
  750. package/node_modules/remote-pi/README.md +0 -675
  751. package/node_modules/remote-pi/dist/actions/handlers.d.ts +0 -122
  752. package/node_modules/remote-pi/dist/actions/handlers.js +0 -160
  753. package/node_modules/remote-pi/dist/actions/handlers.js.map +0 -1
  754. package/node_modules/remote-pi/dist/actions/registry.d.ts +0 -39
  755. package/node_modules/remote-pi/dist/actions/registry.js +0 -58
  756. package/node_modules/remote-pi/dist/actions/registry.js.map +0 -1
  757. package/node_modules/remote-pi/dist/bin/supervisord.d.ts +0 -2
  758. package/node_modules/remote-pi/dist/bin/supervisord.js +0 -86
  759. package/node_modules/remote-pi/dist/bin/supervisord.js.map +0 -1
  760. package/node_modules/remote-pi/dist/commands/builtin_mirror.d.ts +0 -58
  761. package/node_modules/remote-pi/dist/commands/builtin_mirror.js +0 -71
  762. package/node_modules/remote-pi/dist/commands/builtin_mirror.js.map +0 -1
  763. package/node_modules/remote-pi/dist/commands/list_commands.d.ts +0 -60
  764. package/node_modules/remote-pi/dist/commands/list_commands.js +0 -73
  765. package/node_modules/remote-pi/dist/commands/list_commands.js.map +0 -1
  766. package/node_modules/remote-pi/dist/config.d.ts +0 -62
  767. package/node_modules/remote-pi/dist/config.js +0 -115
  768. package/node_modules/remote-pi/dist/config.js.map +0 -1
  769. package/node_modules/remote-pi/dist/daemon/client.d.ts +0 -20
  770. package/node_modules/remote-pi/dist/daemon/client.js +0 -131
  771. package/node_modules/remote-pi/dist/daemon/client.js.map +0 -1
  772. package/node_modules/remote-pi/dist/daemon/control_protocol.d.ts +0 -176
  773. package/node_modules/remote-pi/dist/daemon/control_protocol.js +0 -63
  774. package/node_modules/remote-pi/dist/daemon/control_protocol.js.map +0 -1
  775. package/node_modules/remote-pi/dist/daemon/cron_log.d.ts +0 -45
  776. package/node_modules/remote-pi/dist/daemon/cron_log.js +0 -72
  777. package/node_modules/remote-pi/dist/daemon/cron_log.js.map +0 -1
  778. package/node_modules/remote-pi/dist/daemon/cron_registry.d.ts +0 -80
  779. package/node_modules/remote-pi/dist/daemon/cron_registry.js +0 -194
  780. package/node_modules/remote-pi/dist/daemon/cron_registry.js.map +0 -1
  781. package/node_modules/remote-pi/dist/daemon/id.d.ts +0 -24
  782. package/node_modules/remote-pi/dist/daemon/id.js +0 -36
  783. package/node_modules/remote-pi/dist/daemon/id.js.map +0 -1
  784. package/node_modules/remote-pi/dist/daemon/install.d.ts +0 -170
  785. package/node_modules/remote-pi/dist/daemon/install.js +0 -547
  786. package/node_modules/remote-pi/dist/daemon/install.js.map +0 -1
  787. package/node_modules/remote-pi/dist/daemon/registry.d.ts +0 -63
  788. package/node_modules/remote-pi/dist/daemon/registry.js +0 -153
  789. package/node_modules/remote-pi/dist/daemon/registry.js.map +0 -1
  790. package/node_modules/remote-pi/dist/daemon/rpc_child.d.ts +0 -194
  791. package/node_modules/remote-pi/dist/daemon/rpc_child.js +0 -389
  792. package/node_modules/remote-pi/dist/daemon/rpc_child.js.map +0 -1
  793. package/node_modules/remote-pi/dist/daemon/supervisor.d.ts +0 -93
  794. package/node_modules/remote-pi/dist/daemon/supervisor.js +0 -596
  795. package/node_modules/remote-pi/dist/daemon/supervisor.js.map +0 -1
  796. package/node_modules/remote-pi/dist/extension_ui_bridge.d.ts +0 -26
  797. package/node_modules/remote-pi/dist/extension_ui_bridge.js +0 -341
  798. package/node_modules/remote-pi/dist/extension_ui_bridge.js.map +0 -1
  799. package/node_modules/remote-pi/dist/index.d.ts +0 -213
  800. package/node_modules/remote-pi/dist/index.js +0 -4996
  801. package/node_modules/remote-pi/dist/index.js.map +0 -1
  802. package/node_modules/remote-pi/dist/mcp/mesh_result.d.ts +0 -10
  803. package/node_modules/remote-pi/dist/mcp/mesh_result.js +0 -42
  804. package/node_modules/remote-pi/dist/mcp/mesh_result.js.map +0 -1
  805. package/node_modules/remote-pi/dist/mcp/mesh_server.d.ts +0 -16
  806. package/node_modules/remote-pi/dist/mcp/mesh_server.js +0 -327
  807. package/node_modules/remote-pi/dist/mcp/mesh_server.js.map +0 -1
  808. package/node_modules/remote-pi/dist/mesh/canonical.d.ts +0 -30
  809. package/node_modules/remote-pi/dist/mesh/canonical.js +0 -61
  810. package/node_modules/remote-pi/dist/mesh/canonical.js.map +0 -1
  811. package/node_modules/remote-pi/dist/mesh/client.d.ts +0 -18
  812. package/node_modules/remote-pi/dist/mesh/client.js +0 -125
  813. package/node_modules/remote-pi/dist/mesh/client.js.map +0 -1
  814. package/node_modules/remote-pi/dist/mesh/encoding.d.ts +0 -38
  815. package/node_modules/remote-pi/dist/mesh/encoding.js +0 -216
  816. package/node_modules/remote-pi/dist/mesh/encoding.js.map +0 -1
  817. package/node_modules/remote-pi/dist/mesh/self_revoke.d.ts +0 -73
  818. package/node_modules/remote-pi/dist/mesh/self_revoke.js +0 -401
  819. package/node_modules/remote-pi/dist/mesh/self_revoke.js.map +0 -1
  820. package/node_modules/remote-pi/dist/mesh/siblings.d.ts +0 -49
  821. package/node_modules/remote-pi/dist/mesh/siblings.js +0 -159
  822. package/node_modules/remote-pi/dist/mesh/siblings.js.map +0 -1
  823. package/node_modules/remote-pi/dist/mesh/types.d.ts +0 -34
  824. package/node_modules/remote-pi/dist/mesh/types.js +0 -11
  825. package/node_modules/remote-pi/dist/mesh/types.js.map +0 -1
  826. package/node_modules/remote-pi/dist/mesh/verify.d.ts +0 -17
  827. package/node_modules/remote-pi/dist/mesh/verify.js +0 -86
  828. package/node_modules/remote-pi/dist/mesh/verify.js.map +0 -1
  829. package/node_modules/remote-pi/dist/pairing/crypto.d.ts +0 -8
  830. package/node_modules/remote-pi/dist/pairing/crypto.js +0 -22
  831. package/node_modules/remote-pi/dist/pairing/crypto.js.map +0 -1
  832. package/node_modules/remote-pi/dist/pairing/handshake.d.ts +0 -28
  833. package/node_modules/remote-pi/dist/pairing/handshake.js +0 -113
  834. package/node_modules/remote-pi/dist/pairing/handshake.js.map +0 -1
  835. package/node_modules/remote-pi/dist/pairing/noise-sha256.d.ts +0 -16
  836. package/node_modules/remote-pi/dist/pairing/noise-sha256.js +0 -103
  837. package/node_modules/remote-pi/dist/pairing/noise-sha256.js.map +0 -1
  838. package/node_modules/remote-pi/dist/pairing/qr.d.ts +0 -59
  839. package/node_modules/remote-pi/dist/pairing/qr.js +0 -125
  840. package/node_modules/remote-pi/dist/pairing/qr.js.map +0 -1
  841. package/node_modules/remote-pi/dist/pairing/storage.d.ts +0 -120
  842. package/node_modules/remote-pi/dist/pairing/storage.js +0 -604
  843. package/node_modules/remote-pi/dist/pairing/storage.js.map +0 -1
  844. package/node_modules/remote-pi/dist/protocol/codec.d.ts +0 -7
  845. package/node_modules/remote-pi/dist/protocol/codec.js +0 -49
  846. package/node_modules/remote-pi/dist/protocol/codec.js.map +0 -1
  847. package/node_modules/remote-pi/dist/protocol/types.d.ts +0 -393
  848. package/node_modules/remote-pi/dist/protocol/types.js +0 -2
  849. package/node_modules/remote-pi/dist/protocol/types.js.map +0 -1
  850. package/node_modules/remote-pi/dist/rooms.d.ts +0 -29
  851. package/node_modules/remote-pi/dist/rooms.js +0 -57
  852. package/node_modules/remote-pi/dist/rooms.js.map +0 -1
  853. package/node_modules/remote-pi/dist/session/address.d.ts +0 -49
  854. package/node_modules/remote-pi/dist/session/address.js +0 -58
  855. package/node_modules/remote-pi/dist/session/address.js.map +0 -1
  856. package/node_modules/remote-pi/dist/session/agent_bridge.d.ts +0 -55
  857. package/node_modules/remote-pi/dist/session/agent_bridge.js +0 -146
  858. package/node_modules/remote-pi/dist/session/agent_bridge.js.map +0 -1
  859. package/node_modules/remote-pi/dist/session/bridge.d.ts +0 -37
  860. package/node_modules/remote-pi/dist/session/bridge.js +0 -130
  861. package/node_modules/remote-pi/dist/session/bridge.js.map +0 -1
  862. package/node_modules/remote-pi/dist/session/broker.d.ts +0 -189
  863. package/node_modules/remote-pi/dist/session/broker.js +0 -507
  864. package/node_modules/remote-pi/dist/session/broker.js.map +0 -1
  865. package/node_modules/remote-pi/dist/session/broker_remote.d.ts +0 -74
  866. package/node_modules/remote-pi/dist/session/broker_remote.js +0 -583
  867. package/node_modules/remote-pi/dist/session/broker_remote.js.map +0 -1
  868. package/node_modules/remote-pi/dist/session/cwd_lock.d.ts +0 -33
  869. package/node_modules/remote-pi/dist/session/cwd_lock.js +0 -126
  870. package/node_modules/remote-pi/dist/session/cwd_lock.js.map +0 -1
  871. package/node_modules/remote-pi/dist/session/envelope.d.ts +0 -33
  872. package/node_modules/remote-pi/dist/session/envelope.js +0 -117
  873. package/node_modules/remote-pi/dist/session/envelope.js.map +0 -1
  874. package/node_modules/remote-pi/dist/session/global_config.d.ts +0 -33
  875. package/node_modules/remote-pi/dist/session/global_config.js +0 -73
  876. package/node_modules/remote-pi/dist/session/global_config.js.map +0 -1
  877. package/node_modules/remote-pi/dist/session/ipc.d.ts +0 -27
  878. package/node_modules/remote-pi/dist/session/ipc.js +0 -22
  879. package/node_modules/remote-pi/dist/session/ipc.js.map +0 -1
  880. package/node_modules/remote-pi/dist/session/leader_election.d.ts +0 -32
  881. package/node_modules/remote-pi/dist/session/leader_election.js +0 -106
  882. package/node_modules/remote-pi/dist/session/leader_election.js.map +0 -1
  883. package/node_modules/remote-pi/dist/session/local_config.d.ts +0 -55
  884. package/node_modules/remote-pi/dist/session/local_config.js +0 -157
  885. package/node_modules/remote-pi/dist/session/local_config.js.map +0 -1
  886. package/node_modules/remote-pi/dist/session/mesh_message.d.ts +0 -20
  887. package/node_modules/remote-pi/dist/session/mesh_message.js +0 -28
  888. package/node_modules/remote-pi/dist/session/mesh_message.js.map +0 -1
  889. package/node_modules/remote-pi/dist/session/mesh_node.d.ts +0 -116
  890. package/node_modules/remote-pi/dist/session/mesh_node.js +0 -573
  891. package/node_modules/remote-pi/dist/session/mesh_node.js.map +0 -1
  892. package/node_modules/remote-pi/dist/session/peer.d.ts +0 -139
  893. package/node_modules/remote-pi/dist/session/peer.js +0 -451
  894. package/node_modules/remote-pi/dist/session/peer.js.map +0 -1
  895. package/node_modules/remote-pi/dist/session/peer_inventory.d.ts +0 -13
  896. package/node_modules/remote-pi/dist/session/peer_inventory.js +0 -48
  897. package/node_modules/remote-pi/dist/session/peer_inventory.js.map +0 -1
  898. package/node_modules/remote-pi/dist/session/peer_limits.d.ts +0 -9
  899. package/node_modules/remote-pi/dist/session/peer_limits.js +0 -21
  900. package/node_modules/remote-pi/dist/session/peer_limits.js.map +0 -1
  901. package/node_modules/remote-pi/dist/session/setup_wizard.d.ts +0 -39
  902. package/node_modules/remote-pi/dist/session/setup_wizard.js +0 -63
  903. package/node_modules/remote-pi/dist/session/setup_wizard.js.map +0 -1
  904. package/node_modules/remote-pi/dist/session/tool_gate.d.ts +0 -5
  905. package/node_modules/remote-pi/dist/session/tool_gate.js +0 -11
  906. package/node_modules/remote-pi/dist/session/tool_gate.js.map +0 -1
  907. package/node_modules/remote-pi/dist/session/tools.d.ts +0 -24
  908. package/node_modules/remote-pi/dist/session/tools.js +0 -252
  909. package/node_modules/remote-pi/dist/session/tools.js.map +0 -1
  910. package/node_modules/remote-pi/dist/session/wizard.d.ts +0 -13
  911. package/node_modules/remote-pi/dist/session/wizard.js +0 -20
  912. package/node_modules/remote-pi/dist/session/wizard.js.map +0 -1
  913. package/node_modules/remote-pi/dist/settings.d.ts +0 -15
  914. package/node_modules/remote-pi/dist/settings.js +0 -52
  915. package/node_modules/remote-pi/dist/settings.js.map +0 -1
  916. package/node_modules/remote-pi/dist/transport/peer_channel.d.ts +0 -37
  917. package/node_modules/remote-pi/dist/transport/peer_channel.js +0 -98
  918. package/node_modules/remote-pi/dist/transport/peer_channel.js.map +0 -1
  919. package/node_modules/remote-pi/dist/transport/pi_forward_client.d.ts +0 -29
  920. package/node_modules/remote-pi/dist/transport/pi_forward_client.js +0 -62
  921. package/node_modules/remote-pi/dist/transport/pi_forward_client.js.map +0 -1
  922. package/node_modules/remote-pi/dist/transport/relay_client.d.ts +0 -91
  923. package/node_modules/remote-pi/dist/transport/relay_client.js +0 -206
  924. package/node_modules/remote-pi/dist/transport/relay_client.js.map +0 -1
  925. package/node_modules/remote-pi/dist/ui/footer.d.ts +0 -32
  926. package/node_modules/remote-pi/dist/ui/footer.js +0 -34
  927. package/node_modules/remote-pi/dist/ui/footer.js.map +0 -1
  928. package/node_modules/remote-pi/docs/daemon.md +0 -289
  929. package/node_modules/remote-pi/package.json +0 -92
  930. package/node_modules/remote-pi/service-templates/launchd.plist.template +0 -35
  931. package/node_modules/remote-pi/service-templates/systemd.service.template +0 -19
  932. package/node_modules/remote-pi/service-templates/task-launcher.vbs.template +0 -10
  933. package/node_modules/remote-pi/service-templates/task-scheduler.xml.template +0 -38
  934. package/node_modules/remote-pi/skills/agent-network/SKILL.md +0 -310
@@ -1,2357 +0,0 @@
1
- /**
2
- * Safe cross-platform spawn utilities
3
- *
4
- * Provides both sync (deprecated) and async versions for gradual migration.
5
- *
6
- * Async version features:
7
- * - Non-blocking execution
8
- * - Proper process cleanup on timeout (no zombies)
9
- * - Batch execution with concurrency limits
10
- * - AbortSignal support for cancellation
11
- *
12
- * Migration guide:
13
- * - Change: safeSpawn(cmd, args, opts)
14
- * - To: await safeSpawnAsync(cmd, args, opts)
15
- */
16
- import { spawn, spawnSync, } from "node:child_process";
17
- import * as fs from "node:fs";
18
- import * as path from "node:path";
19
- import { BoundedFifoMap, BoundedSet } from "./bounded-cache.js";
20
- import { logLatency } from "./latency-logger.js";
21
- import { incrementDegradationCount, recordDegradation, recordDegradationOnce, } from "./degradation-ledger.js";
22
- import { logExtension } from "./extension-log.js";
23
- import { isFullyQualifiedWin32 } from "./path-utils.js";
24
- import { startSpawnUsageSampler } from "./resource-sampler.js";
25
- import { DEFAULT_MAX_OUTPUT_BYTES } from "./spawn-output-cap.js";
26
- import { compareOrdinal } from "./string-utils.js";
27
- /** Intent-level spawn failure. `cause` retains the original OS Error/errno. */
28
- export class SpawnFailureError extends Error {
29
- kind;
30
- cause;
31
- name = "SpawnFailureError";
32
- constructor(kind, message, cause) {
33
- super(message, { cause });
34
- this.kind = kind;
35
- this.cause = cause;
36
- }
37
- }
38
- export function hasSpawnFailureKind(error, kind) {
39
- return (error instanceof Error &&
40
- "kind" in error &&
41
- error.kind === kind);
42
- }
43
- /**
44
- * #2010: derive teardown evidence from observed timestamps.
45
- *
46
- * Pure by design so every branch is deterministically unit-testable,
47
- * including the natural-exit-at-budget-expiry race where the timeout
48
- * callback arms a kill against an already-dead child.
49
- *
50
- * - No recorded kill start (the guard saw the child already dying), no
51
- * observed death at all, or death PREDATING the kill start: the child
52
- * left on its own -> "exited", ms 0. Claiming a kill we cannot
53
- * distinguish from a natural exit would be invented evidence.
54
- * - Death observed after the kill started: "escalate-kill" when the
55
- * SIGKILL escalation fired, otherwise "killed-by-signal"; ms is the
56
- * observed gap (floor 0 - clock jitter must never produce a negative).
57
- */
58
- export function resolveTeardownOutcome(input) {
59
- // Another path (abort / output-limit) owns this death: emitting teardown
60
- // evidence here would describe a kill this timeout didn't perform.
61
- if (input.killStartedAtMs === undefined ||
62
- input.deathObservedAtMs === undefined) {
63
- return undefined;
64
- }
65
- // True natural-exit race: the child beat the budget-expired kill by a
66
- // hair - death observed before (or exactly at) the kill start.
67
- if (input.deathObservedAtMs <= input.killStartedAtMs) {
68
- return { ms: 0, outcome: "exited" };
69
- }
70
- return {
71
- ms: Math.max(0, input.deathObservedAtMs - input.killStartedAtMs),
72
- outcome: input.escalated ? "escalate-kill" : "killed-by-signal",
73
- };
74
- }
75
- function toError(error) {
76
- return error instanceof Error ? error : new Error(String(error));
77
- }
78
- function errorCode(error) {
79
- return error.code;
80
- }
81
- async function cwdIsUnresolvable(cwd) {
82
- if (cwd === undefined)
83
- return false;
84
- try {
85
- return !(await fs.promises.stat(cwd)).isDirectory();
86
- }
87
- catch {
88
- return true;
89
- }
90
- }
91
- function cwdIsUnresolvableSync(cwd) {
92
- if (cwd === undefined)
93
- return false;
94
- try {
95
- return !fs.statSync(cwd).isDirectory();
96
- }
97
- catch {
98
- return true;
99
- }
100
- }
101
- /**
102
- * Best-effort presence probe used ONLY to disambiguate ENOENT when the cwd is
103
- * ALSO unresolvable (#1340 review): a genuinely missing tool must classify as
104
- * tool-not-found even under a broken cwd, or auto-install can never repair it.
105
- * Absolute commands are probed directly (with PATHEXT variants on Windows);
106
- * bare names scan PATH. A relative-with-separator command under a broken cwd
107
- * is genuinely ambiguous -- we err toward cwd-unresolvable there, because
108
- * repairing the cwd is actionable while a reinstall loop (#1199) is not.
109
- */
110
- function commandProbablyPresent(command) {
111
- const exts = process.platform === "win32"
112
- ? [
113
- "",
114
- ...(process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
115
- .split(";")
116
- .filter(Boolean),
117
- ]
118
- : [""];
119
- const existsWithExt = (base) => {
120
- for (const ext of exts) {
121
- try {
122
- if (fs.existsSync(base + ext))
123
- return true;
124
- }
125
- catch {
126
- // unreadable candidate -- keep probing
127
- }
128
- }
129
- return false;
130
- };
131
- if (path.isAbsolute(command))
132
- return existsWithExt(command);
133
- if (command.includes("/") || command.includes("\\"))
134
- return "ambiguous";
135
- for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
136
- if (dir && existsWithExt(path.join(dir, command)))
137
- return true;
138
- }
139
- return false;
140
- }
141
- /**
142
- * Shared errno->bucket mapping. `cwdUnresolvable` is the only async-vs-sync
143
- * difference between the two public classifiers, so it arrives as a resolved
144
- * flag and everything else lives once (Sonar duplication finding, #1340).
145
- */
146
- function classifyWithCwdFlag(cause, options, cwdUnresolvable) {
147
- const code = errorCode(cause);
148
- let failure;
149
- if (code === "ENOENT" &&
150
- cwdUnresolvable &&
151
- commandProbablyPresent(options.command) !== false) {
152
- failure = new SpawnFailureError("cwd-unresolvable", `Cannot spawn ${options.command}: working directory is unresolvable (${options.cwd})`, cause);
153
- }
154
- else if (code === "ENOENT") {
155
- failure = new SpawnFailureError("tool-not-found", `Cannot spawn ${options.command}: tool not found (${cause.message})`, cause);
156
- }
157
- else if (code === "EACCES" || code === "EPERM") {
158
- failure = new SpawnFailureError("permission-denied", `Cannot spawn ${options.command}: permission denied`, cause);
159
- }
160
- else {
161
- failure = new SpawnFailureError("spawn-failed", `Cannot spawn ${options.command}: ${cause.message}`, cause);
162
- }
163
- recordSpawnClassification(failure, options);
164
- return failure;
165
- }
166
- const loggedSpawnClassifications = new Set();
167
- const SPAWN_CLASSIFICATION_LOG_CAP = 200;
168
- function recordSpawnClassification(failure, options) {
169
- const pair = `${failure.kind}\0${options.command}`;
170
- if (!loggedSpawnClassifications.has(pair)) {
171
- if (loggedSpawnClassifications.size >= SPAWN_CLASSIFICATION_LOG_CAP) {
172
- loggedSpawnClassifications.clear();
173
- }
174
- loggedSpawnClassifications.add(pair);
175
- logExtension({
176
- subsystem: "safe-spawn",
177
- level: "debug",
178
- message: "spawn failure classified",
179
- metadata: {
180
- kind: failure.kind,
181
- command: options.command,
182
- cwd: options.cwd,
183
- },
184
- });
185
- }
186
- if (failure.kind !== "tool-not-found") {
187
- recordDegradation({
188
- kind: "spawn-failure",
189
- subject: options.command,
190
- reason: `${failure.kind}${options.cwd ? ` in ${options.cwd}` : ""}`,
191
- });
192
- }
193
- }
194
- /** Classify a raw Node spawn error without discarding its errno-bearing Error. */
195
- export async function classifySpawnFailure(error, options) {
196
- const cause = toError(error);
197
- const needsCwdProbe = errorCode(cause) === "ENOENT";
198
- const cwdUnresolvable = needsCwdProbe && (await cwdIsUnresolvable(options.cwd));
199
- return classifyWithCwdFlag(cause, options, cwdUnresolvable);
200
- }
201
- function classifySpawnFailureSync(error, options) {
202
- const cause = toError(error);
203
- const needsCwdProbe = errorCode(cause) === "ENOENT";
204
- const cwdUnresolvable = needsCwdProbe && cwdIsUnresolvableSync(options.cwd);
205
- return classifyWithCwdFlag(cause, options, cwdUnresolvable);
206
- }
207
- // Vitest reloads modules inside a reused worker. Keep this registry on the
208
- // process so those module instances share one signal/exit listener set.
209
- const lifetimeStateKey = Symbol.for("pi-lens.safe-spawn.lifetime-state");
210
- const processWithLifetimeState = process;
211
- const lifetimeState = processWithLifetimeState[lifetimeStateKey] ??
212
- (processWithLifetimeState[lifetimeStateKey] = {
213
- pids: new Set(),
214
- installed: false,
215
- });
216
- /**
217
- * THE ownership predicate for every kill-by-raw-pid in this repo (#2042).
218
- *
219
- * A pid is signalable only when it is a LIVE DIRECT CHILD of this process.
220
- * `tests/clients/lsp/launch.test.ts` mocked `node:child_process` with a fake
221
- * child carrying the literal pid `2468`; `safeSpawnAsync` registered it in
222
- * `lifetimeState.pids`, the fake never emitted `close` so nothing removed it,
223
- * and at fork teardown `installLifetimeCleanup()` fired
224
- * `process.kill(-2468, "SIGKILL")`. On ~10 % of GitHub runners pid 2468 was
225
- * one of the job's own long-lived processes — that is the whole of #2042:
226
- * exit 137, no failing assertion, no kernel record, five weeks of "infra
227
- * kill" reruns. A `pid <= 0` guard would not have stopped it; 2468 is a
228
- * perfectly plausible pid. The defect is OWNERSHIP, not sign.
229
- *
230
- * Three guards fold into this one (net-count rule): `lsp/client.ts`'s
231
- * `pid <= 0`, `lsp/launch.ts`'s `handle.pid <= 0` plus its already-exited
232
- * recycled-pid check, and `killTree`'s `child.pid > 0` below. It closes
233
- * fabricated, stale and recycled pids together.
234
- *
235
- * NOT folded: `instance-reaper.ts#killPidTree`. Its pids come from OTHER
236
- * pi-lens instances' registries and from an OS scan of managed binaries with
237
- * confirmed-dead parents — deliberately NOT our children. Its `!isFinite ||
238
- * pid <= 0` guard is well-formedness on a different axis, and applying this
239
- * predicate there would turn the orphan reaper into a no-op.
240
- *
241
- * Platform behaviour, stated rather than implied:
242
- * - Linux (the authoritative lane, and the one that lost five weeks):
243
- * `/proc/<pid>/status` is the kernel's own answer. Missing entry = the pid
244
- * does not exist, so there is nothing of ours to kill; `PPid` other than
245
- * ours = someone else's process, which is recorded and never signalled.
246
- * - Windows and macOS have no `/proc` (and neither does a Linux host with
247
- * `/proc` unmounted — probed once, not assumed from `process.platform`),
248
- * so ownership is unverifiable from a raw pid and the predicate keeps
249
- * today's best-effort behaviour. The `handle` arm applies THERE and only
250
- * there: it is the only ownership evidence Windows has, and it is what
251
- * `killWindowsTree` already used. It must not gate the POSIX group kill,
252
- * which legitimately runs after its leader has died (#3091 F1).
253
- */
254
- /**
255
- * Whether `/proc/<pid>/status` can actually be read here, probed ONCE against
256
- * this process's own entry (#3091 F4). `process.platform === "linux"` is not
257
- * the same question: a container or a hardened host can run Linux with no
258
- * `/proc` mounted, and there the per-pid read fails exactly the way a dead pid
259
- * does. Without this probe that silently refused every registration, so
260
- * `lifetimeState.pids` stayed empty and the host-exit tree kill stopped working
261
- * with no record anywhere.
262
- */
263
- const PROC_PPID_READABLE = (() => {
264
- if (process.platform !== "linux")
265
- return false;
266
- try {
267
- return /^PPid:\s*\d+$/m.test(fs.readFileSync("/proc/self/status", "utf8"));
268
- }
269
- catch {
270
- return false;
271
- }
272
- })();
273
- /**
274
- * Pids this process has PROVEN, from the kernel, to be its own live children.
275
- * FIFO-bounded (`BoundedFifoMap`, not a wholesale clear): a process that spawns
276
- * more than the cap drops its OLDEST verdicts, where a wholesale clear would
277
- * drop the one it is about to need.
278
- */
279
- export const VERIFIED_OWN_PID_CAP = 512;
280
- const verifiedOwnPids = new BoundedFifoMap(VERIFIED_OWN_PID_CAP);
281
- /**
282
- * Pids whose OWNERSHIP IS HELD for the lifetime of a long-lived resource —
283
- * today, the LSP children `clients/lsp/launch.ts` spawns with `nodeSpawn`
284
- * (#3091 F1-r2b).
285
- *
286
- * These deliberately do NOT live in `verifiedOwnPids`. Age is the wrong
287
- * retirement axis for a resource-scoped verdict: an LSP leader is verified once
288
- * at spawn and then sits untouched for the whole session, so in a FIFO that
289
- * every `safeSpawnAsync` writes to it is the OLDEST entry and therefore the
290
- * FIRST evicted — measured, 520 later verdicts were enough — and its group kill
291
- * at host exit is refused precisely because the server was long-lived enough to
292
- * matter. Retirement here is by RESOURCE STATE instead: `releaseOwnChildPid`
293
- * when the shutdown ladder is done with the pid, plus the confirmed-dead sweep
294
- * below for the paths that never reach a ladder (a server that crashes, or a
295
- * host killed mid-session).
296
- *
297
- * The bound that remains, stated rather than implied: at most
298
- * `HELD_OWN_PID_SWEEP_AT` entries — the confirmed-dead sweep below runs before
299
- * every insert, and `BoundedSet`'s own FIFO overflow is the last resort for the
300
- * case where every held group really is still alive. A crash-looping server cannot grow it without bound
301
- * because each sweep drops every pid whose process GROUP no longer exists,
302
- * which is exactly the condition under which the entry can never be needed
303
- * again.
304
- */
305
- const HELD_OWN_PID_SWEEP_AT = 256;
306
- const heldOwnPids = new BoundedSet(HELD_OWN_PID_SWEEP_AT);
307
- /** A process group that no longer exists can never need another signal. */
308
- function groupIsGone(pid) {
309
- try {
310
- process.kill(-pid, 0);
311
- return false;
312
- }
313
- catch (error) {
314
- return errorCode(error) === "ESRCH";
315
- }
316
- }
317
- function sweepHeldOwnPids() {
318
- if (heldOwnPids.size < HELD_OWN_PID_SWEEP_AT)
319
- return;
320
- // Deleting the current entry mid-iteration is well-defined for a Set, so
321
- // no snapshot copy is needed.
322
- for (const pid of heldOwnPids) {
323
- if (groupIsGone(pid))
324
- heldOwnPids.delete(pid);
325
- }
326
- // No hand-rolled "drop the oldest" block (#2442 / #3091 round 4): the
327
- // last-resort eviction, for the case where every held group is still alive
328
- // at the ceiling, is `BoundedSet.add`'s own FIFO overflow.
329
- }
330
- /**
331
- * Verify a pid at ADMISSION and hold the verdict for the resource's lifetime.
332
- *
333
- * This is AGENTS.md shape 50's own rule, which #3091 round 1 broke at the site
334
- * it was fixing: `isOwnLiveChild`'s three call sites were the `safeSpawnAsync`
335
- * registration, the Windows tree kill, and the SHUTDOWN group kill — so for an
336
- * LSP child, spawned with `nodeSpawn`, the first offer of verification was also
337
- * the last, and on the `processExiting` path the leader could already be dead
338
- * by then (`/proc` gone, nothing on record, signal refused, grandchildren
339
- * orphaned).
340
- */
341
- export function holdOwnChildPid(pid, site) {
342
- const owned = isOwnLiveChild(pid, site);
343
- if (!owned || !PROC_PPID_READABLE || typeof pid !== "number")
344
- return owned;
345
- // Exactly one home: the verification above filed the verdict in the
346
- // age-bounded FIFO, and leaving a copy there would make BOTH the held set
347
- // and its release mutation-inert — the FIFO copy would answer for them.
348
- verifiedOwnPids.delete(pid);
349
- sweepHeldOwnPids();
350
- heldOwnPids.add(pid);
351
- return true;
352
- }
353
- /**
354
- * Retire a held pid once nothing will signal it again — the end of the
355
- * shutdown ladder. Idempotent.
356
- */
357
- export function releaseOwnChildPid(pid) {
358
- if (typeof pid === "number")
359
- heldOwnPids.delete(pid);
360
- }
361
- /** Once per session: a Linux host whose `/proc` cannot answer the question. */
362
- function recordProcUnavailable(site) {
363
- if (process.platform !== "linux")
364
- return;
365
- recordDegradationOnce({
366
- kind: "kill-ownership-unverifiable",
367
- subject: site,
368
- reason: "/proc/self/status is unreadable on this Linux host; kill-by-pid ownership falls back to best-effort",
369
- });
370
- }
371
- export function isOwnLiveChild(pid, site, handle) {
372
- if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
373
- return false;
374
- if (!PROC_PPID_READABLE) {
375
- // No `/proc` to consult. A handle that has already reported exit is then
376
- // the ONLY ownership evidence available, and it is exactly what
377
- // `killWindowsTree` used before this predicate existed: a dead pid may
378
- // have been recycled, and `taskkill /F /T` on a recycled pid destroys an
379
- // unrelated tree. It overrides the memo below deliberately.
380
- if (handle && (handle.exitCode != null || handle.signalCode != null))
381
- return false;
382
- recordProcUnavailable(site);
383
- return true;
384
- }
385
- let status;
386
- try {
387
- status = fs.readFileSync(`/proc/${pid}/status`, "utf8");
388
- }
389
- catch {
390
- // The pid does not exist. Not a degradation — a child that exited before
391
- // the teardown signal is the ordinary case — but not automatically a
392
- // refusal either: a POSIX process GROUP outlives its leader, and #2026's
393
- // whole point is that the escalation must still reach a SIGTERM-hardy
394
- // grandchild after the direct child has died. Ownership verified while
395
- // the leader was alive is what answers that; a pid this process never
396
- // owned is still refused.
397
- return heldOwnPids.has(pid) || verifiedOwnPids.has(pid);
398
- }
399
- const parent = /^PPid:\s*(\d+)$/m.exec(status);
400
- if (!parent)
401
- return true;
402
- if (Number(parent[1]) === process.pid) {
403
- // Memoized only on a KERNEL-verified verdict, and only here: a pid we
404
- // never proved was ours can never enter the memo, and a pid that is
405
- // alive under a different parent is refused above regardless of what the
406
- // memo holds — so a recycled pid cannot be signalled on the strength of
407
- // its previous owner.
408
- //
409
- // Never for a pid already HELD, though: the shutdown ladder verifies a
410
- // still-live leader on its way to the group signal, and filing that
411
- // verdict in the FIFO too would put a held pid back in both stores —
412
- // which outlives `releaseOwnChildPid` (the FIFO copy would keep
413
- // answering for a pid this process has explicitly stopped claiming) and
414
- // makes the held store's own tests unable to fail.
415
- if (!heldOwnPids.has(pid))
416
- verifiedOwnPids.set(pid, true);
417
- return true;
418
- }
419
- // Alive AND someone else's: the dangerous case, and the only one worth a
420
- // record. Subject is the SITE (a fixed, tiny set), never the pid, so the
421
- // ledger stays bounded however often it fires.
422
- incrementDegradationCount({
423
- kind: "kill-foreign-pid-refused",
424
- subject: site,
425
- reason: `pid ${pid} has parent ${parent[1]}, not this process — signal refused`,
426
- });
427
- return false;
428
- }
429
- /**
430
- * Kill one registered pid's tree at host exit. Deliberately NOT re-checked
431
- * against `isOwnLiveChild` here: `lifetimeState.pids` has exactly one writer
432
- * (the registration below), that writer is now gated, and a second gate on
433
- * the same feeder would make BOTH mutation-inert — remove either one and the
434
- * other still blocks the kill, so neither would have a test that reds. The
435
- * gate lives where the pid enters, which is also where ownership is a fact
436
- * rather than a stale reading (#2042).
437
- */
438
- function killPidTreeSync(pid) {
439
- if (process.platform === "win32") {
440
- try {
441
- const taskkill = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\taskkill.exe`;
442
- spawnSync(taskkill, ["/F", "/T", "/PID", String(pid)], {
443
- shell: false,
444
- windowsHide: true,
445
- stdio: "ignore",
446
- });
447
- }
448
- catch {
449
- // Runs from process `exit`/signal handlers — a SYNCHRONOUS spawn throw
450
- // (Windows `spawn UNKNOWN`/EINVAL, the pidusage bug class, #533) would
451
- // become an uncaughtException during shutdown. Best-effort tree-kill:
452
- // swallow it, mirroring the already-guarded POSIX branch below.
453
- }
454
- return;
455
- }
456
- try {
457
- // #2026: negative pid = whole process group (POSIX children spawn
458
- // detached), reaching grandchildren too. Any failure (ESRCH = already
459
- // gone, EPERM = raced) falls through to the direct-child attempt.
460
- try {
461
- process.kill(-pid, "SIGKILL");
462
- }
463
- catch {
464
- process.kill(pid, "SIGKILL");
465
- }
466
- }
467
- catch {
468
- // Child already exited.
469
- }
470
- }
471
- function installLifetimeCleanup() {
472
- if (lifetimeState.installed)
473
- return;
474
- lifetimeState.installed = true;
475
- process.once("exit", () => {
476
- for (const pid of lifetimeState.pids)
477
- killPidTreeSync(pid);
478
- });
479
- for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
480
- process.once(signal, () => {
481
- for (const pid of lifetimeState.pids)
482
- killPidTreeSync(pid);
483
- if (process.platform === "win32" && signal === "SIGHUP") {
484
- // Windows emits SIGHUP when its console closes, but libuv cannot
485
- // self-send it (process.kill(..., "SIGHUP") throws ENOSYS). The
486
- // child cleanup above remains the required shutdown action; record
487
- // the intentional decline synchronously and let the host exit cleanly.
488
- recordDegradationOnce({
489
- kind: "safe-spawn-signal-reraise-unsupported",
490
- subject: `${process.platform}:${signal}`,
491
- reason: `${process.platform} ${signal}: self signal re-raise is unsupported by the Windows libuv signal capability`,
492
- metadata: {
493
- platform: process.platform,
494
- signal,
495
- reason: "unsupported",
496
- },
497
- });
498
- return;
499
- }
500
- try {
501
- process.kill(process.pid, signal);
502
- }
503
- catch (error) {
504
- // #3383: this runs inside a `process.once(signal)` handler, so a
505
- // throw here is an uncaughtException DURING SHUTDOWN, not a
506
- // rejection anyone can see. The one failure measured in the field
507
- // (Windows SIGHUP ENOSYS) is declined in advance just above; this
508
- // covers every other platform/signal/errno pair the OS can refuse —
509
- // same ledger kind, `reason: "refused"` instead of "unsupported",
510
- // because a reader of either row wants the same fact: the host
511
- // exited without the signal's default disposition. The child
512
- // cleanup above has already run either way.
513
- recordDegradationOnce({
514
- kind: "safe-spawn-signal-reraise-unsupported",
515
- subject: `${process.platform}:${signal}`,
516
- reason: `${process.platform} ${signal}: self signal re-raise was refused (${error.code ?? error.message})`,
517
- metadata: {
518
- platform: process.platform,
519
- signal,
520
- reason: "refused",
521
- code: error.code ?? null,
522
- },
523
- });
524
- }
525
- });
526
- }
527
- }
528
- // ============================================================================
529
- // AMBIENT TURN ABORT SIGNAL
530
- // ============================================================================
531
- /**
532
- * The current turn's abort signal, published by the lifecycle handlers from
533
- * pi's `ctx.signal`. Threading the signal explicitly through every
534
- * dispatch → runner → spawn call site would be invasive, so instead
535
- * `safeSpawnAsync` defaults to this ambient signal when a call doesn't pass its
536
- * own. The effect: pressing Esc mid-turn aborts in-flight linter / formatter /
537
- * type-checker child processes (process-tree kill on Windows) instead of letting
538
- * them run to their timeout.
539
- *
540
- * Each spawn captures the signal at call time (attaching its own abort listener),
541
- * so clearing this after a handler returns only affects *future* spawns — work
542
- * already in flight keeps the signal it started with.
543
- */
544
- let ambientAbortSignal;
545
- /** Publish (or clear, with `undefined`) the current turn's abort signal. */
546
- export function setAmbientAbortSignal(signal) {
547
- ambientAbortSignal = signal;
548
- }
549
- /**
550
- * The current turn's abort signal, for in-process awaits that aren't child
551
- * spawns (e.g. an LSP JSON-RPC write that can backpressure on a wedged server).
552
- * Child spawns read the ambient signal internally; this getter lets the
553
- * interactive pipeline honor Escape on non-spawn LSP calls too.
554
- */
555
- export function getAmbientAbortSignal() {
556
- return ambientAbortSignal;
557
- }
558
- // ============================================================================
559
- // INTERNAL HELPERS
560
- // ============================================================================
561
- /**
562
- * Escape a single argument for the Windows command interpreter.
563
- * The result is embedded in the `/c` command string below, so metacharacters
564
- * must remain inside a quoted argument rather than becoming command syntax.
565
- */
566
- function cmdEscapeArg(arg) {
567
- if (!/[\s"&|<>^()]/.test(arg))
568
- return arg;
569
- return `"${arg.replace(/"/g, '""')}"`;
570
- }
571
- /**
572
- * Build the cmd.exe command string used for Windows wrapper spawning.
573
- *
574
- * The COMMAND must be escaped the same way as the args — escaping only the args
575
- * (the bug behind #214) means a tool whose resolved path contains a space (e.g.
576
- * `C:\Program Files\Go\bin\go.exe`) makes cmd.exe parse `C:\Program` as the
577
- * command and fail with "'C:\Program' is not recognized". `cmdEscapeArg` is a
578
- * no-op for space-free commands, so this is safe for the npm/.pi-lens tool paths
579
- * that already worked. The `chcp 65001` prefix forces the UTF-8 code page (so
580
- * tool output isn't mangled by the system code page) and, as a side benefit,
581
- * keeps the (possibly quoted) command off the front of the line, avoiding
582
- * cmd.exe's `/s` outer-quote-stripping quirk. #2023: chcp is invoked via its
583
- * absolute `%SystemRoot%\System32\chcp.com` path and chained with `&`, not
584
- * `&&` — a child environment whose PATH cannot resolve System32 used to fail
585
- * the bare `chcp` lookup, and `&&` then short-circuited EVERY .cmd/.bat spawn
586
- * into exit 1 with empty output. With the pin plus `&`, even a chcp failure
587
- * can never suppress the real command.
588
- */
589
- export function buildWindowsShellCommand(command, args) {
590
- const chcp = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\chcp.com`;
591
- return `${chcp} 65001 >nul 2>&1 & ${[command, ...args].map(cmdEscapeArg).join(" ")}`;
592
- }
593
- /**
594
- * The extensions this resolver treats as executable suffixes on their own,
595
- * independent of PATHEXT: `.exe`/`.com` and `.cmd`/`.bat`. An explicit suffix
596
- * outside this set is not treated as a "known executable extension" unless
597
- * the caller's own PATHEXT says so (#1201).
598
- *
599
- * This is a RESOLUTION set, not a dispatch whitelist — do not read it as "the
600
- * only extensions this module can spawn". Dispatch in
601
- * `safeSpawnAsync`/`safeSpawn` below routes `.cmd`/`.bat` through the cmd.exe
602
- * wrapper and EVERYTHING ELSE to direct spawn, so a resolved path with any
603
- * other suffix (reachable only when the caller's own PATHEXT lists it) is
604
- * direct-spawned. That is deliberate and not a widening: CreateProcess does no
605
- * ShellExecute file-association lookup, the default Windows PATHEXT already
606
- * contains `.VBS`/`.JS`/`.WSF`/`.MSC`, and the set here is strictly narrower
607
- * than the pre-#1201 behavior. Only the doc claim was ever wrong.
608
- */
609
- const WINDOWS_DIRECT_RESOLVABLE_EXTS = new Set([
610
- ".exe",
611
- ".com",
612
- ".cmd",
613
- ".bat",
614
- ]);
615
- /**
616
- * Merge a parent and child environment using Windows' case-insensitive variable
617
- * names. Node's JavaScript environment object can contain both `PATH` and
618
- * `Path`, even though Windows treats them as one variable. Removing an older
619
- * spelling before each assignment gives explicit child overrides precedence
620
- * over every ambient spelling and ensures the environment passed to spawn has
621
- * one unambiguous value.
622
- */
623
- export function mergeWindowsEnvironment(base, overrides) {
624
- const entries = new Map();
625
- const assign = (source) => {
626
- if (!source)
627
- return;
628
- for (const [key, value] of Object.entries(source)) {
629
- const folded = key.toLowerCase();
630
- // Delete before setting so the last explicit spelling wins while the
631
- // folded map remains O(n) instead of scanning the merged object for
632
- // every environment entry.
633
- entries.delete(folded);
634
- if (value !== undefined)
635
- entries.set(folded, { key, value });
636
- }
637
- };
638
- assign(base);
639
- assign(overrides);
640
- const merged = {};
641
- for (const { key, value } of entries.values())
642
- merged[key] = value;
643
- return merged;
644
- }
645
- function getWindowsEnvironmentValue(env, name) {
646
- let value;
647
- for (const [key, entry] of Object.entries(env)) {
648
- if (key.toLowerCase() === name.toLowerCase())
649
- value = entry;
650
- }
651
- return value;
652
- }
653
- function driveLetter(value) {
654
- const match = /^([A-Za-z]):/.exec(value);
655
- return match?.[1]?.toUpperCase();
656
- }
657
- function isDriveAbsolute(value, drive) {
658
- return (value.length >= 3 &&
659
- value[0]?.toUpperCase() === drive.toUpperCase() &&
660
- value[1] === ":" &&
661
- (value[2] === "\\" || value[2] === "/"));
662
- }
663
- /** `X:\...` or `X:/...` — a drive-absolute path, for any drive letter. */
664
- function isDriveAbsoluteAnyDrive(value) {
665
- const drive = driveLetter(value);
666
- return drive !== undefined && isDriveAbsolute(value, drive);
667
- }
668
- /** `\\server\share\...` — a UNC path. Two leading separators, not more. */
669
- function isUncWindowsPath(value) {
670
- return ((value[0] === "\\" || value[0] === "/") &&
671
- (value[1] === "\\" || value[1] === "/") &&
672
- value[2] !== "\\" &&
673
- value[2] !== "/");
674
- }
675
- /**
676
- * A path is "fully qualified" (self-contained, no ambient current-directory
677
- * lookup required) only when it is drive-absolute (`X:\...`) or UNC
678
- * (`\\server\share`). `path.win32.isAbsolute("\tools")` also returns `true`
679
- * for a *rooted* path — one relative to the current drive's root rather than
680
- * a specific drive — and treating that as fully qualified was the #1201
681
- * bug: it got statSync'd against the *host* drive while a different
682
- * `effectiveCwd`/`resolvedDrive` supplied the drive cmd.exe actually used to
683
- * execute it, so a different file could be validated than executed.
684
- */
685
- function isFullyQualifiedWindowsPath(value) {
686
- return isFullyQualifiedWin32(value) && !isRootedWindowsPath(value);
687
- }
688
- /**
689
- * `\tools` or `/tools` — rooted at the current drive's root, but naming no
690
- * drive of its own (distinct from both `X:\tools` fully-qualified and
691
- * `X:tools` drive-relative-to-current-directory). Windows resolves this
692
- * against the root of whichever drive is "current", not the current
693
- * directory on that drive.
694
- */
695
- function isRootedWindowsPath(value) {
696
- return ((value[0] === "\\" || value[0] === "/") &&
697
- driveLetter(value) === undefined &&
698
- !isUncWindowsPath(value));
699
- }
700
- /**
701
- * Resolve a rooted path (`\tools`) against the ROOT of `driveSource`'s drive
702
- * — not `driveSource` itself — matching Windows' own rooted-path semantics.
703
- *
704
- * When `driveSource` carries NO drive letter, this returns the normalized
705
- * value rather than `undefined` (#1201, recurring defect shape 2). A
706
- * drive-less "current drive" provenance means we are not on a drive-lettered
707
- * filesystem at all — a POSIX host, or a UNC `process.cwd()` such as
708
- * `\\server\share\...`, which Node permits on real Windows. In that namespace
709
- * a rooted path is already as qualified as any path can be, so there is
710
- * nothing to anchor and normalizing is the correct (and pre-#1201) answer.
711
- * Returning `undefined` instead used to collapse the whole resolution to
712
- * `null` without a single `statSync`, i.e. a false ENOENT — the exact failure
713
- * signature #1199 exists to remove. This function must therefore never fail
714
- * closed on the *shape* of a value that may not be a Windows path at all;
715
- * the filesystem probe downstream is what decides whether it exists.
716
- */
717
- function resolveRootedWindowsPath(value, driveSource) {
718
- const drive = driveLetter(driveSource);
719
- if (drive === undefined)
720
- return path.win32.normalize(value);
721
- return path.win32.normalize(path.win32.resolve(`${drive}:\\`, value));
722
- }
723
- /**
724
- * Return a validated Windows per-drive current directory. Windows exposes
725
- * these as environment entries such as `=D:`; unlike ordinary environment
726
- * variables they are not safe to synthesize from a drive letter. A malformed
727
- * or wrong-drive value is deliberately ignored so drive-relative resolution
728
- * fails closed instead of guessing a root.
729
- *
730
- * Node never surfaces `=X:` keys from the ambient `process.env` (verified:
731
- * `Object.keys(process.env).filter(k => k.startsWith("="))` is empty), so
732
- * this provenance is only ever available when a caller supplies it
733
- * explicitly in an `env` override — e.g. a test, or a future integration
734
- * that reads it from a lower-level Windows API. That's a real, if narrow,
735
- * use: keep this path rather than deleting it, but never synthesize a value
736
- * Node itself can't hand us.
737
- */
738
- function getValidatedPerDriveCwd(env, drive) {
739
- const value = getWindowsEnvironmentValue(env, `=${drive}:`);
740
- if (value === undefined || !isDriveAbsolute(value, drive))
741
- return undefined;
742
- return path.win32.normalize(value);
743
- }
744
- function resolveEffectiveWindowsCwd(cwd, env) {
745
- if (cwd === undefined) {
746
- // No caller-supplied cwd: `process.cwd()` IS the effective cwd already —
747
- // the real, canonical filesystem cwd of THIS process, not a string that
748
- // needs (re)classifying. On a POSIX CI host it naturally carries no
749
- // drive letter; that's fine, it never needs one here. Only a
750
- // CALLER-SUPPLIED cwd string goes through the fully-qualified/rooted/
751
- // drive-relative classification below — running `process.cwd()`'s own
752
- // value through `isRootedWindowsPath` was itself a #1201 regression: a
753
- // drive-less POSIX path (e.g. Linux CI's real cwd) satisfies that
754
- // predicate too (single leading separator, no drive letter), which then
755
- // sent it to `resolveRootedWindowsPath` looking for a drive letter on
756
- // `process.cwd()` that — being the very same drive-less string — could
757
- // never supply one, resolving to `undefined` (recurring defect shape
758
- // 2/7: `path.win32.resolve` falls back to `process.cwd()`, which
759
- // supplies a drive on Windows and none on Linux).
760
- return path.win32.normalize(process.cwd());
761
- }
762
- const requestedDrive = driveLetter(cwd);
763
- if (requestedDrive !== undefined && !isDriveAbsoluteAnyDrive(cwd)) {
764
- // `D:foo` — drive-relative to that drive's own current directory.
765
- const base = getValidatedPerDriveCwd(env, requestedDrive);
766
- return base === undefined
767
- ? undefined
768
- : path.win32.normalize(path.win32.resolve(base, cwd));
769
- }
770
- if (isFullyQualifiedWindowsPath(cwd))
771
- return path.win32.normalize(cwd);
772
- if (isRootedWindowsPath(cwd)) {
773
- // `\tools` — rooted at the CURRENT drive's root.
774
- //
775
- // Note the deliberate asymmetry with `resolveWindowsPathEntry`, which
776
- // anchors rooted PATH entries to `effectiveCwd`'s drive instead. Both
777
- // use the SAME rule — "anchor to the best available "current drive"
778
- // provenance at this point in the pipeline" — they just sit at
779
- // different points. Here we are *computing* `effectiveCwd`, so it does
780
- // not exist yet and the process's own cwd is the only provenance
781
- // available (on real Windows it always carries a drive letter). By the
782
- // time a PATH entry is resolved, `effectiveCwd` is known and is the
783
- // strictly better answer, because it is the drive the child will
784
- // actually run from — anchoring PATH entries to the host drive there
785
- // would validate one file and execute another. Neither call can fail
786
- // closed on a drive-less provenance; see `resolveRootedWindowsPath`.
787
- return resolveRootedWindowsPath(cwd, process.cwd());
788
- }
789
- return path.win32.normalize(path.win32.resolve(process.cwd(), cwd));
790
- }
791
- function resolveDriveRelativeWindowsPath(value, effectiveCwd, env) {
792
- const drive = driveLetter(value);
793
- if (drive === undefined)
794
- return undefined;
795
- const cwdDrive = effectiveCwd === undefined ? undefined : driveLetter(effectiveCwd);
796
- const base = cwdDrive?.toUpperCase() === drive
797
- ? effectiveCwd
798
- : getValidatedPerDriveCwd(env, drive);
799
- if (base === undefined)
800
- return undefined;
801
- return path.win32.normalize(path.win32.resolve(base, value));
802
- }
803
- function resolveWindowsPathEntry(entry, effectiveCwd, env) {
804
- if (driveLetter(entry) !== undefined && !isDriveAbsoluteAnyDrive(entry)) {
805
- return resolveDriveRelativeWindowsPath(entry, effectiveCwd, env);
806
- }
807
- if (isFullyQualifiedWindowsPath(entry))
808
- return path.win32.normalize(entry);
809
- if (isRootedWindowsPath(entry)) {
810
- // `\tools` — rooted at the effective child cwd's drive root, not the
811
- // host process's drive (#1201: a rooted PATH entry must resolve on
812
- // the SAME drive the child will actually run cmd.exe/the resolved
813
- // binary from). See `resolveEffectiveWindowsCwd`'s rooted branch for
814
- // why that function anchors to `process.cwd()` instead — same rule,
815
- // different point in the pipeline. `undefined` here means the cwd
816
- // itself was unprovable, which is a genuinely different condition
817
- // from "the provenance carries no drive letter".
818
- return effectiveCwd === undefined
819
- ? undefined
820
- : resolveRootedWindowsPath(entry, effectiveCwd);
821
- }
822
- if (effectiveCwd === undefined)
823
- return undefined;
824
- return path.win32.normalize(path.win32.resolve(effectiveCwd, entry));
825
- }
826
- /**
827
- * Cached PATH + PATHEXT resolution results. The effective child environment is
828
- * part of the key, not the ambient process environment: one caller's managed
829
- * bin directory must never poison another caller's resolution. The key also
830
- * carries the canonical child cwd and Windows `=X:` per-drive cwd entries,
831
- * because both affect relative PATH and drive-relative lookup. Session-lived
832
- * (matches this repo's other resolution caches, e.g.
833
- * `clients/workspace-topology.ts`'s `resetWorkspaceTopology`) — cleared via
834
- * `resetSafeSpawnWindowsCommandCache()`, wired into `handleSessionStart`. The
835
- * cache is also count-bounded with oldest-entry eviction because a long-lived
836
- * process can encounter unbounded cwd/environment combinations.
837
- */
838
- export const WINDOWS_COMMAND_CACHE_MAX_ENTRIES = 256;
839
- const WINDOWS_COMMAND_NEGATIVE_CACHE_TTL_MS = 1000;
840
- const windowsCommandCache = new BoundedFifoMap(WINDOWS_COMMAND_CACHE_MAX_ENTRIES);
841
- /** Reset after session replacement or a successful managed install. */
842
- export function resetSafeSpawnWindowsCommandCache() {
843
- windowsCommandCache.clear();
844
- loggedSpawnClassifications.clear();
845
- }
846
- function cacheWindowsCommandResult(key, resolved) {
847
- // Resolution entries are session-scoped and cheap to recompute. The
848
- // caller always deletes any stale entry for `key` before reaching here
849
- // (see resolveWindowsCommandForEnvironment), so this can only grow the
850
- // map; BoundedFifoMap evicts the oldest insertion first once the map
851
- // exceeds capacity, so a long-lived process cannot retain every cwd /
852
- // environment it has ever touched.
853
- windowsCommandCache.set(key, { resolved, checkedAt: Date.now() });
854
- }
855
- function getPathExts(env) {
856
- const raw = getWindowsEnvironmentValue(env, "PATHEXT") ?? ".COM;.EXE;.BAT;.CMD";
857
- return raw
858
- .split(";")
859
- .map((ext) => ext.trim().toLowerCase())
860
- .filter(Boolean);
861
- }
862
- function statIsFile(candidate) {
863
- try {
864
- return fs.statSync(candidate).isFile();
865
- }
866
- catch {
867
- return false;
868
- }
869
- }
870
- function resolveWindowsCommandUncached(command, effectiveCwd, env) {
871
- const pathExts = getPathExts(env);
872
- const existingExt = path.win32.extname(command).toLowerCase();
873
- const hasExplicitExt = existingExt.length > 0;
874
- // A suffix is "known executable" when it's one of the four extensions this
875
- // resolver actually knows how to run (direct .exe/.com, or the
876
- // cmd.exe-wrapped .cmd/.bat) OR it's explicitly listed in the caller's own
877
- // PATHEXT. Anything else with a dot is NOT necessarily "an extension" — a
878
- // versioned interpreter (`python3.11`, `node-v20.1`) has an `extname()` of
879
- // `.11`/`.1` that is part of the basename, not a suffix to match exactly
880
- // (#1201).
881
- const isKnownExecutableExt = (ext) => WINDOWS_DIRECT_RESOLVABLE_EXTS.has(ext) || pathExts.includes(ext);
882
- const tryBase = (base) => {
883
- // Additive, not exclusive: an explicit, known-executable suffix is
884
- // tried as an exact candidate FIRST (this also lets a caller ask for
885
- // `foo.cmd` even when the current PATHEXT doesn't happen to list
886
- // `.CMD`). Whenever the suffix ISN'T a PATHEXT entry — no extension at
887
- // all, or a versioned/unknown one like `.11` — the PATHEXT-append loop
888
- // ALSO runs against the full base, so `python3.11` still finds
889
- // `python3.11.exe`. An unknown suffix (e.g. `foo.txt`) never gets an
890
- // exact-match short-circuit: that would let an arbitrary
891
- // non-executable file resolve as "spawnable" and reach the
892
- // direct-spawn branch downstream.
893
- if (hasExplicitExt &&
894
- isKnownExecutableExt(existingExt) &&
895
- statIsFile(base)) {
896
- return { resolvedPath: base, ext: existingExt };
897
- }
898
- if (!hasExplicitExt || !pathExts.includes(existingExt)) {
899
- for (const ext of pathExts) {
900
- const candidate = base + ext;
901
- if (statIsFile(candidate))
902
- return { resolvedPath: candidate, ext };
903
- }
904
- }
905
- return null;
906
- };
907
- const hasPathSep = /[\\/]/.test(command);
908
- // `D:tool.exe` is drive-relative on Windows, not a bare PATH command. It
909
- // uses the effective cwd only when that cwd is on D:. For another drive we
910
- // require an explicit, validated `=D:` provenance entry; otherwise this
911
- // resolver fails closed rather than guessing `D:\\` or searching PATH.
912
- const hasDrivePrefix = driveLetter(command) !== undefined;
913
- if (hasPathSep || hasDrivePrefix || isFullyQualifiedWindowsPath(command)) {
914
- let base;
915
- if (isFullyQualifiedWindowsPath(command)) {
916
- base = path.win32.normalize(command);
917
- }
918
- else if (hasDrivePrefix) {
919
- base = resolveDriveRelativeWindowsPath(command, effectiveCwd, env);
920
- }
921
- else if (isRootedWindowsPath(command)) {
922
- base =
923
- effectiveCwd === undefined
924
- ? undefined
925
- : resolveRootedWindowsPath(command, effectiveCwd);
926
- }
927
- else if (effectiveCwd !== undefined) {
928
- base = path.win32.normalize(path.win32.resolve(effectiveCwd, command));
929
- }
930
- return base === undefined ? null : tryBase(base);
931
- }
932
- // A bare command (no path separator) is a plain PATH search. Absolute PATH
933
- // entries don't need `effectiveCwd` at all, so an unresolvable cwd must
934
- // only skip the relative entries (handled per-entry in
935
- // resolveWindowsPathEntry below), not abort the whole search (#1201: a bad
936
- // cwd shouldn't make an otherwise-resolvable command look "not installed").
937
- const pathValue = getWindowsEnvironmentValue(env, "PATH") ?? "";
938
- const pathDirs = pathValue.split(path.win32.delimiter);
939
- for (const dir of pathDirs) {
940
- if (!dir)
941
- continue;
942
- const resolvedDir = resolveWindowsPathEntry(dir, effectiveCwd, env);
943
- if (resolvedDir === undefined)
944
- continue;
945
- const found = tryBase(path.win32.join(resolvedDir, command));
946
- if (found)
947
- return found;
948
- }
949
- return null;
950
- }
951
- /**
952
- * Resolve a Windows command using the exact environment that will be passed to
953
- * the child. Exported as a small platform-independent test/diagnostic seam;
954
- * callers should pass a Windows-shaped environment and do not need to mutate
955
- * `process.env` to exercise resolution. Drive-relative commands (`D:tool.exe`)
956
- * use a same-drive effective cwd, or a validated absolute `=D:` entry for a
957
- * different drive; without that provenance they fail closed and never search
958
- * PATH. Relative PATH entries use the canonical effective child cwd.
959
- */
960
- export function resolveWindowsCommandForEnvironment(command, cwd, env) {
961
- const effectiveCwd = resolveEffectiveWindowsCwd(cwd, env);
962
- const pathValue = getWindowsEnvironmentValue(env, "PATH");
963
- const pathExtValue = getWindowsEnvironmentValue(env, "PATHEXT");
964
- const perDriveCwds = Object.entries(env)
965
- .flatMap(([key, value]) => /^=[A-Za-z]:$/.test(key)
966
- ? [[key.toLowerCase(), value]]
967
- : [])
968
- .sort(([left], [right]) => compareOrdinal(left, right));
969
- // Keep presence separate from value: absent PATHEXT means the Windows
970
- // default extension list, while PATHEXT="" means no implicit extensions.
971
- // The per-drive snapshot is equally important: it is provenance for
972
- // drive-relative commands and PATH entries, not ambient decoration.
973
- const cacheKey = JSON.stringify([
974
- "win32",
975
- command,
976
- pathValue === undefined ? ["absent"] : ["present", pathValue],
977
- pathExtValue === undefined ? ["absent"] : ["present", pathExtValue],
978
- effectiveCwd === undefined ? ["unresolved"] : ["resolved", effectiveCwd],
979
- perDriveCwds,
980
- ]);
981
- const cached = windowsCommandCache.get(cacheKey);
982
- if (cached) {
983
- // Positive entries are revalidated on every hit so an executable deleted
984
- // or replaced mid-session cannot remain spawnable through stale cache
985
- // state. Negative entries are short-lived to discover external installs;
986
- // pi-lens-managed installs also reset the cache immediately on success.
987
- if (cached.resolved && statIsFile(cached.resolved.resolvedPath)) {
988
- return cached.resolved;
989
- }
990
- if (!cached.resolved &&
991
- Date.now() - cached.checkedAt <= WINDOWS_COMMAND_NEGATIVE_CACHE_TTL_MS) {
992
- return null;
993
- }
994
- windowsCommandCache.delete(cacheKey);
995
- }
996
- const resolved = resolveWindowsCommandUncached(command, effectiveCwd, env);
997
- cacheWindowsCommandResult(cacheKey, resolved);
998
- return resolved;
999
- }
1000
- /** Cached `where`-equivalent for the effective child environment. */
1001
- function resolveWindowsCommand(command, cwd, env) {
1002
- return resolveWindowsCommandForEnvironment(command, cwd, env);
1003
- }
1004
- function getSpawnEnvironment(overrides) {
1005
- return process.platform === "win32"
1006
- ? mergeWindowsEnvironment(process.env, overrides)
1007
- : { ...process.env, ...overrides };
1008
- }
1009
- /**
1010
- * Characters that make a cmd.exe `/c` command line unsound to build from
1011
- * caller-tainted input: `%`/`!` expand even inside double quotes and cannot
1012
- * be escaped on a `/c` line (only `%%` works, and only in batch files), `"`
1013
- * can toggle cmd's quote-parsing state past the doubling convention, and
1014
- * CR/LF let an argument masquerade as a second command. Reject rather than
1015
- * attempt to escape these (#817) — silent stripping would just move the
1016
- * unsoundness, not remove it.
1017
- */
1018
- const CMD_UNSAFE_CHARS = /["%!\r\n]/;
1019
- /** First caller-tainted value (command or an arg) that can't be safely
1020
- * passed through the .cmd/.bat cmd.exe wrapper, or `undefined` if all clear. */
1021
- function findCmdUnsafeValue(command, args) {
1022
- if (CMD_UNSAFE_CHARS.test(command))
1023
- return command;
1024
- return args.find((arg) => CMD_UNSAFE_CHARS.test(arg));
1025
- }
1026
- function synthesizeEnoentError(command) {
1027
- // Shaped like Node's native `spawn <cmd> ENOENT` error (message/code/
1028
- // syscall/path) so the typed classifier retains the same diagnostic cause
1029
- // now that Windows resolution happens before spawn instead of inside cmd.exe.
1030
- const err = new Error(`spawn ${command} ENOENT`);
1031
- err.code = "ENOENT";
1032
- err.syscall = "spawn";
1033
- err.path = command;
1034
- return err;
1035
- }
1036
- /**
1037
- * One-shot `chcp 65001` before the FIRST direct (.exe/.com) Windows spawn.
1038
- *
1039
- * Mechanism: `chcp` mutates the code page of the console the current process
1040
- * is attached to, and that mutation persists for the console's lifetime —
1041
- * it is not per-child-process state. The old cmd.exe-wrapper path re-ran
1042
- * `chcp 65001 >nul 2>&1 &&` on every single spawn, which was always
1043
- * redundant after the first call; memoizing here (module-level, once per
1044
- * process) is a strict improvement, not a behavior change. Runs through a
1045
- * tiny pinned cmd.exe spawn (same `%SystemRoot%\System32\cmd.exe` pin as
1046
- * `killTree` below) with a static, hardcoded command string — no caller-
1047
- * tainted data anywhere near it.
1048
- */
1049
- let utf8ConsoleCodePageApplied = false;
1050
- function ensureUtf8ConsoleCodePageOnce() {
1051
- if (utf8ConsoleCodePageApplied)
1052
- return;
1053
- utf8ConsoleCodePageApplied = true;
1054
- try {
1055
- const cmdExe = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\cmd.exe`;
1056
- spawnSync(cmdExe, ["/d", "/s", "/c", "chcp 65001 >nul 2>&1"], {
1057
- shell: false,
1058
- windowsHide: true,
1059
- // #1980 population sweep: a synchronous spawn with no timeout parks
1060
- // the event loop for as long as the child takes, and nothing here
1061
- // bounds that. `chcp` is normally instant, but this runs before the
1062
- // FIRST real spawn of the process, which is exactly when a cold or
1063
- // antivirus-throttled cmd.exe is slowest. Matches the 5s bound every
1064
- // other synchronous child-process site in this repo already carries.
1065
- timeout: 5000,
1066
- });
1067
- }
1068
- catch {
1069
- // Best-effort: worst case is non-ASCII tool output decoded incorrectly, not a
1070
- // spawn failure — never let this block the real spawn.
1071
- }
1072
- }
1073
- /**
1074
- * Test-only seam: clear the one-shot `chcp` memoization.
1075
- *
1076
- * No test observes it today — the memoized work only runs inside a real
1077
- * Windows direct spawn, so exercising it would mean a Windows-only test, and
1078
- * the state it guards is a console code page, not behavior any assertion
1079
- * depends on. This is kept purely so a future Windows-only test (or a second
1080
- * caller of `ensureUtf8ConsoleCodePageOnce`) can reset process-lifetime state
1081
- * without reaching into the module; it was dropped once as unrelated scope
1082
- * creep in an earlier #1199 revision and restored on review. Do not read the
1083
- * export as evidence of existing coverage (#1201).
1084
- */
1085
- export function resetUtf8ConsoleCodePageStateForTests() {
1086
- utf8ConsoleCodePageApplied = false;
1087
- }
1088
- // #1656: post-exit pipe-idle wait constants. Node's "close" event only fires
1089
- // once every stdio fd referencing the child has been released — a
1090
- // daemonized descendant that inherited our stdout/stderr pipe (the Windows
1091
- // no-job-object case) can hold that fd open forever, hanging "close" even
1092
- // though the child we spawned is long dead. A fixed post-exit deadline
1093
- // avoids that hang but truncates a legitimately still-streaming child. The
1094
- // idle-grace construction (adopted from pi's `waitForChildProcess`) does
1095
- // neither: after "exit", wait for the pipes to go quiet — no data for
1096
- // EXIT_PIPE_IDLE_GRACE_MS, re-armed on every chunk — so a quiet inherited
1097
- // handle still releases after one grace window while an actively-writing
1098
- // descendant keeps being read. EXIT_PIPE_IDLE_MAX_WAIT_MS caps the total
1099
- // extra wait so a pathological, never-quiet descendant can't extend the
1100
- // caller's overall budget unboundedly.
1101
- const EXIT_PIPE_IDLE_GRACE_MS = 100;
1102
- const EXIT_PIPE_IDLE_MAX_WAIT_MS = 2000;
1103
- /**
1104
- * #2968: how long past its own deadline this spawn's CPU/RSS sampler may keep
1105
- * polling. Everything this function does to end a child after the deadline
1106
- * fits inside it: `killTree`'s SIGTERM plus its 1000ms SIGKILL escalation (or
1107
- * Windows' `taskkill /F /T`), then at most EXIT_PIPE_IDLE_MAX_WAIT_MS of
1108
- * pipe-idle wait. A child still alive past deadline + this grace has outlived
1109
- * every teardown available here, so polling it further only buys the Windows
1110
- * process pile-up the report measured — and `stop()` still returns whatever
1111
- * the sampler gathered before the cap.
1112
- */
1113
- const SPAWN_SAMPLER_TEARDOWN_GRACE_MS = 5_000;
1114
- // ============================================================================
1115
- // ASYNC VERSION (Recommended - Non-blocking)
1116
- // ============================================================================
1117
- /**
1118
- * Async spawn with timeout and proper process cleanup.
1119
- *
1120
- * Unlike spawnSync, this:
1121
- * - Doesn't block the event loop
1122
- * - Kills the process on timeout (preventing zombies)
1123
- * - Supports cancellation via AbortSignal
1124
- *
1125
- * ## Why this bound is NOT `bounded()` — the one documented exception (#2523)
1126
- *
1127
- * #2523 slice 2 folded every other "deadline AND abort signal" implementation
1128
- * in the tree onto `clients/deadline-utils.ts#bounded`. This one stays, and it
1129
- * is not a copy of the same idea:
1130
- *
1131
- * `bounded()` ABANDONS a promise — it stops waiting and lets the work run on
1132
- * unobserved, which is safe precisely because everything it wraps is in-process
1133
- * (a dynamic import, an LSP round trip, a `stat`). This function's work is a
1134
- * CHILD PROCESS, and abandoning one leaks it. So its timer does not settle a
1135
- * race at all: `timeoutId` sets `timedOut`, then runs `killTree()` — SIGTERM,
1136
- * an escalation timer, SIGKILL, and on Windows a `taskkill /T` of the whole
1137
- * tree — and the promise settles only when the child's own `close` arrives (or
1138
- * the escalation gives up). The result it produces carries the teardown
1139
- * EVIDENCE (`timeoutTeardown`, #2010): which signal killed it, whether the
1140
- * tree actually died, how long it took. Replacing that with `bounded()` would
1141
- * return `undefined` at the deadline and leave a live `vitest`/`gradle` behind
1142
- * — the orphan class #2504 measured as 59 concurrent spawns starving a turn.
1143
- *
1144
- * The rule the exception preserves: a bound that can CANCEL its work must
1145
- * cancel it. `bounded()` is for work that cannot be cancelled; this is not
1146
- * that. Anything wrapping THIS function still gets `bounded()` around its own
1147
- * await if it needs one.
1148
- *
1149
- * @example
1150
- * const result = await safeSpawnAsync("npm", ["test"], { timeout: 30000 });
1151
- * if (result.error) console.error("Failed:", result.error);
1152
- */
1153
- export async function safeSpawnAsync(command, args, options) {
1154
- const configuredTimeout = options?.timeout !== undefined &&
1155
- Number.isFinite(options.timeout) &&
1156
- options.timeout >= 0
1157
- ? options.timeout
1158
- : 30000;
1159
- const deadlineRemaining = options?.deadlineAt !== undefined && Number.isFinite(options.deadlineAt)
1160
- ? options.deadlineAt - Date.now()
1161
- : Number.POSITIVE_INFINITY;
1162
- const timeout = Math.max(0, Math.min(configuredTimeout, deadlineRemaining));
1163
- // Fall back to the current turn's ambient signal (set from ctx.signal) so an
1164
- // Esc/abort mid-turn cancels dispatches that didn't thread a signal of their
1165
- // own — unless the caller opts out (installs, which must run to completion).
1166
- const abortSignal = options?.signal ??
1167
- (options?.ignoreAmbientSignal ? undefined : ambientAbortSignal);
1168
- return new Promise((resolve) => {
1169
- // Check for early abort
1170
- if (abortSignal?.aborted) {
1171
- const cause = new Error("Spawn aborted before start");
1172
- resolve({
1173
- stdout: "",
1174
- stderr: "",
1175
- status: null,
1176
- error: cause,
1177
- failure: "aborted",
1178
- spawnFailure: new SpawnFailureError("killed", cause.message, cause),
1179
- });
1180
- return;
1181
- }
1182
- if (timeout <= 0) {
1183
- const cause = new Error(`Process timed out after ${timeout}ms`);
1184
- resolve({
1185
- stdout: "",
1186
- stderr: "",
1187
- status: null,
1188
- error: cause,
1189
- failure: "timeout",
1190
- spawnFailure: new SpawnFailureError("timeout", cause.message, cause),
1191
- });
1192
- return;
1193
- }
1194
- let stdout = "";
1195
- let stderr = "";
1196
- let timedOut = false;
1197
- let aborted = false;
1198
- let killed = false;
1199
- let outputTruncated = false;
1200
- let killedForOutputCap = false;
1201
- // #3375: bytes the child actually EMITTED (not what survived the cap) —
1202
- // the one number the field crash report could not supply, so the ledger
1203
- // row below can name how chatty the offending command really was.
1204
- let observedOutputBytes = 0;
1205
- // #3375: bytes RETAINED so far, kept incrementally. The cap check used to
1206
- // re-measure `Buffer.byteLength(stdout) + Buffer.byteLength(stderr)` on
1207
- // every chunk, which flattens both accumulated strings each time — O(n^2)
1208
- // in total output. Harmless while only 12 of 110 call sites passed a cap;
1209
- // with the cap now on by default it would tax every spawn (measured: a
1210
- // 32 MiB child resolved in 7944 ms before this counter, 469 ms after).
1211
- // Equal to the two byte lengths by construction: before
1212
- // truncation `stdout`/`stderr` are exactly the appended texts, and after
1213
- // it the branch that renders from the retained head/tail never reads it.
1214
- let retainedOutputBytes = 0;
1215
- // #3375: latched the first time a chunk handler throws. Later chunks
1216
- // return immediately, so the same fault cannot be re-entered and
1217
- // `refreshRetainedOutputs` can never overwrite the bytes already kept.
1218
- let outputHandlerFailed = false;
1219
- let streamingMatch = false;
1220
- // #1651 review: a single boolean the close/error handlers both check
1221
- // AND set, so whichever one decides the outcome first wins outright —
1222
- // the DECISION itself is shape-based (see the `close` handler below),
1223
- // this flag only stops the loser from resolving a second time.
1224
- let resolved = false;
1225
- // The most recent 'error' Error, captured the instant the event fires
1226
- // regardless of whether that handler goes on to resolve. The `close`
1227
- // handler's shape-based branch (code === null || code < 0) reads this
1228
- // when it decides FIRST, so a genuinely informative ENOENT/EACCES/etc.
1229
- // Error is used when it happens to already be available, instead of
1230
- // always falling back to a generic synthesized one.
1231
- let pendingSpawnError;
1232
- // #1109: the non-Windows SIGTERM→SIGKILL escalation timer (armed in
1233
- // killTree below). Stored per-call (never shared) so the close/error
1234
- // handlers can clear it if the child exits before it fires — same
1235
- // uncleared-race-timeout class as the LSP client-wait leak (#1097):
1236
- // a ref'd 1s timer that outlives the child it was escalating against
1237
- // would keep a one-shot `pi --print` process alive for up to 1s.
1238
- let escalationTimer;
1239
- let teardownStartedAtMs;
1240
- let teardownEscalated = false;
1241
- // #2010: teardown evidence for the spawn's OWN timeout. The wall clock
1242
- // starts when the budget expires and the tree-kill is issued, and stops
1243
- // when the process tree is observed dead (`await killPromise` settles)
1244
- // - measured SEPARATELY from the budget itself, so a slow teardown is
1245
- // visible instead of silently inflating "how long the tool ran".
1246
- // `teardownEscalated` names whether SIGTERM sufficed or the SIGKILL
1247
- // escalation fired (POSIX; Windows taskkill /F is always forceful).
1248
- // #1114: `child.killed` is set by Node the moment `kill()` successfully
1249
- // SENDS a signal, not when the child actually dies — so gating the
1250
- // escalation on `!child.killed` right after a successful `SIGTERM` send
1251
- // is always false and the SIGKILL branch is unreachable. Track observed
1252
- // death via the close/error handlers instead (set synchronously, before
1253
- // any `await`, so a timer firing during the close handler's `await
1254
- // killPromise` still observes the flag correctly).
1255
- let closed = false;
1256
- // #1656: set only while the post-exit idle-pipe wait is active (see
1257
- // waitForPipeIdle below); the stdout/stderr "data" handlers call this to
1258
- // re-arm the grace timer on every chunk. `undefined` before exit / after
1259
- // the wait settles, so the calls below are no-ops outside that window.
1260
- let rearmIdleGrace;
1261
- // #3375: never `undefined`. An omitted or unusable cap resolves to the
1262
- // module default instead of to unbounded retention, so `appendOutput`
1263
- // below has no arm that concatenates without a ceiling.
1264
- const callerCap = options?.maxOutputBytes;
1265
- const capFromCaller = callerCap !== undefined && Number.isFinite(callerCap) && callerCap > 0;
1266
- const maxOutputBytes = capFromCaller
1267
- ? Math.floor(callerCap)
1268
- : DEFAULT_MAX_OUTPUT_BYTES;
1269
- const outputTruncationMarker = "\n...[output truncated]...\n";
1270
- let retainedHead = [];
1271
- let retainedTail = [];
1272
- let retainedTailBytes = 0;
1273
- let retainedTailLimit = 0;
1274
- let truncationStream = "stderr";
1275
- const streamingMatcher = options?.matchWhileStreaming;
1276
- const streamingCarryBytes = streamingMatcher
1277
- ? Math.max(0, streamingMatcher.source.length - 1)
1278
- : 0;
1279
- const streamingCarry = {
1280
- stdout: "",
1281
- stderr: "",
1282
- };
1283
- const bytePrefix = (text, bytes) => Buffer.from(text).subarray(0, bytes).toString();
1284
- const byteSuffix = (text, bytes) => {
1285
- const buffer = Buffer.from(text);
1286
- return buffer.subarray(Math.max(0, buffer.byteLength - bytes)).toString();
1287
- };
1288
- const appendTail = (part, maxBytes) => {
1289
- if (maxBytes <= 0)
1290
- return;
1291
- retainedTail.push({ stream: part.stream, text: part.text });
1292
- retainedTailBytes += Buffer.byteLength(part.text);
1293
- while (retainedTailBytes > maxBytes && retainedTail.length > 0) {
1294
- const first = retainedTail[0];
1295
- const excess = retainedTailBytes - maxBytes;
1296
- const firstBytes = Buffer.byteLength(first.text);
1297
- if (firstBytes <= excess) {
1298
- retainedTail.shift();
1299
- retainedTailBytes -= firstBytes;
1300
- }
1301
- else {
1302
- first.text = byteSuffix(first.text, firstBytes - excess);
1303
- retainedTailBytes -= excess;
1304
- }
1305
- }
1306
- };
1307
- const renderOutput = (stream) => {
1308
- const head = retainedHead
1309
- .filter((part) => part.stream === stream)
1310
- .map((part) => part.text)
1311
- .join("");
1312
- const tail = retainedTail
1313
- .filter((part) => part.stream === stream)
1314
- .map((part) => part.text)
1315
- .join("");
1316
- const marker = stream === truncationStream ? outputTruncationMarker : "";
1317
- return head + marker + tail;
1318
- };
1319
- const refreshRetainedOutputs = () => {
1320
- stdout = renderOutput("stdout");
1321
- stderr = renderOutput("stderr");
1322
- };
1323
- const appendOutput = (stream, current, chunk) => {
1324
- const text = typeof chunk === "string" ? chunk : chunk.toString();
1325
- if (outputTruncated) {
1326
- appendTail({ stream, text }, retainedTailLimit);
1327
- truncationStream = stream;
1328
- return renderOutput(stream);
1329
- }
1330
- const bytes = Buffer.byteLength(text);
1331
- if (retainedOutputBytes + bytes <= maxOutputBytes) {
1332
- retainedOutputBytes += bytes;
1333
- return current + text;
1334
- }
1335
- outputTruncated = true;
1336
- truncationStream = stream;
1337
- const available = Math.max(0, maxOutputBytes - Buffer.byteLength(outputTruncationMarker));
1338
- const headBytes = Math.floor(available / 2);
1339
- const tailBytes = available - headBytes;
1340
- retainedTailLimit = tailBytes;
1341
- const parts = [
1342
- { stream: "stdout", text: stdout },
1343
- { stream: "stderr", text: stderr },
1344
- { stream, text },
1345
- ];
1346
- let headRemaining = headBytes;
1347
- for (const part of parts) {
1348
- if (headRemaining <= 0)
1349
- break;
1350
- const kept = bytePrefix(part.text, headRemaining);
1351
- if (kept)
1352
- retainedHead.push({ stream: part.stream, text: kept });
1353
- headRemaining -= Buffer.byteLength(kept);
1354
- }
1355
- let tailRemaining = tailBytes;
1356
- for (let index = parts.length - 1; index >= 0 && tailRemaining > 0; index--) {
1357
- const part = parts[index];
1358
- const kept = byteSuffix(part.text, tailRemaining);
1359
- if (kept) {
1360
- retainedTail.unshift({ stream: part.stream, text: kept });
1361
- tailRemaining -= Buffer.byteLength(kept);
1362
- }
1363
- }
1364
- retainedTailBytes = tailBytes - tailRemaining;
1365
- return renderOutput(stream);
1366
- };
1367
- const matchStreamingChunk = (stream, chunk) => {
1368
- if (streamingMatch || !streamingMatcher)
1369
- return;
1370
- const text = typeof chunk === "string" ? chunk : chunk.toString();
1371
- streamingMatcher.lastIndex = 0;
1372
- streamingMatch = streamingMatcher.test(streamingCarry[stream] + text);
1373
- if (streamingCarryBytes > 0) {
1374
- streamingCarry[stream] = Buffer.from(streamingCarry[stream] + text)
1375
- .subarray(-streamingCarryBytes)
1376
- .toString();
1377
- }
1378
- };
1379
- // Spawn the process (non-blocking). Keeping Node's `shell` option false
1380
- // is important on every path here: shell:true concatenates tainted
1381
- // arguments into a command line before spawning (CodeQL #17 / CWE-78).
1382
- //
1383
- // On Windows (#817): resolve `command` ourselves (PATH + PATHEXT walk,
1384
- // cached) instead of always routing through cmd.exe.
1385
- // - .exe/.com → spawn the resolved path directly, args stay a real
1386
- // array, no shell involved at all (injection-proof).
1387
- // - .cmd/.bat → these are scripts cmd.exe must interpret (npm shims
1388
- // are the common case), so keep the wrapper, but pin the
1389
- // interpreter to %SystemRoot%\System32\cmd.exe (never trust
1390
- // ComSpec — CodeQL #18) and VALIDATE args instead of trying to
1391
- // escape them: reject anything containing `"`, `%`, `!`, or CR/LF
1392
- // rather than spawn with an unsound escape.
1393
- // - unresolvable → synthesize an ENOENT-shaped error instead of
1394
- // letting cmd.exe report "not recognized" from inside a shell.
1395
- const isWindows = process.platform === "win32";
1396
- const spawnEnv = getSpawnEnvironment(options?.env);
1397
- // The cwd handed to the CHILD process doesn't need OUR validation —
1398
- // Windows resolves it natively, exactly as it did before #817 ever
1399
- // touched this file. A drive-relative cwd (`D:work`) without a
1400
- // validated `=D:` env entry can't be canonicalized by us (Node never
1401
- // surfaces `=X:` keys from the ambient environment — see
1402
- // `getValidatedPerDriveCwd`'s doc comment), but that is a gap in OUR
1403
- // provenance, not evidence the cwd itself is bad. Fall back to the raw
1404
- // value instead of failing the whole spawn before ever attempting it
1405
- // (#1201) — command resolution just below still fails closed for any
1406
- // relative PATH entry that would need this cwd to be canonical.
1407
- // No `?? process.cwd()` third arm: `resolveEffectiveWindowsCwd(undefined,
1408
- // env)` always returns a string, so the only way to reach `undefined`
1409
- // here is a DEFINED-but-unprovable `options.cwd`, which `?? options?.cwd`
1410
- // already covers. A `process.cwd()` arm would be unreachable, and it
1411
- // would read like a live silent-wrong-directory hazard (#1201).
1412
- const spawnCwd = isWindows
1413
- ? (resolveEffectiveWindowsCwd(options?.cwd, spawnEnv) ?? options?.cwd)
1414
- : options?.cwd;
1415
- let spawnCmd = command;
1416
- let spawnArgs = args;
1417
- let windowsVerbatimArguments = false;
1418
- let resolutionError;
1419
- if (isWindows) {
1420
- // Pass the ORIGINAL (unresolved) cwd here, not `spawnCwd` above — this
1421
- // seam intentionally fails closed on unprovable drive-relative
1422
- // provenance (matches `resolveWindowsCommandForEnvironment`'s
1423
- // documented contract and its dedicated test coverage), independent
1424
- // of the passthrough fallback the actual child cwd gets above.
1425
- const resolved = resolveWindowsCommand(command, options?.cwd, spawnEnv);
1426
- if (!resolved) {
1427
- resolutionError = synthesizeEnoentError(command);
1428
- }
1429
- else if (resolved.ext === ".cmd" || resolved.ext === ".bat") {
1430
- // Validate the RESOLVED path (what actually gets interpolated
1431
- // into the /c line via buildWindowsShellCommand below), not the
1432
- // caller's original `command` string — a resolved path
1433
- // containing `%`/`!` would otherwise reach the shell unvalidated.
1434
- const unsafeValue = findCmdUnsafeValue(resolved.resolvedPath, args);
1435
- if (unsafeValue !== undefined) {
1436
- resolutionError = new Error(`Refusing to spawn "${resolved.resolvedPath}" via cmd.exe: ` +
1437
- `${JSON.stringify(unsafeValue)} contains a character ("` +
1438
- `, %, !, or CR/LF) that cannot be safely escaped on a ` +
1439
- `cmd.exe /c command line (CWE-78, #817). Rename/quote the ` +
1440
- "value or invoke the tool without going through cmd.exe.");
1441
- }
1442
- else {
1443
- // Pin the interpreter — never trust ComSpec/COMSPEC (#18): an
1444
- // env-controlled ComSpec could point anywhere.
1445
- spawnCmd = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\cmd.exe`;
1446
- spawnArgs = [
1447
- "/d",
1448
- "/s",
1449
- "/c",
1450
- buildWindowsShellCommand(resolved.resolvedPath, args),
1451
- ];
1452
- // The `/c` payload is already quoted by buildWindowsShellCommand.
1453
- // Prevent Node from escaping those quotes a second time when it
1454
- // builds cmd.exe's Windows command line.
1455
- windowsVerbatimArguments = true;
1456
- }
1457
- }
1458
- else {
1459
- // .exe / .com: direct spawn, no cmd.exe anywhere in the picture.
1460
- ensureUtf8ConsoleCodePageOnce();
1461
- spawnCmd = resolved.resolvedPath;
1462
- spawnArgs = args;
1463
- }
1464
- }
1465
- if (resolutionError) {
1466
- void classifySpawnFailure(resolutionError, {
1467
- command,
1468
- cwd: options?.cwd,
1469
- }).then((spawnFailure) => resolve({
1470
- stdout: "",
1471
- stderr: "",
1472
- status: null,
1473
- error: resolutionError,
1474
- failure: "spawn",
1475
- spawnFailure,
1476
- }));
1477
- return;
1478
- }
1479
- let child;
1480
- // #2026: on POSIX, run the child in its OWN process group (detached).
1481
- // killTree/killPidTreeSync then signal the whole GROUP (-pid), which
1482
- // makes grandchildren (npm -> node, sh -> sleep, etc.) die with their
1483
- // tool instead of surviving every timeout as orphans. Detachment stops
1484
- // direct terminal-signal delivery, so EVERY POSIX pid registers for
1485
- // lifetime cleanup - pi's signal/exit handlers forward the kill,
1486
- // preserving die-with-host semantics.
1487
- const posixProcessGroup = process.platform !== "win32";
1488
- try {
1489
- child = spawn(spawnCmd, spawnArgs, {
1490
- cwd: spawnCwd,
1491
- env: spawnEnv,
1492
- windowsHide: true,
1493
- shell: false,
1494
- windowsVerbatimArguments,
1495
- detached: posixProcessGroup,
1496
- });
1497
- }
1498
- catch (err) {
1499
- // A SYNCHRONOUS spawn throw (Windows `spawn UNKNOWN`/EINVAL — the
1500
- // pidusage bug class, #533) must NOT reject this Promise: every caller
1501
- // relies on safeSpawnAsync never rejecting (they inspect result.error),
1502
- // and many invoke it fire-and-forget in best-effort/background paths, so
1503
- // a rejection here could surface as an unhandledRejection that crashes
1504
- // the host. Resolve the failure gracefully instead — same contract as an
1505
- // asynchronously-emitted `'error'` event (handled below).
1506
- const cause = toError(err);
1507
- void classifySpawnFailure(cause, { command, cwd: options?.cwd }).then((spawnFailure) => resolve({
1508
- stdout: "",
1509
- stderr: "",
1510
- status: null,
1511
- error: cause,
1512
- failure: "spawn",
1513
- spawnFailure,
1514
- }));
1515
- return;
1516
- }
1517
- // Keep historical open stdin by default. An opted-in payload is written
1518
- // completely and followed by EOF; input: "" closes stdin immediately.
1519
- if (options?.input !== undefined) {
1520
- child.stdin?.on("error", () => { });
1521
- child.stdin?.end(options.input);
1522
- }
1523
- // #2042: ownership is a SPAWN-TIME fact — a real child is alive with
1524
- // `PPid == ours` the instant `spawn` returns, while a test double's
1525
- // invented pid never is. Resolve it once, here, and reuse the verdict:
1526
- // the group escalation below must still fire after the direct child
1527
- // dies (#2027: a SIGTERM-hardy grandchild keeps the group alive), so it
1528
- // cannot re-read `/proc` at kill time.
1529
- const ownsChildPid = isOwnLiveChild(child.pid, "safe-spawn-register");
1530
- if (ownsChildPid && (posixProcessGroup || options?.lifetimeCoupled)) {
1531
- // #2026: POSIX registers unconditionally - detached children no
1532
- // longer receive terminal signals directly, so the lifetime
1533
- // cleanup's signal forwarding IS their die-with-host path.
1534
- installLifetimeCleanup();
1535
- lifetimeState.pids.add(child.pid);
1536
- }
1537
- // #620: bracket this spawn's lifetime with a short-interval CPU/RSS poll
1538
- // (started right here, stopped in finalize() below) so transient
1539
- // analyzer children (jscpd, knip, madge, gitleaks, etc.) — which live too
1540
- // briefly for heartbeat-cadence sampling to reliably catch — still get a
1541
- // peak/average resource reading. `startSpawnUsageSampler` itself is
1542
- // best-effort/never-throws by design, but this call site wraps it anyway
1543
- // (belt and suspenders: the sampling seam must never be the reason a real
1544
- // spawn fails) with a no-op fallback sampler.
1545
- //
1546
- // #2968: the sampler's polling lifetime is bounded by THIS spawn's own
1547
- // deadline plus the teardown grace, not by the child's willingness to
1548
- // exit. `finalize`/the `error` handler still stop it on every settle
1549
- // path, but none of those paths exist for a child that never settles —
1550
- // a hung `.cmd` shim whose tree survives `taskkill /F /T` kept polling
1551
- // for 5-6 hours in the report.
1552
- let usageSampler;
1553
- try {
1554
- usageSampler = startSpawnUsageSampler(
1555
- // The poll CADENCE is the sampler's own policy (it owns the backoff
1556
- // too); the DEADLINE is this function's. Passing `undefined` rather
1557
- // than importing the cadence constant keeps this module's view of
1558
- // the sampler at one export, which is also what every existing
1559
- // `vi.mock` of it provides.
1560
- child.pid, undefined, timeout + SPAWN_SAMPLER_TEARDOWN_GRACE_MS);
1561
- }
1562
- catch {
1563
- usageSampler = { stop: () => null };
1564
- }
1565
- const resourceLabel = options?.resourceLabel ?? command;
1566
- let killFailed = false;
1567
- /** Send one signal. INVARIANT (#3375 r3 / M3384-2): every signal SEND in
1568
- * `killTree` goes through this function. Exactly two `try`/`catch`
1569
- * blocks remain in there and neither contains a bare send: the Windows
1570
- * `await` guard around the taskkill child (its `catch` body sends only
1571
- * through `trySend`), and the group LIVENESS PROBE, whose `catch`
1572
- * computes a predicate rather than sending anything — the same shape as
1573
- * the module-level `groupIsGone`. Returns false instead of throwing, and
1574
- * records nothing itself: a refused signal is often ORDINARY (a negative-pid
1575
- * ESRCH means the group already exited and the direct-child fallback
1576
- * takes over), so only a send with no fallback left behind it pairs
1577
- * with `noteKillFailure`. */
1578
- const trySend = (send) => {
1579
- try {
1580
- send();
1581
- return true;
1582
- }
1583
- catch {
1584
- return false;
1585
- }
1586
- };
1587
- /** Every signal for this teardown was refused, so the child may still be
1588
- * running. One record per spawn — the reviewer's reproduction refused
1589
- * two kills (the fault's and then the timeout's) and a row per attempt
1590
- * would be a flood. A kind of its own rather than a field on
1591
- * `spawn-output-cap-truncated`: this fires on the abort and timeout
1592
- * teardowns too, which have nothing to do with an output cap. */
1593
- const noteKillFailure = (phase) => {
1594
- if (killFailed)
1595
- return;
1596
- killFailed = true;
1597
- incrementDegradationCount({
1598
- kind: "spawn-kill-failed",
1599
- subject: resourceLabel,
1600
- reason: `every signal available for the ${phase} teardown was refused; the child may still be running`,
1601
- metadata: { phase, pid: child.pid },
1602
- });
1603
- };
1604
- // On Windows, shell:true means child.pid is cmd.exe — child.kill() only
1605
- // kills the wrapper, leaving the actual subprocess (e.g. knip/npx) alive
1606
- // as an orphan. Use taskkill /F /T to kill the full process tree instead.
1607
- const killTree = async (phase) => {
1608
- if (isWindows && child.pid && child.pid > 0) {
1609
- const taskkill = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\taskkill.exe`;
1610
- try {
1611
- await new Promise((done) => {
1612
- const killer = spawn(taskkill, ["/F", "/T", "/PID", String(child.pid)], {
1613
- shell: false,
1614
- windowsHide: true,
1615
- stdio: "ignore",
1616
- });
1617
- killer.once("close", () => done());
1618
- killer.once("error", () => {
1619
- // E4: a throw HERE would leave the EventEmitter as an
1620
- // uncaught exception, not a rejection.
1621
- if (!trySend(() => child.kill("SIGKILL")))
1622
- noteKillFailure(phase);
1623
- done();
1624
- });
1625
- });
1626
- }
1627
- catch {
1628
- if (!trySend(() => child.kill("SIGKILL")))
1629
- noteKillFailure(phase);
1630
- }
1631
- }
1632
- else if (posixProcessGroup && ownsChildPid) {
1633
- // #2026: signal the whole process group. Grandchildren spawned
1634
- // by the tool share its group, so one signal reaches the whole
1635
- // tree; a negative-pid ESRCH means it already exited.
1636
- const pgid = -child.pid;
1637
- // #3375 round 3 (M3384-2): through the one sink, like every other
1638
- // send. A second ad-hoc `try`/`catch` here would be a rival
1639
- // signal-safety mechanism inside the same teardown — the shape this
1640
- // change deleted four of — and a later edit to this branch would not
1641
- // inherit `trySend`'s total send or its failure accounting.
1642
- // `trySend` returning false here is ORDINARY, not a failure: #2026's
1643
- // negative-pid ESRCH means the group already exited, so the direct
1644
- // child below is the normal fallback and nothing is recorded. Only
1645
- // the last-resort send can fail the teardown.
1646
- if (!trySend(() => process.kill(pgid, "SIGTERM"))) {
1647
- if (!trySend(() => child.kill("SIGTERM")))
1648
- noteKillFailure(phase);
1649
- }
1650
- // #2027 round-1: gate the SIGKILL escalation on GROUP liveness,
1651
- // not direct-child death - a tool can exit instantly on SIGTERM
1652
- // while a SIGTERM-hardy grandchild keeps the group alive. The
1653
- // timer stays REF'D for group mode (max 1s tail after resolve):
1654
- // it is the only backstop for this group once finalize deletes
1655
- // the pid from lifetimeState.
1656
- escalationTimer = setTimeout(() => {
1657
- // The WHOLE body, not just the signal: this runs in a bare
1658
- // timer callback, where any throw is an uncaught exception.
1659
- const escalated = trySend(() => {
1660
- let groupAlive = true;
1661
- try {
1662
- process.kill(pgid, 0);
1663
- }
1664
- catch {
1665
- groupAlive = false;
1666
- }
1667
- if (!groupAlive)
1668
- return;
1669
- teardownEscalated = true;
1670
- logLatency({
1671
- type: "phase",
1672
- phase: "spawn_group_kill_escalation",
1673
- filePath: "",
1674
- durationMs: 0,
1675
- metadata: { pgid: child.pid },
1676
- });
1677
- // Through the sink too (#3375 r3 / M3384-2): one vocabulary
1678
- // for every send in this function. A false here is ORDINARY
1679
- // — the group raced its own exit — so it is deliberately not
1680
- // escalated to `noteKillFailure`; the enclosing `trySend`
1681
- // already guarantees nothing escapes this timer callback.
1682
- trySend(() => process.kill(pgid, "SIGKILL"));
1683
- });
1684
- if (!escalated)
1685
- noteKillFailure(phase);
1686
- }, 1000);
1687
- }
1688
- else {
1689
- // E2/E3. This branch runs whenever `/proc` cannot prove pid
1690
- // ownership — every macOS host, and any Linux host with an
1691
- // unreadable `/proc` — so it is not a hypothetical path.
1692
- if (!trySend(() => child.kill("SIGTERM")))
1693
- noteKillFailure(phase);
1694
- escalationTimer = setTimeout(() => {
1695
- if (closed)
1696
- return;
1697
- teardownEscalated = true;
1698
- if (!trySend(() => child.kill("SIGKILL")))
1699
- noteKillFailure(phase);
1700
- }, 1000);
1701
- }
1702
- };
1703
- // Handle abort signal
1704
- const onAbort = () => {
1705
- aborted = true;
1706
- if (!killed && !child.killed) {
1707
- killed = true;
1708
- void killTree("abort");
1709
- }
1710
- };
1711
- abortSignal?.addEventListener("abort", onAbort, { once: true });
1712
- // Output-cap kills are awaited by finalize() below, just like
1713
- // timeout/abort kills. An armed, unmatched streaming matcher is the one
1714
- // exception: retention remains bounded, but the caller's timeout is the
1715
- // bound while the child gets a chance to emit its rescue line.
1716
- let killPromise;
1717
- const stopForOutputLimit = () => {
1718
- const waitingForStreamingMatch = streamingMatcher !== undefined && !streamingMatch;
1719
- if (outputTruncated &&
1720
- !waitingForStreamingMatch &&
1721
- !killed &&
1722
- !child.killed) {
1723
- killedForOutputCap = true;
1724
- killed = true;
1725
- killPromise = killTree("output-cap");
1726
- }
1727
- };
1728
- // #3375: one bounded, counted ledger row per command label the moment the
1729
- // cap first bites - the crash report named no command, no byte count and
1730
- // no cap value, so nothing in it could identify the chatty producer.
1731
- // `incrementDegradationCount` keeps one latest-reason entry per subject
1732
- // with an exact tally and writes durable rows only at powers of two, so a
1733
- // tool that trips the cap on every dispatch cannot flood the sink.
1734
- const recordOutputCapTrip = () => {
1735
- incrementDegradationCount({
1736
- kind: "spawn-output-cap-truncated",
1737
- subject: resourceLabel,
1738
- reason: `retained output reached the ${maxOutputBytes}-byte ${capFromCaller ? "caller" : "default"} cap after ${observedOutputBytes} bytes; child ${killedForOutputCap ? "terminated" : "left running"}`,
1739
- metadata: {
1740
- capBytes: maxOutputBytes,
1741
- capSource: capFromCaller ? "caller" : "default",
1742
- observedBytes: observedOutputBytes,
1743
- killed: killedForOutputCap,
1744
- },
1745
- });
1746
- };
1747
- // #3375: the fault path for a chunk handler that threw. Every statement
1748
- // here is total by construction - plain assignments, a ledger call that
1749
- // swallows its own failures, and `killTree()`, which is `async` so a
1750
- // synchronous fault inside it becomes a rejected promise rather than a
1751
- // throw - because a throw raised HERE would escape the same way the
1752
- // original RangeError did.
1753
- const failOutputHandler = (stream, error) => {
1754
- outputHandlerFailed = true;
1755
- // Retention is over. The bytes already in `stdout`/`stderr` stay as
1756
- // the bounded output: nothing renders from the retained head/tail
1757
- // again, because the re-entry guard returns before that can happen.
1758
- // That is also why `truncationStream` is deliberately NOT set here —
1759
- // only `renderOutput` reads it, and nothing renders after a fault, so
1760
- // the assignment was mutation-inert (removing it reds nothing).
1761
- outputTruncated = true;
1762
- incrementDegradationCount({
1763
- kind: "spawn-output-handler-fault",
1764
- subject: resourceLabel,
1765
- reason: `${stream} chunk handling threw ${error.name} after ${observedOutputBytes} bytes; retention stopped and the child was terminated`,
1766
- metadata: {
1767
- stream,
1768
- error: error.name,
1769
- capBytes: maxOutputBytes,
1770
- observedBytes: observedOutputBytes,
1771
- },
1772
- });
1773
- if (!killed && !child.killed) {
1774
- killedForOutputCap = true;
1775
- killed = true;
1776
- killPromise = killTree("handler-fault");
1777
- }
1778
- };
1779
- // Collect output
1780
- child.stdout?.setEncoding("utf-8");
1781
- child.stderr?.setEncoding("utf-8");
1782
- // #3375: a throw raised inside a stream `data` handler is delivered to
1783
- // the process, not to the caller awaiting this spawn - no `try`/`catch`
1784
- // around `await safeSpawnAsync(...)` can contain it, which is why a
1785
- // `RangeError: Invalid string length` from `appendOutput` terminated the
1786
- // Pi host on 2026-09-22. Every statement the handler runs lives inside
1787
- // this `try`, so ANY fault in the retention path (not just that one
1788
- // RangeError) becomes the bounded output-cap result the caller already
1789
- // knows how to read.
1790
- const onOutputChunk = (stream, data) => {
1791
- if (outputHandlerFailed)
1792
- return;
1793
- try {
1794
- observedOutputBytes += Buffer.byteLength(data);
1795
- const wasTruncated = outputTruncated;
1796
- matchStreamingChunk(stream, data);
1797
- if (stream === "stdout")
1798
- stdout = appendOutput(stream, stdout, data);
1799
- else
1800
- stderr = appendOutput(stream, stderr, data);
1801
- if (outputTruncated)
1802
- refreshRetainedOutputs();
1803
- stopForOutputLimit();
1804
- if (!wasTruncated && outputTruncated)
1805
- recordOutputCapTrip();
1806
- rearmIdleGrace?.();
1807
- }
1808
- catch (error) {
1809
- failOutputHandler(stream, toError(error));
1810
- }
1811
- };
1812
- child.stdout?.on("data", (data) => onOutputChunk("stdout", data));
1813
- child.stderr?.on("data", (data) => onOutputChunk("stderr", data));
1814
- // #1673 review round 3 (F2): registered HERE, at spawn time, alongside
1815
- // the other handlers above — not inside `waitForPipeIdle`. Real Node
1816
- // emits `close` a fraction of a millisecond after `exit` (both fire in
1817
- // the same "process exit" turn, well before any microtask), so a
1818
- // listener attached inside `waitForPipeIdle` — which `finalize` only
1819
- // reaches after `await killPromise`, at least one microtask later — is
1820
- // always attaching to an event that has already happened. That's why
1821
- // the round-2 `child.on("close", finish)` placed inside
1822
- // `waitForPipeIdle` never actually caught a real spawn's `close` early:
1823
- // every real call still paid the full grace window. Capturing the
1824
- // boolean here, before any await, means `waitForPipeIdle` can check
1825
- // "did close already happen" synchronously the moment it runs.
1826
- let closeSeen = false;
1827
- // #2010: WHEN death was observed - the stop-clock for teardown evidence
1828
- // and the discriminator for its outcome. A close that predates the
1829
- // budget-expired kill means the child exited on its own ("exited"), no
1830
- // matter that the timer callback raced and armed a kill anyway.
1831
- let closedAtMs;
1832
- child.on("close", () => {
1833
- closeSeen = true;
1834
- closedAtMs ??= Date.now();
1835
- });
1836
- // Timeout handling - KILL the process, don't just abandon it
1837
- const timeoutId = setTimeout(() => {
1838
- timedOut = true;
1839
- if (!killed && !child.killed) {
1840
- killed = true;
1841
- teardownStartedAtMs = Date.now();
1842
- killPromise = killTree("timeout");
1843
- }
1844
- }, timeout);
1845
- // #620: stop the poll and log peak/average CPU%+RSS for this invocation
1846
- // into the existing per-runner latency.log phase entries — best-effort,
1847
- // wrapped so a logging hiccup can never affect the resolved SpawnResult.
1848
- const finishResourceUsage = () => {
1849
- const summary = usageSampler.stop();
1850
- if (!summary)
1851
- return undefined;
1852
- try {
1853
- logLatency({
1854
- type: "phase",
1855
- phase: "spawn_resource_usage",
1856
- filePath: "",
1857
- durationMs: 0,
1858
- metadata: {
1859
- command: resourceLabel,
1860
- ...summary,
1861
- },
1862
- });
1863
- }
1864
- catch {
1865
- // best-effort logging only
1866
- }
1867
- return summary;
1868
- };
1869
- // #1656: wait for stdout/stderr to fall idle after exit instead of
1870
- // waiting for Node's "close" event (see the constants' doc comment
1871
- // above). Resolves immediately if the child has no pipes to drain.
1872
- //
1873
- // These timers stay REF'D, matching `timeoutId`/`escalationTimer`
1874
- // elsewhere in this function: the caller's own settlement depends on
1875
- // them firing, so unref'ing would let a short-lived host process (a
1876
- // one-shot CLI whose only other in-flight work was this spawn) exit
1877
- // the event loop before `finish` ever runs — silently abandoning this
1878
- // promise instead of merely leaking a handle. Both paths to `finish`
1879
- // clear both timers, so nothing outlives settlement either way.
1880
- const waitForPipeIdle = () => {
1881
- if (!child.stdout && !child.stderr)
1882
- return Promise.resolve();
1883
- // #1673 review round 3 (F2): `close` fires a fraction of a
1884
- // millisecond after `exit` for a real Node child — well before
1885
- // `finalize`'s `await killPromise` ever yields, i.e. before this
1886
- // function is even called. By the time we get here, `closeSeen`
1887
- // (set by the listener registered at spawn time, above) already
1888
- // reflects reality for the overwhelming majority of real spawns:
1889
- // nothing is holding this child's stdio open, so don't pay out the
1890
- // 100ms grace window for a signal that already arrived.
1891
- if (closeSeen)
1892
- return Promise.resolve();
1893
- return new Promise((finishIdleWait) => {
1894
- let settled = false;
1895
- let graceTimer;
1896
- const capTimer = setTimeout(() => {
1897
- finish();
1898
- }, EXIT_PIPE_IDLE_MAX_WAIT_MS);
1899
- const finish = () => {
1900
- if (settled)
1901
- return;
1902
- settled = true;
1903
- rearmIdleGrace = undefined;
1904
- if (graceTimer)
1905
- clearTimeout(graceTimer);
1906
- clearTimeout(capTimer);
1907
- // Optional chaining, not a bare call: `child` is a real Node
1908
- // ChildProcess (always an EventEmitter, always has this) in
1909
- // production, but several existing test doubles across the
1910
- // suite fake stdio-bearing children as plain objects with only
1911
- // the handful of methods (`on`, `kill`, ...) their scenario
1912
- // needs — no `removeListener`. Skipping cleanup on those is
1913
- // harmless (the fake is torn down with the test either way);
1914
- // crashing here from inside a timer callback is not: it would
1915
- // throw before `finishIdleWait()` runs, hanging every caller's
1916
- // `safeSpawnAsync` promise (#1673 review round 2 caught this
1917
- // live in tests/clients/installer/version-drift.test.ts).
1918
- child.removeListener?.("close", finish);
1919
- finishIdleWait();
1920
- };
1921
- // The `closeSeen` fast-path above catches the common case (close
1922
- // already happened by the time we get here). This listener is
1923
- // the fallback for the rare case where `close` genuinely lands
1924
- // DURING the wait — e.g. a child whose stdio takes a beat
1925
- // longer than usual to release, but still well under the grace
1926
- // window. `close` is a stronger signal than "no data for
1927
- // EXIT_PIPE_IDLE_GRACE_MS", so finishing on it here still beats
1928
- // waiting out the timer. The anti-hang property is untouched
1929
- // either way: a daemonized descendant holding the pipe open is
1930
- // EXACTLY the case where `close` never fires at all, so the
1931
- // grace/cap timers still bound that case as before.
1932
- child.on("close", finish);
1933
- rearmIdleGrace = () => {
1934
- if (graceTimer)
1935
- clearTimeout(graceTimer);
1936
- graceTimer = setTimeout(finish, EXIT_PIPE_IDLE_GRACE_MS);
1937
- };
1938
- rearmIdleGrace();
1939
- });
1940
- };
1941
- // Process completion
1942
- //
1943
- // #1656 x #1651/#1670: finalize off BOTH `exit` and `close`, not just
1944
- // one — each covers a shape the other misses.
1945
- // - `exit` is what unblocks the #1656 hang: a daemonized descendant
1946
- // that inherited our stdout/stderr pipe can hold that fd open
1947
- // forever, so `close` (which waits for every fd referencing the
1948
- // child to release) never fires even though the child we spawned
1949
- // is long dead. `exit` always fires once the child itself exits,
1950
- // independent of who still holds its pipes.
1951
- // - `close` is still wired because a child that never actually
1952
- // launched (bad binary, EACCES, ...) gets NO `exit` event at all —
1953
- // Node only emits `error` then `close(code<0, null)` for that
1954
- // shape (#1651/#1670's shape guard below decides from exactly
1955
- // this). `exit`-only would make that guard unreachable.
1956
- // Whichever fires first drives `finalize`; `settled` (distinct from
1957
- // `resolved`, which `finalize` itself owns) stops the second event
1958
- // from dispatching a redundant, concurrently-racing `finalize` call —
1959
- // double-invoking it would double-run `waitForPipeIdle` and
1960
- // `finishResourceUsage` against the same child.
1961
- let settled = false;
1962
- const onChildSettle = (code, signal) => {
1963
- if (settled || resolved)
1964
- return;
1965
- settled = true;
1966
- closed = true;
1967
- clearTimeout(timeoutId);
1968
- abortSignal?.removeEventListener("abort", onAbort);
1969
- if (child.pid)
1970
- lifetimeState.pids.delete(child.pid);
1971
- void finalize(code, signal);
1972
- };
1973
- const finalize = async (code, signal) => {
1974
- // Belt, not the decision: whichever path settles `resolved` FIRST
1975
- // wins. A healthy run's outcome must never be discarded by a late,
1976
- // unrelated 'error' (e.g. a post-exit kill() that itself failed
1977
- // with EPERM, #1651 review F4) — so this only bails when something
1978
- // else has ALREADY resolved, never on `error` merely having fired.
1979
- await killPromise;
1980
- if (resolved)
1981
- return;
1982
- // #1109: the child has exited - clear the non-Windows escalation
1983
- // timer so it doesn't linger as a ref'd handle. #2027 EXCEPTION:
1984
- // in POSIX group mode the timer is the liveness-gated SIGKILL
1985
- // backstop for SIGTERM-hardy grandchildren; it stays armed and
1986
- // self-cleans within 1s of firing (max 1s ref'd tail).
1987
- if (escalationTimer && !posixProcessGroup) {
1988
- clearTimeout(escalationTimer);
1989
- }
1990
- // #1673 review F1: latch the verdict as DECIDED here, before the
1991
- // idle-pipe wait — not after it. `code`/`signal`/`timedOut`/`aborted`
1992
- // are already fixed by this point (they don't change during the
1993
- // wait), so nothing below depends on staying unresolved across it.
1994
- // Leaving `resolved` false across the wait would reopen the exact
1995
- // race #1651 review F4 closed: a late, unrelated 'error' (e.g. a
1996
- // post-exit kill() failing with EPERM) landing inside the
1997
- // idle-grace window (bounded, but up to EXIT_PIPE_IDLE_MAX_WAIT_MS)
1998
- // would steal an already-decided healthy verdict. Pre-#1656 that
1999
- // race window was one microtask — the gap between `close` firing
2000
- // and `resolve()` being called synchronously in the same handler.
2001
- // This closes the window back down to that same shape instead of
2002
- // widening it to the whole wait.
2003
- resolved = true;
2004
- await waitForPipeIdle();
2005
- const resourceUsage = finishResourceUsage();
2006
- const outputInfo = {
2007
- ...(outputTruncated ? { outputTruncated: true } : {}),
2008
- ...(killedForOutputCap ? { killedForOutputCap: true } : {}),
2009
- ...(killFailed ? { killFailed: true } : {}),
2010
- };
2011
- const streamingMatchInfo = streamingMatch ? { streamingMatch: true } : {};
2012
- // #1816: surface the signal name as a field on every path where one
2013
- // exists, so callers can NAME it instead of scraping `error.message`.
2014
- const signalInfo = signal ? { signal } : {};
2015
- if (timedOut) {
2016
- const cause = new Error(`Process timed out after ${timeout}ms (killed with ${signal || "SIGTERM"})`);
2017
- // #2010: teardown evidence lives ONLY on the timeout path - it
2018
- // describes how long the tree took to die after the budget
2019
- // expired, and how it died ("exited" = the child beat the signal;
2020
- // "killed-by-signal" / "escalate-kill" = how WE ended it).
2021
- const timeoutTeardown = resolveTeardownOutcome({
2022
- killStartedAtMs: teardownStartedAtMs,
2023
- deathObservedAtMs: closedAtMs,
2024
- escalated: teardownEscalated,
2025
- });
2026
- resolve({
2027
- stdout,
2028
- stderr,
2029
- status: null,
2030
- error: cause,
2031
- failure: "timeout",
2032
- ...signalInfo,
2033
- ...(timeoutTeardown && { timeoutTeardown }),
2034
- spawnFailure: new SpawnFailureError("timeout", cause.message, cause),
2035
- ...outputInfo,
2036
- ...streamingMatchInfo,
2037
- resourceUsage,
2038
- });
2039
- }
2040
- else if (aborted) {
2041
- const cause = new Error("Spawn aborted");
2042
- resolve({
2043
- stdout,
2044
- stderr,
2045
- status: null,
2046
- error: cause,
2047
- failure: "aborted",
2048
- ...signalInfo,
2049
- spawnFailure: new SpawnFailureError("killed", cause.message, cause),
2050
- ...outputInfo,
2051
- ...streamingMatchInfo,
2052
- resourceUsage,
2053
- });
2054
- }
2055
- else if (signal) {
2056
- const cause = new Error(`Process killed by signal: ${signal}`);
2057
- resolve({
2058
- stdout,
2059
- stderr,
2060
- status: null,
2061
- error: cause,
2062
- failure: "signal",
2063
- ...signalInfo,
2064
- spawnFailure: new SpawnFailureError("killed", cause.message, cause),
2065
- ...outputInfo,
2066
- ...streamingMatchInfo,
2067
- resourceUsage,
2068
- });
2069
- }
2070
- else if (code === null || code < 0) {
2071
- // #1651 review F3: a real completed process never exits with a
2072
- // null or negative code (Node reports the negated libuv/errno for
2073
- // a child that never launched — e.g. ENOENT: -2 on Linux, -4058
2074
- // on Windows). This decides from that SHAPE alone, independent of
2075
- // whether/when 'error' has fired — Node's docs leave the
2076
- // 'error'/'close' ordering for a failed spawn unspecified, and a
2077
- // same-microtask re-check (the #1651 first-round fix) still loses
2078
- // if 'error' lands a tick later via setImmediate/nextTick.
2079
- // Reuse `pendingSpawnError` when 'error' already landed (a more
2080
- // informative Error — ENOENT vs EACCES vs other); otherwise
2081
- // synthesize an ENOENT-shaped one so a genuinely missing tool is
2082
- // never read as a clean, answered run.
2083
- const cause = pendingSpawnError ?? synthesizeEnoentError(command);
2084
- const spawnFailure = await classifySpawnFailure(cause, {
2085
- command,
2086
- cwd: options?.cwd,
2087
- });
2088
- resolve({
2089
- stdout,
2090
- stderr,
2091
- status: code,
2092
- error: cause,
2093
- failure: "spawn",
2094
- spawnFailure,
2095
- ...outputInfo,
2096
- ...streamingMatchInfo,
2097
- resourceUsage,
2098
- });
2099
- }
2100
- else {
2101
- resolve({
2102
- stdout,
2103
- stderr,
2104
- status: code,
2105
- ...outputInfo,
2106
- ...streamingMatchInfo,
2107
- resourceUsage,
2108
- });
2109
- }
2110
- };
2111
- child.on("exit", onChildSettle);
2112
- child.on("close", onChildSettle);
2113
- child.on("error", (err) => {
2114
- // Capture unconditionally — even when `close` already resolved, this
2115
- // keeps the most recent Error available for anyone reading state
2116
- // mid-flight, and costs nothing (the assignment, not a resolve).
2117
- pendingSpawnError = err;
2118
- // A completed run's own verdict must never be downgraded by an
2119
- // unrelated LATE error (#1651 review F4 — e.g. a post-exit kill()
2120
- // failing with EPERM after `close(0, null)` already answered).
2121
- if (resolved)
2122
- return;
2123
- resolved = true;
2124
- closed = true;
2125
- clearTimeout(timeoutId);
2126
- abortSignal?.removeEventListener("abort", onAbort);
2127
- // #2027: group-mode timer stays armed as the grandchild backstop.
2128
- if (escalationTimer && !posixProcessGroup) {
2129
- clearTimeout(escalationTimer);
2130
- }
2131
- if (child.pid)
2132
- lifetimeState.pids.delete(child.pid);
2133
- const resourceUsage = finishResourceUsage();
2134
- let failure = "spawn";
2135
- if (aborted)
2136
- failure = "aborted";
2137
- else if (timedOut)
2138
- failure = "timeout";
2139
- const controlFailure = aborted
2140
- ? new SpawnFailureError("killed", err.message, err)
2141
- : timedOut
2142
- ? new SpawnFailureError("timeout", err.message, err)
2143
- : undefined;
2144
- const finish = (spawnFailure) => resolve({
2145
- stdout,
2146
- stderr,
2147
- status: null,
2148
- error: err,
2149
- failure,
2150
- spawnFailure,
2151
- ...(outputTruncated ? { outputTruncated: true } : {}),
2152
- ...(killedForOutputCap ? { killedForOutputCap: true } : {}),
2153
- ...(killFailed ? { killFailed: true } : {}),
2154
- ...(streamingMatch ? { streamingMatch: true } : {}),
2155
- resourceUsage,
2156
- });
2157
- if (controlFailure)
2158
- finish(controlFailure);
2159
- else {
2160
- void classifySpawnFailure(err, { command, cwd: options?.cwd }).then(finish);
2161
- }
2162
- });
2163
- });
2164
- }
2165
- /**
2166
- * Run multiple commands concurrently with limited concurrency.
2167
- *
2168
- * This prevents resource contention when running many linters.
2169
- * Uses async spawn with concurrency limiting built-in.
2170
- *
2171
- * @example
2172
- * const results = await safeSpawnBatch([
2173
- * { command: "biome", args: ["check", "file.ts"] },
2174
- * { command: "ruff", args: ["check", "file.py"] },
2175
- * ], 3); // Max 3 concurrent
2176
- */
2177
- export async function safeSpawnBatch(commands, concurrency = 3) {
2178
- const results = [];
2179
- // Process in batches to limit concurrent processes
2180
- for (let i = 0; i < commands.length; i += concurrency) {
2181
- const batch = commands.slice(i, i + concurrency);
2182
- const batchResults = await Promise.all(batch.map(({ command, args, options }) => safeSpawnAsync(command, args, options)));
2183
- results.push(...batchResults);
2184
- }
2185
- return results;
2186
- }
2187
- /**
2188
- * Check if a command is available in PATH (async version)
2189
- */
2190
- export async function isCommandAvailableAsync(command) {
2191
- const finder = process.platform === "win32" ? "where" : "which";
2192
- const result = await safeSpawnAsync(finder, [command], { timeout: 5000 });
2193
- return result.status === 0 && !result.error;
2194
- }
2195
- /**
2196
- * Find the full path to a command (async version)
2197
- */
2198
- export async function findCommandAsync(command) {
2199
- const finder = process.platform === "win32" ? "where" : "which";
2200
- const result = await safeSpawnAsync(finder, [command], { timeout: 5000 });
2201
- if (result.status !== 0 || result.error)
2202
- return null;
2203
- // Take first line (first match)
2204
- return result.stdout.trim().split("\n")[0] || null;
2205
- }
2206
- // ============================================================================
2207
- // SYNC VERSION (Deprecated - Blocking, for backward compatibility)
2208
- // ============================================================================
2209
- /**
2210
- * ⚠️ DEPRECATED: Use safeSpawnAsync instead.
2211
- *
2212
- * This blocks the entire Node.js event loop until the process exits.
2213
- * If the process hangs, pi will freeze.
2214
- *
2215
- * Kept for backward compatibility during migration (today's only caller:
2216
- * `test-runner-client.ts`'s synchronous pytest-on-PATH probe, called from a
2217
- * sync detection path with many sync callers/tests — not a trivial async
2218
- * migration, see #817 follow-up discussion).
2219
- *
2220
- * #817: the Windows branch used to build a `cmd.exe`/`shell:true` command
2221
- * line from unvalidated caller input (CodeQL #17/#18/#19), same unsoundness
2222
- * as the async version had. It now shares the exact same resolution/
2223
- * validation seams as `safeSpawnAsync` — `resolveWindowsCommand` (cached
2224
- * PATH+PATHEXT walk), direct `spawnSync(resolvedPath, args, { shell: false })`
2225
- * for `.exe`/`.com`, the pinned-cmd.exe wrapper + `findCmdUnsafeValue`
2226
- * rejection for `.cmd`/`.bat`, and a synthesized ENOENT when unresolvable.
2227
- * No `shell: true` anywhere.
2228
- */
2229
- export function safeSpawn(command, args, options) {
2230
- const spawnEnv = getSpawnEnvironment(options?.env);
2231
- if (process.platform === "win32") {
2232
- // See the matching comment in safeSpawnAsync: the child's cwd doesn't
2233
- // need OUR validation (Windows resolves it natively), so an unprovable
2234
- // drive-relative cwd falls back to the raw value instead of failing the
2235
- // whole spawn before ever attempting it (#1201). Command resolution
2236
- // below intentionally still uses the ORIGINAL raw cwd and fails closed
2237
- // for PATH entries that would need it to be canonical.
2238
- // See safeSpawnAsync for why there is no `?? process.cwd()` third arm.
2239
- const spawnCwd = resolveEffectiveWindowsCwd(options?.cwd, spawnEnv) ?? options?.cwd;
2240
- const resolved = resolveWindowsCommand(command, options?.cwd, spawnEnv);
2241
- if (!resolved) {
2242
- const error = synthesizeEnoentError(command);
2243
- return {
2244
- stdout: "",
2245
- stderr: "",
2246
- status: null,
2247
- error,
2248
- failure: "spawn",
2249
- spawnFailure: classifySpawnFailureSync(error, {
2250
- command,
2251
- cwd: options?.cwd,
2252
- }),
2253
- };
2254
- }
2255
- let spawnCmd;
2256
- let spawnArgs;
2257
- let windowsVerbatimArguments = false;
2258
- if (resolved.ext === ".cmd" || resolved.ext === ".bat") {
2259
- // Validate the value that actually gets interpolated into the /c
2260
- // line — the RESOLVED path, not the caller's original (possibly
2261
- // extensionless) `command` string — plus every arg.
2262
- const unsafeValue = findCmdUnsafeValue(resolved.resolvedPath, args);
2263
- if (unsafeValue !== undefined) {
2264
- const error = new Error(`Refusing to spawn "${resolved.resolvedPath}" via cmd.exe: ` +
2265
- `${JSON.stringify(unsafeValue)} contains a character ("` +
2266
- `, %, !, or CR/LF) that cannot be safely escaped on a ` +
2267
- `cmd.exe /c command line (CWE-78, #817). Rename/quote the ` +
2268
- "value or invoke the tool without going through cmd.exe.");
2269
- return {
2270
- stdout: "",
2271
- stderr: "",
2272
- status: null,
2273
- error,
2274
- failure: "spawn",
2275
- spawnFailure: classifySpawnFailureSync(error, {
2276
- command,
2277
- cwd: options?.cwd,
2278
- }),
2279
- };
2280
- }
2281
- spawnCmd = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\cmd.exe`;
2282
- spawnArgs = [
2283
- "/d",
2284
- "/s",
2285
- "/c",
2286
- buildWindowsShellCommand(resolved.resolvedPath, args),
2287
- ];
2288
- windowsVerbatimArguments = true;
2289
- }
2290
- else {
2291
- ensureUtf8ConsoleCodePageOnce();
2292
- spawnCmd = resolved.resolvedPath;
2293
- spawnArgs = args;
2294
- }
2295
- const result = spawnSync(spawnCmd, spawnArgs, {
2296
- ...options,
2297
- cwd: spawnCwd,
2298
- env: spawnEnv,
2299
- encoding: "utf-8",
2300
- shell: false,
2301
- windowsHide: true,
2302
- windowsVerbatimArguments,
2303
- });
2304
- const spawnFailure = result.error
2305
- ? classifySpawnFailureSync(result.error, { command, cwd: options?.cwd })
2306
- : undefined;
2307
- return {
2308
- stdout: result.stdout?.toString() || "",
2309
- stderr: result.stderr?.toString() || "",
2310
- status: result.status,
2311
- error: result.error,
2312
- ...(spawnFailure ? { failure: "spawn", spawnFailure } : {}),
2313
- };
2314
- }
2315
- const result = spawnSync(command, args, {
2316
- ...options,
2317
- // Explicit override, not just the spread above: `options.env` alone
2318
- // would otherwise reach the child as a full replacement (no
2319
- // process.env merge, no PATH) instead of the merged environment the
2320
- // resolver/Windows branch above both use (#1201) — the module's
2321
- // "resolver and child receive the same merged environment" invariant
2322
- // must hold on every platform, not just Windows.
2323
- env: spawnEnv,
2324
- encoding: "utf-8",
2325
- shell: false,
2326
- windowsHide: true,
2327
- });
2328
- const spawnFailure = result.error
2329
- ? classifySpawnFailureSync(result.error, { command, cwd: options?.cwd })
2330
- : undefined;
2331
- return {
2332
- stdout: result.stdout?.toString() || "",
2333
- stderr: result.stderr?.toString() || "",
2334
- status: result.status,
2335
- error: result.error,
2336
- ...(spawnFailure ? { failure: "spawn", spawnFailure } : {}),
2337
- };
2338
- }
2339
- /**
2340
- * Check if a command is available in PATH (sync version - deprecated)
2341
- * @deprecated Use isCommandAvailableAsync
2342
- */
2343
- export function isCommandAvailable(command) {
2344
- const result = safeSpawn(process.platform === "win32" ? "where" : "which", [command], { timeout: 5000 });
2345
- return result.status === 0;
2346
- }
2347
- /**
2348
- * Find the full path to a command (sync version - deprecated)
2349
- * @deprecated Use findCommandAsync
2350
- */
2351
- export function findCommand(command) {
2352
- const finder = process.platform === "win32" ? "where" : "which";
2353
- const result = safeSpawn(finder, [command], { timeout: 5000 });
2354
- if (result.status !== 0)
2355
- return null;
2356
- return result.stdout.trim().split("\n")[0] || null;
2357
- }