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
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
2
|
+
import { mkdtempSync, readdirSync, realpathSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { ShellPermission } from "#app/external";
|
|
6
|
+
import type { ToolState } from "#app/toolState";
|
|
7
|
+
import { log } from "#app/utils/cli";
|
|
8
|
+
import type { OctokitWithPlugins } from "#app/utils/github";
|
|
9
|
+
import { isInsideDocker } from "#app/utils/globals";
|
|
10
|
+
import { $ } from "#app/utils/shell";
|
|
11
|
+
|
|
12
|
+
export interface SetupOptions {
|
|
13
|
+
tempDir: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a shared temp directory for the action
|
|
18
|
+
*/
|
|
19
|
+
export function createTempDirectory(): string {
|
|
20
|
+
const sharedTempDir = mkdtempSync(join(tmpdir(), "terramend-"));
|
|
21
|
+
process.env.TERRAMEND_TEMP_DIR = sharedTempDir;
|
|
22
|
+
log.info(`» created temp dir at ${sharedTempDir}`);
|
|
23
|
+
return sharedTempDir;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* snapshot-and-delete the GHA runner's known credential leak surfaces inside
|
|
28
|
+
* `$RUNNER_TEMP` before the agent spawns. without this, a shell-capable agent
|
|
29
|
+
* can grep:
|
|
30
|
+
* - `_runner_file_commands/set_output_*` for `core.setOutput('token', ghs_…)`
|
|
31
|
+
* calls made by earlier composite-action steps (e.g.
|
|
32
|
+
* terramend/terramend/get-installation-token);
|
|
33
|
+
* - `<uuid>.sh` rendered step scripts whose `run: |` body embeds
|
|
34
|
+
* `${{ steps.token.outputs.token }}` literally (GHA expands BEFORE writing);
|
|
35
|
+
* - `git-credentials-*.config` written by `actions/checkout@v6` for the
|
|
36
|
+
* workflow GITHUB_TOKEN.
|
|
37
|
+
*
|
|
38
|
+
* the running bash process already has its own `.sh` open via fd, so the
|
|
39
|
+
* unlink is safe — `unlink` removes the dirent, the kernel keeps reading.
|
|
40
|
+
*
|
|
41
|
+
* preserves every `_runner_file_commands/` file path the runner pre-allocated
|
|
42
|
+
* for OUR step — `$GITHUB_OUTPUT`, `$GITHUB_ENV`, `$GITHUB_PATH`,
|
|
43
|
+
* `$GITHUB_STATE`, `$GITHUB_STEP_SUMMARY`. those are read by the runner
|
|
44
|
+
* AFTER we exit (or by our own `post:` hook), and wiping them would break
|
|
45
|
+
* terramend's `result` output, `post:` state handoff, and job summary.
|
|
46
|
+
*
|
|
47
|
+
* silent no-op when `$RUNNER_TEMP` is unset (local dev, `pnpm dev:run`).
|
|
48
|
+
* per-file errors are tolerated — the runner may delete files between
|
|
49
|
+
* our readdir and our unlink.
|
|
50
|
+
*/
|
|
51
|
+
export function wipeRunnerLeakSurface(): void {
|
|
52
|
+
const runnerTemp = process.env.RUNNER_TEMP;
|
|
53
|
+
if (!runnerTemp) return;
|
|
54
|
+
|
|
55
|
+
const preserve = new Set<string>();
|
|
56
|
+
for (const envVar of [
|
|
57
|
+
"GITHUB_OUTPUT",
|
|
58
|
+
"GITHUB_ENV",
|
|
59
|
+
"GITHUB_PATH",
|
|
60
|
+
"GITHUB_STATE",
|
|
61
|
+
"GITHUB_STEP_SUMMARY",
|
|
62
|
+
]) {
|
|
63
|
+
const path = process.env[envVar];
|
|
64
|
+
if (!path) continue;
|
|
65
|
+
try {
|
|
66
|
+
preserve.add(realpathSync(path));
|
|
67
|
+
} catch {
|
|
68
|
+
// path may not exist yet — preserve the literal in case it gets created later
|
|
69
|
+
preserve.add(path);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const wiped: string[] = [];
|
|
74
|
+
|
|
75
|
+
const tryUnlink = (path: string): void => {
|
|
76
|
+
let resolved = path;
|
|
77
|
+
try {
|
|
78
|
+
resolved = realpathSync(path);
|
|
79
|
+
} catch {
|
|
80
|
+
// file may already be gone — fall through to unlink for the race-tolerant path
|
|
81
|
+
}
|
|
82
|
+
if (preserve.has(resolved) || preserve.has(path)) return;
|
|
83
|
+
try {
|
|
84
|
+
unlinkSync(path);
|
|
85
|
+
wiped.push(path);
|
|
86
|
+
} catch {
|
|
87
|
+
// race-tolerant: file may have been deleted between readdir and unlink
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const listDir = (dir: string): string[] => {
|
|
92
|
+
try {
|
|
93
|
+
return readdirSync(dir);
|
|
94
|
+
} catch {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const fileCommandsDir = join(runnerTemp, "_runner_file_commands");
|
|
100
|
+
for (const entry of listDir(fileCommandsDir)) {
|
|
101
|
+
tryUnlink(join(fileCommandsDir, entry));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const entry of listDir(runnerTemp)) {
|
|
105
|
+
if (entry.endsWith(".sh") || /^git-credentials-.*\.config$/.test(entry)) {
|
|
106
|
+
tryUnlink(join(runnerTemp, entry));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (wiped.length > 0) {
|
|
111
|
+
log.info(`» wiped ${wiped.length} leak-surface file(s) from $RUNNER_TEMP`);
|
|
112
|
+
log.debug(`» wiped paths: ${wiped.join(", ")}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Setup the test repository for running actions
|
|
118
|
+
*/
|
|
119
|
+
export function setupTestRepo(options: SetupOptions): void {
|
|
120
|
+
const tempDir = options.tempDir;
|
|
121
|
+
const repo = process.env.GITHUB_REPOSITORY;
|
|
122
|
+
if (!repo) throw new Error("GITHUB_REPOSITORY is required");
|
|
123
|
+
log.info(`» cloning ${repo} into ${tempDir}...`);
|
|
124
|
+
|
|
125
|
+
// use https with token in ci or when running inside docker
|
|
126
|
+
if (process.env.CI || isInsideDocker) {
|
|
127
|
+
const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
|
|
128
|
+
if (!token) {
|
|
129
|
+
throw new Error("GITHUB_TOKEN or GH_TOKEN is required for https clone in ci or docker");
|
|
130
|
+
}
|
|
131
|
+
$("git", ["clone", `https://x-access-token:${token}@github.com/${repo}.git`, tempDir]);
|
|
132
|
+
} else {
|
|
133
|
+
$("git", ["clone", `git@github.com:${repo}.git`, tempDir]);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* build an env suitable for targeting a specific git repo via `cwd`.
|
|
139
|
+
*
|
|
140
|
+
* inherited GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE override cwd resolution,
|
|
141
|
+
* which matters when this code runs as a child of `git push` (pre-push hook)
|
|
142
|
+
* or inside another git subcommand. if we don't strip them, a call that
|
|
143
|
+
* names `repoDir` in cwd silently operates on the outer repo instead.
|
|
144
|
+
*/
|
|
145
|
+
function envScopedToRepo(): NodeJS.ProcessEnv {
|
|
146
|
+
const scoped = { ...process.env };
|
|
147
|
+
for (const key of Object.keys(scoped)) {
|
|
148
|
+
if (key.startsWith("GIT_")) delete scoped[key];
|
|
149
|
+
}
|
|
150
|
+
return scoped;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* remove any `[includeIf ...]` entries from the local git config so that
|
|
155
|
+
* actions/checkout-persisted credentials don't ride alongside ASKPASS-provided
|
|
156
|
+
* auth for subsequent git operations.
|
|
157
|
+
*
|
|
158
|
+
* SECURITY: git config subsection values can contain arbitrary characters
|
|
159
|
+
* including `$(...)` command substitutions, and `${IFS}` spacing tricks defeat
|
|
160
|
+
* naive split-on-space filtering. we read keys via the `-z` (null-terminated)
|
|
161
|
+
* output format and feed them to a spawn-array `git config --unset-all` so
|
|
162
|
+
* the shell never interpolates key contents — closing the RCE path that a
|
|
163
|
+
* string-interpolated `execSync(...)` would expose.
|
|
164
|
+
*/
|
|
165
|
+
export function removeIncludeIfEntries(repoDir: string): void {
|
|
166
|
+
const env = envScopedToRepo();
|
|
167
|
+
let configOutput: string;
|
|
168
|
+
try {
|
|
169
|
+
configOutput = execSync("git config --local --get-regexp -z ^includeif\\.", {
|
|
170
|
+
cwd: repoDir,
|
|
171
|
+
encoding: "utf-8",
|
|
172
|
+
stdio: "pipe",
|
|
173
|
+
env,
|
|
174
|
+
});
|
|
175
|
+
} catch {
|
|
176
|
+
log.debug("» no includeIf credential entries to remove");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const seen = new Set<string>();
|
|
180
|
+
for (const entry of configOutput.split("\0")) {
|
|
181
|
+
if (!entry) continue;
|
|
182
|
+
// -z format: each entry is "<key>\n<value>". the key is up to the first newline.
|
|
183
|
+
const nl = entry.indexOf("\n");
|
|
184
|
+
const key = nl === -1 ? entry : entry.slice(0, nl);
|
|
185
|
+
if (!key || seen.has(key)) continue;
|
|
186
|
+
seen.add(key);
|
|
187
|
+
try {
|
|
188
|
+
// execFileSync (not execSync) so the key — which can contain arbitrary
|
|
189
|
+
// characters including shell metacharacters and $() command substitutions
|
|
190
|
+
// — is passed as an argv element and never interpolated by a shell.
|
|
191
|
+
// this is the load-bearing side of a9aa3b2b's injection fix.
|
|
192
|
+
execFileSync("git", ["config", "--local", "--unset-all", key], {
|
|
193
|
+
cwd: repoDir,
|
|
194
|
+
stdio: "pipe",
|
|
195
|
+
env,
|
|
196
|
+
});
|
|
197
|
+
} catch (error) {
|
|
198
|
+
log.debug(
|
|
199
|
+
`» failed to unset ${key}: ${error instanceof Error ? error.message : String(error)}`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (seen.size > 0)
|
|
204
|
+
log.info(
|
|
205
|
+
`» removed ${seen.size} includeIf credential ${seen.size === 1 ? "entry" : "entries"}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface GitContext {
|
|
210
|
+
gitToken: string;
|
|
211
|
+
owner: string;
|
|
212
|
+
name: string;
|
|
213
|
+
octokit: OctokitWithPlugins;
|
|
214
|
+
toolState: ToolState;
|
|
215
|
+
// shell permission level — controls hook and security behavior:
|
|
216
|
+
// enabled: full shell, hooks run, no restrictions
|
|
217
|
+
// restricted: MCP shell in stripped env, hooks run, token protection on auth ops
|
|
218
|
+
// disabled: no shell, hooks disabled globally, all code execution paths blocked
|
|
219
|
+
shell: ShellPermission;
|
|
220
|
+
postCheckoutScript: string | null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export type SetupGitParams = GitContext;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* setup git configuration and authentication for the repository.
|
|
227
|
+
* - configures git identity (user.email, user.name)
|
|
228
|
+
* - sets up authentication via gitToken (minimal contents:write)
|
|
229
|
+
*
|
|
230
|
+
* gitToken is a minimal-permission token (contents + workflows) used for git operations.
|
|
231
|
+
* it is assumed to be potentially exfiltratable, so it has limited scope.
|
|
232
|
+
*/
|
|
233
|
+
export async function setupGit(params: SetupGitParams): Promise<void> {
|
|
234
|
+
const repoDir = process.cwd();
|
|
235
|
+
|
|
236
|
+
// 1. configure git identity
|
|
237
|
+
log.info("» setting up git configuration...");
|
|
238
|
+
try {
|
|
239
|
+
// check current config - only set defaults if not configured or using generic bot
|
|
240
|
+
let currentEmail = "";
|
|
241
|
+
try {
|
|
242
|
+
currentEmail = execSync("git config user.email", {
|
|
243
|
+
cwd: repoDir,
|
|
244
|
+
stdio: "pipe",
|
|
245
|
+
encoding: "utf-8",
|
|
246
|
+
}).trim();
|
|
247
|
+
} catch {
|
|
248
|
+
// not configured
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const shouldSetDefaults =
|
|
252
|
+
!currentEmail || currentEmail === "github-actions[bot]@users.noreply.github.com";
|
|
253
|
+
|
|
254
|
+
if (shouldSetDefaults) {
|
|
255
|
+
// plain noreply email with no numeric account id — the upstream id
|
|
256
|
+
// resolved commits to the wrong bot account on GitHub.
|
|
257
|
+
execSync('git config --local user.email "terramend[bot]@users.noreply.github.com"', {
|
|
258
|
+
cwd: repoDir,
|
|
259
|
+
stdio: "pipe",
|
|
260
|
+
});
|
|
261
|
+
execSync('git config --local user.name "terramend[bot]"', {
|
|
262
|
+
cwd: repoDir,
|
|
263
|
+
stdio: "pipe",
|
|
264
|
+
});
|
|
265
|
+
log.debug("» git user configured (using defaults)");
|
|
266
|
+
} else {
|
|
267
|
+
log.debug(`» git user already configured (${currentEmail}), skipping`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// SECURITY: disable git hooks when shell is disabled to prevent code execution.
|
|
271
|
+
// in restricted mode, hooks run in the stripped sandbox — that's fine.
|
|
272
|
+
// in enabled mode, the agent has full shell anyway.
|
|
273
|
+
// in disabled mode, hooks are the primary code-execution escape vector.
|
|
274
|
+
if (params.shell === "disabled") {
|
|
275
|
+
execSync("git config --local core.hooksPath /dev/null", {
|
|
276
|
+
cwd: repoDir,
|
|
277
|
+
stdio: "pipe",
|
|
278
|
+
});
|
|
279
|
+
log.debug("» git hooks disabled (shell=disabled)");
|
|
280
|
+
}
|
|
281
|
+
} catch (error) {
|
|
282
|
+
// If git config fails, log warning but don't fail the action
|
|
283
|
+
// This can happen if we're not in a git repo or git isn't available
|
|
284
|
+
log.info(`Failed to set git config: ${error instanceof Error ? error.message : String(error)}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 2. setup authentication
|
|
288
|
+
// remove existing git auth headers that actions/checkout might have set
|
|
289
|
+
try {
|
|
290
|
+
execSync("git config --local --unset-all http.https://github.com/.extraheader", {
|
|
291
|
+
cwd: repoDir,
|
|
292
|
+
stdio: "pipe",
|
|
293
|
+
});
|
|
294
|
+
log.info("» removed existing authentication headers");
|
|
295
|
+
} catch {
|
|
296
|
+
log.debug("» no existing authentication headers to remove");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// remove includeIf entries that actions/checkout@v6 uses for credential persistence.
|
|
300
|
+
// v6 stores credentials in an external file loaded via includeIf.gitdir, which our
|
|
301
|
+
// --unset-all above doesn't catch. without this, stale credentials from actions/checkout
|
|
302
|
+
// would be sent alongside ASKPASS-provided credentials.
|
|
303
|
+
removeIncludeIfEntries(repoDir);
|
|
304
|
+
|
|
305
|
+
// SECURITY: set origin URL without token - auth is injected via GIT_ASKPASS
|
|
306
|
+
// in $git() calls. this prevents token leakage to git hooks and subprocesses.
|
|
307
|
+
const originUrl = `https://github.com/${params.owner}/${params.name}.git`;
|
|
308
|
+
$("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
|
|
309
|
+
|
|
310
|
+
// initialize pushUrl to base repo - may be updated by checkout_pr for fork PRs
|
|
311
|
+
params.toolState.pushUrl = originUrl;
|
|
312
|
+
|
|
313
|
+
// disable credential helpers to prevent prompts and ensure clean auth state
|
|
314
|
+
$("git", ["config", "--local", "credential.helper", ""], { cwd: repoDir });
|
|
315
|
+
|
|
316
|
+
// pin the run-entry HEAD for the checkout_pr initial-branch invariant; see
|
|
317
|
+
// captureInitialHead for the named-branch vs detached split and why it
|
|
318
|
+
// matters (zed-industries/cloud 2026-05-18 cross-PR clobber shape).
|
|
319
|
+
params.toolState.initialHead = captureInitialHead(repoDir);
|
|
320
|
+
|
|
321
|
+
log.info("» git authentication configured");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* snapshot the current HEAD as either a branch name (when on a named branch)
|
|
326
|
+
* or a literal SHA (when detached). used by setupGit to pin the run-entry
|
|
327
|
+
* position and by checkout_pr to compare the live HEAD against it.
|
|
328
|
+
*
|
|
329
|
+
* splitting the two cases is load-bearing: `git rev-parse --abbrev-ref HEAD`
|
|
330
|
+
* returns the sentinel string `"HEAD"` on detached entry — which is the
|
|
331
|
+
* default `actions/checkout` state for `pull_request` events. storing that
|
|
332
|
+
* raw string would make any future detached state (including a subagent's
|
|
333
|
+
* `git checkout --detach <sha>`) compare equal.
|
|
334
|
+
*/
|
|
335
|
+
export function captureInitialHead(
|
|
336
|
+
repoDir: string,
|
|
337
|
+
): { kind: "branch"; name: string } | { kind: "detached"; sha: string } {
|
|
338
|
+
try {
|
|
339
|
+
const name = $("git", ["symbolic-ref", "--short", "HEAD"], {
|
|
340
|
+
cwd: repoDir,
|
|
341
|
+
log: false,
|
|
342
|
+
}).trim();
|
|
343
|
+
if (name) return { kind: "branch", name };
|
|
344
|
+
} catch {
|
|
345
|
+
// detached HEAD — fall through
|
|
346
|
+
}
|
|
347
|
+
const sha = $("git", ["rev-parse", "HEAD"], {
|
|
348
|
+
cwd: repoDir,
|
|
349
|
+
log: false,
|
|
350
|
+
}).trim();
|
|
351
|
+
return { kind: "detached", sha };
|
|
352
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { type EnvMode, resolveEnv } from "#app/utils/secrets";
|
|
3
|
+
|
|
4
|
+
interface ShellOptions {
|
|
5
|
+
cwd?: string;
|
|
6
|
+
encoding?:
|
|
7
|
+
| "utf-8"
|
|
8
|
+
| "utf8"
|
|
9
|
+
| "ascii"
|
|
10
|
+
| "base64"
|
|
11
|
+
| "base64url"
|
|
12
|
+
| "hex"
|
|
13
|
+
| "latin1"
|
|
14
|
+
| "ucs-2"
|
|
15
|
+
| "ucs2"
|
|
16
|
+
| "utf16le";
|
|
17
|
+
log?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* env mode: "restricted" (default) filters secrets, "inherit" passes full env,
|
|
20
|
+
* or provide a custom env object (merged with restricted base)
|
|
21
|
+
*/
|
|
22
|
+
env?: EnvMode;
|
|
23
|
+
onError?: (result: { status: number; stdout: string; stderr: string }) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Execute a shell command safely using spawnSync with argument arrays.
|
|
28
|
+
* Prevents shell injection by avoiding string interpolation in shell commands.
|
|
29
|
+
*
|
|
30
|
+
* SECURITY: by default, env vars are filtered to remove secrets (tokens, keys, passwords).
|
|
31
|
+
* this prevents malicious code (git hooks, npm scripts, etc.) from exfiltrating credentials.
|
|
32
|
+
* use env: "inherit" only when absolutely necessary.
|
|
33
|
+
*
|
|
34
|
+
* @param cmd - The command to execute
|
|
35
|
+
* @param args - Array of arguments to pass to the command
|
|
36
|
+
* @param options - Optional configuration (cwd, encoding, onError)
|
|
37
|
+
* @returns The trimmed stdout output
|
|
38
|
+
* @throws Error if command fails and no onError handler is provided
|
|
39
|
+
*/
|
|
40
|
+
export function $(cmd: string, args: string[], options?: ShellOptions): string {
|
|
41
|
+
const encoding = options?.encoding ?? "utf-8";
|
|
42
|
+
const env = resolveEnv(options?.env);
|
|
43
|
+
|
|
44
|
+
// CRITICAL: use "ignore" for stdin instead of "inherit" to avoid breaking MCP transport
|
|
45
|
+
// when running inside an MCP server, stdin is used for JSON-RPC protocol
|
|
46
|
+
const result = spawnSync(cmd, args, {
|
|
47
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
48
|
+
encoding,
|
|
49
|
+
cwd: options?.cwd,
|
|
50
|
+
env,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const stdout = result.stdout ?? "";
|
|
54
|
+
const stderr = result.stderr ?? "";
|
|
55
|
+
|
|
56
|
+
// Write output to process streams so it behaves like stdio: "inherit"
|
|
57
|
+
// CRITICAL: when running inside an MCP server, stdout is used for JSON-RPC protocol
|
|
58
|
+
// so we must write to stderr instead to avoid corrupting the protocol
|
|
59
|
+
// Only log if log option is not explicitly set to false
|
|
60
|
+
if (options?.log !== false) {
|
|
61
|
+
// if stdout is a TTY, it's safe to write to it; otherwise it's likely a pipe used for JSON-RPC
|
|
62
|
+
const canWriteToStdout = process.stdout.isTTY === true;
|
|
63
|
+
if (stdout) {
|
|
64
|
+
if (canWriteToStdout) {
|
|
65
|
+
process.stdout.write(stdout);
|
|
66
|
+
} else {
|
|
67
|
+
// stdout is a pipe (MCP context) - write to stderr instead
|
|
68
|
+
process.stderr.write(stdout);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (stderr) {
|
|
72
|
+
process.stderr.write(stderr);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Handle errors
|
|
77
|
+
if (result.status !== 0) {
|
|
78
|
+
const errorResult = {
|
|
79
|
+
status: result.status ?? -1,
|
|
80
|
+
stdout,
|
|
81
|
+
stderr,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (options?.onError) {
|
|
85
|
+
options.onError(errorResult);
|
|
86
|
+
return stdout.trim();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// many git subcommands write context-bearing diagnostics to stdout, not
|
|
90
|
+
// stderr (merge conflicts, cherry-pick rejections, diff --exit-code,
|
|
91
|
+
// ls-files --error-unmatch). Falling back to "Unknown error" robbed the
|
|
92
|
+
// agent of any signal and forced an extra MCP round-trip. see #766.
|
|
93
|
+
const detail = [stderr, stdout]
|
|
94
|
+
.map((s) => s.trim())
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.join("\n");
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Command failed with exit code ${errorResult.status}: ${detail || "Unknown error"}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return stdout.trim();
|
|
103
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { installBundledSkills } from "#app/utils/skills";
|
|
6
|
+
|
|
7
|
+
vi.mock("#app/utils/cli", () => ({
|
|
8
|
+
log: {
|
|
9
|
+
info: vi.fn(),
|
|
10
|
+
debug: vi.fn(),
|
|
11
|
+
warning: vi.fn(),
|
|
12
|
+
error: vi.fn(),
|
|
13
|
+
success: vi.fn(),
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import { log } from "#app/utils/cli";
|
|
18
|
+
|
|
19
|
+
const successMock = vi.mocked(log.success);
|
|
20
|
+
|
|
21
|
+
const TEMP = mkdtempSync(join(tmpdir(), "terramend-skills-"));
|
|
22
|
+
|
|
23
|
+
afterAll(() => {
|
|
24
|
+
rmSync(TEMP, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("installBundledSkills", () => {
|
|
32
|
+
it("writes every bundled skill into all agent auto-scan dirs under HOME", () => {
|
|
33
|
+
const home = join(TEMP, "home");
|
|
34
|
+
installBundledSkills({ home });
|
|
35
|
+
|
|
36
|
+
const targets = [".opencode/skills", ".claude/skills", ".agents/skills"].map((dir) =>
|
|
37
|
+
join(home, dir, "terraform-best-practices", "SKILL.md"),
|
|
38
|
+
);
|
|
39
|
+
const contents = targets.map((path) => readFileSync(path, "utf8"));
|
|
40
|
+
for (const content of contents) {
|
|
41
|
+
expect(content.length).toBeGreaterThan(0);
|
|
42
|
+
expect(content).toBe(contents[0]);
|
|
43
|
+
}
|
|
44
|
+
expect(successMock).toHaveBeenCalledWith(expect.stringContaining("terraform-best-practices"));
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { log } from "#app/utils/cli";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* skills bundled with the action runtime. the SKILL.md files live in
|
|
8
|
+
* `action/skills/<name>/SKILL.md` and are read at runtime — no esbuild loader,
|
|
9
|
+
* no codegen. this matters because the preview / oss path runs `cli.ts` from
|
|
10
|
+
* source (see `runCli.ts#runLocalCli`) where esbuild loaders don't apply.
|
|
11
|
+
*/
|
|
12
|
+
const BUNDLED_SKILL_NAMES = ["terraform-best-practices"] as const;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* resolve the on-disk path of a bundled SKILL.md by checking the two locations
|
|
16
|
+
* the file may live in:
|
|
17
|
+
* - source mode (`runLocalCli`): `<actionRoot>/skills/<name>/SKILL.md`,
|
|
18
|
+
* reached as `../skills/...` from `utils/skills.ts`.
|
|
19
|
+
* - bundled mode (npx published package): `<distDir>/skills/<name>/SKILL.md`,
|
|
20
|
+
* reached as `./skills/...` from `dist/cli.mjs`.
|
|
21
|
+
*
|
|
22
|
+
* the bundled-mode copy is produced by an esbuild post-build step in
|
|
23
|
+
* `esbuild.config.js`.
|
|
24
|
+
*/
|
|
25
|
+
function resolveSkillPath(name: string): string {
|
|
26
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const candidates = [
|
|
28
|
+
join(here, "..", "skills", name, "SKILL.md"),
|
|
29
|
+
join(here, "skills", name, "SKILL.md"),
|
|
30
|
+
];
|
|
31
|
+
for (const candidate of candidates) {
|
|
32
|
+
if (existsSync(candidate)) return candidate;
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`bundled skill not found: ${name} (looked in ${candidates.join(", ")})`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* each agent has its own auto-scan dir under HOME. we write to all of them so
|
|
39
|
+
* the same `installBundledSkills` call works regardless of which agent is
|
|
40
|
+
* running, without coupling skills.ts to agent identity.
|
|
41
|
+
*
|
|
42
|
+
* verified empirically (PR #565):
|
|
43
|
+
* - OpenCode registers skills from `$HOME/.agents/skills/` and `.opencode/skills/`.
|
|
44
|
+
* - Claude Code only registers skills from `$HOME/.claude/skills/` —
|
|
45
|
+
* it does NOT scan `.agents/skills/`, so writing only there leaves the
|
|
46
|
+
* skill on disk but invisible to Claude's `Skill` tool.
|
|
47
|
+
*/
|
|
48
|
+
const SKILL_TARGET_DIRS = [".opencode/skills", ".claude/skills", ".agents/skills"] as const;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* write all bundled skills into the fake HOME so OpenCode / Claude Code discover
|
|
52
|
+
* them via their auto-scan directories.
|
|
53
|
+
*
|
|
54
|
+
* called once per agent run from each agent's `run()`. cheap (small file
|
|
55
|
+
* writes), no network, idempotent.
|
|
56
|
+
*/
|
|
57
|
+
export function installBundledSkills(params: { home: string }): void {
|
|
58
|
+
for (const name of BUNDLED_SKILL_NAMES) {
|
|
59
|
+
const content = readFileSync(resolveSkillPath(name), "utf8");
|
|
60
|
+
for (const targetDir of SKILL_TARGET_DIRS) {
|
|
61
|
+
const skillDir = join(params.home, targetDir, name);
|
|
62
|
+
mkdirSync(skillDir, { recursive: true });
|
|
63
|
+
writeFileSync(join(skillDir, "SKILL.md"), content);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
log.success(`installed bundled skills: ${BUNDLED_SKILL_NAMES.join(", ")}`);
|
|
67
|
+
}
|