terramend 0.2.0

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 (406) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +145 -0
  3. package/dist/agents/claude.d.ts +73 -0
  4. package/dist/agents/claudePretoolGate.d.ts +99 -0
  5. package/dist/agents/gateServer.d.ts +7 -0
  6. package/dist/agents/index.d.ts +6 -0
  7. package/dist/agents/nativeFsDenies.d.ts +28 -0
  8. package/dist/agents/opencode.d.ts +231 -0
  9. package/dist/agents/opencodePlugin.d.ts +85 -0
  10. package/dist/agents/opencodeShared.d.ts +40 -0
  11. package/dist/agents/postRun.d.ts +132 -0
  12. package/dist/agents/reviewer.d.ts +38 -0
  13. package/dist/agents/sessionLabeler.d.ts +97 -0
  14. package/dist/agents/shared.d.ts +189 -0
  15. package/dist/agents/subagentModels.d.ts +19 -0
  16. package/dist/agents/subagentToolGates.d.ts +55 -0
  17. package/dist/cli.mjs +197426 -0
  18. package/dist/external.d.ts +227 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.js +196783 -0
  21. package/dist/internal/index.d.ts +18 -0
  22. package/dist/internal.js +1714 -0
  23. package/dist/lifecycle.d.ts +2 -0
  24. package/dist/main.d.ts +8 -0
  25. package/dist/mcp/arkConfig.d.ts +1 -0
  26. package/dist/mcp/checkSuite.d.ts +25 -0
  27. package/dist/mcp/checkout.d.ts +77 -0
  28. package/dist/mcp/comment.d.ts +119 -0
  29. package/dist/mcp/commitInfo.d.ts +9 -0
  30. package/dist/mcp/crosswalk.d.ts +105 -0
  31. package/dist/mcp/dependencies.d.ts +8 -0
  32. package/dist/mcp/geminiSanitizer.d.ts +28 -0
  33. package/dist/mcp/git.d.ts +46 -0
  34. package/dist/mcp/guardrails.d.ts +104 -0
  35. package/dist/mcp/issue.d.ts +18 -0
  36. package/dist/mcp/issueComments.d.ts +9 -0
  37. package/dist/mcp/issueEvents.d.ts +9 -0
  38. package/dist/mcp/issueInfo.d.ts +9 -0
  39. package/dist/mcp/labels.d.ts +12 -0
  40. package/dist/mcp/localContext.d.ts +19 -0
  41. package/dist/mcp/moduleExtraction.d.ts +71 -0
  42. package/dist/mcp/moduleTests.d.ts +104 -0
  43. package/dist/mcp/modules.d.ts +179 -0
  44. package/dist/mcp/output.d.ts +12 -0
  45. package/dist/mcp/pathSafety.d.ts +14 -0
  46. package/dist/mcp/policy.d.ts +48 -0
  47. package/dist/mcp/pr.d.ts +49 -0
  48. package/dist/mcp/prInfo.d.ts +9 -0
  49. package/dist/mcp/providerSchema.d.ts +50 -0
  50. package/dist/mcp/review.d.ts +199 -0
  51. package/dist/mcp/reviewComments.d.ts +178 -0
  52. package/dist/mcp/roots.d.ts +58 -0
  53. package/dist/mcp/scope.d.ts +15 -0
  54. package/dist/mcp/selectMode.d.ts +18 -0
  55. package/dist/mcp/server.d.ts +48 -0
  56. package/dist/mcp/shared.d.ts +47 -0
  57. package/dist/mcp/shell.d.ts +37 -0
  58. package/dist/mcp/staleFix.d.ts +51 -0
  59. package/dist/mcp/terraform/cost.d.ts +55 -0
  60. package/dist/mcp/terraform/currency.d.ts +94 -0
  61. package/dist/mcp/terraform/decisions.d.ts +178 -0
  62. package/dist/mcp/terraform/findings.d.ts +75 -0
  63. package/dist/mcp/terraform/plan.d.ts +157 -0
  64. package/dist/mcp/terraform/scanners.d.ts +131 -0
  65. package/dist/mcp/terraform/tools.d.ts +63 -0
  66. package/dist/mcp/terraform/types.d.ts +172 -0
  67. package/dist/mcp/terraform.d.ts +22 -0
  68. package/dist/mcp/terratest.d.ts +83 -0
  69. package/dist/mcp/upload.d.ts +6 -0
  70. package/dist/models.d.ts +171 -0
  71. package/dist/modes.d.ts +26 -0
  72. package/dist/prep/index.d.ts +7 -0
  73. package/dist/prep/installNodeDependencies.d.ts +2 -0
  74. package/dist/prep/installPythonDependencies.d.ts +2 -0
  75. package/dist/prep/types.d.ts +31 -0
  76. package/dist/reviewQuality.d.ts +64 -0
  77. package/dist/skills/terraform-best-practices/SKILL.md +369 -0
  78. package/dist/toolState.d.ts +135 -0
  79. package/dist/utils/activity.d.ts +40 -0
  80. package/dist/utils/agent.d.ts +20 -0
  81. package/dist/utils/agentHangReport.d.ts +38 -0
  82. package/dist/utils/apiFetch.d.ts +19 -0
  83. package/dist/utils/apiKeys.d.ts +41 -0
  84. package/dist/utils/apiUrl.d.ts +20 -0
  85. package/dist/utils/assets.d.ts +8 -0
  86. package/dist/utils/billingErrors.d.ts +85 -0
  87. package/dist/utils/body.d.ts +34 -0
  88. package/dist/utils/buildTerramendFooter.d.ts +25 -0
  89. package/dist/utils/byokFallback.d.ts +85 -0
  90. package/dist/utils/claudeSubscription.d.ts +30 -0
  91. package/dist/utils/cli.d.ts +10 -0
  92. package/dist/utils/codexHome.d.ts +29 -0
  93. package/dist/utils/codexOAuth.d.ts +60 -0
  94. package/dist/utils/diffCoverage.d.ts +63 -0
  95. package/dist/utils/errorReport.d.ts +17 -0
  96. package/dist/utils/exitHandler.d.ts +8 -0
  97. package/dist/utils/fixDoubleEscapedString.d.ts +1 -0
  98. package/dist/utils/gitAuth.d.ts +84 -0
  99. package/dist/utils/gitAuthServer.d.ts +24 -0
  100. package/dist/utils/github.d.ts +78 -0
  101. package/dist/utils/globals.d.ts +3 -0
  102. package/dist/utils/install.d.ts +60 -0
  103. package/dist/utils/instructions.d.ts +48 -0
  104. package/dist/utils/leapingComment.d.ts +11 -0
  105. package/dist/utils/learnings.d.ts +62 -0
  106. package/dist/utils/learningsTruncate.d.ts +25 -0
  107. package/dist/utils/lifecycle.d.ts +57 -0
  108. package/dist/utils/log.d.ts +111 -0
  109. package/dist/utils/normalizeEnv.d.ts +30 -0
  110. package/dist/utils/openCodeModels.d.ts +11 -0
  111. package/dist/utils/overrides.d.ts +40 -0
  112. package/dist/utils/packageManager.d.ts +49 -0
  113. package/dist/utils/patchWorkflowRunFields.d.ts +29 -0
  114. package/dist/utils/payload.d.ts +105 -0
  115. package/dist/utils/prSummary.d.ts +61 -0
  116. package/dist/utils/progressComment.d.ts +146 -0
  117. package/dist/utils/providerErrors.d.ts +31 -0
  118. package/dist/utils/rangeDiff.d.ts +51 -0
  119. package/dist/utils/remediationCommand.d.ts +55 -0
  120. package/dist/utils/retry.d.ts +13 -0
  121. package/dist/utils/reviewCleanup.d.ts +14 -0
  122. package/dist/utils/run.d.ts +9 -0
  123. package/dist/utils/runContext.d.ts +60 -0
  124. package/dist/utils/runContextData.d.ts +23 -0
  125. package/dist/utils/runErrorRenderer.d.ts +64 -0
  126. package/dist/utils/runLifecycle.d.ts +86 -0
  127. package/dist/utils/runStartupLog.d.ts +15 -0
  128. package/dist/utils/secrets.d.ts +22 -0
  129. package/dist/utils/setup.d.ts +90 -0
  130. package/dist/utils/shell.d.ts +32 -0
  131. package/dist/utils/skills.d.ts +10 -0
  132. package/dist/utils/subprocess.d.ts +80 -0
  133. package/dist/utils/terraformMcp.d.ts +42 -0
  134. package/dist/utils/time.d.ts +15 -0
  135. package/dist/utils/timer.d.ts +23 -0
  136. package/dist/utils/todoTracking.d.ts +16 -0
  137. package/dist/utils/token.d.ts +39 -0
  138. package/dist/utils/version.d.ts +2 -0
  139. package/dist/utils/versioning.d.ts +7 -0
  140. package/dist/utils/vertex.d.ts +16 -0
  141. package/dist/utils/workflow.d.ts +13 -0
  142. package/package.json +119 -0
  143. package/src/agents/claude.test.ts +1016 -0
  144. package/src/agents/claude.ts +1246 -0
  145. package/src/agents/claudePretoolGate.test.ts +28 -0
  146. package/src/agents/claudePretoolGate.ts +173 -0
  147. package/src/agents/gateServer.test.ts +204 -0
  148. package/src/agents/gateServer.ts +124 -0
  149. package/src/agents/index.ts +10 -0
  150. package/src/agents/nativeFsDenies.ts +82 -0
  151. package/src/agents/opencode.test.ts +1440 -0
  152. package/src/agents/opencode.ts +1312 -0
  153. package/src/agents/opencodePlugin.ts +222 -0
  154. package/src/agents/opencodeShared.test.ts +34 -0
  155. package/src/agents/opencodeShared.ts +121 -0
  156. package/src/agents/postRun.test.ts +549 -0
  157. package/src/agents/postRun.ts +535 -0
  158. package/src/agents/reviewer.ts +104 -0
  159. package/src/agents/sessionLabeler.test.ts +247 -0
  160. package/src/agents/sessionLabeler.ts +178 -0
  161. package/src/agents/shared.test.ts +76 -0
  162. package/src/agents/shared.ts +292 -0
  163. package/src/agents/subagentModels.test.ts +113 -0
  164. package/src/agents/subagentModels.ts +40 -0
  165. package/src/agents/subagentRegistration.test.ts +41 -0
  166. package/src/agents/subagentToolGates.ts +114 -0
  167. package/src/cli.test.ts +129 -0
  168. package/src/cli.ts +105 -0
  169. package/src/commands/gha.test.ts +192 -0
  170. package/src/commands/gha.ts +188 -0
  171. package/src/commands/mcp.ts +122 -0
  172. package/src/config.ts +1 -0
  173. package/src/entry.ts +7 -0
  174. package/src/entryPost.stdlibOnly.test.ts +109 -0
  175. package/src/entryPost.ts +99 -0
  176. package/src/external.test.ts +16 -0
  177. package/src/external.ts +302 -0
  178. package/src/index.ts +11 -0
  179. package/src/internal/index.ts +71 -0
  180. package/src/lifecycle.ts +2 -0
  181. package/src/main.test.ts +873 -0
  182. package/src/main.ts +712 -0
  183. package/src/mcp/__fixtures__/terramend-scratch-pr-49-review-3485940013.json +110 -0
  184. package/src/mcp/__fixtures__/terramend-scratch-pr-64-review-3531000326.json +14 -0
  185. package/src/mcp/__fixtures__/terramend-test-repo-pr-1.diff.json +67 -0
  186. package/src/mcp/__snapshots__/checkout.test.ts.snap +109 -0
  187. package/src/mcp/__snapshots__/reviewComments.test.ts.snap +71 -0
  188. package/src/mcp/arkConfig.ts +7 -0
  189. package/src/mcp/checkSuite.test.ts +245 -0
  190. package/src/mcp/checkSuite.ts +255 -0
  191. package/src/mcp/checkout.test.ts +752 -0
  192. package/src/mcp/checkout.ts +886 -0
  193. package/src/mcp/comment.test.ts +772 -0
  194. package/src/mcp/comment.ts +582 -0
  195. package/src/mcp/commitInfo.test.ts +127 -0
  196. package/src/mcp/commitInfo.ts +61 -0
  197. package/src/mcp/crosswalk.test.ts +106 -0
  198. package/src/mcp/crosswalk.ts +339 -0
  199. package/src/mcp/dependencies.test.ts +309 -0
  200. package/src/mcp/dependencies.ts +189 -0
  201. package/src/mcp/geminiSanitizer.test.ts +287 -0
  202. package/src/mcp/geminiSanitizer.ts +207 -0
  203. package/src/mcp/git.test.ts +1083 -0
  204. package/src/mcp/git.ts +890 -0
  205. package/src/mcp/guardrails.test.ts +705 -0
  206. package/src/mcp/guardrails.ts +465 -0
  207. package/src/mcp/issue.test.ts +113 -0
  208. package/src/mcp/issue.ts +73 -0
  209. package/src/mcp/issueComments.test.ts +69 -0
  210. package/src/mcp/issueComments.ts +48 -0
  211. package/src/mcp/issueEvents.test.ts +134 -0
  212. package/src/mcp/issueEvents.ts +100 -0
  213. package/src/mcp/issueInfo.test.ts +104 -0
  214. package/src/mcp/issueInfo.ts +72 -0
  215. package/src/mcp/labels.test.ts +52 -0
  216. package/src/mcp/labels.ts +34 -0
  217. package/src/mcp/localContext.ts +28 -0
  218. package/src/mcp/localServer.test.ts +75 -0
  219. package/src/mcp/localServer.ts +131 -0
  220. package/src/mcp/moduleExtraction.test.ts +261 -0
  221. package/src/mcp/moduleExtraction.ts +313 -0
  222. package/src/mcp/moduleTests.test.ts +269 -0
  223. package/src/mcp/moduleTests.ts +421 -0
  224. package/src/mcp/modules.test.ts +640 -0
  225. package/src/mcp/modules.ts +696 -0
  226. package/src/mcp/output.test.ts +96 -0
  227. package/src/mcp/output.ts +70 -0
  228. package/src/mcp/pathSafety.test.ts +44 -0
  229. package/src/mcp/pathSafety.ts +28 -0
  230. package/src/mcp/policy.test.ts +282 -0
  231. package/src/mcp/policy.ts +199 -0
  232. package/src/mcp/pr.test.ts +387 -0
  233. package/src/mcp/pr.ts +194 -0
  234. package/src/mcp/prInfo.test.ts +96 -0
  235. package/src/mcp/prInfo.ts +91 -0
  236. package/src/mcp/providerSchema.test.ts +85 -0
  237. package/src/mcp/providerSchema.ts +175 -0
  238. package/src/mcp/review.test.ts +936 -0
  239. package/src/mcp/review.ts +923 -0
  240. package/src/mcp/reviewComments.test.ts +549 -0
  241. package/src/mcp/reviewComments.ts +896 -0
  242. package/src/mcp/roots.test.ts +175 -0
  243. package/src/mcp/roots.ts +217 -0
  244. package/src/mcp/scope.test.ts +59 -0
  245. package/src/mcp/scope.ts +65 -0
  246. package/src/mcp/security.test.ts +720 -0
  247. package/src/mcp/selectMode.test.ts +210 -0
  248. package/src/mcp/selectMode.ts +181 -0
  249. package/src/mcp/server.test.ts +292 -0
  250. package/src/mcp/server.ts +403 -0
  251. package/src/mcp/shared.ts +100 -0
  252. package/src/mcp/shell.test.ts +520 -0
  253. package/src/mcp/shell.ts +505 -0
  254. package/src/mcp/staleFix.test.ts +237 -0
  255. package/src/mcp/staleFix.ts +277 -0
  256. package/src/mcp/terraform/cost.ts +163 -0
  257. package/src/mcp/terraform/currency.test.ts +338 -0
  258. package/src/mcp/terraform/currency.ts +336 -0
  259. package/src/mcp/terraform/decisions.ts +527 -0
  260. package/src/mcp/terraform/findings.ts +333 -0
  261. package/src/mcp/terraform/plan.ts +348 -0
  262. package/src/mcp/terraform/scanners.ts +809 -0
  263. package/src/mcp/terraform/tools.test.ts +1071 -0
  264. package/src/mcp/terraform/tools.ts +908 -0
  265. package/src/mcp/terraform/types.ts +305 -0
  266. package/src/mcp/terraform.test.ts +1957 -0
  267. package/src/mcp/terraform.ts +23 -0
  268. package/src/mcp/terratest.test.ts +105 -0
  269. package/src/mcp/terratest.ts +196 -0
  270. package/src/mcp/toolFiltering.test.ts +85 -0
  271. package/src/mcp/upload.test.ts +180 -0
  272. package/src/mcp/upload.ts +112 -0
  273. package/src/models.test.ts +300 -0
  274. package/src/models.ts +708 -0
  275. package/src/modes.test.ts +107 -0
  276. package/src/modes.ts +880 -0
  277. package/src/prep/index.ts +43 -0
  278. package/src/prep/installNodeDependencies.test.ts +298 -0
  279. package/src/prep/installNodeDependencies.ts +196 -0
  280. package/src/prep/installPythonDependencies.test.ts +268 -0
  281. package/src/prep/installPythonDependencies.ts +199 -0
  282. package/src/prep/types.ts +38 -0
  283. package/src/reviewQuality.test.ts +63 -0
  284. package/src/reviewQuality.ts +134 -0
  285. package/src/runCli.test.ts +214 -0
  286. package/src/runCli.ts +282 -0
  287. package/src/skills/terraform-best-practices/SKILL.md +369 -0
  288. package/src/toolState.test.ts +45 -0
  289. package/src/toolState.ts +252 -0
  290. package/src/utils/activity.test.ts +188 -0
  291. package/src/utils/activity.ts +210 -0
  292. package/src/utils/agent.test.ts +251 -0
  293. package/src/utils/agent.ts +139 -0
  294. package/src/utils/agentHangReport.test.ts +203 -0
  295. package/src/utils/agentHangReport.ts +170 -0
  296. package/src/utils/apiFetch.test.ts +115 -0
  297. package/src/utils/apiFetch.ts +62 -0
  298. package/src/utils/apiKeys.test.ts +344 -0
  299. package/src/utils/apiKeys.ts +206 -0
  300. package/src/utils/apiUrl.test.ts +30 -0
  301. package/src/utils/apiUrl.ts +59 -0
  302. package/src/utils/assets.test.ts +153 -0
  303. package/src/utils/assets.ts +107 -0
  304. package/src/utils/billingErrors.test.ts +121 -0
  305. package/src/utils/billingErrors.ts +189 -0
  306. package/src/utils/body.test.ts +217 -0
  307. package/src/utils/body.ts +168 -0
  308. package/src/utils/buildTerramendFooter.test.ts +38 -0
  309. package/src/utils/buildTerramendFooter.ts +82 -0
  310. package/src/utils/byokFallback.test.ts +205 -0
  311. package/src/utils/byokFallback.ts +128 -0
  312. package/src/utils/claudeSubscription.test.ts +179 -0
  313. package/src/utils/claudeSubscription.ts +93 -0
  314. package/src/utils/cli.ts +31 -0
  315. package/src/utils/codexHome.test.ts +190 -0
  316. package/src/utils/codexHome.ts +191 -0
  317. package/src/utils/codexOAuth.ts +147 -0
  318. package/src/utils/codexRefreshDetect.test.ts +85 -0
  319. package/src/utils/codexRefreshDetect.ts +35 -0
  320. package/src/utils/diffCoverage.test.ts +468 -0
  321. package/src/utils/diffCoverage.ts +404 -0
  322. package/src/utils/errorReport.test.ts +135 -0
  323. package/src/utils/errorReport.ts +83 -0
  324. package/src/utils/exitHandler.ts +35 -0
  325. package/src/utils/fixDoubleEscapedString.ts +9 -0
  326. package/src/utils/ghaCore.ts +13 -0
  327. package/src/utils/gitAuth.test.ts +322 -0
  328. package/src/utils/gitAuth.ts +263 -0
  329. package/src/utils/gitAuthServer.test.ts +260 -0
  330. package/src/utils/gitAuthServer.ts +182 -0
  331. package/src/utils/github.test.ts +615 -0
  332. package/src/utils/github.ts +538 -0
  333. package/src/utils/globals.ts +9 -0
  334. package/src/utils/humanEditCapture.test.ts +100 -0
  335. package/src/utils/humanEditCapture.ts +193 -0
  336. package/src/utils/install.test.ts +768 -0
  337. package/src/utils/install.ts +492 -0
  338. package/src/utils/instructions.test.ts +240 -0
  339. package/src/utils/instructions.ts +543 -0
  340. package/src/utils/leapingComment.test.ts +51 -0
  341. package/src/utils/leapingComment.ts +18 -0
  342. package/src/utils/learnings.test.ts +87 -0
  343. package/src/utils/learnings.ts +138 -0
  344. package/src/utils/learningsTocRender.test.ts +116 -0
  345. package/src/utils/learningsTruncate.test.ts +39 -0
  346. package/src/utils/learningsTruncate.ts +42 -0
  347. package/src/utils/lifecycle.test.ts +195 -0
  348. package/src/utils/lifecycle.ts +198 -0
  349. package/src/utils/log.test.ts +402 -0
  350. package/src/utils/log.ts +432 -0
  351. package/src/utils/normalizeEnv.test.ts +91 -0
  352. package/src/utils/normalizeEnv.ts +106 -0
  353. package/src/utils/openCodeModels.ts +82 -0
  354. package/src/utils/overrides.test.ts +89 -0
  355. package/src/utils/overrides.ts +98 -0
  356. package/src/utils/packageManager.test.ts +321 -0
  357. package/src/utils/packageManager.ts +257 -0
  358. package/src/utils/patchWorkflowRunFields.test.ts +92 -0
  359. package/src/utils/patchWorkflowRunFields.ts +150 -0
  360. package/src/utils/payload.test.ts +497 -0
  361. package/src/utils/payload.ts +371 -0
  362. package/src/utils/postApiFetch.ts +51 -0
  363. package/src/utils/prSummary.test.ts +224 -0
  364. package/src/utils/prSummary.ts +147 -0
  365. package/src/utils/progressComment.ts +261 -0
  366. package/src/utils/providerErrors.test.ts +315 -0
  367. package/src/utils/providerErrors.ts +172 -0
  368. package/src/utils/rangeDiff.test.ts +236 -0
  369. package/src/utils/rangeDiff.ts +182 -0
  370. package/src/utils/remediationCommand.test.ts +163 -0
  371. package/src/utils/remediationCommand.ts +119 -0
  372. package/src/utils/retry.test.ts +153 -0
  373. package/src/utils/retry.ts +58 -0
  374. package/src/utils/reviewCleanup.ts +106 -0
  375. package/src/utils/run.ts +99 -0
  376. package/src/utils/runContext.ts +145 -0
  377. package/src/utils/runContextData.ts +58 -0
  378. package/src/utils/runErrorRenderer.test.ts +95 -0
  379. package/src/utils/runErrorRenderer.ts +259 -0
  380. package/src/utils/runFixture.ts +76 -0
  381. package/src/utils/runLifecycle.ts +237 -0
  382. package/src/utils/runStartupLog.ts +60 -0
  383. package/src/utils/secrets.test.ts +103 -0
  384. package/src/utils/secrets.ts +177 -0
  385. package/src/utils/setup.test.ts +509 -0
  386. package/src/utils/setup.ts +352 -0
  387. package/src/utils/shell.ts +103 -0
  388. package/src/utils/skills.test.ts +46 -0
  389. package/src/utils/skills.ts +67 -0
  390. package/src/utils/subprocess.test.ts +170 -0
  391. package/src/utils/subprocess.ts +438 -0
  392. package/src/utils/terraformMcp.test.ts +63 -0
  393. package/src/utils/terraformMcp.ts +83 -0
  394. package/src/utils/time.test.ts +105 -0
  395. package/src/utils/time.ts +59 -0
  396. package/src/utils/timer.test.ts +91 -0
  397. package/src/utils/timer.ts +72 -0
  398. package/src/utils/todoTracking.test.ts +223 -0
  399. package/src/utils/todoTracking.ts +167 -0
  400. package/src/utils/token.test.ts +239 -0
  401. package/src/utils/token.ts +186 -0
  402. package/src/utils/version.ts +10 -0
  403. package/src/utils/versioning.test.ts +34 -0
  404. package/src/utils/versioning.ts +44 -0
  405. package/src/utils/vertex.ts +85 -0
  406. package/src/utils/workflow.ts +25 -0
