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,309 @@
1
+ import { join } from "node:path";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import {
4
+ AwaitDependencyInstallationTool,
5
+ StartDependencyInstallationTool,
6
+ startInstallation,
7
+ } from "#app/mcp/dependencies";
8
+ import type { ToolContext } from "#app/mcp/server";
9
+ import { type PrepResult, runPrepPhase } from "#app/prep/index";
10
+ import type { ToolState } from "#app/toolState";
11
+
12
+ vi.mock("#app/prep/index", () => ({
13
+ runPrepPhase: vi.fn(async (): Promise<PrepResult[]> => []),
14
+ }));
15
+
16
+ const runPrepPhaseMock = vi.mocked(runPrepPhase);
17
+
18
+ type ToolResultShape = { content: [{ type: "text"; text: string }]; isError?: boolean };
19
+
20
+ async function runTool(t: { execute?: unknown }, params: unknown): Promise<ToolResultShape> {
21
+ const exec = t.execute as (p: unknown, c: unknown) => Promise<ToolResultShape>;
22
+ return exec(params, {});
23
+ }
24
+
25
+ function makeCtx(overrides?: {
26
+ shell?: "disabled" | "restricted" | "enabled";
27
+ toolState?: Partial<ToolState>;
28
+ }): { ctx: ToolContext; toolState: ToolState } {
29
+ const toolState = { ...overrides?.toolState } as ToolState;
30
+ const ctx = {
31
+ payload: { shell: overrides?.shell ?? "restricted" },
32
+ toolState,
33
+ tmpdir: join("/tmp", "terramend"),
34
+ } as unknown as ToolContext;
35
+ return { ctx, toolState };
36
+ }
37
+
38
+ function deferred<T>(): {
39
+ promise: Promise<T>;
40
+ resolve: (v: T) => void;
41
+ reject: (e: Error) => void;
42
+ } {
43
+ let resolve: (v: T) => void = () => undefined;
44
+ let reject: (e: Error) => void = () => undefined;
45
+ const promise = new Promise<T>((res, rej) => {
46
+ resolve = res;
47
+ reject = rej;
48
+ });
49
+ return { promise, resolve, reject };
50
+ }
51
+
52
+ const nodeOk: PrepResult = {
53
+ language: "node",
54
+ packageManager: "pnpm",
55
+ dependenciesInstalled: true,
56
+ issues: [],
57
+ };
58
+ const pythonOk: PrepResult = {
59
+ language: "python",
60
+ packageManager: "pip",
61
+ configFile: "requirements.txt",
62
+ dependenciesInstalled: true,
63
+ issues: [],
64
+ };
65
+ const nodeFailed: PrepResult = {
66
+ language: "node",
67
+ packageManager: "npm",
68
+ dependenciesInstalled: false,
69
+ issues: ["ERESOLVE unable to resolve dependency tree"],
70
+ };
71
+ const pythonFailedNoIssues: PrepResult = {
72
+ language: "python",
73
+ packageManager: "poetry",
74
+ configFile: "pyproject.toml",
75
+ dependenciesInstalled: false,
76
+ issues: [],
77
+ };
78
+ const unknownLang: PrepResult = { language: "unknown", dependenciesInstalled: false, issues: [] };
79
+
80
+ beforeEach(() => {
81
+ vi.clearAllMocks();
82
+ runPrepPhaseMock.mockImplementation(async () => []);
83
+ });
84
+
85
+ describe("startInstallation", () => {
86
+ it("suppresses lifecycle scripts when shell is disabled", () => {
87
+ const { ctx } = makeCtx({ shell: "disabled" });
88
+ startInstallation(ctx);
89
+ expect(runPrepPhaseMock).toHaveBeenCalledWith({
90
+ ignoreScripts: true,
91
+ binDir: join(join("/tmp", "terramend"), "pm-bin"),
92
+ });
93
+ });
94
+
95
+ it("allows lifecycle scripts when shell is not disabled", () => {
96
+ const { ctx } = makeCtx({ shell: "restricted" });
97
+ startInstallation(ctx);
98
+ expect(runPrepPhaseMock).toHaveBeenCalledWith(
99
+ expect.objectContaining({ ignoreScripts: false }),
100
+ );
101
+ });
102
+
103
+ it("is idempotent — a second call does not restart the prep phase", () => {
104
+ const { ctx } = makeCtx();
105
+ startInstallation(ctx);
106
+ startInstallation(ctx);
107
+ expect(runPrepPhaseMock).toHaveBeenCalledTimes(1);
108
+ });
109
+
110
+ it("transitions to completed when every result installed", async () => {
111
+ const d = deferred<PrepResult[]>();
112
+ runPrepPhaseMock.mockReturnValueOnce(d.promise);
113
+ const { ctx, toolState } = makeCtx();
114
+ startInstallation(ctx);
115
+ expect(toolState.dependencyInstallation?.status).toBe("in_progress");
116
+
117
+ d.resolve([nodeOk]);
118
+ await d.promise;
119
+ expect(toolState.dependencyInstallation?.status).toBe("completed");
120
+ expect(toolState.dependencyInstallation?.results).toEqual([nodeOk]);
121
+ });
122
+
123
+ it("transitions to failed when a result has install issues", async () => {
124
+ const d = deferred<PrepResult[]>();
125
+ runPrepPhaseMock.mockReturnValueOnce(d.promise);
126
+ const { ctx, toolState } = makeCtx();
127
+ startInstallation(ctx);
128
+
129
+ d.resolve([nodeFailed]);
130
+ await d.promise;
131
+ expect(toolState.dependencyInstallation?.status).toBe("failed");
132
+ });
133
+
134
+ it("stays completed when a failure carries no issues (not treated as failed)", async () => {
135
+ const d = deferred<PrepResult[]>();
136
+ runPrepPhaseMock.mockReturnValueOnce(d.promise);
137
+ const { ctx, toolState } = makeCtx();
138
+ startInstallation(ctx);
139
+
140
+ d.resolve([pythonFailedNoIssues]);
141
+ await d.promise;
142
+ expect(toolState.dependencyInstallation?.status).toBe("completed");
143
+ });
144
+
145
+ it("transitions to failed when the prep phase rejects", async () => {
146
+ const d = deferred<PrepResult[]>();
147
+ runPrepPhaseMock.mockReturnValueOnce(d.promise);
148
+ const { ctx, toolState } = makeCtx();
149
+ startInstallation(ctx);
150
+
151
+ d.reject(new Error("prep blew up"));
152
+ await d.promise.catch(() => undefined);
153
+ // give the rejection handler a microtask to run
154
+ await Promise.resolve();
155
+ expect(toolState.dependencyInstallation?.status).toBe("failed");
156
+ });
157
+
158
+ it("ignores settlement when the installation state was cleared meanwhile", async () => {
159
+ const clearInstallation = (state: ToolState): void => {
160
+ delete state.dependencyInstallation;
161
+ };
162
+ const ok = deferred<PrepResult[]>();
163
+ runPrepPhaseMock.mockReturnValueOnce(ok.promise);
164
+ const { ctx, toolState } = makeCtx();
165
+ startInstallation(ctx);
166
+ clearInstallation(toolState);
167
+ ok.resolve([nodeOk]);
168
+ await ok.promise;
169
+ expect(toolState.dependencyInstallation).toBeUndefined();
170
+
171
+ const bad = deferred<PrepResult[]>();
172
+ runPrepPhaseMock.mockReturnValueOnce(bad.promise);
173
+ startInstallation(ctx);
174
+ clearInstallation(toolState);
175
+ bad.reject(new Error("late failure"));
176
+ await bad.promise.catch(() => undefined);
177
+ await Promise.resolve();
178
+ expect(toolState.dependencyInstallation).toBeUndefined();
179
+ });
180
+ });
181
+
182
+ describe("StartDependencyInstallationTool", () => {
183
+ it("starts the installation and reports started", async () => {
184
+ const { ctx } = makeCtx();
185
+ const result = await runTool(StartDependencyInstallationTool(ctx), {});
186
+
187
+ expect(result.isError).toBeUndefined();
188
+ expect(result.content[0].text).toContain("status: started");
189
+ expect(runPrepPhaseMock).toHaveBeenCalledTimes(1);
190
+ });
191
+
192
+ it("reports in_progress without restarting", async () => {
193
+ const { ctx } = makeCtx();
194
+ startInstallation(ctx);
195
+ const result = await runTool(StartDependencyInstallationTool(ctx), {});
196
+
197
+ expect(result.content[0].text).toContain("status: in_progress");
198
+ expect(runPrepPhaseMock).toHaveBeenCalledTimes(1);
199
+ });
200
+
201
+ it("returns the cached summary once completed", async () => {
202
+ const { ctx } = makeCtx({
203
+ toolState: {
204
+ dependencyInstallation: {
205
+ status: "completed",
206
+ promise: undefined,
207
+ results: [nodeOk, pythonOk],
208
+ },
209
+ },
210
+ });
211
+ const result = await runTool(StartDependencyInstallationTool(ctx), {});
212
+
213
+ expect(result.content[0].text).toContain("status: completed");
214
+ expect(result.content[0].text).toContain("Node.js dependencies installed successfully");
215
+ expect(result.content[0].text).toContain("via pip (from requirements.txt)");
216
+ expect(runPrepPhaseMock).not.toHaveBeenCalled();
217
+ });
218
+
219
+ it("summarizes a failed run with undefined results as no-language-detected", async () => {
220
+ const { ctx } = makeCtx({
221
+ toolState: {
222
+ dependencyInstallation: { status: "failed", promise: undefined, results: undefined },
223
+ },
224
+ });
225
+ const result = await runTool(StartDependencyInstallationTool(ctx), {});
226
+
227
+ expect(result.content[0].text).toContain("status: failed");
228
+ expect(result.content[0].text).toContain("No supported language detected");
229
+ });
230
+ });
231
+
232
+ describe("AwaitDependencyInstallationTool", () => {
233
+ it("auto-starts the installation and awaits the results", async () => {
234
+ runPrepPhaseMock.mockResolvedValueOnce([nodeOk]);
235
+ const { ctx } = makeCtx();
236
+ const result = await runTool(AwaitDependencyInstallationTool(ctx), {});
237
+
238
+ expect(result.isError).toBeUndefined();
239
+ expect(runPrepPhaseMock).toHaveBeenCalledTimes(1);
240
+ expect(result.content[0].text).toContain("Node.js dependencies installed successfully");
241
+ });
242
+
243
+ it("returns cached results when the installation already completed", async () => {
244
+ const { ctx } = makeCtx({
245
+ toolState: {
246
+ dependencyInstallation: { status: "completed", promise: undefined, results: [pythonOk] },
247
+ },
248
+ });
249
+ const result = await runTool(AwaitDependencyInstallationTool(ctx), {});
250
+
251
+ expect(result.content[0].text).toContain("status: completed");
252
+ expect(runPrepPhaseMock).not.toHaveBeenCalled();
253
+ });
254
+
255
+ it("returns the failure summary with errors and remediation guidance", async () => {
256
+ const { ctx } = makeCtx({
257
+ toolState: {
258
+ dependencyInstallation: {
259
+ status: "failed",
260
+ promise: undefined,
261
+ results: [nodeFailed, pythonFailedNoIssues],
262
+ },
263
+ },
264
+ });
265
+ const result = await runTool(AwaitDependencyInstallationTool(ctx), {});
266
+
267
+ expect(result.content[0].text).toContain("dependency installation failed via npm");
268
+ expect(result.content[0].text).toContain("ERESOLVE");
269
+ expect(result.content[0].text).toContain("via poetry (from pyproject.toml)");
270
+ expect(result.content[0].text).toContain("unknown error");
271
+ });
272
+
273
+ it("skips unknown-language results and falls back to the no-language message", async () => {
274
+ runPrepPhaseMock.mockResolvedValueOnce([unknownLang]);
275
+ const { ctx } = makeCtx();
276
+ const result = await runTool(AwaitDependencyInstallationTool(ctx), {});
277
+
278
+ expect(result.content[0].text).toContain("No supported language detected");
279
+ });
280
+
281
+ it("errors when the in-progress state lost its promise (corrupted)", async () => {
282
+ const { ctx } = makeCtx({
283
+ toolState: {
284
+ dependencyInstallation: { status: "in_progress", promise: undefined, results: undefined },
285
+ },
286
+ });
287
+ const result = await runTool(AwaitDependencyInstallationTool(ctx), {});
288
+
289
+ expect(result.isError).toBe(true);
290
+ expect(result.content[0].text).toContain("corrupted");
291
+ });
292
+
293
+ it("errors when the installation state cannot be initialized", async () => {
294
+ const toolState = {} as ToolState;
295
+ Object.defineProperty(toolState, "dependencyInstallation", {
296
+ get: () => undefined,
297
+ set: () => undefined,
298
+ });
299
+ const ctx = {
300
+ payload: { shell: "restricted" },
301
+ toolState,
302
+ tmpdir: join("/tmp", "terramend"),
303
+ } as unknown as ToolContext;
304
+ const result = await runTool(AwaitDependencyInstallationTool(ctx), {});
305
+
306
+ expect(result.isError).toBe(true);
307
+ expect(result.content[0].text).toContain("failed to initialize");
308
+ });
309
+ });
@@ -0,0 +1,189 @@
1
+ import { type } from "arktype";
2
+ import type { ToolContext } from "#app/mcp/server";
3
+ import { execute, tool } from "#app/mcp/shared";
4
+ import type { PrepOptions, PrepResult } from "#app/prep/index";
5
+ import { runPrepPhase } from "#app/prep/index";
6
+ import { packageManagerBinDir } from "#app/utils/packageManager";
7
+
8
+ // empty schema for tools with no parameters
9
+ const EmptyParams = type({});
10
+
11
+ /**
12
+ * format prep results into agent-friendly message
13
+ */
14
+ function formatPrepResults(results: PrepResult[]): string {
15
+ if (results.length === 0) {
16
+ return `No supported language detected in this repository (checked for package.json, requirements.txt, pyproject.toml, etc.).
17
+
18
+ Inspect the repository structure to determine how dependencies should be installed, then use shell to install them.`;
19
+ }
20
+
21
+ const lines: string[] = [];
22
+
23
+ for (const result of results) {
24
+ if (result.language === "unknown") {
25
+ continue;
26
+ }
27
+
28
+ const langDisplay = result.language === "node" ? "Node.js" : "Python";
29
+
30
+ if (result.dependenciesInstalled) {
31
+ if (result.language === "node") {
32
+ lines.push(
33
+ `${langDisplay} dependencies installed successfully via ${result.packageManager}.`,
34
+ );
35
+ } else if (result.language === "python") {
36
+ lines.push(
37
+ `${langDisplay} dependencies installed successfully via ${result.packageManager} (from ${result.configFile}).`,
38
+ );
39
+ }
40
+ } else {
41
+ const errorMsg = result.issues.length > 0 ? result.issues.join("\n") : "unknown error";
42
+
43
+ if (result.language === "node") {
44
+ lines.push(`${langDisplay} dependency installation failed via ${result.packageManager}.
45
+
46
+ Error:
47
+ ${errorMsg}
48
+
49
+ Use shell or other tools at your disposal to diagnose and resolve the issue, then install dependencies manually.`);
50
+ } else if (result.language === "python") {
51
+ lines.push(`${langDisplay} dependency installation failed via ${result.packageManager} (from ${result.configFile}).
52
+
53
+ Error:
54
+ ${errorMsg}
55
+
56
+ Use shell or other tools at your disposal to diagnose and resolve the issue, then install dependencies manually.`);
57
+ }
58
+ }
59
+ }
60
+
61
+ if (lines.length === 0) {
62
+ return `No supported language detected in this repository (checked for package.json, requirements.txt, pyproject.toml, etc.).
63
+
64
+ Inspect the repository structure to determine how dependencies should be installed, then use shell to install them.`;
65
+ }
66
+
67
+ return lines.join("\n\n");
68
+ }
69
+
70
+ /**
71
+ * start dependency installation in the background (non-blocking, idempotent).
72
+ * called eagerly from main.ts at startup and also available via MCP tools.
73
+ */
74
+ export function startInstallation(ctx: ToolContext): void {
75
+ // already started or completed - do nothing
76
+ if (ctx.toolState.dependencyInstallation) {
77
+ return;
78
+ }
79
+
80
+ // SECURITY: when shell is disabled, suppress lifecycle scripts to prevent
81
+ // agents from using package.json scripts as a backdoor for code execution
82
+ const prepOptions: PrepOptions = {
83
+ ignoreScripts: ctx.payload.shell === "disabled",
84
+ binDir: packageManagerBinDir(ctx.tmpdir),
85
+ };
86
+
87
+ // initialize state and start installation
88
+ const promise = runPrepPhase(prepOptions);
89
+ ctx.toolState.dependencyInstallation = {
90
+ status: "in_progress",
91
+ promise,
92
+ results: undefined,
93
+ };
94
+
95
+ // when promise completes, update state
96
+ promise.then(
97
+ (results) => {
98
+ if (ctx.toolState.dependencyInstallation) {
99
+ const hasFailure = results.some((r) => !r.dependenciesInstalled && r.issues.length > 0);
100
+ ctx.toolState.dependencyInstallation.status = hasFailure ? "failed" : "completed";
101
+ ctx.toolState.dependencyInstallation.results = results;
102
+ }
103
+ },
104
+ () => {
105
+ if (ctx.toolState.dependencyInstallation) {
106
+ ctx.toolState.dependencyInstallation.status = "failed";
107
+ }
108
+ },
109
+ );
110
+ }
111
+
112
+ export function StartDependencyInstallationTool(ctx: ToolContext) {
113
+ return tool({
114
+ name: "start_dependency_installation",
115
+ description:
116
+ "Start installing project dependencies in the background. This is non-blocking and returns immediately. Call this early (right after branch checkout) if you anticipate needing to run tests, builds, or other commands that require dependencies. Idempotent - safe to call multiple times.",
117
+ parameters: EmptyParams,
118
+ execute: execute(async () => {
119
+ const state = ctx.toolState.dependencyInstallation;
120
+
121
+ // already completed
122
+ if (state?.status === "completed" || state?.status === "failed") {
123
+ return {
124
+ status: state.status,
125
+ message: `Dependency installation already completed.`,
126
+ summary: formatPrepResults(state.results || []),
127
+ };
128
+ }
129
+
130
+ // already in progress
131
+ if (state?.status === "in_progress") {
132
+ return {
133
+ status: "in_progress",
134
+ message:
135
+ "Dependency installation is already in progress. Call await_dependency_installation when you need to use them.",
136
+ };
137
+ }
138
+
139
+ // start installation
140
+ startInstallation(ctx);
141
+
142
+ return {
143
+ status: "started",
144
+ message:
145
+ "Dependency installation started in background. Continue with other tasks and call await_dependency_installation when you need to run tests, builds, or other commands that require dependencies.",
146
+ };
147
+ }),
148
+ });
149
+ }
150
+
151
+ export function AwaitDependencyInstallationTool(ctx: ToolContext) {
152
+ return tool({
153
+ name: "await_dependency_installation",
154
+ description:
155
+ "Wait for dependency installation to complete and get the results. If installation hasn't been started yet, this will start it automatically. Call this before running tests, builds, or other commands that require dependencies.",
156
+ parameters: EmptyParams,
157
+ execute: execute(async () => {
158
+ // auto-start if not started
159
+ if (!ctx.toolState.dependencyInstallation) {
160
+ startInstallation(ctx);
161
+ }
162
+
163
+ const state = ctx.toolState.dependencyInstallation;
164
+ if (!state) {
165
+ throw new Error("failed to initialize dependency installation state");
166
+ }
167
+
168
+ // if already completed, return cached results
169
+ if (state.status === "completed" || state.status === "failed") {
170
+ return {
171
+ status: state.status,
172
+ message: formatPrepResults(state.results || []),
173
+ };
174
+ }
175
+
176
+ // await the promise
177
+ if (!state.promise) {
178
+ throw new Error("dependency installation state is corrupted - no promise found");
179
+ }
180
+
181
+ const results = await state.promise;
182
+
183
+ return {
184
+ status: state.status,
185
+ message: formatPrepResults(results),
186
+ };
187
+ }),
188
+ });
189
+ }