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,520 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { existsSync, mkdtempSync, readFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import type { ToolContext } from "#app/mcp/server";
7
+ import type { ToolResult } from "#app/mcp/shared";
8
+
9
+ const cp = vi.hoisted(() => ({
10
+ spawn: vi.fn(),
11
+ spawnSync: vi.fn(),
12
+ }));
13
+
14
+ vi.mock("node:child_process", async (importOriginal) => {
15
+ const actual = await importOriginal<typeof import("node:child_process")>();
16
+ return { ...actual, spawn: cp.spawn, spawnSync: cp.spawnSync };
17
+ });
18
+
19
+ /** minimal ChildProcess stand-in. emits nothing until the test says so. */
20
+ class FakeProc extends EventEmitter {
21
+ stdout = new EventEmitter();
22
+ stderr = new EventEmitter();
23
+ pid: number | undefined = 12345;
24
+ unref = vi.fn();
25
+ kill = vi.fn(() => {
26
+ this.emit("exit", null);
27
+ return true;
28
+ });
29
+ }
30
+
31
+ type ShellModule = typeof import("#app/mcp/shell");
32
+
33
+ /** fresh module instance so module-level caches (sandbox method, repo root)
34
+ * don't leak between tests. */
35
+ async function loadShell(): Promise<ShellModule> {
36
+ vi.resetModules();
37
+ return await import("#app/mcp/shell");
38
+ }
39
+
40
+ function runTool(t: { execute: unknown }, params: Record<string, unknown>): Promise<ToolResult> {
41
+ const exec = t.execute as (args: unknown, context?: unknown) => Promise<ToolResult>;
42
+ return exec(params);
43
+ }
44
+
45
+ function textOf(result: ToolResult): string {
46
+ return result.content[0]?.text ?? "";
47
+ }
48
+
49
+ function makeCtx(over: Partial<Record<string, unknown>> = {}): {
50
+ ctx: ToolContext;
51
+ toolState: {
52
+ backgroundProcesses: Map<string, { pid: number; outputPath: string; pidPath: string }>;
53
+ };
54
+ } {
55
+ const toolState = {
56
+ backgroundProcesses: new Map<string, { pid: number; outputPath: string; pidPath: string }>(),
57
+ };
58
+ const ctx = {
59
+ payload: { shell: over.shell ?? "restricted" },
60
+ toolState,
61
+ } as unknown as ToolContext;
62
+ return { ctx, toolState };
63
+ }
64
+
65
+ let tempDir: string;
66
+
67
+ beforeEach(() => {
68
+ vi.resetAllMocks();
69
+ tempDir = mkdtempSync(join(tmpdir(), "terramend-shell-test-"));
70
+ vi.stubEnv("TERRAMEND_TEMP_DIR", tempDir);
71
+ // default: no CI, no GHA workspace — sandbox detection returns "none" and
72
+ // the repo-root probe is exercised explicitly where needed.
73
+ vi.stubEnv("CI", "false");
74
+ vi.stubEnv("GITHUB_WORKSPACE", "/work/repo");
75
+ });
76
+
77
+ afterEach(() => {
78
+ vi.unstubAllEnvs();
79
+ });
80
+
81
+ describe("getSandboxMethod", () => {
82
+ it("is 'none' outside CI and caches the result", async () => {
83
+ const shell = await loadShell();
84
+ expect(shell.getSandboxMethod()).toBe("none");
85
+ expect(shell.getSandboxMethod()).toBe("none");
86
+ expect(cp.spawnSync).not.toHaveBeenCalled();
87
+ });
88
+
89
+ it("uses unprivileged unshare when the probe succeeds in CI", async () => {
90
+ vi.stubEnv("CI", "true");
91
+ cp.spawnSync.mockReturnValue({ status: 0 });
92
+ const shell = await loadShell();
93
+ expect(shell.getSandboxMethod()).toBe("unshare");
94
+ expect(cp.spawnSync).toHaveBeenCalledWith(
95
+ "unshare",
96
+ ["--pid", "--fork", "--mount-proc", "true"],
97
+ { timeout: 5000, stdio: "ignore" },
98
+ );
99
+ // cached — no second probe
100
+ shell.getSandboxMethod();
101
+ expect(cp.spawnSync).toHaveBeenCalledTimes(1);
102
+ });
103
+
104
+ it("falls back to sudo unshare when unprivileged unshare fails", async () => {
105
+ vi.stubEnv("CI", "true");
106
+ cp.spawnSync.mockReturnValueOnce({ status: 1 }).mockReturnValueOnce({ status: 0 });
107
+ const shell = await loadShell();
108
+ expect(shell.getSandboxMethod()).toBe("sudo-unshare");
109
+ expect(cp.spawnSync).toHaveBeenLastCalledWith(
110
+ "sudo",
111
+ ["unshare", "--pid", "--fork", "--mount-proc", "true"],
112
+ { timeout: 5000, stdio: "ignore" },
113
+ );
114
+ });
115
+
116
+ it("is 'none' when both probes fail (including thrown probes)", async () => {
117
+ vi.stubEnv("CI", "true");
118
+ cp.spawnSync.mockImplementation(() => {
119
+ throw new Error("spawnSync ENOENT");
120
+ });
121
+ const shell = await loadShell();
122
+ expect(shell.getSandboxMethod()).toBe("none");
123
+ });
124
+ });
125
+
126
+ describe("shell tool git-command rejection", () => {
127
+ it("rejects git invocations in all the shapes it detects", async () => {
128
+ const shell = await loadShell();
129
+ const { ctx } = makeCtx();
130
+ const tool = shell.ShellTool(ctx);
131
+ for (const command of [
132
+ "git status",
133
+ "git",
134
+ "sudo git push origin main",
135
+ "ls; git checkout main",
136
+ "true && git commit -m x",
137
+ "false || sudo git rebase",
138
+ // hardened shapes: newline-separated, subshell, and command substitution
139
+ "echo hi\ngit push",
140
+ "(git status)",
141
+ "echo $(git rev-parse HEAD)",
142
+ "echo `git log -1`",
143
+ // further-hardened shapes: brace group, carriage return, launcher prefixes
144
+ "{ git push; }",
145
+ "echo hi\rgit push",
146
+ "env git push",
147
+ "command git commit -m x",
148
+ ]) {
149
+ const result = await runTool(tool, { command, description: "d" });
150
+ expect(result.isError).toBe(true);
151
+ expect(textOf(result)).toContain("git commands are not allowed");
152
+ }
153
+ expect(cp.spawn).not.toHaveBeenCalled();
154
+ });
155
+
156
+ it("does not reject benign commands that merely contain the substring 'git'", async () => {
157
+ const shell = await loadShell();
158
+ const { ctx } = makeCtx();
159
+ const proc = new FakeProc();
160
+ cp.spawn.mockReturnValue(proc);
161
+ const resultP = runTool(shell.ShellTool(ctx), {
162
+ command: "echo digit && echo legit",
163
+ description: "d",
164
+ });
165
+ proc.stdout.emit("data", Buffer.from("digit\nlegit\n"));
166
+ proc.emit("exit", 0);
167
+ const result = await resultP;
168
+ expect(result.isError).toBeUndefined();
169
+ });
170
+
171
+ it("does not reject git as part of another word", async () => {
172
+ const shell = await loadShell();
173
+ const { ctx } = makeCtx();
174
+ const proc = new FakeProc();
175
+ cp.spawn.mockReturnValue(proc);
176
+ const resultP = runTool(shell.ShellTool(ctx), {
177
+ command: "cat .gitignore",
178
+ description: "d",
179
+ });
180
+ proc.stdout.emit("data", Buffer.from("node_modules\n"));
181
+ proc.emit("exit", 0);
182
+ const result = await resultP;
183
+ expect(result.isError).toBeUndefined();
184
+ expect(textOf(result)).toContain("node_modules");
185
+ });
186
+ });
187
+
188
+ describe("shell tool foreground execution (no sandbox)", () => {
189
+ it("runs bash -c and returns trimmed output with the exit code", async () => {
190
+ const shell = await loadShell();
191
+ const { ctx } = makeCtx();
192
+ const proc = new FakeProc();
193
+ cp.spawn.mockReturnValue(proc);
194
+ const resultP = runTool(shell.ShellTool(ctx), { command: "echo hi", description: "d" });
195
+ proc.stdout.emit("data", Buffer.from("hi\n"));
196
+ proc.emit("exit", 0);
197
+ const result = await resultP;
198
+ expect(textOf(result)).toContain("hi");
199
+ expect(textOf(result)).toContain("exit_code: 0");
200
+ expect(cp.spawn).toHaveBeenCalledTimes(1);
201
+ const call = cp.spawn.mock.calls[0] ?? [];
202
+ expect(call[0]).toBe("bash");
203
+ expect(call[1]).toEqual(["-c", "echo hi"]);
204
+ expect(call[2]).toMatchObject({ detached: true });
205
+ });
206
+
207
+ it("combines stdout and stderr and surfaces non-zero exit codes", async () => {
208
+ const shell = await loadShell();
209
+ const { ctx } = makeCtx();
210
+ const proc = new FakeProc();
211
+ cp.spawn.mockReturnValue(proc);
212
+ const resultP = runTool(shell.ShellTool(ctx), { command: "boom", description: "d" });
213
+ proc.stdout.emit("data", Buffer.from("partial output\n"));
214
+ proc.stderr.emit("data", Buffer.from("error: kaboom\n"));
215
+ proc.emit("exit", 3);
216
+ const result = await resultP;
217
+ const text = textOf(result);
218
+ expect(text).toContain("partial output");
219
+ expect(text).toContain("error: kaboom");
220
+ expect(text).toContain("exit_code: 3");
221
+ });
222
+
223
+ it("reports exit_code -1 when the process errors before exiting", async () => {
224
+ const shell = await loadShell();
225
+ const { ctx } = makeCtx();
226
+ const proc = new FakeProc();
227
+ cp.spawn.mockReturnValue(proc);
228
+ const resultP = runTool(shell.ShellTool(ctx), { command: "nope", description: "d" });
229
+ proc.emit("error", new Error("spawn failed"));
230
+ const result = await resultP;
231
+ expect(textOf(result)).toContain("exit_code: -1");
232
+ });
233
+
234
+ it("kills the process group and reports a timeout", async () => {
235
+ const killSpy = vi.spyOn(process, "kill").mockImplementation(() => {
236
+ throw new Error("ESRCH");
237
+ });
238
+ try {
239
+ const shell = await loadShell();
240
+ const { ctx } = makeCtx();
241
+ const proc = new FakeProc();
242
+ cp.spawn.mockReturnValue(proc);
243
+ const result = await runTool(shell.ShellTool(ctx), {
244
+ command: "sleep 999",
245
+ description: "d",
246
+ timeout: 20,
247
+ });
248
+ const text = textOf(result);
249
+ expect(text).toContain("timed_out: true");
250
+ expect(text).toContain("exit_code: 124");
251
+ expect(text).toContain("[timed out after 20ms]");
252
+ expect(killSpy).toHaveBeenCalledWith(-12345, "SIGTERM");
253
+ expect(proc.kill).toHaveBeenCalledWith("SIGKILL");
254
+ } finally {
255
+ killSpy.mockRestore();
256
+ }
257
+ });
258
+
259
+ it("escalates SIGTERM to SIGKILL when the group survives the grace period", async () => {
260
+ const proc = new FakeProc();
261
+ const killSpy = vi.spyOn(process, "kill").mockImplementation(((
262
+ _pid: number,
263
+ signal?: string | number,
264
+ ) => {
265
+ if (signal === "SIGKILL") proc.emit("exit", null);
266
+ return true;
267
+ }) as typeof process.kill);
268
+ try {
269
+ const shell = await loadShell();
270
+ const { ctx } = makeCtx();
271
+ cp.spawn.mockReturnValue(proc);
272
+ const result = await runTool(shell.ShellTool(ctx), {
273
+ command: "sleep 999",
274
+ description: "d",
275
+ timeout: 20,
276
+ });
277
+ expect(textOf(result)).toContain("timed_out: true");
278
+ expect(killSpy).toHaveBeenCalledWith(-12345, "SIGTERM");
279
+ expect(killSpy).toHaveBeenCalledWith(-12345, "SIGKILL");
280
+ } finally {
281
+ killSpy.mockRestore();
282
+ }
283
+ });
284
+
285
+ it("filters secrets from the child env in restricted mode", async () => {
286
+ vi.stubEnv("SOME_PROVIDER_TOKEN", "sekrit");
287
+ const shell = await loadShell();
288
+ const { ctx } = makeCtx({ shell: "restricted" });
289
+ const proc = new FakeProc();
290
+ cp.spawn.mockReturnValue(proc);
291
+ const resultP = runTool(shell.ShellTool(ctx), { command: "env", description: "d" });
292
+ proc.emit("exit", 0);
293
+ await resultP;
294
+ const opts = (cp.spawn.mock.calls[0] ?? [])[2] as { env: Record<string, string> };
295
+ expect(opts.env.SOME_PROVIDER_TOKEN).toBeUndefined();
296
+ });
297
+
298
+ it("passes the full env through in enabled mode", async () => {
299
+ vi.stubEnv("SOME_PROVIDER_TOKEN", "sekrit");
300
+ const shell = await loadShell();
301
+ const { ctx } = makeCtx({ shell: "enabled" });
302
+ const proc = new FakeProc();
303
+ cp.spawn.mockReturnValue(proc);
304
+ const resultP = runTool(shell.ShellTool(ctx), { command: "env", description: "d" });
305
+ proc.emit("exit", 0);
306
+ await resultP;
307
+ const opts = (cp.spawn.mock.calls[0] ?? [])[2] as { env: Record<string, string> };
308
+ expect(opts.env.SOME_PROVIDER_TOKEN).toBe("sekrit");
309
+ });
310
+
311
+ it("spills output beyond MAX_OUTPUT_CHARS to a tempfile, returning the tail", async () => {
312
+ const shell = await loadShell();
313
+ const { ctx } = makeCtx();
314
+ const proc = new FakeProc();
315
+ cp.spawn.mockReturnValue(proc);
316
+ const body = `HEAD-MARKER${"x".repeat(6000)}TAIL-MARKER`;
317
+ const resultP = runTool(shell.ShellTool(ctx), { command: "biglog", description: "d" });
318
+ proc.stdout.emit("data", Buffer.from(body));
319
+ proc.emit("exit", 0);
320
+ const result = await resultP;
321
+ const text = textOf(result);
322
+ expect(text).toContain("chars truncated; full output saved to");
323
+ expect(text).toContain("TAIL-MARKER");
324
+ expect(text).not.toContain("HEAD-MARKER");
325
+ const match = text.match(/saved to (\S*shell-[0-9a-f]{8}\.log)/);
326
+ expect(match).not.toBeNull();
327
+ expect(readFileSync(match?.[1] ?? "", "utf-8")).toBe(body);
328
+ });
329
+ });
330
+
331
+ describe("shell tool sandbox command construction", () => {
332
+ it("wraps the command with proc/socket/fs mounts under unshare", async () => {
333
+ vi.stubEnv("CI", "true");
334
+ vi.stubEnv("GITHUB_WORKSPACE", "/work/it's repo");
335
+ cp.spawnSync.mockReturnValue({ status: 0 }); // unprivileged unshare available
336
+ const shell = await loadShell();
337
+ const { ctx } = makeCtx();
338
+ const proc = new FakeProc();
339
+ cp.spawn.mockReturnValue(proc);
340
+ const resultP = runTool(shell.ShellTool(ctx), { command: "ls", description: "d" });
341
+ proc.emit("exit", 0);
342
+ await resultP;
343
+
344
+ const call = cp.spawn.mock.calls[0] ?? [];
345
+ expect(call[0]).toBe("unshare");
346
+ const args = call[1] as string[];
347
+ expect(args.slice(0, 5)).toEqual(["--pid", "--fork", "--mount-proc", "bash", "-c"]);
348
+ const script = args[5] ?? "";
349
+ expect(script).toContain("umount /proc");
350
+ expect(script).toContain("mount --bind /dev/null /var/run/docker.sock");
351
+ expect(script).toContain("mkdir -p /var/lib/terramend");
352
+ expect(script).toContain("mount -t tmpfs tmpfs /var/lib/terramend");
353
+ expect(script).toContain('"$RUNNER_TEMP/_runner_file_commands"');
354
+ // repo root comes from GITHUB_WORKSPACE, shell-escaped for the embedded quote
355
+ expect(script).toContain("'/work/it'\\''s repo/.git'");
356
+ expect(script.endsWith("ls")).toBe(true);
357
+ });
358
+
359
+ it("drops privileges back to the user under sudo-unshare with a scrubbed env", async () => {
360
+ vi.stubEnv("CI", "true");
361
+ cp.spawnSync.mockReturnValueOnce({ status: 1 }).mockReturnValueOnce({ status: 0 });
362
+ const shell = await loadShell();
363
+ const { ctx } = makeCtx();
364
+ const proc = new FakeProc();
365
+ cp.spawn.mockReturnValue(proc);
366
+ const resultP = runTool(shell.ShellTool(ctx), { command: "make build", description: "d" });
367
+ proc.emit("exit", 0);
368
+ await resultP;
369
+
370
+ const call = cp.spawn.mock.calls[0] ?? [];
371
+ expect(call[0]).toBe("sudo");
372
+ const args = call[1] as string[];
373
+ expect(args[0]).toBe("env");
374
+ // env is forwarded as KEY=value args (incl. the PATH restore var)
375
+ expect(args.some((a) => a.startsWith("SANDBOX_PATH="))).toBe(true);
376
+ const unshareIdx = args.indexOf("unshare");
377
+ expect(unshareIdx).toBeGreaterThan(0);
378
+ const script = args[args.length - 1] ?? "";
379
+ expect(script).toContain("exec su -p -s /bin/bash");
380
+ expect(script).toContain("export PATH=");
381
+ expect(script).toContain("make build");
382
+ // spawn options env is emptied — secrets ride only the explicit env args
383
+ const opts = call[2] as { env: Record<string, string> };
384
+ expect(opts.env).toEqual({});
385
+ });
386
+
387
+ it("refuses to run unsandboxed in CI", async () => {
388
+ vi.stubEnv("CI", "true");
389
+ cp.spawnSync.mockReturnValue({ status: 1 }); // both probes fail
390
+ const shell = await loadShell();
391
+ const { ctx } = makeCtx();
392
+ const result = await runTool(shell.ShellTool(ctx), { command: "ls", description: "d" });
393
+ expect(result.isError).toBe(true);
394
+ expect(textOf(result)).toContain("pid namespace isolation is required in CI");
395
+ expect(cp.spawn).not.toHaveBeenCalled();
396
+ });
397
+
398
+ it("falls back to `git rev-parse --show-toplevel` when GITHUB_WORKSPACE is unset", async () => {
399
+ vi.stubEnv("CI", "true");
400
+ vi.stubEnv("GITHUB_WORKSPACE", "");
401
+ cp.spawnSync.mockImplementation((cmd: string) => {
402
+ if (cmd === "git") return { status: 0, stdout: "/resolved/root\n" };
403
+ return { status: 0 };
404
+ });
405
+ const shell = await loadShell();
406
+ const { ctx } = makeCtx();
407
+ const proc = new FakeProc();
408
+ cp.spawn.mockReturnValue(proc);
409
+ const resultP = runTool(shell.ShellTool(ctx), { command: "ls", description: "d" });
410
+ proc.emit("exit", 0);
411
+ await resultP;
412
+ const args = (cp.spawn.mock.calls[0] ?? [])[1] as string[];
413
+ expect(args[5] ?? "").toContain("'/resolved/root/.git'");
414
+ expect(cp.spawnSync).toHaveBeenCalledWith(
415
+ "git",
416
+ ["rev-parse", "--show-toplevel"],
417
+ expect.objectContaining({ encoding: "utf-8" }),
418
+ );
419
+ });
420
+
421
+ it("falls back to process.cwd() when the git probe yields nothing", async () => {
422
+ vi.stubEnv("CI", "true");
423
+ vi.stubEnv("GITHUB_WORKSPACE", "");
424
+ cp.spawnSync.mockImplementation((cmd: string) => {
425
+ if (cmd === "git") return { status: 128, stdout: undefined };
426
+ return { status: 0 };
427
+ });
428
+ const shell = await loadShell();
429
+ const { ctx } = makeCtx();
430
+ const proc = new FakeProc();
431
+ cp.spawn.mockReturnValue(proc);
432
+ const resultP = runTool(shell.ShellTool(ctx), { command: "ls", description: "d" });
433
+ proc.emit("exit", 0);
434
+ await resultP;
435
+ const args = (cp.spawn.mock.calls[0] ?? [])[1] as string[];
436
+ expect(args[5] ?? "").toContain(".git");
437
+ });
438
+ });
439
+
440
+ describe("background processes", () => {
441
+ it("starts a detached background process and records its handle", async () => {
442
+ const shell = await loadShell();
443
+ const { ctx, toolState } = makeCtx();
444
+ const proc = new FakeProc();
445
+ proc.pid = 4242;
446
+ cp.spawn.mockReturnValue(proc);
447
+ const result = await runTool(shell.ShellTool(ctx), {
448
+ command: "npm run dev",
449
+ description: "d",
450
+ background: true,
451
+ });
452
+ expect(result.isError).toBeUndefined();
453
+ const text = textOf(result);
454
+ expect(text).toMatch(/bg-[0-9a-f]{8}/);
455
+ expect(text).toContain("pid 4242");
456
+ expect(proc.unref).toHaveBeenCalled();
457
+ expect(toolState.backgroundProcesses.size).toBe(1);
458
+ const entry = [...toolState.backgroundProcesses.values()][0];
459
+ expect(entry?.pid).toBe(4242);
460
+ expect(existsSync(entry?.pidPath ?? "")).toBe(true);
461
+ expect(readFileSync(entry?.pidPath ?? "", "utf-8")).toBe("4242\n");
462
+ });
463
+
464
+ it("errors when TERRAMEND_TEMP_DIR is unset for a background process", async () => {
465
+ vi.stubEnv("TERRAMEND_TEMP_DIR", "");
466
+ const shell = await loadShell();
467
+ const { ctx } = makeCtx();
468
+ const result = await runTool(shell.ShellTool(ctx), {
469
+ command: "npm run dev",
470
+ description: "d",
471
+ background: true,
472
+ });
473
+ expect(result.isError).toBe(true);
474
+ expect(textOf(result)).toContain("TERRAMEND_TEMP_DIR not set");
475
+ expect(cp.spawn).not.toHaveBeenCalled();
476
+ });
477
+
478
+ it("errors when the background process fails to start (no pid)", async () => {
479
+ const shell = await loadShell();
480
+ const { ctx } = makeCtx();
481
+ const proc = new FakeProc();
482
+ proc.pid = undefined;
483
+ cp.spawn.mockReturnValue(proc);
484
+ const result = await runTool(shell.ShellTool(ctx), {
485
+ command: "npm run dev",
486
+ description: "d",
487
+ background: true,
488
+ });
489
+ expect(result.isError).toBe(true);
490
+ expect(textOf(result)).toContain("failed to start background process");
491
+ });
492
+
493
+ it("kill_background reports unknown handles", async () => {
494
+ const shell = await loadShell();
495
+ const { ctx } = makeCtx();
496
+ const result = await runTool(shell.KillBackgroundTool(ctx), { handle: "bg-deadbeef" });
497
+ expect(result.isError).toBeUndefined();
498
+ expect(textOf(result)).toContain("no background process with handle bg-deadbeef");
499
+ });
500
+
501
+ it("kill_background terminates the group and forgets the handle", async () => {
502
+ const killSpy = vi.spyOn(process, "kill").mockImplementation(() => true);
503
+ try {
504
+ const shell = await loadShell();
505
+ const { ctx, toolState } = makeCtx();
506
+ toolState.backgroundProcesses.set("bg-cafe0123", {
507
+ pid: 777,
508
+ outputPath: join(tempDir, "bg-cafe0123.log"),
509
+ pidPath: join(tempDir, "bg-cafe0123.pid"),
510
+ });
511
+ const result = await runTool(shell.KillBackgroundTool(ctx), { handle: "bg-cafe0123" });
512
+ expect(textOf(result)).toContain("killed background process bg-cafe0123 (pid 777)");
513
+ expect(killSpy).toHaveBeenCalledWith(-777, "SIGTERM");
514
+ expect(killSpy).toHaveBeenCalledWith(-777, "SIGKILL");
515
+ expect(toolState.backgroundProcesses.size).toBe(0);
516
+ } finally {
517
+ killSpy.mockRestore();
518
+ }
519
+ });
520
+ });