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,129 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { runCli as runGhaCli } from "#app/commands/gha";
3
+
4
+ vi.mock("#app/commands/gha", () => ({ runCli: vi.fn(async () => {}) }));
5
+
6
+ const ORIGINAL_ARGV = process.argv;
7
+
8
+ type ConsoleSpy = { mock: { calls: unknown[][] } };
9
+
10
+ let logSpy: ConsoleSpy;
11
+ let errorSpy: ConsoleSpy;
12
+ let exitSpy: { mock: { calls: unknown[][] } };
13
+
14
+ /**
15
+ * cli.ts runs its command dispatch at module top level, so each scenario
16
+ * re-imports it with a fresh module registry and stubbed process.argv.
17
+ * `process.exit` is mocked to throw, which the module's own top-level catch
18
+ * converts into a second exit(1) — flows that exit therefore reject the
19
+ * dynamic import with "process.exit:1".
20
+ */
21
+ async function importCli(argv: string[]): Promise<{ rejection: unknown }> {
22
+ process.argv = ["node", "/usr/local/bin/terramend", ...argv];
23
+ vi.resetModules();
24
+ try {
25
+ await import("#app/cli");
26
+ return { rejection: undefined };
27
+ } catch (error) {
28
+ return { rejection: error };
29
+ }
30
+ }
31
+
32
+ function loggedLines(spy: ConsoleSpy): string {
33
+ return spy.mock.calls.map((call) => call.join(" ")).join("\n");
34
+ }
35
+
36
+ beforeEach(() => {
37
+ logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
38
+ errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
39
+ exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
40
+ throw new Error(`process.exit:${code}`);
41
+ }) as never);
42
+ });
43
+
44
+ afterEach(() => {
45
+ process.argv = ORIGINAL_ARGV;
46
+ vi.unstubAllEnvs();
47
+ vi.restoreAllMocks();
48
+ vi.clearAllMocks();
49
+ });
50
+
51
+ describe("cli entry", () => {
52
+ it("prints the version and exits 0 on --version", async () => {
53
+ vi.stubEnv("CLI_VERSION", "1.2.3");
54
+
55
+ const { rejection } = await importCli(["--version"]);
56
+
57
+ expect(logSpy).toHaveBeenCalledWith("1.2.3");
58
+ expect(exitSpy.mock.calls[0]).toEqual([0]);
59
+ expect(rejection).toBeInstanceOf(Error);
60
+ });
61
+
62
+ it("prints usage and exits 0 when no command is given", async () => {
63
+ await importCli([]);
64
+
65
+ expect(loggedLines(logSpy)).toContain("usage: terramend <command>");
66
+ expect(loggedLines(logSpy)).toContain("gha");
67
+ expect(exitSpy.mock.calls[0]).toEqual([0]);
68
+ });
69
+
70
+ it("prints the banner and usage on --help without a command", async () => {
71
+ await importCli(["--help"]);
72
+
73
+ expect(loggedLines(logSpy)).toContain("terramend");
74
+ expect(loggedLines(logSpy)).toContain("usage: terramend <command>");
75
+ expect(exitSpy.mock.calls[0]).toEqual([0]);
76
+ });
77
+
78
+ it("prints usage and exits 0 on --help with an unknown command", async () => {
79
+ await importCli(["--help", "wat"]);
80
+
81
+ expect(loggedLines(logSpy)).toContain("usage: terramend <command>");
82
+ expect(exitSpy.mock.calls[0]).toEqual([0]);
83
+ expect(runGhaCli).not.toHaveBeenCalled();
84
+ });
85
+
86
+ it("dispatches the gha command with its arguments", async () => {
87
+ const { rejection } = await importCli(["gha", "token", "--post"]);
88
+
89
+ expect(rejection).toBeUndefined();
90
+ expect(runGhaCli).toHaveBeenCalledWith({
91
+ args: ["token", "--post"],
92
+ prog: "terramend",
93
+ showHelp: false,
94
+ });
95
+ expect(exitSpy).not.toHaveBeenCalled();
96
+ });
97
+
98
+ it("forwards showHelp to the gha command on --help gha", async () => {
99
+ const { rejection } = await importCli(["--help", "gha"]);
100
+
101
+ expect(rejection).toBeUndefined();
102
+ expect(runGhaCli).toHaveBeenCalledWith({ args: [], prog: "terramend", showHelp: true });
103
+ });
104
+
105
+ it("errors with usage on an unknown command", async () => {
106
+ await importCli(["frobnicate"]);
107
+
108
+ expect(loggedLines(errorSpy)).toContain("unknown command:");
109
+ expect(loggedLines(errorSpy)).toContain("usage: terramend <command>");
110
+ expect(exitSpy.mock.calls[0]).toEqual([1]);
111
+ });
112
+
113
+ it("errors with usage on an unknown global option", async () => {
114
+ await importCli(["--bogus"]);
115
+
116
+ expect(loggedLines(errorSpy)).toContain("unknown or unexpected option: --bogus");
117
+ expect(exitSpy.mock.calls[0]).toEqual([1]);
118
+ });
119
+
120
+ it("reports command failures via the top-level catch and exits 1", async () => {
121
+ vi.mocked(runGhaCli).mockRejectedValueOnce(new Error("gha blew up"));
122
+
123
+ const { rejection } = await importCli(["gha"]);
124
+
125
+ expect(loggedLines(errorSpy)).toContain("gha blew up");
126
+ expect(exitSpy.mock.calls[0]).toEqual([1]);
127
+ expect(rejection).toBeInstanceOf(Error);
128
+ });
129
+ });
package/src/cli.ts ADDED
@@ -0,0 +1,105 @@
1
+ import { basename } from "node:path";
2
+ import arg from "arg";
3
+ import pc from "picocolors";
4
+ import { runCli as runGhaCli } from "#app/commands/gha";
5
+ import { runMcpCli } from "#app/commands/mcp";
6
+
7
+ const VERSION = process.env.CLI_VERSION ?? "0.0.0";
8
+ const bin = basename(process.argv[1] || "");
9
+ const PROG = bin === "pf" || bin === "terramend" ? bin : "terramend";
10
+ const rawArgs = process.argv.slice(2);
11
+
12
+ function printMainUsage(stream: typeof console.log): void {
13
+ stream(`usage: ${PROG} <command>\n`);
14
+ stream("commands:");
15
+ stream(" gha run the github action runtime flow (used by action.yml)");
16
+ stream(" mcp start the local MCP server (stdio) with read-only terraform tools");
17
+ stream("");
18
+ stream("global options:");
19
+ stream(" -h, --help show help");
20
+ stream(" -v, --version show version");
21
+ }
22
+
23
+ function parseGlobalArgs(args: string[]) {
24
+ return arg(
25
+ {
26
+ "--help": Boolean,
27
+ "--version": Boolean,
28
+ "-h": "--help",
29
+ "-v": "--version",
30
+ },
31
+ {
32
+ argv: args,
33
+ stopAtPositional: true,
34
+ },
35
+ );
36
+ }
37
+
38
+ function exitWithUsageError(message: string): never {
39
+ console.error(`${message}\n`);
40
+ printMainUsage(console.error);
41
+ process.exit(1);
42
+ }
43
+
44
+ async function run(): Promise<void> {
45
+ let globalParsed: ReturnType<typeof parseGlobalArgs>;
46
+ try {
47
+ globalParsed = parseGlobalArgs(rawArgs);
48
+ } catch (error) {
49
+ const message = error instanceof Error ? error.message : String(error);
50
+ exitWithUsageError(message);
51
+ }
52
+
53
+ if (globalParsed["--version"]) {
54
+ console.log(VERSION);
55
+ process.exit(0);
56
+ }
57
+
58
+ const command = globalParsed._[0];
59
+ const commandArgs = globalParsed._.slice(1);
60
+
61
+ if (!command) {
62
+ if (globalParsed["--help"]) {
63
+ console.log(`${pc.bold("terramend")} v${VERSION}\n`);
64
+ printMainUsage(console.log);
65
+ process.exit(0);
66
+ }
67
+ printMainUsage(console.log);
68
+ process.exit(0);
69
+ }
70
+
71
+ if (command === "gha") {
72
+ await runGhaCli({
73
+ args: commandArgs,
74
+ prog: PROG,
75
+ showHelp: globalParsed["--help"] === true,
76
+ });
77
+ return;
78
+ }
79
+
80
+ if (command === "mcp") {
81
+ await runMcpCli({
82
+ args: commandArgs,
83
+ prog: PROG,
84
+ showHelp: globalParsed["--help"] === true,
85
+ });
86
+ return;
87
+ }
88
+
89
+ if (globalParsed["--help"]) {
90
+ printMainUsage(console.log);
91
+ process.exit(0);
92
+ }
93
+
94
+ console.error(`unknown command: ${pc.bold(command)}\n`);
95
+ printMainUsage(console.error);
96
+ process.exit(1);
97
+ }
98
+
99
+ try {
100
+ await run();
101
+ } catch (error) {
102
+ const message = error instanceof Error ? error.message : String(error);
103
+ console.error(pc.red(message));
104
+ process.exit(1);
105
+ }
@@ -0,0 +1,192 @@
1
+ import { dirname } from "node:path";
2
+ import * as core from "@actions/core";
3
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { main } from "#app/main";
5
+ import { acquireInstallationToken, revokeInstallationToken } from "#app/utils/token";
6
+
7
+ vi.mock("@actions/core", () => ({
8
+ getInput: vi.fn(() => ""),
9
+ getState: vi.fn(() => ""),
10
+ setFailed: vi.fn(),
11
+ setSecret: vi.fn(),
12
+ saveState: vi.fn(),
13
+ setOutput: vi.fn(),
14
+ info: vi.fn(),
15
+ debug: vi.fn(),
16
+ }));
17
+ vi.mock("#app/main", () => ({ main: vi.fn(async () => ({ success: true })) }));
18
+ vi.mock("#app/utils/token", () => ({
19
+ acquireInstallationToken: vi.fn(async () => "inst-token"),
20
+ revokeInstallationToken: vi.fn(async () => {}),
21
+ }));
22
+
23
+ // gha.ts prepends the action runtime's node bin dir to PATH at import time —
24
+ // import dynamically so the original PATH can be captured and restored.
25
+ const SAVED_PATH = process.env.PATH;
26
+ let gha: typeof import("#app/commands/gha");
27
+
28
+ beforeAll(async () => {
29
+ gha = await import("#app/commands/gha");
30
+ });
31
+
32
+ afterAll(() => {
33
+ process.env.PATH = SAVED_PATH;
34
+ });
35
+
36
+ type ConsoleSpy = { mock: { calls: unknown[][] } };
37
+
38
+ let logSpy: ConsoleSpy;
39
+ let errorSpy: ConsoleSpy;
40
+
41
+ beforeEach(() => {
42
+ logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
43
+ errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
44
+ vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
45
+ throw new Error(`process.exit:${code}`);
46
+ }) as never);
47
+ });
48
+
49
+ afterEach(() => {
50
+ vi.restoreAllMocks();
51
+ vi.clearAllMocks();
52
+ });
53
+
54
+ function loggedLines(spy: ConsoleSpy): string {
55
+ return spy.mock.calls.map((call) => call.join(" ")).join("\n");
56
+ }
57
+
58
+ describe("gha module import", () => {
59
+ it("prepends the action runtime's node bin dir to PATH", () => {
60
+ expect(process.env.PATH).toContain(dirname(process.execPath));
61
+ });
62
+ });
63
+
64
+ describe("runCli – help and arg handling", () => {
65
+ it("prints gha usage when showHelp is set", async () => {
66
+ await gha.runCli({ args: [], prog: "terramend", showHelp: true });
67
+
68
+ expect(loggedLines(logSpy)).toContain("usage: terramend gha [subcommand]");
69
+ expect(main).not.toHaveBeenCalled();
70
+ });
71
+
72
+ it("prints gha usage on --help", async () => {
73
+ await gha.runCli({ args: ["--help"], prog: "terramend" });
74
+
75
+ expect(loggedLines(logSpy)).toContain("usage: terramend gha [subcommand]");
76
+ });
77
+
78
+ it("errors with usage and exits 1 on an unknown option", async () => {
79
+ await expect(gha.runCli({ args: ["--bogus"], prog: "terramend" })).rejects.toThrow(
80
+ "process.exit:1",
81
+ );
82
+ expect(loggedLines(errorSpy)).toContain("usage: terramend gha [subcommand]");
83
+ });
84
+
85
+ it("errors and exits 1 on an unknown subcommand", async () => {
86
+ await expect(gha.runCli({ args: ["frobnicate"], prog: "terramend" })).rejects.toThrow(
87
+ "process.exit:1",
88
+ );
89
+ expect(loggedLines(errorSpy)).toContain("unknown gha subcommand: frobnicate");
90
+ });
91
+
92
+ it("runs the main action flow when no subcommand is given", async () => {
93
+ await gha.runCli({ args: [], prog: "terramend" });
94
+
95
+ expect(main).toHaveBeenCalledOnce();
96
+ expect(core.setFailed).not.toHaveBeenCalled();
97
+ });
98
+ });
99
+
100
+ describe("runCli – gha token", () => {
101
+ it("prints token usage on gha token --help", async () => {
102
+ await gha.runCli({ args: ["token", "--help"], prog: "terramend" });
103
+
104
+ expect(loggedLines(logSpy)).toContain("usage: terramend gha token [--post]");
105
+ expect(acquireInstallationToken).not.toHaveBeenCalled();
106
+ });
107
+
108
+ it("rejects unexpected positional arguments", async () => {
109
+ await expect(gha.runCli({ args: ["token", "extra"], prog: "terramend" })).rejects.toThrow(
110
+ "process.exit:1",
111
+ );
112
+ expect(loggedLines(errorSpy)).toContain("unexpected positional arguments for gha token: extra");
113
+ });
114
+
115
+ it("errors with token usage on an unknown token option", async () => {
116
+ await expect(gha.runCli({ args: ["token", "--bogus"], prog: "terramend" })).rejects.toThrow(
117
+ "process.exit:1",
118
+ );
119
+ expect(loggedLines(errorSpy)).toContain("usage: terramend gha token [--post]");
120
+ });
121
+
122
+ it("acquires a token scoped to the current repo by default", async () => {
123
+ await gha.runCli({ args: ["token"], prog: "terramend" });
124
+
125
+ expect(acquireInstallationToken).toHaveBeenCalledWith({ repos: [] });
126
+ expect(core.setSecret).toHaveBeenCalledWith("inst-token");
127
+ expect(core.saveState).toHaveBeenCalledWith("token", "inst-token");
128
+ expect(core.setOutput).toHaveBeenCalledWith("token", "inst-token");
129
+ expect(core.info).toHaveBeenCalledWith("» installation token acquired (current repo only)");
130
+ });
131
+
132
+ it("acquires a token for additional repos from the repos input", async () => {
133
+ vi.mocked(core.getInput).mockReturnValueOnce("octo/a, octo/b ,");
134
+
135
+ await gha.runCli({ args: ["token"], prog: "terramend" });
136
+
137
+ expect(acquireInstallationToken).toHaveBeenCalledWith({ repos: ["octo/a", "octo/b"] });
138
+ expect(core.info).toHaveBeenCalledWith(
139
+ "» installation token acquired (current repo + octo/a, octo/b)",
140
+ );
141
+ });
142
+
143
+ it("revokes the saved token in the post step", async () => {
144
+ vi.mocked(core.getState).mockReturnValueOnce("saved-token");
145
+
146
+ await gha.runCli({ args: ["token", "--post"], prog: "terramend" });
147
+
148
+ expect(revokeInstallationToken).toHaveBeenCalledWith("saved-token");
149
+ expect(core.info).toHaveBeenCalledWith("» installation token revoked");
150
+ });
151
+
152
+ it("skips revocation when no token was saved", async () => {
153
+ await gha.runCli({ args: ["token", "--post"], prog: "terramend" });
154
+
155
+ expect(revokeInstallationToken).not.toHaveBeenCalled();
156
+ expect(core.debug).toHaveBeenCalledWith("no token found in state, skipping revocation");
157
+ });
158
+ });
159
+
160
+ describe("run – failure reporting", () => {
161
+ it("setFailed when main reports an unsuccessful run", async () => {
162
+ vi.mocked(main).mockResolvedValueOnce({ success: false, error: "agent gave up" });
163
+
164
+ await gha.run([]);
165
+
166
+ expect(core.setFailed).toHaveBeenCalledWith("action failed: agent gave up");
167
+ });
168
+
169
+ it("setFailed with a fallback message when main fails without an error", async () => {
170
+ vi.mocked(main).mockResolvedValueOnce({ success: false });
171
+
172
+ await gha.run([]);
173
+
174
+ expect(core.setFailed).toHaveBeenCalledWith("action failed: agent execution failed");
175
+ });
176
+
177
+ it("setFailed when main throws", async () => {
178
+ vi.mocked(main).mockRejectedValueOnce(new Error("boom"));
179
+
180
+ await gha.run([]);
181
+
182
+ expect(core.setFailed).toHaveBeenCalledWith("action failed: boom");
183
+ });
184
+
185
+ it("setFailed when token acquisition throws", async () => {
186
+ vi.mocked(acquireInstallationToken).mockRejectedValueOnce(new Error("no app key"));
187
+
188
+ await gha.run(["token"]);
189
+
190
+ expect(core.setFailed).toHaveBeenCalledWith("no app key");
191
+ });
192
+ });
@@ -0,0 +1,188 @@
1
+ import { dirname } from "node:path";
2
+ import * as core from "@actions/core";
3
+ import arg from "arg";
4
+ import { main } from "#app/main";
5
+ import { acquireInstallationToken, revokeInstallationToken } from "#app/utils/token";
6
+
7
+ // GitHub Actions runs the action entry point with the node24 binary specified
8
+ // in action.yml, but doesn't add that binary's directory to PATH. Without this,
9
+ // spawned processes (pnpm, npm, etc.) resolve to the runner's default node (v20).
10
+ process.env.PATH = `${dirname(process.execPath)}:${process.env.PATH}`;
11
+
12
+ const STATE_TOKEN = "token";
13
+
14
+ interface GhaCliParams {
15
+ args: string[];
16
+ prog: string;
17
+ showHelp?: boolean;
18
+ }
19
+
20
+ async function runMain(): Promise<void> {
21
+ try {
22
+ const result = await main();
23
+ if (!result.success) {
24
+ throw new Error(result.error || "agent execution failed");
25
+ }
26
+ } catch (error) {
27
+ const errorMessage = error instanceof Error ? error.message : "unknown error occurred";
28
+ core.setFailed(`action failed: ${errorMessage}`);
29
+ }
30
+ }
31
+
32
+ async function tokenMain(): Promise<void> {
33
+ const reposInput = core.getInput("repos");
34
+ const additionalRepos = reposInput
35
+ ? reposInput
36
+ .split(",")
37
+ .map((r) => r.trim())
38
+ .filter(Boolean)
39
+ : [];
40
+
41
+ const token = await acquireInstallationToken({ repos: additionalRepos });
42
+
43
+ core.setSecret(token);
44
+ core.saveState(STATE_TOKEN, token);
45
+ core.setOutput("token", token);
46
+
47
+ const scope = additionalRepos.length
48
+ ? `current repo + ${additionalRepos.join(", ")}`
49
+ : "current repo only";
50
+ core.info(`» installation token acquired (${scope})`);
51
+ }
52
+
53
+ async function tokenPost(): Promise<void> {
54
+ const token = core.getState(STATE_TOKEN);
55
+ if (!token) {
56
+ core.debug("no token found in state, skipping revocation");
57
+ return;
58
+ }
59
+ await revokeInstallationToken(token);
60
+ core.info("» installation token revoked");
61
+ }
62
+
63
+ function printGhaUsage(params: { stream: typeof console.log; prog: string }): void {
64
+ params.stream(`usage: ${params.prog} gha [subcommand]\n`);
65
+ params.stream("run the github action runtime flow.");
66
+ params.stream("");
67
+ params.stream("subcommands:");
68
+ params.stream(" token acquire a github app installation token");
69
+ params.stream("");
70
+ params.stream("options:");
71
+ params.stream(" -h, --help show help");
72
+ }
73
+
74
+ function printGhaTokenUsage(params: { stream: typeof console.log; prog: string }): void {
75
+ params.stream(`usage: ${params.prog} gha token [--post]\n`);
76
+ params.stream("acquire a github app installation token, or revoke it in the post step.");
77
+ params.stream("");
78
+ params.stream("options:");
79
+ params.stream(" -h, --help show help");
80
+ params.stream(" --post revoke the previously-acquired token (post-step usage only)");
81
+ }
82
+
83
+ function parseGhaArgs(args: string[]) {
84
+ return arg(
85
+ {
86
+ "--help": Boolean,
87
+ "-h": "--help",
88
+ },
89
+ {
90
+ argv: args,
91
+ stopAtPositional: true,
92
+ },
93
+ );
94
+ }
95
+
96
+ function parseGhaTokenArgs(args: string[]) {
97
+ return arg(
98
+ {
99
+ "--help": Boolean,
100
+ "--post": Boolean,
101
+ "-h": "--help",
102
+ },
103
+ {
104
+ argv: args,
105
+ },
106
+ );
107
+ }
108
+
109
+ export async function runCli(params: GhaCliParams): Promise<void> {
110
+ if (params.showHelp) {
111
+ printGhaUsage({ stream: console.log, prog: params.prog });
112
+ return;
113
+ }
114
+
115
+ let parsed: ReturnType<typeof parseGhaArgs>;
116
+ try {
117
+ parsed = parseGhaArgs(params.args);
118
+ } catch (error) {
119
+ const message = error instanceof Error ? error.message : String(error);
120
+ console.error(`${message}\n`);
121
+ printGhaUsage({ stream: console.error, prog: params.prog });
122
+ process.exit(1);
123
+ }
124
+
125
+ if (parsed["--help"]) {
126
+ printGhaUsage({ stream: console.log, prog: params.prog });
127
+ return;
128
+ }
129
+
130
+ const positional = parsed._;
131
+ const subcommand = positional[0];
132
+
133
+ if (!subcommand) {
134
+ await run(["gha"]);
135
+ return;
136
+ }
137
+
138
+ if (subcommand !== "token") {
139
+ console.error(`unknown gha subcommand: ${subcommand}\n`);
140
+ printGhaUsage({ stream: console.error, prog: params.prog });
141
+ process.exit(1);
142
+ }
143
+
144
+ // gha token [--post]
145
+ let tokenParsed: ReturnType<typeof parseGhaTokenArgs>;
146
+ try {
147
+ tokenParsed = parseGhaTokenArgs(positional.slice(1));
148
+ } catch (error) {
149
+ const message = error instanceof Error ? error.message : String(error);
150
+ console.error(`${message}\n`);
151
+ printGhaTokenUsage({ stream: console.error, prog: params.prog });
152
+ process.exit(1);
153
+ }
154
+
155
+ if (tokenParsed["--help"]) {
156
+ printGhaTokenUsage({ stream: console.log, prog: params.prog });
157
+ return;
158
+ }
159
+
160
+ if (tokenParsed._.length > 0) {
161
+ console.error(`unexpected positional arguments for gha token: ${tokenParsed._.join(" ")}\n`);
162
+ printGhaTokenUsage({ stream: console.error, prog: params.prog });
163
+ process.exit(1);
164
+ }
165
+
166
+ const normalizedArgs = ["gha", "token"];
167
+ if (tokenParsed["--post"]) {
168
+ normalizedArgs.push("--post");
169
+ }
170
+ await run(normalizedArgs);
171
+ }
172
+
173
+ export async function run(args: string[]) {
174
+ try {
175
+ if (args.includes("token")) {
176
+ if (args.includes("--post")) {
177
+ await tokenPost();
178
+ } else {
179
+ await tokenMain();
180
+ }
181
+ } else {
182
+ await runMain();
183
+ }
184
+ } catch (error) {
185
+ const message = error instanceof Error ? error.message : String(error);
186
+ core.setFailed(message);
187
+ }
188
+ }