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,82 @@
|
|
|
1
|
+
// OpenCode-as-source-of-truth for BYOK detection.
|
|
2
|
+
//
|
|
3
|
+
// `opencode models` returns the `provider/model` specifiers that OpenCode
|
|
4
|
+
// can actually route given the current env (workflow env block + GH Actions
|
|
5
|
+
// secrets) and `auth.json` (Codex / future managed credentials). This is
|
|
6
|
+
// authoritative — strictly more accurate than the static
|
|
7
|
+
// `provider.envVars + provider.managedCredentials` catalog in `models.ts`
|
|
8
|
+
// for the "do we have BYOK auth?" gate. The catalog can (and will) miss
|
|
9
|
+
// new auth shapes; OpenCode itself can't.
|
|
10
|
+
//
|
|
11
|
+
// Two captures per run:
|
|
12
|
+
// 1. `captureBaselineModels` — called BEFORE Codex `auth.json` is
|
|
13
|
+
// materialized. The set OpenCode can serve from the runner's pre-existing
|
|
14
|
+
// environment alone (workflow `env:` block + GH Actions secrets).
|
|
15
|
+
// 2. `captureAuthorizedModels` — called AFTER Codex `auth.json`
|
|
16
|
+
// materialization. The authoritative set for BYOK decisions (fallback +
|
|
17
|
+
// validateAgentApiKey).
|
|
18
|
+
//
|
|
19
|
+
// The set difference (`authorized - baseline`) is the contribution of the
|
|
20
|
+
// Codex OAuth credential to this run — logged once for operator visibility.
|
|
21
|
+
//
|
|
22
|
+
// Memoized at module scope so the two consumers
|
|
23
|
+
// (`selectFallbackModelIfNeeded` + `autoSelectModel`) share one shell-out.
|
|
24
|
+
|
|
25
|
+
import { execFileSync } from "node:child_process";
|
|
26
|
+
import { log } from "#app/utils/cli";
|
|
27
|
+
|
|
28
|
+
let baseline: Set<string> | undefined;
|
|
29
|
+
let authorized: Set<string> | undefined;
|
|
30
|
+
|
|
31
|
+
function readModels(cliPath: string): Set<string> {
|
|
32
|
+
try {
|
|
33
|
+
const output = execFileSync(cliPath, ["models"], {
|
|
34
|
+
encoding: "utf-8",
|
|
35
|
+
timeout: 30_000,
|
|
36
|
+
env: process.env,
|
|
37
|
+
});
|
|
38
|
+
return new Set(
|
|
39
|
+
output
|
|
40
|
+
.split("\n")
|
|
41
|
+
.map((line) => line.trim())
|
|
42
|
+
.filter(Boolean),
|
|
43
|
+
);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
log.debug(
|
|
46
|
+
`» \`opencode models\` failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
47
|
+
);
|
|
48
|
+
return new Set();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Snapshot the set of models OpenCode can serve from the current env, BEFORE
|
|
53
|
+
* Terramend-stored credentials are merged in. Call once early in `main.ts`. */
|
|
54
|
+
export function captureBaselineModels(cliPath: string): void {
|
|
55
|
+
baseline = readModels(cliPath);
|
|
56
|
+
log.debug(`» opencode baseline: ${baseline.size} models`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Snapshot the set of models OpenCode can serve AFTER dbSecrets +
|
|
60
|
+
* Codex auth.json are in place. Logs the diff against the baseline as
|
|
61
|
+
* `» BYOK auth enabled N model(s): …`. */
|
|
62
|
+
export function captureAuthorizedModels(cliPath: string): void {
|
|
63
|
+
authorized = readModels(cliPath);
|
|
64
|
+
const base = baseline;
|
|
65
|
+
if (base) {
|
|
66
|
+
const diff = [...authorized].filter((m) => !base.has(m));
|
|
67
|
+
if (diff.length > 0) {
|
|
68
|
+
log.info(`» BYOK auth enabled ${diff.length} model(s): ${diff.join(", ")}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
log.debug(`» opencode authorized: ${authorized.size} models`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Authorized set captured after Terramend-stored auth is applied. Throws if
|
|
75
|
+
* called before `captureAuthorizedModels` — the call sites (fallback gate,
|
|
76
|
+
* api-key validation, auto-select) all run strictly after capture. */
|
|
77
|
+
export function getAuthorizedModels(): Set<string> {
|
|
78
|
+
if (!authorized) {
|
|
79
|
+
throw new Error("getAuthorizedModels called before captureAuthorizedModels");
|
|
80
|
+
}
|
|
81
|
+
return authorized;
|
|
82
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { applyOverrides, DENIED_OVERRIDE_NAMES, parseOverrides } from "#app/utils/overrides";
|
|
3
|
+
|
|
4
|
+
vi.mock("@actions/core", () => ({
|
|
5
|
+
setSecret: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
import * as core from "@actions/core";
|
|
9
|
+
|
|
10
|
+
const setSecretMock = vi.mocked(core.setSecret);
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("parseOverrides", () => {
|
|
17
|
+
it("returns an empty object for empty or whitespace input", () => {
|
|
18
|
+
expect(parseOverrides("")).toEqual({});
|
|
19
|
+
expect(parseOverrides(" \n\t ")).toEqual({});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("parses a valid JSON object of string values", () => {
|
|
23
|
+
expect(parseOverrides('{"A":"1","B":""}')).toEqual({ A: "1", B: "" });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("throws on invalid JSON", () => {
|
|
27
|
+
expect(() => parseOverrides("{nope")).toThrow(/invalid UNSAFE_OVERRIDES: not valid JSON/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("throws on non-object JSON", () => {
|
|
31
|
+
expect(() => parseOverrides('"string"')).toThrow(/must be a JSON object/);
|
|
32
|
+
expect(() => parseOverrides("null")).toThrow(/must be a JSON object/);
|
|
33
|
+
expect(() => parseOverrides('["a"]')).toThrow(/must be a JSON object/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("throws when a value is not a string", () => {
|
|
37
|
+
expect(() => parseOverrides('{"A":42}')).toThrow(/key "A" must have a string value/);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("applyOverrides", () => {
|
|
42
|
+
it("applies overrides, masks values, and strips the raw input var", () => {
|
|
43
|
+
const env: NodeJS.ProcessEnv = { UNSAFE_OVERRIDES: '{"MY_KEY":"secret-value"}' };
|
|
44
|
+
|
|
45
|
+
const result = applyOverrides({ raw: '{"MY_KEY":"secret-value"}', env });
|
|
46
|
+
|
|
47
|
+
expect(result).toEqual({ applied: ["MY_KEY"], denied: [] });
|
|
48
|
+
expect(env.MY_KEY).toBe("secret-value");
|
|
49
|
+
expect(env.UNSAFE_OVERRIDES).toBeUndefined();
|
|
50
|
+
expect(setSecretMock).toHaveBeenCalledWith("secret-value");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("refuses denied names while applying the rest", () => {
|
|
54
|
+
const env: NodeJS.ProcessEnv = {};
|
|
55
|
+
|
|
56
|
+
const result = applyOverrides({
|
|
57
|
+
raw: '{"GITHUB_TOKEN":"stolen","ANTHROPIC_API_KEY":"sk-test"}',
|
|
58
|
+
env,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(result.denied).toEqual(["GITHUB_TOKEN"]);
|
|
62
|
+
expect(result.applied).toEqual(["ANTHROPIC_API_KEY"]);
|
|
63
|
+
expect(env.GITHUB_TOKEN).toBeUndefined();
|
|
64
|
+
expect(env.ANTHROPIC_API_KEY).toBe("sk-test");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("does not register empty values as secrets", () => {
|
|
68
|
+
const env: NodeJS.ProcessEnv = {};
|
|
69
|
+
|
|
70
|
+
const result = applyOverrides({ raw: '{"EMPTY":""}', env });
|
|
71
|
+
|
|
72
|
+
expect(result.applied).toEqual(["EMPTY"]);
|
|
73
|
+
expect(env.EMPTY).toBe("");
|
|
74
|
+
expect(setSecretMock).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("denies every name on the deny list", () => {
|
|
78
|
+
const raw = JSON.stringify(
|
|
79
|
+
Object.fromEntries(Array.from(DENIED_OVERRIDE_NAMES, (name) => [name, "x"])),
|
|
80
|
+
);
|
|
81
|
+
const env: NodeJS.ProcessEnv = {};
|
|
82
|
+
|
|
83
|
+
const result = applyOverrides({ raw, env });
|
|
84
|
+
|
|
85
|
+
expect(result.applied).toEqual([]);
|
|
86
|
+
expect(result.denied.sort()).toEqual(Array.from(DENIED_OVERRIDE_NAMES).sort());
|
|
87
|
+
expect(setSecretMock).not.toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse + apply the action's `unsafe_overrides` input — a JSON object of env
|
|
3
|
+
* var overrides that mutate `process.env` at the start of a run. Designed for
|
|
4
|
+
* e2e testing / debugging from `workflow_dispatch`; only callers with
|
|
5
|
+
* `actions:write` on the repo can supply it.
|
|
6
|
+
*
|
|
7
|
+
* The `unsafe` prefix is load-bearing: GH Actions echoes the value verbatim
|
|
8
|
+
* in the runner's step-header log, so the raw JSON (including any values
|
|
9
|
+
* passed in) is visible to anyone with `actions:read` on the calling repo.
|
|
10
|
+
* Treat the run log as compromised for any value placed in `unsafe_overrides`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as core from "@actions/core";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Names refused even when present in the input. Overriding these would let a
|
|
17
|
+
* caller escape terramend's scope (GITHUB_TOKEN), break runner internals
|
|
18
|
+
* (ACTIONS_RUNTIME_*), forge OIDC tokens (ACTIONS_ID_TOKEN_REQUEST_*), or
|
|
19
|
+
* substitute our server-side auth (TERRAMEND_API_SECRET). Customer-facing
|
|
20
|
+
* provider keys (ANTHROPIC_API_KEY, OPENAI_API_KEY, CLAUDE_CODE_OAUTH_TOKEN,
|
|
21
|
+
* etc.) are intentionally NOT denied — overriding those is the use case.
|
|
22
|
+
*/
|
|
23
|
+
export const DENIED_OVERRIDE_NAMES: ReadonlySet<string> = new Set([
|
|
24
|
+
"GITHUB_TOKEN",
|
|
25
|
+
"GH_TOKEN",
|
|
26
|
+
"ACTIONS_RUNTIME_TOKEN",
|
|
27
|
+
"ACTIONS_RUNTIME_URL",
|
|
28
|
+
"ACTIONS_ID_TOKEN_REQUEST_URL",
|
|
29
|
+
"ACTIONS_ID_TOKEN_REQUEST_TOKEN",
|
|
30
|
+
"ACTIONS_CACHE_URL",
|
|
31
|
+
"TERRAMEND_API_SECRET",
|
|
32
|
+
"VERCEL_AUTOMATION_BYPASS_SECRET",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export interface ApplyOverridesResult {
|
|
36
|
+
applied: string[];
|
|
37
|
+
denied: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Parse the JSON input. Returns `{}` for empty/whitespace. Throws on shape errors. */
|
|
41
|
+
export function parseOverrides(raw: string): Record<string, string> {
|
|
42
|
+
const trimmed = raw.trim();
|
|
43
|
+
if (!trimmed) return {};
|
|
44
|
+
|
|
45
|
+
let parsed: unknown;
|
|
46
|
+
try {
|
|
47
|
+
parsed = JSON.parse(trimmed);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`invalid UNSAFE_OVERRIDES: not valid JSON (${err instanceof Error ? err.message : String(err)})`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
55
|
+
throw new Error(`invalid UNSAFE_OVERRIDES: must be a JSON object`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const out: Record<string, string> = {};
|
|
59
|
+
for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
|
|
60
|
+
if (typeof value !== "string") {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`invalid UNSAFE_OVERRIDES: key "${key}" must have a string value (got ${typeof value})`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
out[key] = value;
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Mutate `params.env` in place with the supplied JSON overrides, skipping any
|
|
72
|
+
* names in `DENIED_OVERRIDE_NAMES`. Each applied value is registered with
|
|
73
|
+
* `core.setSecret` so the runner masks it in subsequent log output, and the
|
|
74
|
+
* raw `UNSAFE_OVERRIDES` env var is deleted so spawned subprocesses don't
|
|
75
|
+
* inherit the original JSON (which would defeat both the deny-list and the
|
|
76
|
+
* masking by exposing the values verbatim).
|
|
77
|
+
*
|
|
78
|
+
* Returns the applied/denied breakdown so the caller can render an audit log.
|
|
79
|
+
*/
|
|
80
|
+
export function applyOverrides(params: {
|
|
81
|
+
raw: string;
|
|
82
|
+
env: NodeJS.ProcessEnv;
|
|
83
|
+
}): ApplyOverridesResult {
|
|
84
|
+
const overrides = parseOverrides(params.raw);
|
|
85
|
+
const applied: string[] = [];
|
|
86
|
+
const denied: string[] = [];
|
|
87
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
88
|
+
if (DENIED_OVERRIDE_NAMES.has(key)) {
|
|
89
|
+
denied.push(key);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (value.length > 0) core.setSecret(value);
|
|
93
|
+
params.env[key] = value;
|
|
94
|
+
applied.push(key);
|
|
95
|
+
}
|
|
96
|
+
delete params.env.UNSAFE_OVERRIDES;
|
|
97
|
+
return { applied, denied };
|
|
98
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { delimiter, join } from "node:path";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
ensurePackageManager,
|
|
5
|
+
type PackageManagerSpec,
|
|
6
|
+
packageManagerBinDir,
|
|
7
|
+
resolvePackageManagerSpec,
|
|
8
|
+
} from "#app/utils/packageManager";
|
|
9
|
+
|
|
10
|
+
vi.mock("node:fs", () => ({
|
|
11
|
+
existsSync: vi.fn(() => true),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("node:fs/promises", () => ({
|
|
15
|
+
mkdir: vi.fn(async () => undefined),
|
|
16
|
+
readFile: vi.fn(async () => "{}"),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock("#app/utils/cli", () => ({
|
|
20
|
+
log: {
|
|
21
|
+
info: vi.fn(),
|
|
22
|
+
debug: vi.fn(),
|
|
23
|
+
warning: vi.fn(),
|
|
24
|
+
error: vi.fn(),
|
|
25
|
+
success: vi.fn(),
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock("#app/utils/subprocess", () => ({
|
|
30
|
+
spawn: vi.fn(),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
import { existsSync } from "node:fs";
|
|
34
|
+
import { readFile } from "node:fs/promises";
|
|
35
|
+
import { log } from "#app/utils/cli";
|
|
36
|
+
import { spawn } from "#app/utils/subprocess";
|
|
37
|
+
|
|
38
|
+
const existsSyncMock = vi.mocked(existsSync);
|
|
39
|
+
const readFileMock = vi.mocked(readFile);
|
|
40
|
+
const spawnMock = vi.mocked(spawn);
|
|
41
|
+
const warningMock = vi.mocked(log.warning);
|
|
42
|
+
|
|
43
|
+
function spawnResult(init: { exitCode?: number; stdout?: string; stderr?: string } = {}) {
|
|
44
|
+
return {
|
|
45
|
+
exitCode: init.exitCode ?? 0,
|
|
46
|
+
stdout: init.stdout ?? "",
|
|
47
|
+
stderr: init.stderr ?? "",
|
|
48
|
+
durationMs: 1,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function mockPackageJson(pkg: unknown): void {
|
|
53
|
+
existsSyncMock.mockReturnValue(true);
|
|
54
|
+
readFileMock.mockResolvedValue(JSON.stringify(pkg));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe("resolvePackageManagerSpec", () => {
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
vi.clearAllMocks();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("returns null when package.json does not exist", async () => {
|
|
63
|
+
existsSyncMock.mockReturnValue(false);
|
|
64
|
+
await expect(resolvePackageManagerSpec("/repo")).resolves.toBeNull();
|
|
65
|
+
expect(existsSyncMock).toHaveBeenCalledWith(join("/repo", "package.json"));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("returns null and warns when package.json is unparseable", async () => {
|
|
69
|
+
existsSyncMock.mockReturnValue(true);
|
|
70
|
+
readFileMock.mockResolvedValue("{nope");
|
|
71
|
+
await expect(resolvePackageManagerSpec("/repo")).resolves.toBeNull();
|
|
72
|
+
expect(warningMock).toHaveBeenCalledWith(expect.stringContaining("failed to parse"));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("returns null when neither field is declared", async () => {
|
|
76
|
+
mockPackageJson({});
|
|
77
|
+
await expect(resolvePackageManagerSpec("/repo")).resolves.toBeNull();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("parses packageManager field and strips the integrity hash", async () => {
|
|
81
|
+
mockPackageJson({ packageManager: "pnpm@11.1.1+sha512.abcdef" });
|
|
82
|
+
await expect(resolvePackageManagerSpec("/repo")).resolves.toEqual({
|
|
83
|
+
name: "pnpm",
|
|
84
|
+
version: "11.1.1",
|
|
85
|
+
concrete: true,
|
|
86
|
+
source: "packageManager",
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("flags range versions as non-concrete", async () => {
|
|
91
|
+
mockPackageJson({ packageManager: "yarn@^4.0.0" });
|
|
92
|
+
await expect(resolvePackageManagerSpec("/repo")).resolves.toEqual({
|
|
93
|
+
name: "yarn",
|
|
94
|
+
version: "^4.0.0",
|
|
95
|
+
concrete: false,
|
|
96
|
+
source: "packageManager",
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("rejects unsupported packageManager names with a warning", async () => {
|
|
101
|
+
mockPackageJson({ packageManager: "lerna@9.0.0" });
|
|
102
|
+
await expect(resolvePackageManagerSpec("/repo")).resolves.toBeNull();
|
|
103
|
+
expect(warningMock).toHaveBeenCalledWith(expect.stringContaining("unknown packageManager"));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("rejects a packageManager value without a name", async () => {
|
|
107
|
+
mockPackageJson({ packageManager: "@11.0.0" });
|
|
108
|
+
await expect(resolvePackageManagerSpec("/repo")).resolves.toBeNull();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("parses devEngines.packageManager", async () => {
|
|
112
|
+
mockPackageJson({ devEngines: { packageManager: { name: "pnpm", version: " 11.0.0 " } } });
|
|
113
|
+
await expect(resolvePackageManagerSpec("/repo")).resolves.toEqual({
|
|
114
|
+
name: "pnpm",
|
|
115
|
+
version: "11.0.0",
|
|
116
|
+
concrete: true,
|
|
117
|
+
source: "devEngines",
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("ignores devEngines entries missing name or version", async () => {
|
|
122
|
+
mockPackageJson({ devEngines: { packageManager: { name: "pnpm" } } });
|
|
123
|
+
await expect(resolvePackageManagerSpec("/repo")).resolves.toBeNull();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("rejects unsupported devEngines names with a warning", async () => {
|
|
127
|
+
mockPackageJson({ devEngines: { packageManager: { name: "vlt", version: "1.0.0" } } });
|
|
128
|
+
await expect(resolvePackageManagerSpec("/repo")).resolves.toBeNull();
|
|
129
|
+
expect(warningMock).toHaveBeenCalledWith(
|
|
130
|
+
expect.stringContaining("unknown devEngines.packageManager.name"),
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("prefers devEngines when the two fields name different managers", async () => {
|
|
135
|
+
mockPackageJson({
|
|
136
|
+
packageManager: "yarn@4.0.0",
|
|
137
|
+
devEngines: { packageManager: { name: "pnpm", version: "^11.0.0" } },
|
|
138
|
+
});
|
|
139
|
+
const spec = await resolvePackageManagerSpec("/repo");
|
|
140
|
+
expect(spec?.name).toBe("pnpm");
|
|
141
|
+
expect(spec?.source).toBe("devEngines");
|
|
142
|
+
expect(warningMock).toHaveBeenCalledWith(expect.stringContaining("disagrees"));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("uses a concrete devEngines version, warning when packageManager disagrees", async () => {
|
|
146
|
+
mockPackageJson({
|
|
147
|
+
packageManager: "pnpm@11.2.0",
|
|
148
|
+
devEngines: { packageManager: { name: "pnpm", version: "11.1.0" } },
|
|
149
|
+
});
|
|
150
|
+
const spec = await resolvePackageManagerSpec("/repo");
|
|
151
|
+
expect(spec?.version).toBe("11.1.0");
|
|
152
|
+
expect(spec?.source).toBe("devEngines");
|
|
153
|
+
expect(warningMock).toHaveBeenCalledWith(expect.stringContaining("disagrees"));
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("keeps a concrete devEngines version without warning when both agree", async () => {
|
|
157
|
+
mockPackageJson({
|
|
158
|
+
packageManager: "pnpm@11.1.0",
|
|
159
|
+
devEngines: { packageManager: { name: "pnpm", version: "11.1.0" } },
|
|
160
|
+
});
|
|
161
|
+
const spec = await resolvePackageManagerSpec("/repo");
|
|
162
|
+
expect(spec?.version).toBe("11.1.0");
|
|
163
|
+
expect(warningMock).not.toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("prefers a concrete packageManager that satisfies the devEngines range", async () => {
|
|
167
|
+
mockPackageJson({
|
|
168
|
+
packageManager: "pnpm@11.2.3",
|
|
169
|
+
devEngines: { packageManager: { name: "pnpm", version: "^11.0.0" } },
|
|
170
|
+
});
|
|
171
|
+
const spec = await resolvePackageManagerSpec("/repo");
|
|
172
|
+
expect(spec).toEqual({
|
|
173
|
+
name: "pnpm",
|
|
174
|
+
version: "11.2.3",
|
|
175
|
+
concrete: true,
|
|
176
|
+
source: "packageManager",
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("falls back to devEngines when packageManager does not satisfy the range", async () => {
|
|
181
|
+
mockPackageJson({
|
|
182
|
+
packageManager: "pnpm@10.0.0",
|
|
183
|
+
devEngines: { packageManager: { name: "pnpm", version: "^11.0.0" } },
|
|
184
|
+
});
|
|
185
|
+
const spec = await resolvePackageManagerSpec("/repo");
|
|
186
|
+
expect(spec?.source).toBe("devEngines");
|
|
187
|
+
expect(spec?.version).toBe("^11.0.0");
|
|
188
|
+
expect(warningMock).toHaveBeenCalledWith(expect.stringContaining("does not satisfy"));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("falls back to devEngines when both are ranges without warning", async () => {
|
|
192
|
+
mockPackageJson({
|
|
193
|
+
packageManager: "pnpm@^11.0.0",
|
|
194
|
+
devEngines: { packageManager: { name: "pnpm", version: "^11.0.0" } },
|
|
195
|
+
});
|
|
196
|
+
const spec = await resolvePackageManagerSpec("/repo");
|
|
197
|
+
expect(spec?.source).toBe("devEngines");
|
|
198
|
+
expect(warningMock).not.toHaveBeenCalled();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("falls back to packageManager when devEngines is absent", async () => {
|
|
202
|
+
mockPackageJson({ packageManager: "npm@10.9.0" });
|
|
203
|
+
const spec = await resolvePackageManagerSpec("/repo");
|
|
204
|
+
expect(spec?.name).toBe("npm");
|
|
205
|
+
expect(spec?.source).toBe("packageManager");
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("packageManagerBinDir", () => {
|
|
210
|
+
it("nests pm-bin under the run tmpdir", () => {
|
|
211
|
+
expect(packageManagerBinDir("/tmp/run")).toBe(join("/tmp/run", "pm-bin"));
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("ensurePackageManager", () => {
|
|
216
|
+
const binDir = join("/tmp/run", "pm-bin");
|
|
217
|
+
let originalPath: string | undefined;
|
|
218
|
+
|
|
219
|
+
function spec(overrides: Partial<PackageManagerSpec> = {}): PackageManagerSpec {
|
|
220
|
+
return {
|
|
221
|
+
name: "pnpm",
|
|
222
|
+
version: "11.1.1",
|
|
223
|
+
concrete: true,
|
|
224
|
+
source: "packageManager",
|
|
225
|
+
...overrides,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
beforeEach(() => {
|
|
230
|
+
vi.clearAllMocks();
|
|
231
|
+
originalPath = process.env.PATH;
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
afterEach(() => {
|
|
235
|
+
process.env.PATH = originalPath;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("returns true for npm without spawning anything", async () => {
|
|
239
|
+
await expect(ensurePackageManager({ spec: spec({ name: "npm" }), binDir })).resolves.toBe(true);
|
|
240
|
+
expect(spawnMock).not.toHaveBeenCalled();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("returns false for managers corepack does not ship (bun)", async () => {
|
|
244
|
+
await expect(ensurePackageManager({ spec: spec({ name: "bun" }), binDir })).resolves.toBe(
|
|
245
|
+
false,
|
|
246
|
+
);
|
|
247
|
+
expect(spawnMock).not.toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("returns false for range versions with a warning", async () => {
|
|
251
|
+
const result = await ensurePackageManager({
|
|
252
|
+
spec: spec({ version: "^11.0.0", concrete: false }),
|
|
253
|
+
binDir,
|
|
254
|
+
});
|
|
255
|
+
expect(result).toBe(false);
|
|
256
|
+
expect(warningMock).toHaveBeenCalledWith(expect.stringContaining("requires a concrete pin"));
|
|
257
|
+
expect(spawnMock).not.toHaveBeenCalled();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("short-circuits when the requested version is already active", async () => {
|
|
261
|
+
spawnMock.mockResolvedValueOnce(spawnResult({ stdout: "11.1.1\n" }));
|
|
262
|
+
await expect(ensurePackageManager({ spec: spec(), binDir })).resolves.toBe(true);
|
|
263
|
+
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
264
|
+
expect(spawnMock).toHaveBeenCalledWith(
|
|
265
|
+
expect.objectContaining({ cmd: "pnpm", args: ["--version"] }),
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("returns false when corepack enable fails", async () => {
|
|
270
|
+
spawnMock
|
|
271
|
+
.mockResolvedValueOnce(spawnResult({ exitCode: 1 })) // pnpm --version
|
|
272
|
+
.mockResolvedValueOnce(spawnResult({ exitCode: 1, stderr: "enable broke" })); // enable
|
|
273
|
+
await expect(ensurePackageManager({ spec: spec(), binDir })).resolves.toBe(false);
|
|
274
|
+
expect(warningMock).toHaveBeenCalledWith(expect.stringContaining("corepack enable failed"));
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("returns false when corepack prepare fails, after prepending binDir to PATH", async () => {
|
|
278
|
+
process.env.PATH = "/usr/bin";
|
|
279
|
+
spawnMock
|
|
280
|
+
.mockResolvedValueOnce(spawnResult({ stdout: "10.0.0\n" })) // wrong version active
|
|
281
|
+
.mockResolvedValueOnce(spawnResult()) // enable ok
|
|
282
|
+
.mockResolvedValueOnce(spawnResult({ exitCode: 1, stderr: "" })); // prepare fails
|
|
283
|
+
await expect(ensurePackageManager({ spec: spec(), binDir })).resolves.toBe(false);
|
|
284
|
+
expect(process.env.PATH).toBe(`${binDir}${delimiter}/usr/bin`);
|
|
285
|
+
expect(warningMock).toHaveBeenCalledWith(expect.stringContaining("(empty)"));
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("returns true on full corepack success and verifies the resolved version", async () => {
|
|
289
|
+
spawnMock
|
|
290
|
+
.mockResolvedValueOnce(spawnResult({ exitCode: 1 })) // not on PATH yet
|
|
291
|
+
.mockImplementationOnce(async (call) => {
|
|
292
|
+
// exercise the corepack stream-forwarding callbacks
|
|
293
|
+
call.onStdout?.("");
|
|
294
|
+
call.onStderr?.("");
|
|
295
|
+
return spawnResult(); // enable ok
|
|
296
|
+
})
|
|
297
|
+
.mockResolvedValueOnce(spawnResult()) // prepare ok
|
|
298
|
+
.mockResolvedValueOnce(spawnResult({ stdout: "11.1.1\n" })); // verify
|
|
299
|
+
await expect(ensurePackageManager({ spec: spec(), binDir })).resolves.toBe(true);
|
|
300
|
+
expect(warningMock).not.toHaveBeenCalled();
|
|
301
|
+
expect(spawnMock).toHaveBeenCalledWith(
|
|
302
|
+
expect.objectContaining({
|
|
303
|
+
cmd: "corepack",
|
|
304
|
+
args: ["enable", "--install-directory", binDir, "pnpm"],
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
307
|
+
expect(spawnMock).toHaveBeenCalledWith(
|
|
308
|
+
expect.objectContaining({ cmd: "corepack", args: ["prepare", "pnpm@11.1.1", "--activate"] }),
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("returns true but warns when PATH still resolves to another version", async () => {
|
|
313
|
+
spawnMock
|
|
314
|
+
.mockResolvedValueOnce(spawnResult({ exitCode: 1 })) // not on PATH yet
|
|
315
|
+
.mockResolvedValueOnce(spawnResult()) // enable ok
|
|
316
|
+
.mockResolvedValueOnce(spawnResult()) // prepare ok
|
|
317
|
+
.mockResolvedValueOnce(spawnResult({ exitCode: 1 })); // verify fails → null
|
|
318
|
+
await expect(ensurePackageManager({ spec: spec(), binDir })).resolves.toBe(true);
|
|
319
|
+
expect(warningMock).toHaveBeenCalledWith(expect.stringContaining("(missing)"));
|
|
320
|
+
});
|
|
321
|
+
});
|