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,432 @@
1
+ /**
2
+ * Logging utilities that work well in both local and GitHub Actions environments
3
+ */
4
+
5
+ import { AsyncLocalStorage } from "node:async_hooks";
6
+ import * as core from "@actions/core";
7
+ import { table } from "table";
8
+ import { type AgentUsage, formatCostUsd } from "#app/agents/shared";
9
+ import { isGitHubActions, isInsideDocker } from "#app/utils/globals";
10
+
11
+ // --- log prefix via AsyncLocalStorage ---
12
+
13
+ type LogContext = { prefix: string };
14
+
15
+ const logContext = new AsyncLocalStorage<LogContext>();
16
+
17
+ const MAGENTA = "\x1b[35m";
18
+ const RESET = "\x1b[0m";
19
+
20
+ /** run `fn` with every log line prefixed by `prefix` (e.g. "[task-label]") in magenta */
21
+ export function withLogPrefix<T>(prefix: string, fn: () => Promise<T>): Promise<T> {
22
+ return logContext.run({ prefix }, fn);
23
+ }
24
+
25
+ function prefixLines(message: string): string {
26
+ const ctx = logContext.getStore();
27
+ if (!ctx) return message;
28
+ const colored = `${MAGENTA}${ctx.prefix}${RESET} `;
29
+ return message
30
+ .split("\n")
31
+ .map((line) => `${colored}${line}`)
32
+ .join("\n");
33
+ }
34
+
35
+ /** plain-text prefix (no ANSI) for GitHub Actions group names */
36
+ function prefixPlain(name: string): string {
37
+ const ctx = logContext.getStore();
38
+ if (!ctx) return name;
39
+ return `${ctx.prefix} ${name}`;
40
+ }
41
+
42
+ // --- log sink ----------------------------------------------------------------
43
+
44
+ type LogSink = "actions" | "stderr";
45
+
46
+ let sink: LogSink = "actions";
47
+
48
+ /**
49
+ * Route ALL log output to stderr instead of `@actions/core` (which writes to
50
+ * stdout). Required before starting a stdio MCP server: stdout is the JSON-RPC
51
+ * channel there, and a single stray diagnostic line corrupts the framing.
52
+ * The GitHub Action path never calls this — its sink stays `@actions/core`.
53
+ */
54
+ export function setLogSink(next: LogSink): void {
55
+ sink = next;
56
+ }
57
+
58
+ /** info-level line via the active sink. */
59
+ function emitInfo(line: string): void {
60
+ if (sink === "stderr") {
61
+ process.stderr.write(`${line}\n`);
62
+ return;
63
+ }
64
+ core.info(line);
65
+ }
66
+
67
+ const isRunnerDebugEnabled = () => core.isDebug();
68
+
69
+ const isLocalDebugEnabled = () =>
70
+ process.env.LOG_LEVEL === "debug" || process.env.ACTIONS_STEP_DEBUG === "true";
71
+
72
+ const isDebugEnabled = () => isLocalDebugEnabled() || isRunnerDebugEnabled();
73
+
74
+ /** timestamp prefix for debug mode — empty string when debug is off */
75
+ function ts(): string {
76
+ return isDebugEnabled() ? `[${new Date().toISOString()}] ` : "";
77
+ }
78
+
79
+ /**
80
+ * Format arguments into a single string for logging
81
+ */
82
+ function formatArgs(args: unknown[]): string {
83
+ return args
84
+ .map((arg) => {
85
+ if (typeof arg === "string") return arg;
86
+ if (arg instanceof Error) return `${arg.message}\n${arg.stack}`;
87
+ return JSON.stringify(arg);
88
+ })
89
+ .join(" ");
90
+ }
91
+
92
+ /**
93
+ * Start a collapsed group (GitHub Actions) or regular group (local)
94
+ */
95
+ function startGroup(name: string): void {
96
+ const prefixed = prefixPlain(name);
97
+ if (sink === "stderr") {
98
+ emitInfo(`▼ ${prefixed}`);
99
+ return;
100
+ }
101
+ if (isGitHubActions) {
102
+ core.startGroup(prefixed);
103
+ } else {
104
+ console.group(prefixed);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * End a collapsed group
110
+ */
111
+ function endGroup(): void {
112
+ if (sink === "stderr") return;
113
+ if (isGitHubActions) {
114
+ core.endGroup();
115
+ } else {
116
+ console.groupEnd();
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Run a callback within a collapsed group
122
+ */
123
+ function group(name: string, fn: () => void): void {
124
+ startGroup(name);
125
+ fn();
126
+ endGroup();
127
+ }
128
+
129
+ /**
130
+ * Print a formatted box with text (for console output)
131
+ */
132
+ function boxString(
133
+ text: string,
134
+ options?: {
135
+ title?: string;
136
+ maxWidth?: number;
137
+ indent?: string;
138
+ padding?: number;
139
+ },
140
+ ): string {
141
+ const { title, maxWidth = 80, indent = "", padding = 1 } = options || {};
142
+
143
+ const lines = text.trim().split("\n");
144
+ const wrappedLines: string[] = [];
145
+
146
+ for (const line of lines) {
147
+ if (line.length <= maxWidth - padding * 2) {
148
+ wrappedLines.push(line);
149
+ } else {
150
+ const words = line.split(" ");
151
+ let currentLine = "";
152
+
153
+ for (const word of words) {
154
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
155
+ if (testLine.length <= maxWidth - padding * 2) {
156
+ currentLine = testLine;
157
+ } else {
158
+ if (currentLine) {
159
+ wrappedLines.push(currentLine);
160
+ currentLine = "";
161
+ }
162
+ // wrap long words by breaking them into chunks
163
+ const maxLineLength = maxWidth - padding * 2;
164
+ let remainingWord = word;
165
+ while (remainingWord.length > maxLineLength) {
166
+ wrappedLines.push(remainingWord.substring(0, maxLineLength));
167
+ remainingWord = remainingWord.substring(maxLineLength);
168
+ }
169
+ currentLine = remainingWord;
170
+ }
171
+ }
172
+
173
+ if (currentLine) {
174
+ wrappedLines.push(currentLine);
175
+ }
176
+ }
177
+ }
178
+
179
+ const maxLineLength = Math.max(...wrappedLines.map((line) => line.length));
180
+ const contentBoxWidth = maxLineLength + padding * 2;
181
+
182
+ // ensure box width is at least as wide as the title line when title exists
183
+ const titleLineLength = title ? ` ${title} `.length : 0;
184
+ const boxWidth = Math.max(contentBoxWidth, titleLineLength);
185
+
186
+ let result = "";
187
+
188
+ if (title) {
189
+ const titleLine = ` ${title} `;
190
+ const titlePadding = Math.max(0, boxWidth - titleLine.length);
191
+ result += `${indent}┌${titleLine}${"─".repeat(titlePadding)}┐\n`;
192
+ }
193
+
194
+ if (!title) {
195
+ result += `${indent}┌${"─".repeat(boxWidth)}┐\n`;
196
+ }
197
+
198
+ for (const line of wrappedLines) {
199
+ const paddedLine = line.padEnd(maxLineLength);
200
+ result += `${indent}│${" ".repeat(padding)}${paddedLine}${" ".repeat(padding)}│\n`;
201
+ }
202
+
203
+ result += `${indent}└${"─".repeat(boxWidth)}┘`;
204
+
205
+ return result;
206
+ }
207
+
208
+ /**
209
+ * Print a formatted box with text
210
+ */
211
+ function box(
212
+ text: string,
213
+ options?: {
214
+ title?: string;
215
+ maxWidth?: number;
216
+ },
217
+ ): void {
218
+ const boxContent = boxString(text, options);
219
+ emitInfo(prefixLines(boxContent));
220
+ }
221
+
222
+ /**
223
+ * Overwrite the job summary with the given text.
224
+ * Skips if:
225
+ * - Not in GitHub Actions
226
+ * - Running inside Docker (CI tests inherit host env vars but can't access host paths)
227
+ * - GITHUB_STEP_SUMMARY not set
228
+ */
229
+ export async function writeSummary(text: string): Promise<void> {
230
+ if (!isGitHubActions) return;
231
+
232
+ // CI tests run in Docker with GITHUB_ACTIONS=true inherited from host,
233
+ // but the GITHUB_STEP_SUMMARY path points to a host filesystem location
234
+ // that doesn't exist inside the container
235
+ if (isInsideDocker) return;
236
+
237
+ if (!process.env.GITHUB_STEP_SUMMARY) return;
238
+
239
+ await core.summary.addRaw(text).write({ overwrite: true });
240
+ }
241
+
242
+ /**
243
+ * Print a formatted table using the table package
244
+ */
245
+ function printTable(
246
+ rows: Array<Array<{ data: string; header?: boolean } | string>>,
247
+ options?: {
248
+ title?: string;
249
+ },
250
+ ): void {
251
+ const { title } = options || {};
252
+
253
+ // Convert rows to string arrays for the table package
254
+ const tableData = rows.map((row) =>
255
+ row.map((cell) => {
256
+ if (typeof cell === "string") {
257
+ return cell;
258
+ }
259
+ return cell.data;
260
+ }),
261
+ );
262
+
263
+ const formatted = table(tableData);
264
+
265
+ if (title) {
266
+ emitInfo(prefixLines(`\n${title}`));
267
+ }
268
+ emitInfo(prefixLines(`\n${formatted}\n`));
269
+ }
270
+
271
+ /**
272
+ * Print a separator line
273
+ */
274
+ function separator(length: number = 50): void {
275
+ const separatorText = "─".repeat(length);
276
+ emitInfo(prefixLines(separatorText));
277
+ }
278
+
279
+ /**
280
+ * Main logging utility object - import this once and access all utilities
281
+ */
282
+ export const log = {
283
+ /** Print info message */
284
+ info: (...args: unknown[]): void => {
285
+ emitInfo(prefixLines(`${ts()}${formatArgs(args)}`));
286
+ },
287
+
288
+ /** Print a warning message. Use only for warnings that should be displayed in the job summary. */
289
+ warning: (...args: unknown[]): void => {
290
+ if (sink === "stderr") {
291
+ emitInfo(prefixLines(`${ts()}warning: ${formatArgs(args)}`));
292
+ return;
293
+ }
294
+ core.warning(prefixLines(`${ts()}${formatArgs(args)}`));
295
+ },
296
+
297
+ /** Print an error message. Use only for errors that should be displayed in the job summary. */
298
+ error: (...args: unknown[]): void => {
299
+ if (sink === "stderr") {
300
+ emitInfo(prefixLines(`${ts()}error: ${formatArgs(args)}`));
301
+ return;
302
+ }
303
+ core.error(prefixLines(`${ts()}${formatArgs(args)}`));
304
+ },
305
+
306
+ /** Print success message */
307
+ success: (...args: unknown[]): void => {
308
+ emitInfo(prefixLines(`${ts()}» ${formatArgs(args)}`));
309
+ },
310
+
311
+ /** Print debug message (only when debug mode is enabled) */
312
+ debug: (...args: unknown[]): void => {
313
+ if (sink === "actions" && isRunnerDebugEnabled()) {
314
+ core.debug(prefixLines(formatArgs(args)));
315
+ return;
316
+ }
317
+ if (isLocalDebugEnabled()) {
318
+ emitInfo(prefixLines(`${ts()}[DEBUG] ${formatArgs(args)}`));
319
+ }
320
+ },
321
+
322
+ /** Print a formatted box with text */
323
+ box,
324
+
325
+ /** Print a formatted table using the table package */
326
+ table: printTable,
327
+
328
+ /** Print a separator line */
329
+ separator,
330
+
331
+ /** Start a collapsed group (GitHub Actions) or regular group (local) */
332
+ startGroup,
333
+
334
+ /** End a collapsed group */
335
+ endGroup,
336
+
337
+ /** Run a callback within a collapsed group */
338
+ group,
339
+
340
+ /** Log tool call information to console with formatted output */
341
+ toolCall: ({ toolName, input }: { toolName: string; input: unknown }): void => {
342
+ const inputFormatted = formatJsonValue(input);
343
+ const output = inputFormatted !== "{}" ? `» ${toolName}(${inputFormatted})` : `» ${toolName}()`;
344
+
345
+ log.info(output.trimEnd());
346
+ },
347
+ };
348
+
349
+ /**
350
+ * Format a value as JSON, using compact format for simple values and pretty-printed for complex ones
351
+ */
352
+ export function formatJsonValue(value: unknown): string {
353
+ const compact = JSON.stringify(value);
354
+ return compact.length > 80 || compact.includes("\n") ? JSON.stringify(value, null, 2) : compact;
355
+ }
356
+
357
+ /**
358
+ * Format a multi-line string with proper indentation for tool call output
359
+ * First line has the label, subsequent lines are indented 4 spaces
360
+ */
361
+ export function formatIndentedField(label: string, content: string): string {
362
+ if (!content.includes("\n")) {
363
+ return ` ${label}: ${content}\n`;
364
+ }
365
+
366
+ const lines = content.split("\n");
367
+ let formatted = ` ${label}: ${lines[0]}\n`;
368
+ for (let i = 1; i < lines.length; i++) {
369
+ formatted += ` ${lines[i]}\n`;
370
+ }
371
+ return formatted;
372
+ }
373
+
374
+ /**
375
+ * format aggregated usage data as a markdown table for the GitHub step summary.
376
+ *
377
+ * columns mirror the per-run stdout token table emitted by `logTokenTable`
378
+ * (Input / Cache Read / Cache Write / Output / Total / Cost ($)) so the job
379
+ * summary and the in-run logs can be compared row-for-row.
380
+ *
381
+ * notes:
382
+ * - `AgentUsage.inputTokens` is the sum of non-cached input + cache read
383
+ * + cache write (set that way by both agent harnesses' `buildUsage`),
384
+ * so the non-cached Input column is recovered by subtracting cache fields.
385
+ * - `costUsd` is sourced from models.dev (OpenCode) or `total_cost_usd`
386
+ * (Claude CLI). absent rows show `—` so per-agent coverage is obvious.
387
+ */
388
+ export function formatUsageSummary(entries: AgentUsage[]): string {
389
+ if (entries.length === 0) return "";
390
+
391
+ const header = "| Agent | Input | Cache Read | Cache Write | Output | Total | Cost ($) |";
392
+ const separatorRow = "| --- | ---: | ---: | ---: | ---: | ---: | ---: |";
393
+ const fmt = (n: number) => n.toLocaleString("en-US");
394
+
395
+ const nonCachedInput = (e: AgentUsage): number =>
396
+ Math.max(0, e.inputTokens - (e.cacheReadTokens ?? 0) - (e.cacheWriteTokens ?? 0));
397
+ const totalFor = (e: AgentUsage): number =>
398
+ nonCachedInput(e) + (e.cacheReadTokens ?? 0) + (e.cacheWriteTokens ?? 0) + e.outputTokens;
399
+ const costCell = (e: AgentUsage): string =>
400
+ typeof e.costUsd === "number" && e.costUsd > 0 ? formatCostUsd(e.costUsd) : "—";
401
+
402
+ const rows = entries.map(
403
+ (e) =>
404
+ `| ${e.agent} | ${fmt(nonCachedInput(e))} | ${fmt(e.cacheReadTokens ?? 0)} | ${fmt(e.cacheWriteTokens ?? 0)} | ${fmt(e.outputTokens)} | ${fmt(totalFor(e))} | ${costCell(e)} |`,
405
+ );
406
+
407
+ const totalsRows: string[] = [];
408
+ if (entries.length > 1) {
409
+ const totalInput = entries.reduce((sum, e) => sum + nonCachedInput(e), 0);
410
+ const totalOutput = entries.reduce((sum, e) => sum + e.outputTokens, 0);
411
+ const totalCacheRead = entries.reduce((sum, e) => sum + (e.cacheReadTokens ?? 0), 0);
412
+ const totalCacheWrite = entries.reduce((sum, e) => sum + (e.cacheWriteTokens ?? 0), 0);
413
+ const grandTotal = totalInput + totalCacheRead + totalCacheWrite + totalOutput;
414
+ const totalCostUsd = entries.reduce((sum, e) => sum + (e.costUsd ?? 0), 0);
415
+ const totalCostCell = totalCostUsd > 0 ? `**${formatCostUsd(totalCostUsd)}**` : "—";
416
+ totalsRows.push(
417
+ `| **Total** | **${fmt(totalInput)}** | **${fmt(totalCacheRead)}** | **${fmt(totalCacheWrite)}** | **${fmt(totalOutput)}** | **${fmt(grandTotal)}** | ${totalCostCell} |`,
418
+ );
419
+ }
420
+
421
+ return [
422
+ "<details>",
423
+ "<summary>Usage</summary>",
424
+ "",
425
+ header,
426
+ separatorRow,
427
+ ...rows,
428
+ ...totalsRows,
429
+ "",
430
+ "</details>",
431
+ ].join("\n");
432
+ }
@@ -0,0 +1,91 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
+ import { normalizeEnv, sanitizeSecret } from "#app/utils/normalizeEnv";
3
+
4
+ /**
5
+ * These tests pin the load-bearing invariants of secret sanitisation:
6
+ * - sensitive values are trimmed before downstream code reads them
7
+ * - whitespace-only values are NOT silently zeroed (leave env unchanged)
8
+ * - case normalisation still happens
9
+ *
10
+ * Masking (`core.setSecret`) is delegated to `@actions/core` and trusted to
11
+ * work as documented — we don't spy on stdout to re-test the toolkit.
12
+ */
13
+
14
+ describe("normalizeEnv: process.env state contract", () => {
15
+ let originalEnv: NodeJS.ProcessEnv;
16
+
17
+ beforeEach(() => {
18
+ // normalizeEnv() iterates the entire process.env, so the test must
19
+ // control it. snapshot + full wipe + restore is the cleanest isolation.
20
+ originalEnv = { ...process.env };
21
+ for (const k of Object.keys(process.env)) delete process.env[k];
22
+ });
23
+
24
+ afterEach(() => {
25
+ for (const k of Object.keys(process.env)) delete process.env[k];
26
+ Object.assign(process.env, originalEnv);
27
+ });
28
+
29
+ it("trims trailing newline from sensitive env vars", () => {
30
+ process.env.ANTHROPIC_API_KEY = "sk-ant-secret-value\n";
31
+ normalizeEnv();
32
+ expect(process.env.ANTHROPIC_API_KEY).toBe("sk-ant-secret-value");
33
+ });
34
+
35
+ it("trims surrounding whitespace including \\r\\n and spaces", () => {
36
+ process.env.OPENAI_API_KEY = " sk-openai-value\r\n ";
37
+ normalizeEnv();
38
+ expect(process.env.OPENAI_API_KEY).toBe("sk-openai-value");
39
+ });
40
+
41
+ it("leaves clean sensitive values untouched", () => {
42
+ process.env.ANTHROPIC_API_KEY = "sk-ant-clean";
43
+ normalizeEnv();
44
+ expect(process.env.ANTHROPIC_API_KEY).toBe("sk-ant-clean");
45
+ });
46
+
47
+ it("ignores non-sensitive env vars", () => {
48
+ process.env.NODE_ENV = "production\n";
49
+ normalizeEnv();
50
+ expect(process.env.NODE_ENV).toBe("production\n");
51
+ });
52
+
53
+ it("canonicalises case and trims the value", () => {
54
+ process.env.anthropic_api_key = "sk-ant-lowercase\n";
55
+ normalizeEnv();
56
+ expect(process.env.ANTHROPIC_API_KEY).toBe("sk-ant-lowercase");
57
+ expect(process.env.anthropic_api_key).toBeUndefined();
58
+ });
59
+
60
+ it("preserves whitespace-only values rather than silently zeroing them", () => {
61
+ // contract: don't mutate when value is whitespace-only. caller sees the
62
+ // misconfigured value verbatim and either fails clearly downstream or
63
+ // logs a missing-key error.
64
+ process.env.ANTHROPIC_API_KEY = " \n ";
65
+ normalizeEnv();
66
+ expect(process.env.ANTHROPIC_API_KEY).toBe(" \n ");
67
+ });
68
+
69
+ it("preserves embedded newlines (toolkit masks each line)", () => {
70
+ // multi-line PEMs aren't used in practice, but if one slipped in via a
71
+ // DB secret we don't want to silently mutate it. trim() only touches
72
+ // the ends; @actions/core handles per-line masking via the runner.
73
+ process.env.ANTHROPIC_API_KEY = "line1\nline2";
74
+ normalizeEnv();
75
+ expect(process.env.ANTHROPIC_API_KEY).toBe("line1\nline2");
76
+ });
77
+ });
78
+
79
+ describe("sanitizeSecret return value", () => {
80
+ it("returns the trimmed value for a sensitive secret with trailing newline", () => {
81
+ expect(sanitizeSecret("ANTHROPIC_API_KEY", "sk-ant-secret\n")).toBe("sk-ant-secret");
82
+ });
83
+
84
+ it("returns the value unchanged when no trimming is needed", () => {
85
+ expect(sanitizeSecret("ANTHROPIC_API_KEY", "sk-ant-clean")).toBe("sk-ant-clean");
86
+ });
87
+
88
+ it("returns null for whitespace-only input so caller can skip injection", () => {
89
+ expect(sanitizeSecret("ANTHROPIC_API_KEY", " \n")).toBeNull();
90
+ });
91
+ });
@@ -0,0 +1,106 @@
1
+ import * as core from "@actions/core";
2
+ import { log } from "#app/utils/cli";
3
+ import { isSensitiveEnvName } from "#app/utils/secrets";
4
+
5
+ /**
6
+ * Trim surrounding whitespace from a sensitive value and register it as a
7
+ * GitHub Actions log mask. Trailing newlines from terminal-copy paste are a
8
+ * common footgun: the value travels through GH Actions logs and any tool
9
+ * that re-emits parts of it leaks the unmasked tail. Trimming canonicalises
10
+ * the value so the mask matches exactly what downstream tools will print.
11
+ *
12
+ * Masking is delegated to `core.setSecret` (not raw `console.log`) so the
13
+ * toolkit percent-encodes `\r`/`\n`; the runner V2 parser decodes them and
14
+ * registers the full value plus every non-empty line as separate masks. That
15
+ * keeps us safe for embedded-newline values (PEMs, kubeconfigs, JSON blobs)
16
+ * even though they aren't currently used.
17
+ *
18
+ * Returns the trimmed value, or `null` when the input was whitespace-only —
19
+ * callers must leave `process.env` untouched in that case so a misconfigured
20
+ * value surfaces as a clear "missing key" downstream rather than silently
21
+ * mutating to the empty string.
22
+ */
23
+ export function sanitizeSecret(key: string, value: string): string | null {
24
+ const trimmed = value.trim();
25
+ if (trimmed.length === 0) {
26
+ log.warning(
27
+ `» ${key} is whitespace-only — leaving env var unchanged. check your secret value.`,
28
+ );
29
+ return null;
30
+ }
31
+ if (trimmed !== value) {
32
+ log.warning(
33
+ `» stripped whitespace from ${key} (whitespace in secret values breaks GitHub Actions log masking)`,
34
+ );
35
+ }
36
+ core.setSecret(trimmed);
37
+ return trimmed;
38
+ }
39
+
40
+ /**
41
+ * Normalize environment variables to uppercase.
42
+ * This handles case-insensitive env var names (e.g., `anthropic_api_key` -> `ANTHROPIC_API_KEY`).
43
+ *
44
+ * If there are conflicts (same key with different capitalizations but different values),
45
+ * logs a warning and keeps the uppercase version.
46
+ *
47
+ * Also trims and masks sensitive values so accidental trailing whitespace
48
+ * doesn't defeat GitHub Actions log masking.
49
+ */
50
+ export function normalizeEnv(): void {
51
+ const upperKeys = new Map<string, string[]>();
52
+
53
+ // group keys by their uppercase form
54
+ for (const key of Object.keys(process.env)) {
55
+ const upper = key.toUpperCase();
56
+ const existing = upperKeys.get(upper) || [];
57
+ existing.push(key);
58
+ upperKeys.set(upper, existing);
59
+ }
60
+
61
+ // process each group
62
+ for (const [upperKey, keys] of upperKeys) {
63
+ if (keys.length === 1) {
64
+ const key = keys[0]!;
65
+ if (key !== upperKey) {
66
+ // single key, just needs uppercasing
67
+ process.env[upperKey] = process.env[key];
68
+ delete process.env[key];
69
+ }
70
+ continue;
71
+ }
72
+
73
+ // multiple keys with different capitalizations
74
+ const values = keys.map((k) => process.env[k]);
75
+ const uniqueValues = new Set(values);
76
+
77
+ if (uniqueValues.size > 1) {
78
+ // conflict: different values for different capitalizations
79
+ log.warning(
80
+ `env var conflict: ${keys.join(", ")} have different values. using uppercase ${upperKey}.`,
81
+ );
82
+ }
83
+
84
+ // prefer the uppercase version if it exists, otherwise use the first one
85
+ const preferredKey = keys.find((k) => k === upperKey) || keys[0]!;
86
+ const preferredValue = process.env[preferredKey];
87
+
88
+ // delete all variants
89
+ for (const key of keys) {
90
+ delete process.env[key];
91
+ }
92
+
93
+ // set the uppercase version
94
+ process.env[upperKey] = preferredValue;
95
+ }
96
+
97
+ // trim + mask sensitive values after case normalisation so each key is
98
+ // visited exactly once with its final, canonical value
99
+ for (const key of Object.keys(process.env)) {
100
+ if (!isSensitiveEnvName(key)) continue;
101
+ const value = process.env[key];
102
+ if (typeof value !== "string" || value.length === 0) continue;
103
+ const sanitized = sanitizeSecret(key, value);
104
+ if (sanitized !== null) process.env[key] = sanitized;
105
+ }
106
+ }