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,492 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { chmodSync, createWriteStream, existsSync, mkdirSync } from "node:fs";
3
+ import { mkdtemp } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { pipeline } from "node:stream/promises";
7
+ import { setTimeout as sleep } from "node:timers/promises";
8
+ import { log } from "#app/utils/cli";
9
+
10
+ export interface InstallFromNpmTarballParams {
11
+ packageName: string;
12
+ version: string;
13
+ executablePath: string;
14
+ installDependencies?: boolean;
15
+ }
16
+
17
+ export interface InstallFromCurlParams {
18
+ installUrl: string;
19
+ executableName: string;
20
+ }
21
+
22
+ export interface InstallFromDirectTarballParams {
23
+ url: string;
24
+ executablePath: string;
25
+ stripComponents?: number;
26
+ }
27
+
28
+ export interface InstallFromGithubParams {
29
+ owner: string;
30
+ repo: string;
31
+ tag?: string;
32
+ assetName?: string;
33
+ executablePath?: string;
34
+ githubInstallationToken?: string;
35
+ }
36
+
37
+ export interface InstallFromGithubTarballParams {
38
+ owner: string;
39
+ repo: string;
40
+ tag?: string;
41
+ assetNamePattern: string;
42
+ executablePath: string;
43
+ githubInstallationToken?: string;
44
+ }
45
+
46
+ interface NpmRegistryData {
47
+ "dist-tags": { latest: string };
48
+ versions: Record<string, unknown>;
49
+ }
50
+
51
+ /**
52
+ * Install a CLI tool from an npm package tarball
53
+ * Downloads the tarball, extracts it to a temp directory, and returns the path to the CLI executable
54
+ * The temp directory will be cleaned up by the OS automatically
55
+ */
56
+ export async function installFromNpmTarball(params: InstallFromNpmTarballParams): Promise<string> {
57
+ const tempDir = process.env.TERRAMEND_TEMP_DIR;
58
+ if (!tempDir) throw new Error("TERRAMEND_TEMP_DIR is not set");
59
+
60
+ const extractedDir = join(tempDir, "package");
61
+ const cliPath = join(extractedDir, params.executablePath);
62
+
63
+ if (existsSync(cliPath)) {
64
+ log.debug(`» using cached binary at ${cliPath}`);
65
+ return cliPath;
66
+ }
67
+
68
+ // Resolve version if it's a range or "latest"
69
+ let resolvedVersion = params.version;
70
+ if (
71
+ params.version.startsWith("^") ||
72
+ params.version.startsWith("~") ||
73
+ params.version === "latest"
74
+ ) {
75
+ const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
76
+ log.debug(`» resolving version for ${params.version}...`);
77
+ try {
78
+ const registryResponse = await fetch(`${npmRegistry}/${params.packageName}`);
79
+ if (!registryResponse.ok) {
80
+ throw new Error(`Failed to query registry: ${registryResponse.status}`);
81
+ }
82
+ const registryData = (await registryResponse.json()) as NpmRegistryData;
83
+ resolvedVersion = registryData["dist-tags"].latest;
84
+ log.debug(`» resolved to version ${resolvedVersion}`);
85
+ } catch (error) {
86
+ log.warning(
87
+ `Failed to resolve version from registry: ${error instanceof Error ? error.message : String(error)}`,
88
+ );
89
+ throw error;
90
+ }
91
+ }
92
+
93
+ log.debug(`» installing ${params.packageName}@${resolvedVersion}...`);
94
+
95
+ const tarballPath = join(tempDir, "package.tgz");
96
+
97
+ // Download tarball from npm
98
+ const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
99
+ // Handle scoped packages (e.g., @scope/package -> @scope%2Fpackage/-/package-version.tgz)
100
+ let tarballUrl: string;
101
+ if (params.packageName.startsWith("@")) {
102
+ const [scope, name] = params.packageName.slice(1).split("/");
103
+ const scopedPackageName = `@${scope}%2F${name}`;
104
+ tarballUrl = `${npmRegistry}/${scopedPackageName}/-/${name}-${resolvedVersion}.tgz`;
105
+ } else {
106
+ tarballUrl = `${npmRegistry}/${params.packageName}/-/${params.packageName}-${resolvedVersion}.tgz`;
107
+ }
108
+
109
+ log.debug(`» downloading from ${tarballUrl}...`);
110
+ const response = await fetch(tarballUrl);
111
+ if (!response.ok) {
112
+ throw new Error(`Failed to download tarball: ${response.status} ${response.statusText}`);
113
+ }
114
+
115
+ // Write tarball to file
116
+ if (!response.body) throw new Error("Response body is null");
117
+ const fileStream = createWriteStream(tarballPath);
118
+ await pipeline(response.body, fileStream);
119
+ log.debug(`» downloaded tarball to ${tarballPath}`);
120
+
121
+ // Extract tarball
122
+ log.debug(`» extracting tarball...`);
123
+ const extractResult = spawnSync("tar", ["-xzf", tarballPath, "-C", tempDir], {
124
+ stdio: "pipe",
125
+ encoding: "utf-8",
126
+ });
127
+ if (extractResult.status !== 0) {
128
+ throw new Error(
129
+ `Failed to extract tarball: ${extractResult.stderr || extractResult.stdout || "Unknown error"}`,
130
+ );
131
+ }
132
+
133
+ if (!existsSync(cliPath)) {
134
+ throw new Error(`Executable not found in extracted package at ${cliPath}`);
135
+ }
136
+
137
+ // Install dependencies if requested
138
+ if (params.installDependencies) {
139
+ log.debug(`» installing dependencies for ${params.packageName}...`);
140
+ const installResult = spawnSync("npm", ["install", "--production"], {
141
+ cwd: extractedDir,
142
+ stdio: "pipe",
143
+ encoding: "utf-8",
144
+ // @anthropic-ai/claude-code (2.1.113+) guards its `prepare` script with an
145
+ // `AUTHORIZED` env check that hard-fails any non-release install. we need
146
+ // its `postinstall` (which copies the native binary from the platform
147
+ // optionalDependency over the bin/claude.exe stub) to run, so we set the
148
+ // flag to no-op the guard. harmless for packages without such a guard.
149
+ env: { ...process.env, AUTHORIZED: "1" },
150
+ });
151
+ if (installResult.status !== 0) {
152
+ throw new Error(
153
+ `Failed to install dependencies: ${installResult.stderr || installResult.stdout || "Unknown error"}`,
154
+ );
155
+ }
156
+ log.debug(`» dependencies installed`);
157
+ }
158
+
159
+ // Make the file executable
160
+ chmodSync(cliPath, 0o755);
161
+
162
+ log.debug(`» ${params.packageName} installed at ${cliPath}`);
163
+
164
+ return cliPath;
165
+ }
166
+
167
+ /**
168
+ * Fetch with retry logic if Retry-After header is present
169
+ */
170
+ async function fetchWithRetry(
171
+ url: string,
172
+ headers: Record<string, string>,
173
+ errorMessage: string,
174
+ ): Promise<Response> {
175
+ const response = await fetch(url, { headers });
176
+ if (!response.ok) {
177
+ const retryAfter = response.headers.get("Retry-After") || response.headers.get("retry-after");
178
+ if (retryAfter) {
179
+ const waitSeconds = parseInt(retryAfter, 10);
180
+ if (!Number.isNaN(waitSeconds) && waitSeconds > 0) {
181
+ log.info(`» rate limited, waiting ${waitSeconds} seconds before retry...`);
182
+ await sleep(waitSeconds * 1000);
183
+ const retryResponse = await fetch(url, { headers });
184
+ if (!retryResponse.ok) {
185
+ throw new Error(
186
+ `${errorMessage}: ${retryResponse.status} ${retryResponse.statusText} (retry failed)`,
187
+ );
188
+ }
189
+ return retryResponse;
190
+ }
191
+ }
192
+ throw new Error(`${errorMessage}: ${response.status} ${response.statusText}`);
193
+ }
194
+ return response;
195
+ }
196
+
197
+ /**
198
+ * Install a CLI tool from GitHub releases
199
+ * Downloads the latest release asset from GitHub and returns the path to the executable
200
+ * The temp directory will be cleaned up by the OS automatically
201
+ */
202
+ export async function installFromGithub(params: InstallFromGithubParams): Promise<string> {
203
+ // use a deterministic subdir in TERRAMEND_TEMP_DIR so repeated calls are cached
204
+ const terramendTemp = process.env.TERRAMEND_TEMP_DIR;
205
+ const installDir = terramendTemp
206
+ ? join(terramendTemp, `github-${params.owner}-${params.repo}`)
207
+ : await mkdtemp(join(tmpdir(), `${params.owner}-${params.repo}-github-`));
208
+
209
+ const expectedCliPath = join(installDir, params.executablePath ?? params.assetName ?? "asset");
210
+
211
+ if (existsSync(expectedCliPath)) {
212
+ log.debug(`» using cached binary at ${expectedCliPath}`);
213
+ return expectedCliPath;
214
+ }
215
+
216
+ log.info(`» installing ${params.owner}/${params.repo} from GitHub releases...`);
217
+
218
+ // fetch release from GitHub API (pinned tag or latest)
219
+ const releaseUrl = params.tag
220
+ ? `https://api.github.com/repos/${params.owner}/${params.repo}/releases/tags/${params.tag}`
221
+ : `https://api.github.com/repos/${params.owner}/${params.repo}/releases/latest`;
222
+ log.debug(`» fetching release from ${releaseUrl}...`);
223
+
224
+ const headers: Record<string, string> = {};
225
+ if (params.githubInstallationToken) {
226
+ headers.Authorization = `Bearer ${params.githubInstallationToken}`;
227
+ }
228
+
229
+ const releaseResponse = await fetchWithRetry(releaseUrl, headers, "Failed to fetch release");
230
+
231
+ const releaseData = (await releaseResponse.json()) as {
232
+ tag_name: string;
233
+ assets: Array<{
234
+ name: string;
235
+ browser_download_url: string;
236
+ }>;
237
+ };
238
+
239
+ log.debug(`» found release ${releaseData.tag_name}`);
240
+
241
+ const asset = releaseData.assets.find((a) => a.name === params.assetName);
242
+ if (!asset) {
243
+ throw new Error(`Asset '${params.assetName}' not found in release ${releaseData.tag_name}`);
244
+ }
245
+ const assetUrl = asset.browser_download_url;
246
+
247
+ log.debug(`» downloading asset from ${assetUrl}...`);
248
+
249
+ mkdirSync(installDir, { recursive: true });
250
+
251
+ // determine file extension and download path
252
+ const urlPath = new URL(assetUrl).pathname;
253
+ const fileName = urlPath.split("/").pop() || "asset";
254
+ const downloadPath = join(installDir, fileName);
255
+
256
+ // download the asset
257
+ const assetResponse = await fetchWithRetry(assetUrl, headers, "Failed to download asset");
258
+
259
+ if (!assetResponse.body) throw new Error("Response body is null");
260
+ const fileStream = createWriteStream(downloadPath);
261
+ await pipeline(assetResponse.body, fileStream);
262
+ log.debug(`» downloaded asset to ${downloadPath}`);
263
+
264
+ // determine the executable path
265
+ const cliPath = params.executablePath ? join(installDir, params.executablePath) : downloadPath;
266
+
267
+ if (!existsSync(cliPath)) {
268
+ throw new Error(`Executable not found at ${cliPath}`);
269
+ }
270
+
271
+ chmodSync(cliPath, 0o755);
272
+ log.info(`» installed from GitHub release at ${cliPath}`);
273
+
274
+ return cliPath;
275
+ }
276
+
277
+ /**
278
+ * Install a CLI tool from a GitHub release tarball
279
+ * Downloads the tar.gz from GitHub releases, extracts it, and returns the path to the CLI executable
280
+ * The temp directory will be cleaned up by the OS automatically
281
+ */
282
+ export async function installFromGithubTarball(
283
+ params: InstallFromGithubTarballParams,
284
+ ): Promise<string> {
285
+ const tempDir = process.env.TERRAMEND_TEMP_DIR;
286
+ if (!tempDir) throw new Error("TERRAMEND_TEMP_DIR is not set");
287
+
288
+ const cliPath = join(tempDir, params.executablePath);
289
+
290
+ if (existsSync(cliPath)) {
291
+ log.debug(`» using cached binary at ${cliPath}`);
292
+ return cliPath;
293
+ }
294
+
295
+ log.info(`» installing ${params.owner}/${params.repo} from GitHub releases...`);
296
+
297
+ // determine platform-specific asset name
298
+ const os = process.platform === "darwin" ? "darwin" : "linux";
299
+ const arch = process.arch === "arm64" ? "arm64" : "x64";
300
+ const assetName = params.assetNamePattern.replace("{os}", os).replace("{arch}", arch);
301
+
302
+ // fetch release from GitHub API (pinned tag or latest)
303
+ const releaseUrl = params.tag
304
+ ? `https://api.github.com/repos/${params.owner}/${params.repo}/releases/tags/${params.tag}`
305
+ : `https://api.github.com/repos/${params.owner}/${params.repo}/releases/latest`;
306
+ log.info(`» fetching release from ${releaseUrl}...`);
307
+
308
+ const headers: Record<string, string> = {};
309
+ if (params.githubInstallationToken) {
310
+ headers.Authorization = `Bearer ${params.githubInstallationToken}`;
311
+ }
312
+
313
+ const releaseResponse = await fetchWithRetry(releaseUrl, headers, "Failed to fetch release");
314
+
315
+ const releaseData = (await releaseResponse.json()) as {
316
+ tag_name: string;
317
+ assets: Array<{
318
+ name: string;
319
+ browser_download_url: string;
320
+ }>;
321
+ };
322
+
323
+ log.debug(`» found release: ${releaseData.tag_name}`);
324
+
325
+ const asset = releaseData.assets.find((a) => a.name === assetName);
326
+ if (!asset) {
327
+ throw new Error(`Asset '${assetName}' not found in release ${releaseData.tag_name}`);
328
+ }
329
+ const assetUrl = asset.browser_download_url;
330
+
331
+ log.debug(`» downloading asset from ${assetUrl}...`);
332
+
333
+ const tarballPath = join(tempDir, assetName);
334
+
335
+ // download the asset
336
+ const assetResponse = await fetchWithRetry(assetUrl, headers, "Failed to download asset");
337
+
338
+ if (!assetResponse.body) throw new Error("Response body is null");
339
+ const fileStream = createWriteStream(tarballPath);
340
+ await pipeline(assetResponse.body, fileStream);
341
+ log.debug(`» downloaded tarball to ${tarballPath}`);
342
+
343
+ // extract tar.gz
344
+ log.debug(`» extracting tarball...`);
345
+ const extractResult = spawnSync("tar", ["-xzf", tarballPath, "-C", tempDir], {
346
+ stdio: "pipe",
347
+ encoding: "utf-8",
348
+ });
349
+ if (extractResult.status !== 0) {
350
+ throw new Error(
351
+ `Failed to extract tarball: ${extractResult.stderr || extractResult.stdout || "Unknown error"}`,
352
+ );
353
+ }
354
+
355
+ if (!existsSync(cliPath)) {
356
+ throw new Error(`Executable not found in extracted tarball at ${cliPath}`);
357
+ }
358
+
359
+ // make the file executable
360
+ chmodSync(cliPath, 0o755);
361
+
362
+ log.info(`» ${params.owner}/${params.repo} installed at ${cliPath}`);
363
+
364
+ return cliPath;
365
+ }
366
+
367
+ /**
368
+ * Install a CLI tool from a direct tarball URL.
369
+ * Downloads the tarball, extracts it to a temp directory, and returns the path to the CLI executable.
370
+ */
371
+ export async function installFromDirectTarball(
372
+ params: InstallFromDirectTarballParams,
373
+ ): Promise<string> {
374
+ const tempDir = process.env.TERRAMEND_TEMP_DIR;
375
+ if (!tempDir) throw new Error("TERRAMEND_TEMP_DIR is not set");
376
+
377
+ const extractDir = join(tempDir, "direct-package");
378
+ const cliPath = join(extractDir, params.executablePath);
379
+
380
+ if (existsSync(cliPath)) {
381
+ log.debug(`» using cached binary at ${cliPath}`);
382
+ return cliPath;
383
+ }
384
+
385
+ log.info(`» downloading tarball from ${params.url}...`);
386
+
387
+ const tarballPath = join(tempDir, "direct-package.tgz");
388
+
389
+ const response = await fetchWithRetry(params.url, {}, "failed to download tarball");
390
+ if (!response.body) throw new Error("response body is null");
391
+
392
+ const fileStream = createWriteStream(tarballPath);
393
+ await pipeline(response.body, fileStream);
394
+ log.debug(`» downloaded tarball to ${tarballPath}`);
395
+
396
+ mkdirSync(extractDir, { recursive: true });
397
+
398
+ const tarArgs = ["-xzf", tarballPath, "-C", extractDir];
399
+ if (params.stripComponents !== undefined && params.stripComponents > 0) {
400
+ tarArgs.push(`--strip-components=${Math.floor(params.stripComponents)}`);
401
+ }
402
+
403
+ log.debug(`» extracting tarball...`);
404
+ const extractResult = spawnSync("tar", tarArgs, {
405
+ stdio: "pipe",
406
+ encoding: "utf-8",
407
+ });
408
+ if (extractResult.status !== 0) {
409
+ throw new Error(
410
+ `failed to extract tarball: ${extractResult.stderr || extractResult.stdout || "unknown error"}`,
411
+ );
412
+ }
413
+
414
+ if (!existsSync(cliPath)) {
415
+ throw new Error(`executable not found in extracted tarball at ${cliPath}`);
416
+ }
417
+
418
+ chmodSync(cliPath, 0o755);
419
+ log.info(`» installed at ${cliPath}`);
420
+
421
+ return cliPath;
422
+ }
423
+
424
+ /**
425
+ * Install a CLI tool from a curl-based install script
426
+ * Downloads the install script, runs it with HOME set to temp directory, and returns the path to the CLI executable
427
+ * The temp directory will be cleaned up by the OS automatically
428
+ */
429
+ export async function installFromCurl(params: InstallFromCurlParams): Promise<string> {
430
+ const tempDir = process.env.TERRAMEND_TEMP_DIR;
431
+ if (!tempDir) throw new Error("TERRAMEND_TEMP_DIR is not set");
432
+
433
+ const cliPath = join(tempDir, ".local", "bin", params.executableName);
434
+
435
+ if (existsSync(cliPath)) {
436
+ log.debug(`» using cached binary at ${cliPath}`);
437
+ return cliPath;
438
+ }
439
+
440
+ log.info(`» installing ${params.executableName}...`);
441
+
442
+ const installScriptPath = join(tempDir, "install.sh");
443
+
444
+ // Download the install script
445
+ log.debug(`» downloading install script from ${params.installUrl}...`);
446
+ const installScriptResponse = await fetch(params.installUrl);
447
+ if (!installScriptResponse.ok) {
448
+ throw new Error(`Failed to download install script: ${installScriptResponse.status}`);
449
+ }
450
+
451
+ if (!installScriptResponse.body) throw new Error("Response body is null");
452
+ const fileStream = createWriteStream(installScriptPath);
453
+ await pipeline(installScriptResponse.body, fileStream);
454
+ log.debug(`» downloaded install script to ${installScriptPath}`);
455
+
456
+ // Make install script executable
457
+ chmodSync(installScriptPath, 0o755);
458
+
459
+ log.debug(`» installing to temp directory at ${tempDir}...`);
460
+
461
+ const installResult = spawnSync("bash", [installScriptPath], {
462
+ cwd: tempDir,
463
+ env: {
464
+ // Run the install script with HOME set to temp directory
465
+ // ensuring a fresh install for each run
466
+ HOME: tempDir,
467
+ // XDG_CONFIG_HOME must match HOME so CLI tools find config in the right place
468
+ XDG_CONFIG_HOME: join(tempDir, ".config"),
469
+ SHELL: process.env.SHELL,
470
+ USER: process.env.USER,
471
+ },
472
+ stdio: "pipe",
473
+ encoding: "utf-8",
474
+ });
475
+
476
+ if (installResult.status !== 0) {
477
+ const errorOutput = installResult.stderr || installResult.stdout || "No output";
478
+ throw new Error(
479
+ `Failed to install ${params.executableName}. Install script exited with code ${installResult.status}. Output: ${errorOutput}`,
480
+ );
481
+ }
482
+
483
+ if (!existsSync(cliPath)) {
484
+ throw new Error(`Executable not found at ${cliPath}`);
485
+ }
486
+
487
+ // Ensure binary is executable
488
+ chmodSync(cliPath, 0o755);
489
+ log.info(`» ${params.executableName} installed at ${cliPath}`);
490
+
491
+ return cliPath;
492
+ }