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,509 @@
1
+ import { execFileSync, execSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import type { OctokitWithPlugins } from "#app/utils/github";
7
+ import {
8
+ captureInitialHead,
9
+ createTempDirectory,
10
+ removeIncludeIfEntries,
11
+ type SetupGitParams,
12
+ setupGit,
13
+ setupTestRepo,
14
+ wipeRunnerLeakSurface,
15
+ } from "#app/utils/setup";
16
+ import { $ } from "#app/utils/shell";
17
+
18
+ // pass-through wrappers: the real-git suites below keep their original
19
+ // behavior, while the mocked suites override implementations per test and
20
+ // restore them via mockReset (vi.fn(impl) resets back to `impl`).
21
+ vi.mock("node:child_process", async (importOriginal) => {
22
+ const actual = await importOriginal<typeof import("node:child_process")>();
23
+ return {
24
+ ...actual,
25
+ execSync: vi.fn(actual.execSync),
26
+ execFileSync: vi.fn(actual.execFileSync),
27
+ };
28
+ });
29
+
30
+ vi.mock("#app/utils/shell", () => ({ $: vi.fn(() => "") }));
31
+
32
+ vi.mock("#app/utils/globals", () => ({
33
+ isCloudflareSandbox: false,
34
+ isGitHubActions: false,
35
+ isInsideDocker: false,
36
+ }));
37
+
38
+ describe("removeIncludeIfEntries", () => {
39
+ let repoDir: string;
40
+
41
+ // git push sets GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE for pre-push hooks
42
+ // and those propagate to execSync's child processes by default. a `git init`
43
+ // inheriting GIT_DIR from the outer repo modifies the outer repo's config
44
+ // rather than creating one in `repoDir`, which makes subsequent writeFileSync
45
+ // on `repoDir/.git/config` fail with ENOENT and masquerades as a test bug.
46
+ // strip the git-specific env vars so this suite runs identically whether
47
+ // invoked directly, via `pnpm -r test`, or via a pre-push hook.
48
+ const cleanEnv = (() => {
49
+ const next = { ...process.env };
50
+ for (const k of Object.keys(next)) {
51
+ if (k.startsWith("GIT_")) delete next[k];
52
+ }
53
+ return next;
54
+ })();
55
+
56
+ beforeEach(() => {
57
+ repoDir = mkdtempSync(join(tmpdir(), "terramend-setup-test-"));
58
+ execSync("git init -q", { cwd: repoDir, env: cleanEnv });
59
+ });
60
+
61
+ afterEach(() => {
62
+ rmSync(repoDir, { recursive: true, force: true });
63
+ });
64
+
65
+ it("removes a benign includeIf.gitdir entry", () => {
66
+ execSync('git config --local "includeIf.gitdir:/work/.gitconfig" "/tmp/included-config"', {
67
+ cwd: repoDir,
68
+ env: cleanEnv,
69
+ });
70
+ expect(
71
+ execSync('git config --local --get-all "includeIf.gitdir:/work/.gitconfig"', {
72
+ cwd: repoDir,
73
+ encoding: "utf-8",
74
+ env: cleanEnv,
75
+ }).trim(),
76
+ ).toBe("/tmp/included-config");
77
+
78
+ removeIncludeIfEntries(repoDir);
79
+
80
+ expect(() =>
81
+ execSync('git config --local --get-all "includeIf.gitdir:/work/.gitconfig"', {
82
+ cwd: repoDir,
83
+ stdio: "pipe",
84
+ env: cleanEnv,
85
+ }),
86
+ ).toThrow();
87
+ });
88
+
89
+ it("does not execute $(...) command substitution embedded in a subsection name", () => {
90
+ // regression: setup previously did
91
+ // execSync(`git config --local --unset "${key}"`)
92
+ // where `key` was derived from `git config --get-regexp ^includeif\.` output.
93
+ // a subsection like `gitdir:$(touch${IFS}/tmp/pwn)safe` bypasses the
94
+ // split-on-space filter and, when interpolated into a shell command,
95
+ // lets the shell evaluate the command substitution.
96
+ const proof = join(repoDir, "pwn-proof.txt");
97
+ expect(existsSync(proof)).toBe(false);
98
+
99
+ const configPath = join(repoDir, ".git", "config");
100
+ writeFileSync(
101
+ configPath,
102
+ [
103
+ "[core]",
104
+ "\trepositoryformatversion = 0",
105
+ // space-free payload: ${IFS} expands to whitespace only if evaluated by a shell.
106
+ // the subsection name is preserved literally by git.
107
+ `[includeIf "gitdir:$(touch\${IFS}${proof})safe"]`,
108
+ `\tpath = /tmp/unused`,
109
+ "",
110
+ ].join("\n"),
111
+ );
112
+
113
+ removeIncludeIfEntries(repoDir);
114
+
115
+ expect(existsSync(proof)).toBe(false);
116
+ });
117
+
118
+ it("handles keys containing whitespace in the subsection name", () => {
119
+ // the old split-on-space approach truncated keys at the first space, so
120
+ // subsections with internal whitespace survived cleanup. the -z path
121
+ // reads keys whole.
122
+ const configPath = join(repoDir, ".git", "config");
123
+ writeFileSync(
124
+ configPath,
125
+ [
126
+ "[core]",
127
+ "\trepositoryformatversion = 0",
128
+ '[includeIf "gitdir:/a b c"]',
129
+ "\tpath = /tmp/unused",
130
+ "",
131
+ ].join("\n"),
132
+ );
133
+
134
+ removeIncludeIfEntries(repoDir);
135
+
136
+ const remaining = execSync("git config --local --get-regexp ^includeif\\. || true", {
137
+ cwd: repoDir,
138
+ encoding: "utf-8",
139
+ shell: "/bin/bash",
140
+ env: cleanEnv,
141
+ });
142
+ expect(remaining.trim()).toBe("");
143
+ });
144
+
145
+ it("is a no-op when no includeIf entries exist", () => {
146
+ expect(() => removeIncludeIfEntries(repoDir)).not.toThrow();
147
+ });
148
+ });
149
+
150
+ describe("removeIncludeIfEntries — key handling (mocked git)", () => {
151
+ afterEach(() => {
152
+ vi.mocked(execSync).mockReset();
153
+ vi.mocked(execFileSync).mockReset();
154
+ });
155
+
156
+ it("dedupes repeated keys and tolerates unset failures per key", () => {
157
+ // the pass-through wrapper accumulates calls from the real-git suites
158
+ // above — drop them so the call-count assertion sees only this test.
159
+ vi.mocked(execFileSync).mockClear();
160
+ // -z format: "<key>\n<value>" entries, null-separated. same key twice +
161
+ // one valueless entry (no newline → whole entry is the key).
162
+ vi.mocked(execSync).mockImplementation(
163
+ (() =>
164
+ "includeif.gitdir:/w/.gitconfig\n/tmp/a\0includeif.gitdir:/w/.gitconfig\n/tmp/b\0includeif.onbranch:main\0") as unknown as typeof execSync,
165
+ );
166
+ let unsetCalls = 0;
167
+ vi.mocked(execFileSync).mockImplementation((() => {
168
+ unsetCalls += 1;
169
+ // alternate Error / non-Error throws to cover both log-format paths
170
+ if (unsetCalls === 1) throw new Error("unset rejected");
171
+ throw "unset rejected as string";
172
+ }) as unknown as typeof execFileSync);
173
+
174
+ expect(() => removeIncludeIfEntries("/repo")).not.toThrow();
175
+
176
+ expect(execFileSync).toHaveBeenCalledTimes(2);
177
+ expect(execFileSync).toHaveBeenCalledWith(
178
+ "git",
179
+ ["config", "--local", "--unset-all", "includeif.gitdir:/w/.gitconfig"],
180
+ expect.objectContaining({ cwd: "/repo", stdio: "pipe" }),
181
+ );
182
+ expect(execFileSync).toHaveBeenCalledWith(
183
+ "git",
184
+ ["config", "--local", "--unset-all", "includeif.onbranch:main"],
185
+ expect.objectContaining({ cwd: "/repo", stdio: "pipe" }),
186
+ );
187
+ });
188
+
189
+ it("skips the unset pass entirely when the -z output holds no keys", () => {
190
+ vi.mocked(execFileSync).mockClear();
191
+ vi.mocked(execSync).mockImplementation((() => "\0\0") as unknown as typeof execSync);
192
+ vi.mocked(execFileSync).mockImplementation((() => "") as unknown as typeof execFileSync);
193
+
194
+ expect(() => removeIncludeIfEntries("/repo")).not.toThrow();
195
+
196
+ expect(execFileSync).not.toHaveBeenCalled();
197
+ });
198
+ });
199
+
200
+ describe("createTempDirectory", () => {
201
+ it("creates a terramend temp dir and exports TERRAMEND_TEMP_DIR", () => {
202
+ const saved = process.env.TERRAMEND_TEMP_DIR;
203
+ const dir = createTempDirectory();
204
+ try {
205
+ expect(existsSync(dir)).toBe(true);
206
+ expect(dir).toContain("terramend-");
207
+ expect(process.env.TERRAMEND_TEMP_DIR).toBe(dir);
208
+ } finally {
209
+ rmSync(dir, { recursive: true, force: true });
210
+ if (saved === undefined) delete process.env.TERRAMEND_TEMP_DIR;
211
+ else process.env.TERRAMEND_TEMP_DIR = saved;
212
+ }
213
+ });
214
+ });
215
+
216
+ describe("wipeRunnerLeakSurface", () => {
217
+ let runnerTemp: string;
218
+
219
+ beforeEach(() => {
220
+ runnerTemp = mkdtempSync(join(tmpdir(), "terramend-runner-"));
221
+ });
222
+
223
+ afterEach(() => {
224
+ rmSync(runnerTemp, { recursive: true, force: true });
225
+ vi.unstubAllEnvs();
226
+ });
227
+
228
+ it("is a silent no-op when RUNNER_TEMP is unset", () => {
229
+ vi.stubEnv("RUNNER_TEMP", undefined);
230
+ expect(() => wipeRunnerLeakSurface()).not.toThrow();
231
+ });
232
+
233
+ it("wipes leak surfaces while preserving our own file-command paths", () => {
234
+ const fileCommandsDir = join(runnerTemp, "_runner_file_commands");
235
+ mkdirSync(fileCommandsDir);
236
+ const leakedOutput = join(fileCommandsDir, "set_output_abc123");
237
+ const ourOutput = join(fileCommandsDir, "github_output_1");
238
+ const stepScript = join(runnerTemp, "step-uuid.sh");
239
+ const credConfig = join(runnerTemp, "git-credentials-xyz.config");
240
+ const unrelated = join(runnerTemp, "keep.txt");
241
+ for (const file of [leakedOutput, ourOutput, stepScript, credConfig, unrelated]) {
242
+ writeFileSync(file, "x");
243
+ }
244
+
245
+ vi.stubEnv("RUNNER_TEMP", runnerTemp);
246
+ vi.stubEnv("GITHUB_OUTPUT", ourOutput);
247
+ // not created yet — exercises the literal-path preserve fallback
248
+ vi.stubEnv("GITHUB_ENV", join(fileCommandsDir, "github_env_not_created_yet"));
249
+ vi.stubEnv("GITHUB_PATH", undefined);
250
+ vi.stubEnv("GITHUB_STATE", undefined);
251
+ vi.stubEnv("GITHUB_STEP_SUMMARY", undefined);
252
+
253
+ wipeRunnerLeakSurface();
254
+
255
+ expect(existsSync(leakedOutput)).toBe(false);
256
+ expect(existsSync(stepScript)).toBe(false);
257
+ expect(existsSync(credConfig)).toBe(false);
258
+ expect(existsSync(ourOutput)).toBe(true);
259
+ expect(existsSync(unrelated)).toBe(true);
260
+ });
261
+
262
+ it("tolerates a missing _runner_file_commands dir", () => {
263
+ vi.stubEnv("RUNNER_TEMP", runnerTemp);
264
+ vi.stubEnv("GITHUB_OUTPUT", undefined);
265
+ vi.stubEnv("GITHUB_ENV", undefined);
266
+ vi.stubEnv("GITHUB_PATH", undefined);
267
+ vi.stubEnv("GITHUB_STATE", undefined);
268
+ vi.stubEnv("GITHUB_STEP_SUMMARY", undefined);
269
+ expect(() => wipeRunnerLeakSurface()).not.toThrow();
270
+ });
271
+ });
272
+
273
+ describe("setupTestRepo", () => {
274
+ afterEach(() => {
275
+ vi.mocked($).mockReset();
276
+ vi.unstubAllEnvs();
277
+ });
278
+
279
+ it("throws when GITHUB_REPOSITORY is unset", () => {
280
+ vi.stubEnv("GITHUB_REPOSITORY", undefined);
281
+ expect(() => setupTestRepo({ tempDir: "/tmp/x" })).toThrow("GITHUB_REPOSITORY is required");
282
+ });
283
+
284
+ it("clones over https with the token in CI", () => {
285
+ vi.stubEnv("GITHUB_REPOSITORY", "acme/repo");
286
+ vi.stubEnv("CI", "true");
287
+ vi.stubEnv("GITHUB_TOKEN", "ghs_token");
288
+ vi.stubEnv("GH_TOKEN", undefined);
289
+
290
+ setupTestRepo({ tempDir: "/tmp/clone" });
291
+
292
+ expect($).toHaveBeenCalledWith("git", [
293
+ "clone",
294
+ "https://x-access-token:ghs_token@github.com/acme/repo.git",
295
+ "/tmp/clone",
296
+ ]);
297
+ });
298
+
299
+ it("falls back to GH_TOKEN when GITHUB_TOKEN is unset", () => {
300
+ vi.stubEnv("GITHUB_REPOSITORY", "acme/repo");
301
+ vi.stubEnv("CI", "true");
302
+ vi.stubEnv("GITHUB_TOKEN", undefined);
303
+ vi.stubEnv("GH_TOKEN", "gh_alt");
304
+
305
+ setupTestRepo({ tempDir: "/tmp/clone" });
306
+
307
+ expect($).toHaveBeenCalledWith("git", [
308
+ "clone",
309
+ "https://x-access-token:gh_alt@github.com/acme/repo.git",
310
+ "/tmp/clone",
311
+ ]);
312
+ });
313
+
314
+ it("throws in CI when no token is available", () => {
315
+ vi.stubEnv("GITHUB_REPOSITORY", "acme/repo");
316
+ vi.stubEnv("CI", "true");
317
+ vi.stubEnv("GITHUB_TOKEN", undefined);
318
+ vi.stubEnv("GH_TOKEN", undefined);
319
+
320
+ expect(() => setupTestRepo({ tempDir: "/tmp/clone" })).toThrow(
321
+ "GITHUB_TOKEN or GH_TOKEN is required",
322
+ );
323
+ });
324
+
325
+ it("clones over ssh outside CI / Docker", () => {
326
+ vi.stubEnv("GITHUB_REPOSITORY", "acme/repo");
327
+ vi.stubEnv("CI", undefined);
328
+
329
+ setupTestRepo({ tempDir: "/tmp/clone" });
330
+
331
+ expect($).toHaveBeenCalledWith("git", ["clone", "git@github.com:acme/repo.git", "/tmp/clone"]);
332
+ });
333
+ });
334
+
335
+ describe("captureInitialHead (mocked git)", () => {
336
+ afterEach(() => {
337
+ vi.mocked($).mockReset();
338
+ });
339
+
340
+ it("returns a branch head when symbolic-ref resolves", () => {
341
+ vi.mocked($).mockImplementation((_cmd, args) => {
342
+ if (args[0] === "symbolic-ref") return "main\n";
343
+ throw new Error(`unexpected git call: ${args.join(" ")}`);
344
+ });
345
+ expect(captureInitialHead("/repo")).toEqual({ kind: "branch", name: "main" });
346
+ });
347
+
348
+ it("returns a detached head when symbolic-ref throws", () => {
349
+ vi.mocked($).mockImplementation((_cmd, args) => {
350
+ if (args[0] === "symbolic-ref") throw new Error("fatal: ref HEAD is not a symbolic ref");
351
+ if (args[0] === "rev-parse") return "abc1234\n";
352
+ throw new Error(`unexpected git call: ${args.join(" ")}`);
353
+ });
354
+ expect(captureInitialHead("/repo")).toEqual({ kind: "detached", sha: "abc1234" });
355
+ });
356
+
357
+ it("falls through to the sha when symbolic-ref returns an empty name", () => {
358
+ vi.mocked($).mockImplementation((_cmd, args) => {
359
+ if (args[0] === "symbolic-ref") return " ";
360
+ if (args[0] === "rev-parse") return "def5678";
361
+ throw new Error(`unexpected git call: ${args.join(" ")}`);
362
+ });
363
+ expect(captureInitialHead("/repo")).toEqual({ kind: "detached", sha: "def5678" });
364
+ });
365
+ });
366
+
367
+ describe("setupGit (mocked git)", () => {
368
+ function makeParams(overrides: Partial<SetupGitParams> = {}): SetupGitParams {
369
+ return {
370
+ gitToken: "tok",
371
+ owner: "acme",
372
+ name: "repo",
373
+ octokit: {} as unknown as OctokitWithPlugins,
374
+ toolState: {
375
+ prepushFailureCount: 0,
376
+ backgroundProcesses: new Map(),
377
+ progressComment: undefined,
378
+ hadProgressComment: false,
379
+ usageEntries: [],
380
+ },
381
+ shell: "restricted",
382
+ postCheckoutScript: null,
383
+ ...overrides,
384
+ };
385
+ }
386
+
387
+ /** dispatch execSync by command; returns the recorded command list. */
388
+ function mockGitExec(opts: { currentEmail?: string; failOnSet?: boolean } = {}): string[] {
389
+ const commands: string[] = [];
390
+ vi.mocked(execSync).mockImplementation(((command: string) => {
391
+ commands.push(command);
392
+ if (command === "git config user.email") {
393
+ if (opts.currentEmail === undefined) throw new Error("not configured");
394
+ return opts.currentEmail;
395
+ }
396
+ if (opts.failOnSet && command.startsWith("git config --local user.email")) {
397
+ throw new Error("config file locked");
398
+ }
399
+ if (command.includes("--unset-all http")) throw new Error("no extraheader");
400
+ if (command.includes("--get-regexp")) throw new Error("no includeif entries");
401
+ return "";
402
+ }) as unknown as typeof execSync);
403
+ return commands;
404
+ }
405
+
406
+ function mockGitShell(): void {
407
+ vi.mocked($).mockImplementation((_cmd, args) => {
408
+ if (args[0] === "symbolic-ref") return "main\n";
409
+ if (args[0] === "rev-parse") return "abc1234\n";
410
+ return "";
411
+ });
412
+ }
413
+
414
+ afterEach(() => {
415
+ vi.mocked(execSync).mockReset();
416
+ vi.mocked(execFileSync).mockReset();
417
+ vi.mocked($).mockReset();
418
+ });
419
+
420
+ it("sets the terramend identity when no user.email is configured", async () => {
421
+ const commands = mockGitExec();
422
+ mockGitShell();
423
+ const params = makeParams();
424
+
425
+ await setupGit(params);
426
+
427
+ expect(
428
+ commands.some((c) => c.includes('user.email "terramend[bot]@users.noreply.github.com"')),
429
+ ).toBe(true);
430
+ expect(commands.some((c) => c.includes('user.name "terramend[bot]"'))).toBe(true);
431
+ expect(commands.some((c) => c.includes("core.hooksPath"))).toBe(false);
432
+ expect($).toHaveBeenCalledWith(
433
+ "git",
434
+ ["remote", "set-url", "origin", "https://github.com/acme/repo.git"],
435
+ { cwd: process.cwd() },
436
+ );
437
+ expect($).toHaveBeenCalledWith("git", ["config", "--local", "credential.helper", ""], {
438
+ cwd: process.cwd(),
439
+ });
440
+ expect(params.toolState.pushUrl).toBe("https://github.com/acme/repo.git");
441
+ expect(params.toolState.initialHead).toEqual({ kind: "branch", name: "main" });
442
+ });
443
+
444
+ it("replaces the generic github-actions bot identity", async () => {
445
+ const commands = mockGitExec({
446
+ currentEmail: "github-actions[bot]@users.noreply.github.com\n",
447
+ });
448
+ mockGitShell();
449
+
450
+ await setupGit(makeParams());
451
+
452
+ expect(commands.some((c) => c.includes('user.email "terramend[bot]'))).toBe(true);
453
+ });
454
+
455
+ it("keeps a custom git identity untouched", async () => {
456
+ const commands = mockGitExec({ currentEmail: "dev@example.com\n" });
457
+ mockGitShell();
458
+
459
+ await setupGit(makeParams());
460
+
461
+ expect(commands.some((c) => c.includes('user.email "terramend[bot]'))).toBe(false);
462
+ });
463
+
464
+ it("disables git hooks when shell is disabled", async () => {
465
+ const commands = mockGitExec({ currentEmail: "dev@example.com\n" });
466
+ mockGitShell();
467
+
468
+ await setupGit(makeParams({ shell: "disabled" }));
469
+
470
+ expect(commands.some((c) => c.includes("core.hooksPath /dev/null"))).toBe(true);
471
+ });
472
+
473
+ it("continues past a failing git-config step and still configures auth", async () => {
474
+ mockGitExec({ failOnSet: true });
475
+ mockGitShell();
476
+ const params = makeParams();
477
+
478
+ await expect(setupGit(params)).resolves.toBeUndefined();
479
+
480
+ expect(params.toolState.pushUrl).toBe("https://github.com/acme/repo.git");
481
+ });
482
+
483
+ it("tolerates a non-Error throw from the git-config step", async () => {
484
+ vi.mocked(execSync).mockImplementation((() => {
485
+ // non-Error throw — exercises the String(error) logging fallback
486
+ throw "config locked";
487
+ }) as unknown as typeof execSync);
488
+ mockGitShell();
489
+ const params = makeParams();
490
+
491
+ await expect(setupGit(params)).resolves.toBeUndefined();
492
+
493
+ expect(params.toolState.pushUrl).toBe("https://github.com/acme/repo.git");
494
+ });
495
+
496
+ it("records a detached initial head on detached-entry runs", async () => {
497
+ mockGitExec({ currentEmail: "dev@example.com\n" });
498
+ vi.mocked($).mockImplementation((_cmd, args) => {
499
+ if (args[0] === "symbolic-ref") throw new Error("not a symbolic ref");
500
+ if (args[0] === "rev-parse") return "face0ff\n";
501
+ return "";
502
+ });
503
+ const params = makeParams();
504
+
505
+ await setupGit(params);
506
+
507
+ expect(params.toolState.initialHead).toEqual({ kind: "detached", sha: "face0ff" });
508
+ });
509
+ });