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.
- package/LICENSE +661 -0
- package/README.md +145 -0
- package/dist/agents/claude.d.ts +73 -0
- package/dist/agents/claudePretoolGate.d.ts +99 -0
- package/dist/agents/gateServer.d.ts +7 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/nativeFsDenies.d.ts +28 -0
- package/dist/agents/opencode.d.ts +231 -0
- package/dist/agents/opencodePlugin.d.ts +85 -0
- package/dist/agents/opencodeShared.d.ts +40 -0
- package/dist/agents/postRun.d.ts +132 -0
- package/dist/agents/reviewer.d.ts +38 -0
- package/dist/agents/sessionLabeler.d.ts +97 -0
- package/dist/agents/shared.d.ts +189 -0
- package/dist/agents/subagentModels.d.ts +19 -0
- package/dist/agents/subagentToolGates.d.ts +55 -0
- package/dist/cli.mjs +197426 -0
- package/dist/external.d.ts +227 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +196783 -0
- package/dist/internal/index.d.ts +18 -0
- package/dist/internal.js +1714 -0
- package/dist/lifecycle.d.ts +2 -0
- package/dist/main.d.ts +8 -0
- package/dist/mcp/arkConfig.d.ts +1 -0
- package/dist/mcp/checkSuite.d.ts +25 -0
- package/dist/mcp/checkout.d.ts +77 -0
- package/dist/mcp/comment.d.ts +119 -0
- package/dist/mcp/commitInfo.d.ts +9 -0
- package/dist/mcp/crosswalk.d.ts +105 -0
- package/dist/mcp/dependencies.d.ts +8 -0
- package/dist/mcp/geminiSanitizer.d.ts +28 -0
- package/dist/mcp/git.d.ts +46 -0
- package/dist/mcp/guardrails.d.ts +104 -0
- package/dist/mcp/issue.d.ts +18 -0
- package/dist/mcp/issueComments.d.ts +9 -0
- package/dist/mcp/issueEvents.d.ts +9 -0
- package/dist/mcp/issueInfo.d.ts +9 -0
- package/dist/mcp/labels.d.ts +12 -0
- package/dist/mcp/localContext.d.ts +19 -0
- package/dist/mcp/moduleExtraction.d.ts +71 -0
- package/dist/mcp/moduleTests.d.ts +104 -0
- package/dist/mcp/modules.d.ts +179 -0
- package/dist/mcp/output.d.ts +12 -0
- package/dist/mcp/pathSafety.d.ts +14 -0
- package/dist/mcp/policy.d.ts +48 -0
- package/dist/mcp/pr.d.ts +49 -0
- package/dist/mcp/prInfo.d.ts +9 -0
- package/dist/mcp/providerSchema.d.ts +50 -0
- package/dist/mcp/review.d.ts +199 -0
- package/dist/mcp/reviewComments.d.ts +178 -0
- package/dist/mcp/roots.d.ts +58 -0
- package/dist/mcp/scope.d.ts +15 -0
- package/dist/mcp/selectMode.d.ts +18 -0
- package/dist/mcp/server.d.ts +48 -0
- package/dist/mcp/shared.d.ts +47 -0
- package/dist/mcp/shell.d.ts +37 -0
- package/dist/mcp/staleFix.d.ts +51 -0
- package/dist/mcp/terraform/cost.d.ts +55 -0
- package/dist/mcp/terraform/currency.d.ts +94 -0
- package/dist/mcp/terraform/decisions.d.ts +178 -0
- package/dist/mcp/terraform/findings.d.ts +75 -0
- package/dist/mcp/terraform/plan.d.ts +157 -0
- package/dist/mcp/terraform/scanners.d.ts +131 -0
- package/dist/mcp/terraform/tools.d.ts +63 -0
- package/dist/mcp/terraform/types.d.ts +172 -0
- package/dist/mcp/terraform.d.ts +22 -0
- package/dist/mcp/terratest.d.ts +83 -0
- package/dist/mcp/upload.d.ts +6 -0
- package/dist/models.d.ts +171 -0
- package/dist/modes.d.ts +26 -0
- package/dist/prep/index.d.ts +7 -0
- package/dist/prep/installNodeDependencies.d.ts +2 -0
- package/dist/prep/installPythonDependencies.d.ts +2 -0
- package/dist/prep/types.d.ts +31 -0
- package/dist/reviewQuality.d.ts +64 -0
- package/dist/skills/terraform-best-practices/SKILL.md +369 -0
- package/dist/toolState.d.ts +135 -0
- package/dist/utils/activity.d.ts +40 -0
- package/dist/utils/agent.d.ts +20 -0
- package/dist/utils/agentHangReport.d.ts +38 -0
- package/dist/utils/apiFetch.d.ts +19 -0
- package/dist/utils/apiKeys.d.ts +41 -0
- package/dist/utils/apiUrl.d.ts +20 -0
- package/dist/utils/assets.d.ts +8 -0
- package/dist/utils/billingErrors.d.ts +85 -0
- package/dist/utils/body.d.ts +34 -0
- package/dist/utils/buildTerramendFooter.d.ts +25 -0
- package/dist/utils/byokFallback.d.ts +85 -0
- package/dist/utils/claudeSubscription.d.ts +30 -0
- package/dist/utils/cli.d.ts +10 -0
- package/dist/utils/codexHome.d.ts +29 -0
- package/dist/utils/codexOAuth.d.ts +60 -0
- package/dist/utils/diffCoverage.d.ts +63 -0
- package/dist/utils/errorReport.d.ts +17 -0
- package/dist/utils/exitHandler.d.ts +8 -0
- package/dist/utils/fixDoubleEscapedString.d.ts +1 -0
- package/dist/utils/gitAuth.d.ts +84 -0
- package/dist/utils/gitAuthServer.d.ts +24 -0
- package/dist/utils/github.d.ts +78 -0
- package/dist/utils/globals.d.ts +3 -0
- package/dist/utils/install.d.ts +60 -0
- package/dist/utils/instructions.d.ts +48 -0
- package/dist/utils/leapingComment.d.ts +11 -0
- package/dist/utils/learnings.d.ts +62 -0
- package/dist/utils/learningsTruncate.d.ts +25 -0
- package/dist/utils/lifecycle.d.ts +57 -0
- package/dist/utils/log.d.ts +111 -0
- package/dist/utils/normalizeEnv.d.ts +30 -0
- package/dist/utils/openCodeModels.d.ts +11 -0
- package/dist/utils/overrides.d.ts +40 -0
- package/dist/utils/packageManager.d.ts +49 -0
- package/dist/utils/patchWorkflowRunFields.d.ts +29 -0
- package/dist/utils/payload.d.ts +105 -0
- package/dist/utils/prSummary.d.ts +61 -0
- package/dist/utils/progressComment.d.ts +146 -0
- package/dist/utils/providerErrors.d.ts +31 -0
- package/dist/utils/rangeDiff.d.ts +51 -0
- package/dist/utils/remediationCommand.d.ts +55 -0
- package/dist/utils/retry.d.ts +13 -0
- package/dist/utils/reviewCleanup.d.ts +14 -0
- package/dist/utils/run.d.ts +9 -0
- package/dist/utils/runContext.d.ts +60 -0
- package/dist/utils/runContextData.d.ts +23 -0
- package/dist/utils/runErrorRenderer.d.ts +64 -0
- package/dist/utils/runLifecycle.d.ts +86 -0
- package/dist/utils/runStartupLog.d.ts +15 -0
- package/dist/utils/secrets.d.ts +22 -0
- package/dist/utils/setup.d.ts +90 -0
- package/dist/utils/shell.d.ts +32 -0
- package/dist/utils/skills.d.ts +10 -0
- package/dist/utils/subprocess.d.ts +80 -0
- package/dist/utils/terraformMcp.d.ts +42 -0
- package/dist/utils/time.d.ts +15 -0
- package/dist/utils/timer.d.ts +23 -0
- package/dist/utils/todoTracking.d.ts +16 -0
- package/dist/utils/token.d.ts +39 -0
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/versioning.d.ts +7 -0
- package/dist/utils/vertex.d.ts +16 -0
- package/dist/utils/workflow.d.ts +13 -0
- package/package.json +119 -0
- package/src/agents/claude.test.ts +1016 -0
- package/src/agents/claude.ts +1246 -0
- package/src/agents/claudePretoolGate.test.ts +28 -0
- package/src/agents/claudePretoolGate.ts +173 -0
- package/src/agents/gateServer.test.ts +204 -0
- package/src/agents/gateServer.ts +124 -0
- package/src/agents/index.ts +10 -0
- package/src/agents/nativeFsDenies.ts +82 -0
- package/src/agents/opencode.test.ts +1440 -0
- package/src/agents/opencode.ts +1312 -0
- package/src/agents/opencodePlugin.ts +222 -0
- package/src/agents/opencodeShared.test.ts +34 -0
- package/src/agents/opencodeShared.ts +121 -0
- package/src/agents/postRun.test.ts +549 -0
- package/src/agents/postRun.ts +535 -0
- package/src/agents/reviewer.ts +104 -0
- package/src/agents/sessionLabeler.test.ts +247 -0
- package/src/agents/sessionLabeler.ts +178 -0
- package/src/agents/shared.test.ts +76 -0
- package/src/agents/shared.ts +292 -0
- package/src/agents/subagentModels.test.ts +113 -0
- package/src/agents/subagentModels.ts +40 -0
- package/src/agents/subagentRegistration.test.ts +41 -0
- package/src/agents/subagentToolGates.ts +114 -0
- package/src/cli.test.ts +129 -0
- package/src/cli.ts +105 -0
- package/src/commands/gha.test.ts +192 -0
- package/src/commands/gha.ts +188 -0
- package/src/commands/mcp.ts +122 -0
- package/src/config.ts +1 -0
- package/src/entry.ts +7 -0
- package/src/entryPost.stdlibOnly.test.ts +109 -0
- package/src/entryPost.ts +99 -0
- package/src/external.test.ts +16 -0
- package/src/external.ts +302 -0
- package/src/index.ts +11 -0
- package/src/internal/index.ts +71 -0
- package/src/lifecycle.ts +2 -0
- package/src/main.test.ts +873 -0
- package/src/main.ts +712 -0
- package/src/mcp/__fixtures__/terramend-scratch-pr-49-review-3485940013.json +110 -0
- package/src/mcp/__fixtures__/terramend-scratch-pr-64-review-3531000326.json +14 -0
- package/src/mcp/__fixtures__/terramend-test-repo-pr-1.diff.json +67 -0
- package/src/mcp/__snapshots__/checkout.test.ts.snap +109 -0
- package/src/mcp/__snapshots__/reviewComments.test.ts.snap +71 -0
- package/src/mcp/arkConfig.ts +7 -0
- package/src/mcp/checkSuite.test.ts +245 -0
- package/src/mcp/checkSuite.ts +255 -0
- package/src/mcp/checkout.test.ts +752 -0
- package/src/mcp/checkout.ts +886 -0
- package/src/mcp/comment.test.ts +772 -0
- package/src/mcp/comment.ts +582 -0
- package/src/mcp/commitInfo.test.ts +127 -0
- package/src/mcp/commitInfo.ts +61 -0
- package/src/mcp/crosswalk.test.ts +106 -0
- package/src/mcp/crosswalk.ts +339 -0
- package/src/mcp/dependencies.test.ts +309 -0
- package/src/mcp/dependencies.ts +189 -0
- package/src/mcp/geminiSanitizer.test.ts +287 -0
- package/src/mcp/geminiSanitizer.ts +207 -0
- package/src/mcp/git.test.ts +1083 -0
- package/src/mcp/git.ts +890 -0
- package/src/mcp/guardrails.test.ts +705 -0
- package/src/mcp/guardrails.ts +465 -0
- package/src/mcp/issue.test.ts +113 -0
- package/src/mcp/issue.ts +73 -0
- package/src/mcp/issueComments.test.ts +69 -0
- package/src/mcp/issueComments.ts +48 -0
- package/src/mcp/issueEvents.test.ts +134 -0
- package/src/mcp/issueEvents.ts +100 -0
- package/src/mcp/issueInfo.test.ts +104 -0
- package/src/mcp/issueInfo.ts +72 -0
- package/src/mcp/labels.test.ts +52 -0
- package/src/mcp/labels.ts +34 -0
- package/src/mcp/localContext.ts +28 -0
- package/src/mcp/localServer.test.ts +75 -0
- package/src/mcp/localServer.ts +131 -0
- package/src/mcp/moduleExtraction.test.ts +261 -0
- package/src/mcp/moduleExtraction.ts +313 -0
- package/src/mcp/moduleTests.test.ts +269 -0
- package/src/mcp/moduleTests.ts +421 -0
- package/src/mcp/modules.test.ts +640 -0
- package/src/mcp/modules.ts +696 -0
- package/src/mcp/output.test.ts +96 -0
- package/src/mcp/output.ts +70 -0
- package/src/mcp/pathSafety.test.ts +44 -0
- package/src/mcp/pathSafety.ts +28 -0
- package/src/mcp/policy.test.ts +282 -0
- package/src/mcp/policy.ts +199 -0
- package/src/mcp/pr.test.ts +387 -0
- package/src/mcp/pr.ts +194 -0
- package/src/mcp/prInfo.test.ts +96 -0
- package/src/mcp/prInfo.ts +91 -0
- package/src/mcp/providerSchema.test.ts +85 -0
- package/src/mcp/providerSchema.ts +175 -0
- package/src/mcp/review.test.ts +936 -0
- package/src/mcp/review.ts +923 -0
- package/src/mcp/reviewComments.test.ts +549 -0
- package/src/mcp/reviewComments.ts +896 -0
- package/src/mcp/roots.test.ts +175 -0
- package/src/mcp/roots.ts +217 -0
- package/src/mcp/scope.test.ts +59 -0
- package/src/mcp/scope.ts +65 -0
- package/src/mcp/security.test.ts +720 -0
- package/src/mcp/selectMode.test.ts +210 -0
- package/src/mcp/selectMode.ts +181 -0
- package/src/mcp/server.test.ts +292 -0
- package/src/mcp/server.ts +403 -0
- package/src/mcp/shared.ts +100 -0
- package/src/mcp/shell.test.ts +520 -0
- package/src/mcp/shell.ts +505 -0
- package/src/mcp/staleFix.test.ts +237 -0
- package/src/mcp/staleFix.ts +277 -0
- package/src/mcp/terraform/cost.ts +163 -0
- package/src/mcp/terraform/currency.test.ts +338 -0
- package/src/mcp/terraform/currency.ts +336 -0
- package/src/mcp/terraform/decisions.ts +527 -0
- package/src/mcp/terraform/findings.ts +333 -0
- package/src/mcp/terraform/plan.ts +348 -0
- package/src/mcp/terraform/scanners.ts +809 -0
- package/src/mcp/terraform/tools.test.ts +1071 -0
- package/src/mcp/terraform/tools.ts +908 -0
- package/src/mcp/terraform/types.ts +305 -0
- package/src/mcp/terraform.test.ts +1957 -0
- package/src/mcp/terraform.ts +23 -0
- package/src/mcp/terratest.test.ts +105 -0
- package/src/mcp/terratest.ts +196 -0
- package/src/mcp/toolFiltering.test.ts +85 -0
- package/src/mcp/upload.test.ts +180 -0
- package/src/mcp/upload.ts +112 -0
- package/src/models.test.ts +300 -0
- package/src/models.ts +708 -0
- package/src/modes.test.ts +107 -0
- package/src/modes.ts +880 -0
- package/src/prep/index.ts +43 -0
- package/src/prep/installNodeDependencies.test.ts +298 -0
- package/src/prep/installNodeDependencies.ts +196 -0
- package/src/prep/installPythonDependencies.test.ts +268 -0
- package/src/prep/installPythonDependencies.ts +199 -0
- package/src/prep/types.ts +38 -0
- package/src/reviewQuality.test.ts +63 -0
- package/src/reviewQuality.ts +134 -0
- package/src/runCli.test.ts +214 -0
- package/src/runCli.ts +282 -0
- package/src/skills/terraform-best-practices/SKILL.md +369 -0
- package/src/toolState.test.ts +45 -0
- package/src/toolState.ts +252 -0
- package/src/utils/activity.test.ts +188 -0
- package/src/utils/activity.ts +210 -0
- package/src/utils/agent.test.ts +251 -0
- package/src/utils/agent.ts +139 -0
- package/src/utils/agentHangReport.test.ts +203 -0
- package/src/utils/agentHangReport.ts +170 -0
- package/src/utils/apiFetch.test.ts +115 -0
- package/src/utils/apiFetch.ts +62 -0
- package/src/utils/apiKeys.test.ts +344 -0
- package/src/utils/apiKeys.ts +206 -0
- package/src/utils/apiUrl.test.ts +30 -0
- package/src/utils/apiUrl.ts +59 -0
- package/src/utils/assets.test.ts +153 -0
- package/src/utils/assets.ts +107 -0
- package/src/utils/billingErrors.test.ts +121 -0
- package/src/utils/billingErrors.ts +189 -0
- package/src/utils/body.test.ts +217 -0
- package/src/utils/body.ts +168 -0
- package/src/utils/buildTerramendFooter.test.ts +38 -0
- package/src/utils/buildTerramendFooter.ts +82 -0
- package/src/utils/byokFallback.test.ts +205 -0
- package/src/utils/byokFallback.ts +128 -0
- package/src/utils/claudeSubscription.test.ts +179 -0
- package/src/utils/claudeSubscription.ts +93 -0
- package/src/utils/cli.ts +31 -0
- package/src/utils/codexHome.test.ts +190 -0
- package/src/utils/codexHome.ts +191 -0
- package/src/utils/codexOAuth.ts +147 -0
- package/src/utils/codexRefreshDetect.test.ts +85 -0
- package/src/utils/codexRefreshDetect.ts +35 -0
- package/src/utils/diffCoverage.test.ts +468 -0
- package/src/utils/diffCoverage.ts +404 -0
- package/src/utils/errorReport.test.ts +135 -0
- package/src/utils/errorReport.ts +83 -0
- package/src/utils/exitHandler.ts +35 -0
- package/src/utils/fixDoubleEscapedString.ts +9 -0
- package/src/utils/ghaCore.ts +13 -0
- package/src/utils/gitAuth.test.ts +322 -0
- package/src/utils/gitAuth.ts +263 -0
- package/src/utils/gitAuthServer.test.ts +260 -0
- package/src/utils/gitAuthServer.ts +182 -0
- package/src/utils/github.test.ts +615 -0
- package/src/utils/github.ts +538 -0
- package/src/utils/globals.ts +9 -0
- package/src/utils/humanEditCapture.test.ts +100 -0
- package/src/utils/humanEditCapture.ts +193 -0
- package/src/utils/install.test.ts +768 -0
- package/src/utils/install.ts +492 -0
- package/src/utils/instructions.test.ts +240 -0
- package/src/utils/instructions.ts +543 -0
- package/src/utils/leapingComment.test.ts +51 -0
- package/src/utils/leapingComment.ts +18 -0
- package/src/utils/learnings.test.ts +87 -0
- package/src/utils/learnings.ts +138 -0
- package/src/utils/learningsTocRender.test.ts +116 -0
- package/src/utils/learningsTruncate.test.ts +39 -0
- package/src/utils/learningsTruncate.ts +42 -0
- package/src/utils/lifecycle.test.ts +195 -0
- package/src/utils/lifecycle.ts +198 -0
- package/src/utils/log.test.ts +402 -0
- package/src/utils/log.ts +432 -0
- package/src/utils/normalizeEnv.test.ts +91 -0
- package/src/utils/normalizeEnv.ts +106 -0
- package/src/utils/openCodeModels.ts +82 -0
- package/src/utils/overrides.test.ts +89 -0
- package/src/utils/overrides.ts +98 -0
- package/src/utils/packageManager.test.ts +321 -0
- package/src/utils/packageManager.ts +257 -0
- package/src/utils/patchWorkflowRunFields.test.ts +92 -0
- package/src/utils/patchWorkflowRunFields.ts +150 -0
- package/src/utils/payload.test.ts +497 -0
- package/src/utils/payload.ts +371 -0
- package/src/utils/postApiFetch.ts +51 -0
- package/src/utils/prSummary.test.ts +224 -0
- package/src/utils/prSummary.ts +147 -0
- package/src/utils/progressComment.ts +261 -0
- package/src/utils/providerErrors.test.ts +315 -0
- package/src/utils/providerErrors.ts +172 -0
- package/src/utils/rangeDiff.test.ts +236 -0
- package/src/utils/rangeDiff.ts +182 -0
- package/src/utils/remediationCommand.test.ts +163 -0
- package/src/utils/remediationCommand.ts +119 -0
- package/src/utils/retry.test.ts +153 -0
- package/src/utils/retry.ts +58 -0
- package/src/utils/reviewCleanup.ts +106 -0
- package/src/utils/run.ts +99 -0
- package/src/utils/runContext.ts +145 -0
- package/src/utils/runContextData.ts +58 -0
- package/src/utils/runErrorRenderer.test.ts +95 -0
- package/src/utils/runErrorRenderer.ts +259 -0
- package/src/utils/runFixture.ts +76 -0
- package/src/utils/runLifecycle.ts +237 -0
- package/src/utils/runStartupLog.ts +60 -0
- package/src/utils/secrets.test.ts +103 -0
- package/src/utils/secrets.ts +177 -0
- package/src/utils/setup.test.ts +509 -0
- package/src/utils/setup.ts +352 -0
- package/src/utils/shell.ts +103 -0
- package/src/utils/skills.test.ts +46 -0
- package/src/utils/skills.ts +67 -0
- package/src/utils/subprocess.test.ts +170 -0
- package/src/utils/subprocess.ts +438 -0
- package/src/utils/terraformMcp.test.ts +63 -0
- package/src/utils/terraformMcp.ts +83 -0
- package/src/utils/time.test.ts +105 -0
- package/src/utils/time.ts +59 -0
- package/src/utils/timer.test.ts +91 -0
- package/src/utils/timer.ts +72 -0
- package/src/utils/todoTracking.test.ts +223 -0
- package/src/utils/todoTracking.ts +167 -0
- package/src/utils/token.test.ts +239 -0
- package/src/utils/token.ts +186 -0
- package/src/utils/version.ts +10 -0
- package/src/utils/versioning.test.ts +34 -0
- package/src/utils/versioning.ts +44 -0
- package/src/utils/vertex.ts +85 -0
- package/src/utils/workflow.ts +25 -0
package/src/mcp/pr.ts
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
import { assertUnderPrCap, recordRemediationPrOpened } from "#app/mcp/guardrails";
|
|
3
|
+
import { assertTargetInScope, recordCreatedTarget } from "#app/mcp/scope";
|
|
4
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
5
|
+
import { execute, tool } from "#app/mcp/shared";
|
|
6
|
+
import { buildTerramendFooter, stripExistingFooter } from "#app/utils/buildTerramendFooter";
|
|
7
|
+
import { log } from "#app/utils/cli";
|
|
8
|
+
import { fixDoubleEscapedString } from "#app/utils/fixDoubleEscapedString";
|
|
9
|
+
import { patchWorkflowRunFields } from "#app/utils/patchWorkflowRunFields";
|
|
10
|
+
import { $ } from "#app/utils/shell";
|
|
11
|
+
|
|
12
|
+
export const PullRequest = type({
|
|
13
|
+
title: type.string.describe("the title of the pull request"),
|
|
14
|
+
body: type.string.describe("the body content of the pull request"),
|
|
15
|
+
"base?": type.string.describe(
|
|
16
|
+
"the base branch to merge into (e.g. 'main'). Omit to use the run's resolved base branch: the `base_branch` input, else the repository's default branch (main, or master).",
|
|
17
|
+
),
|
|
18
|
+
"draft?": type.boolean.describe(
|
|
19
|
+
"if true, create the pull request as a draft. use when the user explicitly asks for a draft PR.",
|
|
20
|
+
),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Pure base-branch picker. Precedence: an explicit `base_branch` declaration
|
|
25
|
+
* always wins; otherwise the repository's default branch (GitHub reports it —
|
|
26
|
+
* normally `main`, sometimes `master`); otherwise prefer `main`, then `master`;
|
|
27
|
+
* final fallback `main`. Split out from git/ctx so it's exhaustively testable.
|
|
28
|
+
*/
|
|
29
|
+
export function pickBaseBranch(opts: {
|
|
30
|
+
declared?: string | undefined;
|
|
31
|
+
defaultBranch?: string | undefined;
|
|
32
|
+
mainExists: boolean;
|
|
33
|
+
masterExists: boolean;
|
|
34
|
+
}): string {
|
|
35
|
+
if (opts.declared) return opts.declared;
|
|
36
|
+
if (opts.defaultBranch) return opts.defaultBranch;
|
|
37
|
+
if (opts.mainExists) return "main";
|
|
38
|
+
if (opts.masterExists) return "master";
|
|
39
|
+
return "main";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** true when `branch` exists as a remote-tracking or local ref. */
|
|
43
|
+
function branchExists(branch: string): boolean {
|
|
44
|
+
for (const ref of [`refs/remotes/origin/${branch}`, `refs/heads/${branch}`]) {
|
|
45
|
+
try {
|
|
46
|
+
$("git", ["rev-parse", "--verify", "--quiet", ref], { log: false });
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
// ref absent — try the next form
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Deterministically resolve the branch a PR targets, so the base is never left
|
|
57
|
+
* to the agent's guess. Resolves to the repository's default branch (`main`, or
|
|
58
|
+
* `master`), overridable by the `base_branch` input. Git is only probed for
|
|
59
|
+
* main/master in the last-resort case where neither an explicit declaration nor
|
|
60
|
+
* a GitHub default branch is available (essentially never).
|
|
61
|
+
*/
|
|
62
|
+
export function resolveBaseBranch(ctx: ToolContext): string {
|
|
63
|
+
const declared = ctx.payload.baseBranch?.trim() || undefined;
|
|
64
|
+
const defaultBranch = ctx.repo.data.default_branch?.trim() || undefined;
|
|
65
|
+
const needsProbe = !declared && !defaultBranch;
|
|
66
|
+
return pickBaseBranch({
|
|
67
|
+
declared,
|
|
68
|
+
defaultBranch,
|
|
69
|
+
mainExists: needsProbe && branchExists("main"),
|
|
70
|
+
masterExists: needsProbe && branchExists("master"),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildPrBodyWithFooter(ctx: ToolContext, body: string): string {
|
|
75
|
+
const footer = buildTerramendFooter({
|
|
76
|
+
triggeredBy: true,
|
|
77
|
+
model: ctx.toolState.model,
|
|
78
|
+
fallbackFrom: ctx.toolState.modelFallback?.from,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
|
|
82
|
+
return `${bodyWithoutFooter}${footer}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const UpdatePullRequestBody = type({
|
|
86
|
+
pull_number: type.number.describe("the pull request number to update"),
|
|
87
|
+
body: type.string.describe("the new body content for the pull request"),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export function UpdatePullRequestBodyTool(ctx: ToolContext) {
|
|
91
|
+
return tool({
|
|
92
|
+
name: "update_pull_request_body",
|
|
93
|
+
description: "Update the body/description of an existing pull request",
|
|
94
|
+
parameters: UpdatePullRequestBody,
|
|
95
|
+
execute: execute(async (params) => {
|
|
96
|
+
assertTargetInScope(ctx, params.pull_number, "update the body of");
|
|
97
|
+
const bodyWithFooter = buildPrBodyWithFooter(ctx, params.body);
|
|
98
|
+
|
|
99
|
+
const result = await ctx.octokit.rest.pulls.update({
|
|
100
|
+
owner: ctx.repo.owner,
|
|
101
|
+
repo: ctx.repo.name,
|
|
102
|
+
pull_number: params.pull_number,
|
|
103
|
+
body: bodyWithFooter,
|
|
104
|
+
});
|
|
105
|
+
log.info(`» updated pull request #${result.data.number}`);
|
|
106
|
+
|
|
107
|
+
ctx.toolState.wasUpdated = true;
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
success: true,
|
|
111
|
+
number: result.data.number,
|
|
112
|
+
url: result.data.html_url,
|
|
113
|
+
};
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function CreatePullRequestTool(ctx: ToolContext) {
|
|
119
|
+
return tool({
|
|
120
|
+
name: "create_pull_request",
|
|
121
|
+
description: "Create a pull request from the current branch",
|
|
122
|
+
parameters: PullRequest,
|
|
123
|
+
execute: execute(async (params) => {
|
|
124
|
+
// permission gate: opening a PR is a repo write. `push: disabled` means the
|
|
125
|
+
// repository is configured for read-only access, so block it here the same
|
|
126
|
+
// way push_branch/create_issue do — an injected or read-only-intended agent
|
|
127
|
+
// must not be able to open PRs.
|
|
128
|
+
if (ctx.payload.push === "disabled") {
|
|
129
|
+
throw new Error(
|
|
130
|
+
"Creating a pull request is disabled. This repository is configured for read-only access (push: disabled).",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Remediate-mode guardrail: stop at the configured max_prs. No-op otherwise.
|
|
135
|
+
assertUnderPrCap(ctx);
|
|
136
|
+
|
|
137
|
+
const currentBranch = $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
|
|
138
|
+
const base = params.base ?? resolveBaseBranch(ctx);
|
|
139
|
+
log.debug(`Current branch: ${currentBranch}; PR base: ${base}`);
|
|
140
|
+
|
|
141
|
+
const bodyWithFooter = buildPrBodyWithFooter(ctx, params.body);
|
|
142
|
+
|
|
143
|
+
const result = await ctx.octokit.rest.pulls.create({
|
|
144
|
+
owner: ctx.repo.owner,
|
|
145
|
+
repo: ctx.repo.name,
|
|
146
|
+
title: params.title,
|
|
147
|
+
body: bodyWithFooter,
|
|
148
|
+
head: currentBranch,
|
|
149
|
+
base,
|
|
150
|
+
draft: params.draft ?? false,
|
|
151
|
+
});
|
|
152
|
+
log.info(`» created pull request #${result.data.number} (id ${result.data.id})`);
|
|
153
|
+
|
|
154
|
+
// record so the agent may update THIS PR's body / comment on it later even
|
|
155
|
+
// though its number differs from the run's triggering issue_number.
|
|
156
|
+
recordCreatedTarget(ctx, result.data.number);
|
|
157
|
+
|
|
158
|
+
// best-effort: request review from the user who triggered the workflow
|
|
159
|
+
const reviewer = ctx.payload.triggerer;
|
|
160
|
+
if (reviewer) {
|
|
161
|
+
try {
|
|
162
|
+
log.debug(`requesting review from ${reviewer} on PR #${result.data.number}`);
|
|
163
|
+
await ctx.octokit.rest.pulls.requestReviewers({
|
|
164
|
+
owner: ctx.repo.owner,
|
|
165
|
+
repo: ctx.repo.name,
|
|
166
|
+
pull_number: result.data.number,
|
|
167
|
+
reviewers: [reviewer],
|
|
168
|
+
});
|
|
169
|
+
} catch {
|
|
170
|
+
log.info(`failed to request review from ${reviewer} on PR #${result.data.number}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (typeof result.data.node_id === "string" && result.data.node_id.length > 0) {
|
|
175
|
+
await patchWorkflowRunFields(ctx, {
|
|
176
|
+
prNodeId: result.data.node_id,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// count this toward the per-run remediation PR cap (Remediate mode only).
|
|
181
|
+
recordRemediationPrOpened(ctx);
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
success: true,
|
|
185
|
+
pullRequestId: result.data.id,
|
|
186
|
+
number: result.data.number,
|
|
187
|
+
url: result.data.html_url,
|
|
188
|
+
title: result.data.title,
|
|
189
|
+
head: result.data.head.ref,
|
|
190
|
+
base: result.data.base.ref,
|
|
191
|
+
};
|
|
192
|
+
}),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { PullRequestInfoTool } from "#app/mcp/prInfo";
|
|
3
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
4
|
+
|
|
5
|
+
type ToolResultShape = { content: [{ type: "text"; text: string }]; isError?: boolean };
|
|
6
|
+
|
|
7
|
+
async function runTool(t: { execute?: unknown }, params: unknown): Promise<ToolResultShape> {
|
|
8
|
+
const exec = t.execute as (p: unknown, c: unknown) => Promise<ToolResultShape>;
|
|
9
|
+
return exec(params, {});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function makePullData(overrides?: Record<string, unknown>) {
|
|
13
|
+
return {
|
|
14
|
+
number: 7,
|
|
15
|
+
html_url: "https://gh/pull/7",
|
|
16
|
+
title: "Enable bucket versioning",
|
|
17
|
+
body: "pr body",
|
|
18
|
+
state: "open",
|
|
19
|
+
draft: false,
|
|
20
|
+
merged: false,
|
|
21
|
+
maintainer_can_modify: true,
|
|
22
|
+
base: { ref: "main", repo: { full_name: "octo/repo" } },
|
|
23
|
+
head: { ref: "feat/versioning", repo: { full_name: "octo/repo" } },
|
|
24
|
+
user: { login: "alice" },
|
|
25
|
+
assignees: [{ login: "bob" }],
|
|
26
|
+
labels: [{ name: "infra" }],
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeCtx(pullData: Record<string, unknown>) {
|
|
32
|
+
const pullsGet = vi.fn(async (_p: unknown) => ({ data: pullData }));
|
|
33
|
+
const issuesGet = vi.fn(async (_p: unknown) => ({
|
|
34
|
+
data: { body_html: "<p>pr body</p>" },
|
|
35
|
+
}));
|
|
36
|
+
const graphql = vi.fn(async (_q: string, _v: unknown) => ({
|
|
37
|
+
repository: {
|
|
38
|
+
pullRequest: {
|
|
39
|
+
closingIssuesReferences: { nodes: [{ number: 3, title: "Bucket is unversioned" }] },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
}));
|
|
43
|
+
const ctx = {
|
|
44
|
+
octokit: { rest: { pulls: { get: pullsGet }, issues: { get: issuesGet } }, graphql },
|
|
45
|
+
repo: { owner: "octo", name: "repo" },
|
|
46
|
+
tmpdir: "/tmp",
|
|
47
|
+
githubInstallationToken: "tok",
|
|
48
|
+
} as unknown as ToolContext;
|
|
49
|
+
return { ctx, pullsGet, issuesGet, graphql };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("PullRequestInfoTool", () => {
|
|
53
|
+
it("returns PR metadata with linked closing issues", async () => {
|
|
54
|
+
const { ctx, pullsGet, issuesGet, graphql } = makeCtx(makePullData());
|
|
55
|
+
const result = await runTool(PullRequestInfoTool(ctx), { pull_number: 7 });
|
|
56
|
+
|
|
57
|
+
expect(result.isError).toBeUndefined();
|
|
58
|
+
expect(pullsGet).toHaveBeenCalledWith({ owner: "octo", repo: "repo", pull_number: 7 });
|
|
59
|
+
expect(issuesGet).toHaveBeenCalledWith(
|
|
60
|
+
expect.objectContaining({
|
|
61
|
+
issue_number: 7,
|
|
62
|
+
headers: { accept: "application/vnd.github.full+json" },
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
expect(graphql).toHaveBeenCalledWith(expect.stringContaining("closingIssuesReferences"), {
|
|
66
|
+
owner: "octo",
|
|
67
|
+
repo: "repo",
|
|
68
|
+
number: 7,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const text = result.content[0].text;
|
|
72
|
+
expect(text).toContain("Enable bucket versioning");
|
|
73
|
+
expect(text).toContain("isFork: false");
|
|
74
|
+
expect(text).toContain("base: main");
|
|
75
|
+
expect(text).toContain("head: feat/versioning");
|
|
76
|
+
expect(text).toContain("Bucket is unversioned");
|
|
77
|
+
expect(text).toContain("infra");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("flags fork PRs when the head repo differs (or is gone)", async () => {
|
|
81
|
+
const { ctx } = makeCtx(makePullData({ head: { ref: "feat", repo: null } }));
|
|
82
|
+
const result = await runTool(PullRequestInfoTool(ctx), { pull_number: 7 });
|
|
83
|
+
|
|
84
|
+
expect(result.isError).toBeUndefined();
|
|
85
|
+
expect(result.content[0].text).toContain("isFork: true");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("surfaces API failures as tool errors", async () => {
|
|
89
|
+
const { ctx, graphql } = makeCtx(makePullData());
|
|
90
|
+
graphql.mockRejectedValueOnce(new Error("GraphQL exploded"));
|
|
91
|
+
const result = await runTool(PullRequestInfoTool(ctx), { pull_number: 7 });
|
|
92
|
+
|
|
93
|
+
expect(result.isError).toBe(true);
|
|
94
|
+
expect(result.content[0].text).toContain("GraphQL exploded");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
3
|
+
import { execute, tool } from "#app/mcp/shared";
|
|
4
|
+
import { resolveBodyAssets } from "#app/utils/body";
|
|
5
|
+
|
|
6
|
+
const CLOSING_ISSUES_QUERY = `
|
|
7
|
+
query($owner: String!, $repo: String!, $number: Int!) {
|
|
8
|
+
repository(owner: $owner, name: $repo) {
|
|
9
|
+
pullRequest(number: $number) {
|
|
10
|
+
closingIssuesReferences(first: 10) {
|
|
11
|
+
nodes { number title }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
type ClosingIssuesResponse = {
|
|
19
|
+
repository: {
|
|
20
|
+
pullRequest: {
|
|
21
|
+
closingIssuesReferences: { nodes: Array<{ number: number; title: string }> };
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const PullRequestInfo = type({
|
|
27
|
+
pull_number: type.number.describe("The pull request number to fetch"),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export function PullRequestInfoTool(ctx: ToolContext) {
|
|
31
|
+
return tool({
|
|
32
|
+
name: "get_pull_request",
|
|
33
|
+
description:
|
|
34
|
+
"Retrieve PR metadata (title, body, state, branches, author, labels, linked issues). " +
|
|
35
|
+
"Example: `get_pull_request({ pull_number: 1234 })`. " +
|
|
36
|
+
"To checkout a PR branch locally, use checkout_pr instead.",
|
|
37
|
+
parameters: PullRequestInfo,
|
|
38
|
+
execute: execute(async ({ pull_number }) => {
|
|
39
|
+
// fetch REST and GraphQL in parallel. the issues.get call is only for body_html
|
|
40
|
+
// (PRs are issues; the pulls.get response type omits body_html) so attachment urls
|
|
41
|
+
// resolve to signed CDN urls — see resolveBodyAssets.
|
|
42
|
+
const [restResponse, issueResponse, graphqlResponse] = await Promise.all([
|
|
43
|
+
ctx.octokit.rest.pulls.get({
|
|
44
|
+
owner: ctx.repo.owner,
|
|
45
|
+
repo: ctx.repo.name,
|
|
46
|
+
pull_number,
|
|
47
|
+
}),
|
|
48
|
+
ctx.octokit.rest.issues.get({
|
|
49
|
+
owner: ctx.repo.owner,
|
|
50
|
+
repo: ctx.repo.name,
|
|
51
|
+
issue_number: pull_number,
|
|
52
|
+
headers: { accept: "application/vnd.github.full+json" },
|
|
53
|
+
}),
|
|
54
|
+
ctx.octokit.graphql<ClosingIssuesResponse>(CLOSING_ISSUES_QUERY, {
|
|
55
|
+
owner: ctx.repo.owner,
|
|
56
|
+
repo: ctx.repo.name,
|
|
57
|
+
number: pull_number,
|
|
58
|
+
}),
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const data = restResponse.data;
|
|
62
|
+
const isFork = data.head.repo?.full_name !== data.base.repo.full_name;
|
|
63
|
+
const closingIssues = graphqlResponse.repository.pullRequest.closingIssuesReferences.nodes;
|
|
64
|
+
|
|
65
|
+
const body = await resolveBodyAssets({
|
|
66
|
+
body: data.body,
|
|
67
|
+
bodyHtml: issueResponse.data.body_html,
|
|
68
|
+
tmpdir: ctx.tmpdir,
|
|
69
|
+
githubToken: ctx.githubInstallationToken,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
number: data.number,
|
|
74
|
+
url: data.html_url,
|
|
75
|
+
title: data.title,
|
|
76
|
+
body: body,
|
|
77
|
+
state: data.state,
|
|
78
|
+
draft: data.draft,
|
|
79
|
+
merged: data.merged,
|
|
80
|
+
maintainerCanModify: data.maintainer_can_modify,
|
|
81
|
+
base: data.base.ref,
|
|
82
|
+
head: data.head.ref,
|
|
83
|
+
isFork,
|
|
84
|
+
author: data.user?.login,
|
|
85
|
+
assignees: data.assignees?.map((a) => a.login),
|
|
86
|
+
labels: data.labels.map((l) => l.name),
|
|
87
|
+
closingIssues: closingIssues.map((i) => ({ number: i.number, title: i.title })),
|
|
88
|
+
};
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { parseProvidersSchema, unknownArgsForResource } from "#app/mcp/providerSchema";
|
|
3
|
+
|
|
4
|
+
const SCHEMA = JSON.stringify({
|
|
5
|
+
format_version: "1.0",
|
|
6
|
+
provider_schemas: {
|
|
7
|
+
"registry.terraform.io/hashicorp/aws": {
|
|
8
|
+
resource_schemas: {
|
|
9
|
+
aws_s3_bucket: {
|
|
10
|
+
block: {
|
|
11
|
+
attributes: { bucket: {}, acl: {}, tags: {} },
|
|
12
|
+
block_types: { versioning: {}, logging: {} },
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
aws_instance: {
|
|
16
|
+
block: {
|
|
17
|
+
attributes: { ami: {}, instance_type: {} },
|
|
18
|
+
block_types: { metadata_options: {} },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("parseProvidersSchema", () => {
|
|
27
|
+
it("maps each resource type to its attributes and nested blocks", () => {
|
|
28
|
+
const schema = parseProvidersSchema(SCHEMA);
|
|
29
|
+
const bucket = schema.get("aws_s3_bucket")!;
|
|
30
|
+
expect([...bucket.attributes].sort()).toEqual(["acl", "bucket", "tags"]);
|
|
31
|
+
expect([...bucket.blocks].sort()).toEqual(["logging", "versioning"]);
|
|
32
|
+
expect(schema.get("aws_instance")!.blocks.has("metadata_options")).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("tolerates empty / malformed input", () => {
|
|
36
|
+
expect(parseProvidersSchema("").size).toBe(0);
|
|
37
|
+
expect(parseProvidersSchema("not json").size).toBe(0);
|
|
38
|
+
expect(parseProvidersSchema(JSON.stringify({})).size).toBe(0);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("merges resource schemas across multiple providers", () => {
|
|
42
|
+
const merged = JSON.stringify({
|
|
43
|
+
provider_schemas: {
|
|
44
|
+
"registry.terraform.io/hashicorp/aws": {
|
|
45
|
+
resource_schemas: { aws_s3_bucket: { block: { attributes: { bucket: {} } } } },
|
|
46
|
+
},
|
|
47
|
+
"registry.terraform.io/hashicorp/random": {
|
|
48
|
+
resource_schemas: { random_id: { block: { attributes: { byte_length: {} } } } },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
const schema = parseProvidersSchema(merged);
|
|
53
|
+
expect(schema.has("aws_s3_bucket")).toBe(true);
|
|
54
|
+
expect(schema.has("random_id")).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("unknownArgsForResource", () => {
|
|
59
|
+
const schema = parseProvidersSchema(SCHEMA);
|
|
60
|
+
|
|
61
|
+
it("accepts valid attributes and nested blocks", () => {
|
|
62
|
+
expect(
|
|
63
|
+
unknownArgsForResource(schema, "aws_s3_bucket", ["bucket", "acl", "versioning"]),
|
|
64
|
+
).toEqual({
|
|
65
|
+
unknownResourceType: false,
|
|
66
|
+
unknown: [],
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("flags an argument that does not exist for the resource (would break plan)", () => {
|
|
71
|
+
// `server_side_encryption_configuration` is inline-deprecated on aws v5 → not in schema.
|
|
72
|
+
expect(
|
|
73
|
+
unknownArgsForResource(schema, "aws_s3_bucket", [
|
|
74
|
+
"bucket",
|
|
75
|
+
"server_side_encryption_configuration",
|
|
76
|
+
]),
|
|
77
|
+
).toEqual({ unknownResourceType: false, unknown: ["server_side_encryption_configuration"] });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("reports an unknown resource type rather than guessing", () => {
|
|
81
|
+
expect(unknownArgsForResource(schema, "aws_not_a_real_type", ["x"]).unknownResourceType).toBe(
|
|
82
|
+
true,
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { type } from "arktype";
|
|
3
|
+
import type { LocalToolContext } from "#app/mcp/localContext";
|
|
4
|
+
import { execute, tool } from "#app/mcp/shared";
|
|
5
|
+
import { SUBPROCESS_TIMEOUT_MS } from "#app/mcp/terraform/types";
|
|
6
|
+
import { log } from "#app/utils/cli";
|
|
7
|
+
import { resolveEnv } from "#app/utils/secrets";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Provider-schema awareness (§4.15 next). A "correct" fix for the wrong provider
|
|
11
|
+
* major just breaks `plan` — argument names and nested blocks differ across
|
|
12
|
+
* majors. After `terraform init`, the installed provider's exact schema is
|
|
13
|
+
* available via `terraform providers schema -json`; this parses it so a fix can
|
|
14
|
+
* be checked against the REAL attributes/blocks for the version in use, not the
|
|
15
|
+
* model's memory of the provider.
|
|
16
|
+
*
|
|
17
|
+
* The schema fetch is cached per `cwd` for the process (schemas don't change
|
|
18
|
+
* within a run) and degrades green (returns `ok: false`) when terraform isn't
|
|
19
|
+
* installed or the dir isn't initialised.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export interface ResourceSchema {
|
|
23
|
+
attributes: Set<string>;
|
|
24
|
+
blocks: Set<string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** resourceType → its attribute + nested-block names, across all providers. */
|
|
28
|
+
export type ProvidersSchema = Map<string, ResourceSchema>;
|
|
29
|
+
|
|
30
|
+
interface RawBlock {
|
|
31
|
+
attributes?: Record<string, unknown>;
|
|
32
|
+
block_types?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
interface RawResourceSchema {
|
|
35
|
+
block?: RawBlock;
|
|
36
|
+
}
|
|
37
|
+
interface RawProviderSchema {
|
|
38
|
+
resource_schemas?: Record<string, RawResourceSchema>;
|
|
39
|
+
}
|
|
40
|
+
interface RawSchema {
|
|
41
|
+
provider_schemas?: Record<string, RawProviderSchema>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse `terraform providers schema -json` into a resource-type → {attributes,
|
|
46
|
+
* blocks} map. Merges every provider's `resource_schemas`. Pure; tolerant of a
|
|
47
|
+
* missing/empty schema.
|
|
48
|
+
*/
|
|
49
|
+
export function parseProvidersSchema(json: string): ProvidersSchema {
|
|
50
|
+
const out: ProvidersSchema = new Map();
|
|
51
|
+
let parsed: RawSchema;
|
|
52
|
+
try {
|
|
53
|
+
parsed = JSON.parse(json || "{}") as RawSchema;
|
|
54
|
+
} catch {
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
for (const provider of Object.values(parsed.provider_schemas ?? {})) {
|
|
58
|
+
for (const [resourceType, schema] of Object.entries(provider.resource_schemas ?? {})) {
|
|
59
|
+
const attributes = new Set(Object.keys(schema.block?.attributes ?? {}));
|
|
60
|
+
const blocks = new Set(Object.keys(schema.block?.block_types ?? {}));
|
|
61
|
+
const existing = out.get(resourceType);
|
|
62
|
+
if (existing) {
|
|
63
|
+
for (const a of attributes) existing.attributes.add(a);
|
|
64
|
+
for (const b of blocks) existing.blocks.add(b);
|
|
65
|
+
} else {
|
|
66
|
+
out.set(resourceType, { attributes, blocks });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Given a resource type and the argument names a fix introduces, return the ones
|
|
75
|
+
* that are NOT valid attributes or nested blocks for that resource in the
|
|
76
|
+
* installed provider — i.e. names that would break `plan`. Returns `null` for
|
|
77
|
+
* `unknownResourceType` when the schema has no entry for the type (can't judge).
|
|
78
|
+
*/
|
|
79
|
+
export function unknownArgsForResource(
|
|
80
|
+
schema: ProvidersSchema,
|
|
81
|
+
resourceType: string,
|
|
82
|
+
args: string[],
|
|
83
|
+
): { unknownResourceType: boolean; unknown: string[] } {
|
|
84
|
+
const res = schema.get(resourceType);
|
|
85
|
+
if (!res) return { unknownResourceType: true, unknown: [] };
|
|
86
|
+
const unknown = args.filter((a) => !res.attributes.has(a) && !res.blocks.has(a));
|
|
87
|
+
return { unknownResourceType: false, unknown };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// per-process cache: cwd → parsed schema (schemas are stable within a run).
|
|
91
|
+
const schemaCache = new Map<string, ProvidersSchema | null>();
|
|
92
|
+
|
|
93
|
+
/** fetch + cache the providers schema for `cwd`. null when unavailable. */
|
|
94
|
+
export function loadProvidersSchema(cwd: string): ProvidersSchema | null {
|
|
95
|
+
if (schemaCache.has(cwd)) return schemaCache.get(cwd) ?? null;
|
|
96
|
+
const r = spawnSync("terraform", ["providers", "schema", "-json"], {
|
|
97
|
+
cwd,
|
|
98
|
+
encoding: "utf-8",
|
|
99
|
+
env: resolveEnv("restricted") as NodeJS.ProcessEnv,
|
|
100
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
101
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
102
|
+
// bound a hung `terraform providers schema` (e.g. provider plugin install
|
|
103
|
+
// stalling on the network); a timeout surfaces as r.error → null schema.
|
|
104
|
+
timeout: SUBPROCESS_TIMEOUT_MS,
|
|
105
|
+
});
|
|
106
|
+
if (r.error || r.status !== 0 || !r.stdout?.trim()) {
|
|
107
|
+
schemaCache.set(cwd, null);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const parsed = parseProvidersSchema(r.stdout);
|
|
111
|
+
schemaCache.set(cwd, parsed);
|
|
112
|
+
return parsed;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** test-only: clear the per-process schema cache. */
|
|
116
|
+
export function _clearProviderSchemaCache(): void {
|
|
117
|
+
schemaCache.clear();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const TerraformProviderSchemaParams = type({
|
|
121
|
+
resource_type: type.string.describe(
|
|
122
|
+
"the Terraform resource type to inspect, e.g. 'aws_s3_bucket'.",
|
|
123
|
+
),
|
|
124
|
+
"args?": type.string
|
|
125
|
+
.array()
|
|
126
|
+
.describe(
|
|
127
|
+
"optional argument/block names a fix introduces — the tool reports which are NOT valid for the installed provider version.",
|
|
128
|
+
),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
export function TerraformProviderSchemaTool(ctx: LocalToolContext) {
|
|
132
|
+
return tool({
|
|
133
|
+
name: "terraform_provider_schema",
|
|
134
|
+
description:
|
|
135
|
+
"Inspect the INSTALLED provider's schema for a resource type (§4.15) so a fix targets the right " +
|
|
136
|
+
"arguments for the pinned provider major. Returns the resource's valid `attributes` and nested " +
|
|
137
|
+
"`blocks`; when you pass `args`, it reports which are `unknown` (would break `plan`). Requires the dir " +
|
|
138
|
+
"to be `terraform init`-ed (run `terraform_validate`/`terraform_plan` first); degrades green " +
|
|
139
|
+
"(`ok: false`) when terraform isn't installed or the schema isn't available. Cached per run.",
|
|
140
|
+
parameters: TerraformProviderSchemaParams,
|
|
141
|
+
execute: execute(async ({ resource_type, args }) => {
|
|
142
|
+
const cwd = ctx.payload.cwd ?? process.cwd();
|
|
143
|
+
const schema = loadProvidersSchema(cwd);
|
|
144
|
+
if (!schema) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
code: "schema_unavailable",
|
|
148
|
+
detail:
|
|
149
|
+
"provider schema unavailable — run terraform_validate/terraform_plan first to init the dir, or terraform isn't installed.",
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const res = schema.get(resource_type);
|
|
153
|
+
if (!res) {
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
code: "unknown_resource_type",
|
|
157
|
+
detail: `no schema for '${resource_type}' in the installed providers — check the type name and that its provider is required.`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const verdict =
|
|
161
|
+
args && args.length > 0 ? unknownArgsForResource(schema, resource_type, args) : null;
|
|
162
|
+
log.info(
|
|
163
|
+
`» terraform_provider_schema(${resource_type}): ${res.attributes.size} attr / ${res.blocks.size} block(s)` +
|
|
164
|
+
(verdict ? `, ${verdict.unknown.length} unknown arg(s)` : ""),
|
|
165
|
+
);
|
|
166
|
+
return {
|
|
167
|
+
ok: true,
|
|
168
|
+
resource_type,
|
|
169
|
+
attributes: [...res.attributes].sort(),
|
|
170
|
+
blocks: [...res.blocks].sort(),
|
|
171
|
+
...(verdict ? { unknown_args: verdict.unknown } : {}),
|
|
172
|
+
};
|
|
173
|
+
}),
|
|
174
|
+
});
|
|
175
|
+
}
|