@@ -0,0 +1,82 @@
1
+ // OpenCode-as-source-of-truth for BYOK detection.
2
+ //
3
+ // `opencode models` returns the `provider/model` specifiers that OpenCode
4
+ // can actually route given the current env (workflow env block + GH Actions
5
+ // secrets) and `auth.json` (Codex / future managed credentials). This is
6
+ // authoritative — strictly more accurate than the static
7
+ // `provider.envVars + provider.managedCredentials` catalog in `models.ts`
8
+ // for the "do we have BYOK auth?" gate. The catalog can (and will) miss
9
+ // new auth shapes; OpenCode itself can't.
10
+ //
11
+ // Two captures per run:
12
+ // 1. `captureBaselineModels` — called BEFORE Codex `auth.json` is
13
+ // materialized. The set OpenCode can serve from the runner's pre-existing
14
+ // environment alone (workflow `env:` block + GH Actions secrets).
15
+ // 2. `captureAuthorizedModels` — called AFTER Codex `auth.json`
16
+ // materialization. The authoritative set for BYOK decisions (fallback +
17
+ // validateAgentApiKey).
18
+ //
19
+ // The set difference (`authorized - baseline`) is the contribution of the
20
+ // Codex OAuth credential to this run — logged once for operator visibility.
21
+ //
22
+ // Memoized at module scope so the two consumers
23
+ // (`selectFallbackModelIfNeeded` + `autoSelectModel`) share one shell-out.
24
+
25
+ import { execFileSync } from "node:child_process";
26
+ import { log } from "#app/utils/cli";
27
+
28
+ let baseline: Set<string> | undefined;
29
+ let authorized: Set<string> | undefined;
30
+
31
+ function readModels(cliPath: string): Set<string> {
32
+ try {
33
+ const output = execFileSync(cliPath, ["models"], {
34
+ encoding: "utf-8",
35
+ timeout: 30_000,
36
+ env: process.env,
37
+ });
38
+ return new Set(
39
+ output
40
+ .split("\n")
41
+ .map((line) => line.trim())
42
+ .filter(Boolean),
43
+ );
44
+ } catch (error) {
45
+ log.debug(
46
+ `» \`opencode models\` failed: ${error instanceof Error ? error.message : String(error)}`,
47
+ );
48
+ return new Set();
49
+ }
50
+ }
51
+
52
+ /** Snapshot the set of models OpenCode can serve from the current env, BEFORE
53
+ * Terramend-stored credentials are merged in. Call once early in `main.ts`. */
54
+ export function captureBaselineModels(cliPath: string): void {
55
+ baseline = readModels(cliPath);
56
+ log.debug(`» opencode baseline: ${baseline.size} models`);
57
+ }
58
+
59
+ /** Snapshot the set of models OpenCode can serve AFTER dbSecrets +
60
+ * Codex auth.json are in place. Logs the diff against the baseline as
61
+ * `» BYOK auth enabled N model(s): …`. */
62
+ export function captureAuthorizedModels(cliPath: string): void {
63
+ authorized = readModels(cliPath);
64
+ const base = baseline;
65
+ if (base) {
66
+ const diff = [...authorized].filter((m) => !base.has(m));
67
+ if (diff.length > 0) {
68
+ log.info(`» BYOK auth enabled ${diff.length} model(s): ${diff.join(", ")}`);
69
+ }
70
+ }
71
+ log.debug(`» opencode authorized: ${authorized.size} models`);
72
+ }
73
+
74
+ /** Authorized set captured after Terramend-stored auth is applied. Throws if
75
+ * called before `captureAuthorizedModels` — the call sites (fallback gate,
76
+ * api-key validation, auto-select) all run strictly after capture. */
77
+ export function getAuthorizedModels(): Set<string> {
78
+ if (!authorized) {
79
+ throw new Error("getAuthorizedModels called before captureAuthorizedModels");
80
+ }
81
+ return authorized;
82
+ }
@@ -0,0 +1,89 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { applyOverrides, DENIED_OVERRIDE_NAMES, parseOverrides } from "#app/utils/overrides";
3
+
4
+ vi.mock("@actions/core", () => ({
5
+ setSecret: vi.fn(),
6
+ }));
7
+
8
+ import * as core from "@actions/core";
9
+
10
+ const setSecretMock = vi.mocked(core.setSecret);
11
+
12
+ beforeEach(() => {
13
+ vi.clearAllMocks();
14
+ });
15
+
16
+ describe("parseOverrides", () => {
17
+ it("returns an empty object for empty or whitespace input", () => {
18
+ expect(parseOverrides("")).toEqual({});
19
+ expect(parseOverrides(" \n\t ")).toEqual({});
20
+ });
21
+
22
+ it("parses a valid JSON object of string values", () => {
23
+ expect(parseOverrides('{"A":"1","B":""}')).toEqual({ A: "1", B: "" });
24
+ });
25
+
26
+ it("throws on invalid JSON", () => {
27
+ expect(() => parseOverrides("{nope")).toThrow(/invalid UNSAFE_OVERRIDES: not valid JSON/);
28
+ });
29
+
30
+ it("throws on non-object JSON", () => {
31
+ expect(() => parseOverrides('"string"')).toThrow(/must be a JSON object/);
32
+ expect(() => parseOverrides("null")).toThrow(/must be a JSON object/);
33
+ expect(() => parseOverrides('["a"]')).toThrow(/must be a JSON object/);
34
+ });
35
+
36
+ it("throws when a value is not a string", () => {
37
+ expect(() => parseOverrides('{"A":42}')).toThrow(/key "A" must have a string value/);
38
+ });
39
+ });
40
+
41
+ describe("applyOverrides", () => {
42
+ it("applies overrides, masks values, and strips the raw input var", () => {
43
+ const env: NodeJS.ProcessEnv = { UNSAFE_OVERRIDES: '{"MY_KEY":"secret-value"}' };
44
+
45
+ const result = applyOverrides({ raw: '{"MY_KEY":"secret-value"}', env });
46
+
47
+ expect(result).toEqual({ applied: ["MY_KEY"], denied: [] });
48
+ expect(env.MY_KEY).toBe("secret-value");
49
+ expect(env.UNSAFE_OVERRIDES).toBeUndefined();
50
+ expect(setSecretMock).toHaveBeenCalledWith("secret-value");
51
+ });
52
+
53
+ it("refuses denied names while applying the rest", () => {
54
+ const env: NodeJS.ProcessEnv = {};
55
+
56
+ const result = applyOverrides({
57
+ raw: '{"GITHUB_TOKEN":"stolen","ANTHROPIC_API_KEY":"sk-test"}',
58
+ env,
59
+ });
60
+
61
+ expect(result.denied).toEqual(["GITHUB_TOKEN"]);
62
+ expect(result.applied).toEqual(["ANTHROPIC_API_KEY"]);
63
+ expect(env.GITHUB_TOKEN).toBeUndefined();
64
+ expect(env.ANTHROPIC_API_KEY).toBe("sk-test");
65
+ });
66
+
67
+ it("does not register empty values as secrets", () => {
68
+ const env: NodeJS.ProcessEnv = {};
69
+
70
+ const result = applyOverrides({ raw: '{"EMPTY":""}', env });
71
+
72
+ expect(result.applied).toEqual(["EMPTY"]);
73
+ expect(env.EMPTY).toBe("");
74
+ expect(setSecretMock).not.toHaveBeenCalled();
75
+ });
76
+
77
+ it("denies every name on the deny list", () => {
78
+ const raw = JSON.stringify(
79
+ Object.fromEntries(Array.from(DENIED_OVERRIDE_NAMES, (name) => [name, "x"])),
80
+ );
81
+ const env: NodeJS.ProcessEnv = {};
82
+
83
+ const result = applyOverrides({ raw, env });
84
+
85
+ expect(result.applied).toEqual([]);
86
+ expect(result.denied.sort()).toEqual(Array.from(DENIED_OVERRIDE_NAMES).sort());
87
+ expect(setSecretMock).not.toHaveBeenCalled();
88
+ });
89
+ });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Parse + apply the action's `unsafe_overrides` input — a JSON object of env
3
+ * var overrides that mutate `process.env` at the start of a run. Designed for
4
+ * e2e testing / debugging from `workflow_dispatch`; only callers with
5
+ * `actions:write` on the repo can supply it.
6
+ *
7
+ * The `unsafe` prefix is load-bearing: GH Actions echoes the value verbatim
8
+ * in the runner's step-header log, so the raw JSON (including any values
9
+ * passed in) is visible to anyone with `actions:read` on the calling repo.
10
+ * Treat the run log as compromised for any value placed in `unsafe_overrides`.
11
+ */
12
+
13
+ import * as core from "@actions/core";
14
+
15
+ /**
16
+ * Names refused even when present in the input. Overriding these would let a
17
+ * caller escape terramend's scope (GITHUB_TOKEN), break runner internals
18
+ * (ACTIONS_RUNTIME_*), forge OIDC tokens (ACTIONS_ID_TOKEN_REQUEST_*), or
19
+ * substitute our server-side auth (TERRAMEND_API_SECRET). Customer-facing
20
+ * provider keys (ANTHROPIC_API_KEY, OPENAI_API_KEY, CLAUDE_CODE_OAUTH_TOKEN,
21
+ * etc.) are intentionally NOT denied — overriding those is the use case.
22
+ */
23
+ export const DENIED_OVERRIDE_NAMES: ReadonlySet<string> = new Set([
24
+ "GITHUB_TOKEN",
25
+ "GH_TOKEN",
26
+ "ACTIONS_RUNTIME_TOKEN",
27
+ "ACTIONS_RUNTIME_URL",
28
+ "ACTIONS_ID_TOKEN_REQUEST_URL",
29
+ "ACTIONS_ID_TOKEN_REQUEST_TOKEN",
30
+ "ACTIONS_CACHE_URL",
31
+ "TERRAMEND_API_SECRET",
32
+ "VERCEL_AUTOMATION_BYPASS_SECRET",
33
+ ]);
34
+
35
+ export interface ApplyOverridesResult {
36
+ applied: string[];
37
+ denied: string[];
38
+ }
39
+
40
+ /** Parse the JSON input. Returns `{}` for empty/whitespace. Throws on shape errors. */
41
+ export function parseOverrides(raw: string): Record<string, string> {
42
+ const trimmed = raw.trim();
43
+ if (!trimmed) return {};
44
+
45
+ let parsed: unknown;
46
+ try {
47
+ parsed = JSON.parse(trimmed);
48
+ } catch (err) {
49
+ throw new Error(
50
+ `invalid UNSAFE_OVERRIDES: not valid JSON (${err instanceof Error ? err.message : String(err)})`,
51
+ );
52
+ }
53
+
54
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
55
+ throw new Error(`invalid UNSAFE_OVERRIDES: must be a JSON object`);
56
+ }
57
+
58
+ const out: Record<string, string> = {};
59
+ for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
60
+ if (typeof value !== "string") {
61
+ throw new Error(
62
+ `invalid UNSAFE_OVERRIDES: key "${key}" must have a string value (got ${typeof value})`,
63
+ );
64
+ }
65
+ out[key] = value;
66
+ }
67
+ return out;
68
+ }
69
+
70
+ /**
71
+ * Mutate `params.env` in place with the supplied JSON overrides, skipping any
72
+ * names in `DENIED_OVERRIDE_NAMES`. Each applied value is registered with
73
+ * `core.setSecret` so the runner masks it in subsequent log output, and the
74
+ * raw `UNSAFE_OVERRIDES` env var is deleted so spawned subprocesses don't
75
+ * inherit the original JSON (which would defeat both the deny-list and the
76
+ * masking by exposing the values verbatim).
77
+ *
78
+ * Returns the applied/denied breakdown so the caller can render an audit log.
79
+ */
80
+ export function applyOverrides(params: {
81
+ raw: string;
82
+ env: NodeJS.ProcessEnv;
83
+ }): ApplyOverridesResult {
84
+ const overrides = parseOverrides(params.raw);
85
+ const applied: string[] = [];
86
+ const denied: string[] = [];
87
+ for (const [key, value] of Object.entries(overrides)) {
88
+ if (DENIED_OVERRIDE_NAMES.has(key)) {
89
+ denied.push(key);
90
+ continue;
91
+ }
92
+ if (value.length > 0) core.setSecret(value);
93
+ params.env[key] = value;
94
+ applied.push(key);
95
+ }
96
+ delete params.env.UNSAFE_OVERRIDES;
97
+ return { applied, denied };
98
+ }
@@ -0,0 +1,321 @@
1
+ import { delimiter, join } from "node:path";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import {
4
+ ensurePackageManager,
5
+ type PackageManagerSpec,
6
+ packageManagerBinDir,
7
+ resolvePackageManagerSpec,
8
+ } from "#app/utils/packageManager";
9
+
10
+ vi.mock("node:fs", () => ({
11
+ existsSync: vi.fn(() => true),
12
+ }));
13
+
14
+ vi.mock("node:fs/promises", () => ({
15
+ mkdir: vi.fn(async () => undefined),
16
+ readFile: vi.fn(async () => "{}"),
17
+ }));
18
+
19
+ vi.mock("#app/utils/cli", () => ({
20
+ log: {
21
+ info: vi.fn(),
22
+ debug: vi.fn(),
23
+ warning: vi.fn(),
24
+ error: vi.fn(),
25
+ success: vi.fn(),
26
+ },
27
+ }));
28
+
29
+ vi.mock("#app/utils/subprocess", () => ({
30
+ spawn: vi.fn(),
31
+ }));
32
+
33
+ import { existsSync } from "node:fs";
34
+ import { readFile } from "node:fs/promises";
35
+ import { log } from "#app/utils/cli";
36
+ import { spawn } from "#app/utils/subprocess";
37
+
38
+ const existsSyncMock = vi.mocked(existsSync);
39
+ const readFileMock = vi.mocked(readFile);
40
+ const spawnMock = vi.mocked(spawn);
41
+ const warningMock = vi.mocked(log.warning);
42
+
43
+ function spawnResult(init: { exitCode?: number; stdout?: string; stderr?: string } = {}) {
44
+ return {
45
+ exitCode: init.exitCode ?? 0,
46
+ stdout: init.stdout ?? "",
47
+ stderr: init.stderr ?? "",
48
+ durationMs: 1,
49
+ };
50
+ }
51
+
52
+ function mockPackageJson(pkg: unknown): void {
53
+ existsSyncMock.mockReturnValue(true);
54
+ readFileMock.mockResolvedValue(JSON.stringify(pkg));
55
+ }
56
+
57
+ describe("resolvePackageManagerSpec", () => {
58
+ beforeEach(() => {
59
+ vi.clearAllMocks();
60
+ });
61
+
62
+ it("returns null when package.json does not exist", async () => {
63
+ existsSyncMock.mockReturnValue(false);
64
+ await expect(resolvePackageManagerSpec("/repo")).resolves.toBeNull();
65
+ expect(existsSyncMock).toHaveBeenCalledWith(join("/repo", "package.json"));
66
+ });
67
+
68
+ it("returns null and warns when package.json is unparseable", async () => {
69
+ existsSyncMock.mockReturnValue(true);
70
+ readFileMock.mockResolvedValue("{nope");
71
+ await expect(resolvePackageManagerSpec("/repo")).resolves.toBeNull();
72
+ expect(warningMock).toHaveBeenCalledWith(expect.stringContaining("failed to parse"));
73
+ });
74
+
75
+ it("returns null when neither field is declared", async () => {
76
+ mockPackageJson({});
77
+ await expect(resolvePackageManagerSpec("/repo")).resolves.toBeNull();
78
+ });
79
+
80
+ it("parses packageManager field and strips the integrity hash", async () => {
81
+ mockPackageJson({ packageManager: "pnpm@11.1.1+sha512.abcdef" });
82
+ await expect(resolvePackageManagerSpec("/repo")).resolves.toEqual({
83
+ name: "pnpm",
84
+ version: "11.1.1",
85
+ concrete: true,
86
+ source: "packageManager",
87
+ });
88
+ });
89
+
90
+ it("flags range versions as non-concrete", async () => {
91
+ mockPackageJson({ packageManager: "yarn@^4.0.0" });
92
+ await expect(resolvePackageManagerSpec("/repo")).resolves.toEqual({
93
+ name: "yarn",
94
+ version: "^4.0.0",
95
+ concrete: false,
96
+ source: "packageManager",
97
+ });
98
+ });
99
+
100
+ it("rejects unsupported packageManager names with a warning", async () => {
101
+ mockPackageJson({ packageManager: "lerna@9.0.0" });
102
+ await expect(resolvePackageManagerSpec("/repo")).resolves.toBeNull();
103
+ expect(warningMock).toHaveBeenCalledWith(expect.stringContaining("unknown packageManager"));
104
+ });
105
+
106
+ it("rejects a packageManager value without a name", async () => {
107
+ mockPackageJson({ packageManager: "@11.0.0" });
108
+ await expect(resolvePackageManagerSpec("/repo")).resolves.toBeNull();
109
+ });
110
+
111
+ it("parses devEngines.packageManager", async () => {
112
+ mockPackageJson({ devEngines: { packageManager: { name: "pnpm", version: " 11.0.0 " } } });
113
+ await expect(resolvePackageManagerSpec("/repo")).resolves.toEqual({
114
+ name: "pnpm",
115
+ version: "11.0.0",
116
+ concrete: true,
117
+ source: "devEngines",
118
+ });
119
+ });
120
+
121
+ it("ignores devEngines entries missing name or version", async () => {
122
+ mockPackageJson({ devEngines: { packageManager: { name: "pnpm" } } });
123
+ await expect(resolvePackageManagerSpec("/repo")).resolves.toBeNull();
124
+ });
125
+
126
+ it("rejects unsupported devEngines names with a warning", async () => {
127
+ mockPackageJson({ devEngines: { packageManager: { name: "vlt", version: "1.0.0" } } });
128
+ await expect(resolvePackageManagerSpec("/repo")).resolves.toBeNull();
129
+ expect(warningMock).toHaveBeenCalledWith(
130
+ expect.stringContaining("unknown devEngines.packageManager.name"),
131
+ );
132
+ });
133
+
134
+ it("prefers devEngines when the two fields name different managers", async () => {
135
+ mockPackageJson({
136
+ packageManager: "yarn@4.0.0",
137
+ devEngines: { packageManager: { name: "pnpm", version: "^11.0.0" } },
138
+ });
139
+ const spec = await resolvePackageManagerSpec("/repo");
140
+ expect(spec?.name).toBe("pnpm");
141
+ expect(spec?.source).toBe("devEngines");
142
+ expect(warningMock).toHaveBeenCalledWith(expect.stringContaining("disagrees"));
143
+ });
144
+
145
+ it("uses a concrete devEngines version, warning when packageManager disagrees", async () => {
146
+ mockPackageJson({
147
+ packageManager: "pnpm@11.2.0",
148
+ devEngines: { packageManager: { name: "pnpm", version: "11.1.0" } },
149
+ });
150
+ const spec = await resolvePackageManagerSpec("/repo");
151
+ expect(spec?.version).toBe("11.1.0");
152
+ expect(spec?.source).toBe("devEngines");
153
+ expect(warningMock).toHaveBeenCalledWith(expect.stringContaining("disagrees"));
154
+ });
155
+
156
+ it("keeps a concrete devEngines version without warning when both agree", async () => {
157
+ mockPackageJson({
158
+ packageManager: "pnpm@11.1.0",
159
+ devEngines: { packageManager: { name: "pnpm", version: "11.1.0" } },
160
+ });
161
+ const spec = await resolvePackageManagerSpec("/repo");
162
+ expect(spec?.version).toBe("11.1.0");
163
+ expect(warningMock).not.toHaveBeenCalled();
164
+ });
165
+
166
+ it("prefers a concrete packageManager that satisfies the devEngines range", async () => {
167
+ mockPackageJson({
168
+ packageManager: "pnpm@11.2.3",
169
+ devEngines: { packageManager: { name: "pnpm", version: "^11.0.0" } },
170
+ });
171
+ const spec = await resolvePackageManagerSpec("/repo");
172
+ expect(spec).toEqual({
173
+ name: "pnpm",
174
+ version: "11.2.3",
175
+ concrete: true,
176
+ source: "packageManager",
177
+ });
178
+ });
179
+
180
+ it("falls back to devEngines when packageManager does not satisfy the range", async () => {
181
+ mockPackageJson({
182
+ packageManager: "pnpm@10.0.0",
183
+ devEngines: { packageManager: { name: "pnpm", version: "^11.0.0" } },
184
+ });
185
+ const spec = await resolvePackageManagerSpec("/repo");
186
+ expect(spec?.source).toBe("devEngines");
187
+ expect(spec?.version).toBe("^11.0.0");
188
+ expect(warningMock).toHaveBeenCalledWith(expect.stringContaining("does not satisfy"));
189
+ });
190
+
191
+ it("falls back to devEngines when both are ranges without warning", async () => {
192
+ mockPackageJson({
193
+ packageManager: "pnpm@^11.0.0",
194
+ devEngines: { packageManager: { name: "pnpm", version: "^11.0.0" } },
195
+ });
196
+ const spec = await resolvePackageManagerSpec("/repo");
197
+ expect(spec?.source).toBe("devEngines");
198
+ expect(warningMock).not.toHaveBeenCalled();
199
+ });
200
+
201
+ it("falls back to packageManager when devEngines is absent", async () => {
202
+ mockPackageJson({ packageManager: "npm@10.9.0" });
203
+ const spec = await resolvePackageManagerSpec("/repo");
204
+ expect(spec?.name).toBe("npm");
205
+ expect(spec?.source).toBe("packageManager");
206
+ });
207
+ });
208
+
209
+ describe("packageManagerBinDir", () => {
210
+ it("nests pm-bin under the run tmpdir", () => {
211
+ expect(packageManagerBinDir("/tmp/run")).toBe(join("/tmp/run", "pm-bin"));
212
+ });
213
+ });
214
+
215
+ describe("ensurePackageManager", () => {
216
+ const binDir = join("/tmp/run", "pm-bin");
217
+ let originalPath: string | undefined;
218
+
219
+ function spec(overrides: Partial<PackageManagerSpec> = {}): PackageManagerSpec {
220
+ return {
221
+ name: "pnpm",
222
+ version: "11.1.1",
223
+ concrete: true,
224
+ source: "packageManager",
225
+ ...overrides,
226
+ };
227
+ }
228
+
229
+ beforeEach(() => {
230
+ vi.clearAllMocks();
231
+ originalPath = process.env.PATH;
232
+ });
233
+
234
+ afterEach(() => {
235
+ process.env.PATH = originalPath;
236
+ });
237
+
238
+ it("returns true for npm without spawning anything", async () => {
239
+ await expect(ensurePackageManager({ spec: spec({ name: "npm" }), binDir })).resolves.toBe(true);
240
+ expect(spawnMock).not.toHaveBeenCalled();
241
+ });
242
+
243
+ it("returns false for managers corepack does not ship (bun)", async () => {
244
+ await expect(ensurePackageManager({ spec: spec({ name: "bun" }), binDir })).resolves.toBe(
245
+ false,
246
+ );
247
+ expect(spawnMock).not.toHaveBeenCalled();
248
+ });
249
+
250
+ it("returns false for range versions with a warning", async () => {
251
+ const result = await ensurePackageManager({
252
+ spec: spec({ version: "^11.0.0", concrete: false }),
253
+ binDir,
254
+ });
255
+ expect(result).toBe(false);
256
+ expect(warningMock).toHaveBeenCalledWith(expect.stringContaining("requires a concrete pin"));
257
+ expect(spawnMock).not.toHaveBeenCalled();
258
+ });
259
+
260
+ it("short-circuits when the requested version is already active", async () => {
261
+ spawnMock.mockResolvedValueOnce(spawnResult({ stdout: "11.1.1\n" }));
262
+ await expect(ensurePackageManager({ spec: spec(), binDir })).resolves.toBe(true);
263
+ expect(spawnMock).toHaveBeenCalledTimes(1);
264
+ expect(spawnMock).toHaveBeenCalledWith(
265
+ expect.objectContaining({ cmd: "pnpm", args: ["--version"] }),
266
+ );
267
+ });
268
+
269
+ it("returns false when corepack enable fails", async () => {
270
+ spawnMock
271
+ .mockResolvedValueOnce(spawnResult({ exitCode: 1 })) // pnpm --version
272
+ .mockResolvedValueOnce(spawnResult({ exitCode: 1, stderr: "enable broke" })); // enable
273
+ await expect(ensurePackageManager({ spec: spec(), binDir })).resolves.toBe(false);
274
+ expect(warningMock).toHaveBeenCalledWith(expect.stringContaining("corepack enable failed"));
275
+ });
276
+
277
+ it("returns false when corepack prepare fails, after prepending binDir to PATH", async () => {
278
+ process.env.PATH = "/usr/bin";
279
+ spawnMock
280
+ .mockResolvedValueOnce(spawnResult({ stdout: "10.0.0\n" })) // wrong version active
281
+ .mockResolvedValueOnce(spawnResult()) // enable ok
282
+ .mockResolvedValueOnce(spawnResult({ exitCode: 1, stderr: "" })); // prepare fails
283
+ await expect(ensurePackageManager({ spec: spec(), binDir })).resolves.toBe(false);
284
+ expect(process.env.PATH).toBe(`${binDir}${delimiter}/usr/bin`);
285
+ expect(warningMock).toHaveBeenCalledWith(expect.stringContaining("(empty)"));
286
+ });
287
+
288
+ it("returns true on full corepack success and verifies the resolved version", async () => {
289
+ spawnMock
290
+ .mockResolvedValueOnce(spawnResult({ exitCode: 1 })) // not on PATH yet
291
+ .mockImplementationOnce(async (call) => {
292
+ // exercise the corepack stream-forwarding callbacks
293
+ call.onStdout?.("");
294
+ call.onStderr?.("");
295
+ return spawnResult(); // enable ok
296
+ })
297
+ .mockResolvedValueOnce(spawnResult()) // prepare ok
298
+ .mockResolvedValueOnce(spawnResult({ stdout: "11.1.1\n" })); // verify
299
+ await expect(ensurePackageManager({ spec: spec(), binDir })).resolves.toBe(true);
300
+ expect(warningMock).not.toHaveBeenCalled();
301
+ expect(spawnMock).toHaveBeenCalledWith(
302
+ expect.objectContaining({
303
+ cmd: "corepack",
304
+ args: ["enable", "--install-directory", binDir, "pnpm"],
305
+ }),
306
+ );
307
+ expect(spawnMock).toHaveBeenCalledWith(
308
+ expect.objectContaining({ cmd: "corepack", args: ["prepare", "pnpm@11.1.1", "--activate"] }),
309
+ );
310
+ });
311
+
312
+ it("returns true but warns when PATH still resolves to another version", async () => {
313
+ spawnMock
314
+ .mockResolvedValueOnce(spawnResult({ exitCode: 1 })) // not on PATH yet
315
+ .mockResolvedValueOnce(spawnResult()) // enable ok
316
+ .mockResolvedValueOnce(spawnResult()) // prepare ok
317
+ .mockResolvedValueOnce(spawnResult({ exitCode: 1 })); // verify fails → null
318
+ await expect(ensurePackageManager({ spec: spec(), binDir })).resolves.toBe(true);
319
+ expect(warningMock).toHaveBeenCalledWith(expect.stringContaining("(missing)"));
320
+ });
321
+ });