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,492 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { chmodSync, createWriteStream, existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { mkdtemp } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { pipeline } from "node:stream/promises";
|
|
7
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
8
|
+
import { log } from "#app/utils/cli";
|
|
9
|
+
|
|
10
|
+
export interface InstallFromNpmTarballParams {
|
|
11
|
+
packageName: string;
|
|
12
|
+
version: string;
|
|
13
|
+
executablePath: string;
|
|
14
|
+
installDependencies?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface InstallFromCurlParams {
|
|
18
|
+
installUrl: string;
|
|
19
|
+
executableName: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface InstallFromDirectTarballParams {
|
|
23
|
+
url: string;
|
|
24
|
+
executablePath: string;
|
|
25
|
+
stripComponents?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface InstallFromGithubParams {
|
|
29
|
+
owner: string;
|
|
30
|
+
repo: string;
|
|
31
|
+
tag?: string;
|
|
32
|
+
assetName?: string;
|
|
33
|
+
executablePath?: string;
|
|
34
|
+
githubInstallationToken?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface InstallFromGithubTarballParams {
|
|
38
|
+
owner: string;
|
|
39
|
+
repo: string;
|
|
40
|
+
tag?: string;
|
|
41
|
+
assetNamePattern: string;
|
|
42
|
+
executablePath: string;
|
|
43
|
+
githubInstallationToken?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface NpmRegistryData {
|
|
47
|
+
"dist-tags": { latest: string };
|
|
48
|
+
versions: Record<string, unknown>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Install a CLI tool from an npm package tarball
|
|
53
|
+
* Downloads the tarball, extracts it to a temp directory, and returns the path to the CLI executable
|
|
54
|
+
* The temp directory will be cleaned up by the OS automatically
|
|
55
|
+
*/
|
|
56
|
+
export async function installFromNpmTarball(params: InstallFromNpmTarballParams): Promise<string> {
|
|
57
|
+
const tempDir = process.env.TERRAMEND_TEMP_DIR;
|
|
58
|
+
if (!tempDir) throw new Error("TERRAMEND_TEMP_DIR is not set");
|
|
59
|
+
|
|
60
|
+
const extractedDir = join(tempDir, "package");
|
|
61
|
+
const cliPath = join(extractedDir, params.executablePath);
|
|
62
|
+
|
|
63
|
+
if (existsSync(cliPath)) {
|
|
64
|
+
log.debug(`» using cached binary at ${cliPath}`);
|
|
65
|
+
return cliPath;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Resolve version if it's a range or "latest"
|
|
69
|
+
let resolvedVersion = params.version;
|
|
70
|
+
if (
|
|
71
|
+
params.version.startsWith("^") ||
|
|
72
|
+
params.version.startsWith("~") ||
|
|
73
|
+
params.version === "latest"
|
|
74
|
+
) {
|
|
75
|
+
const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
|
|
76
|
+
log.debug(`» resolving version for ${params.version}...`);
|
|
77
|
+
try {
|
|
78
|
+
const registryResponse = await fetch(`${npmRegistry}/${params.packageName}`);
|
|
79
|
+
if (!registryResponse.ok) {
|
|
80
|
+
throw new Error(`Failed to query registry: ${registryResponse.status}`);
|
|
81
|
+
}
|
|
82
|
+
const registryData = (await registryResponse.json()) as NpmRegistryData;
|
|
83
|
+
resolvedVersion = registryData["dist-tags"].latest;
|
|
84
|
+
log.debug(`» resolved to version ${resolvedVersion}`);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
log.warning(
|
|
87
|
+
`Failed to resolve version from registry: ${error instanceof Error ? error.message : String(error)}`,
|
|
88
|
+
);
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
log.debug(`» installing ${params.packageName}@${resolvedVersion}...`);
|
|
94
|
+
|
|
95
|
+
const tarballPath = join(tempDir, "package.tgz");
|
|
96
|
+
|
|
97
|
+
// Download tarball from npm
|
|
98
|
+
const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
|
|
99
|
+
// Handle scoped packages (e.g., @scope/package -> @scope%2Fpackage/-/package-version.tgz)
|
|
100
|
+
let tarballUrl: string;
|
|
101
|
+
if (params.packageName.startsWith("@")) {
|
|
102
|
+
const [scope, name] = params.packageName.slice(1).split("/");
|
|
103
|
+
const scopedPackageName = `@${scope}%2F${name}`;
|
|
104
|
+
tarballUrl = `${npmRegistry}/${scopedPackageName}/-/${name}-${resolvedVersion}.tgz`;
|
|
105
|
+
} else {
|
|
106
|
+
tarballUrl = `${npmRegistry}/${params.packageName}/-/${params.packageName}-${resolvedVersion}.tgz`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
log.debug(`» downloading from ${tarballUrl}...`);
|
|
110
|
+
const response = await fetch(tarballUrl);
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
throw new Error(`Failed to download tarball: ${response.status} ${response.statusText}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Write tarball to file
|
|
116
|
+
if (!response.body) throw new Error("Response body is null");
|
|
117
|
+
const fileStream = createWriteStream(tarballPath);
|
|
118
|
+
await pipeline(response.body, fileStream);
|
|
119
|
+
log.debug(`» downloaded tarball to ${tarballPath}`);
|
|
120
|
+
|
|
121
|
+
// Extract tarball
|
|
122
|
+
log.debug(`» extracting tarball...`);
|
|
123
|
+
const extractResult = spawnSync("tar", ["-xzf", tarballPath, "-C", tempDir], {
|
|
124
|
+
stdio: "pipe",
|
|
125
|
+
encoding: "utf-8",
|
|
126
|
+
});
|
|
127
|
+
if (extractResult.status !== 0) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Failed to extract tarball: ${extractResult.stderr || extractResult.stdout || "Unknown error"}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!existsSync(cliPath)) {
|
|
134
|
+
throw new Error(`Executable not found in extracted package at ${cliPath}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Install dependencies if requested
|
|
138
|
+
if (params.installDependencies) {
|
|
139
|
+
log.debug(`» installing dependencies for ${params.packageName}...`);
|
|
140
|
+
const installResult = spawnSync("npm", ["install", "--production"], {
|
|
141
|
+
cwd: extractedDir,
|
|
142
|
+
stdio: "pipe",
|
|
143
|
+
encoding: "utf-8",
|
|
144
|
+
// @anthropic-ai/claude-code (2.1.113+) guards its `prepare` script with an
|
|
145
|
+
// `AUTHORIZED` env check that hard-fails any non-release install. we need
|
|
146
|
+
// its `postinstall` (which copies the native binary from the platform
|
|
147
|
+
// optionalDependency over the bin/claude.exe stub) to run, so we set the
|
|
148
|
+
// flag to no-op the guard. harmless for packages without such a guard.
|
|
149
|
+
env: { ...process.env, AUTHORIZED: "1" },
|
|
150
|
+
});
|
|
151
|
+
if (installResult.status !== 0) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Failed to install dependencies: ${installResult.stderr || installResult.stdout || "Unknown error"}`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
log.debug(`» dependencies installed`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Make the file executable
|
|
160
|
+
chmodSync(cliPath, 0o755);
|
|
161
|
+
|
|
162
|
+
log.debug(`» ${params.packageName} installed at ${cliPath}`);
|
|
163
|
+
|
|
164
|
+
return cliPath;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Fetch with retry logic if Retry-After header is present
|
|
169
|
+
*/
|
|
170
|
+
async function fetchWithRetry(
|
|
171
|
+
url: string,
|
|
172
|
+
headers: Record<string, string>,
|
|
173
|
+
errorMessage: string,
|
|
174
|
+
): Promise<Response> {
|
|
175
|
+
const response = await fetch(url, { headers });
|
|
176
|
+
if (!response.ok) {
|
|
177
|
+
const retryAfter = response.headers.get("Retry-After") || response.headers.get("retry-after");
|
|
178
|
+
if (retryAfter) {
|
|
179
|
+
const waitSeconds = parseInt(retryAfter, 10);
|
|
180
|
+
if (!Number.isNaN(waitSeconds) && waitSeconds > 0) {
|
|
181
|
+
log.info(`» rate limited, waiting ${waitSeconds} seconds before retry...`);
|
|
182
|
+
await sleep(waitSeconds * 1000);
|
|
183
|
+
const retryResponse = await fetch(url, { headers });
|
|
184
|
+
if (!retryResponse.ok) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`${errorMessage}: ${retryResponse.status} ${retryResponse.statusText} (retry failed)`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return retryResponse;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
throw new Error(`${errorMessage}: ${response.status} ${response.statusText}`);
|
|
193
|
+
}
|
|
194
|
+
return response;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Install a CLI tool from GitHub releases
|
|
199
|
+
* Downloads the latest release asset from GitHub and returns the path to the executable
|
|
200
|
+
* The temp directory will be cleaned up by the OS automatically
|
|
201
|
+
*/
|
|
202
|
+
export async function installFromGithub(params: InstallFromGithubParams): Promise<string> {
|
|
203
|
+
// use a deterministic subdir in TERRAMEND_TEMP_DIR so repeated calls are cached
|
|
204
|
+
const terramendTemp = process.env.TERRAMEND_TEMP_DIR;
|
|
205
|
+
const installDir = terramendTemp
|
|
206
|
+
? join(terramendTemp, `github-${params.owner}-${params.repo}`)
|
|
207
|
+
: await mkdtemp(join(tmpdir(), `${params.owner}-${params.repo}-github-`));
|
|
208
|
+
|
|
209
|
+
const expectedCliPath = join(installDir, params.executablePath ?? params.assetName ?? "asset");
|
|
210
|
+
|
|
211
|
+
if (existsSync(expectedCliPath)) {
|
|
212
|
+
log.debug(`» using cached binary at ${expectedCliPath}`);
|
|
213
|
+
return expectedCliPath;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
log.info(`» installing ${params.owner}/${params.repo} from GitHub releases...`);
|
|
217
|
+
|
|
218
|
+
// fetch release from GitHub API (pinned tag or latest)
|
|
219
|
+
const releaseUrl = params.tag
|
|
220
|
+
? `https://api.github.com/repos/${params.owner}/${params.repo}/releases/tags/${params.tag}`
|
|
221
|
+
: `https://api.github.com/repos/${params.owner}/${params.repo}/releases/latest`;
|
|
222
|
+
log.debug(`» fetching release from ${releaseUrl}...`);
|
|
223
|
+
|
|
224
|
+
const headers: Record<string, string> = {};
|
|
225
|
+
if (params.githubInstallationToken) {
|
|
226
|
+
headers.Authorization = `Bearer ${params.githubInstallationToken}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const releaseResponse = await fetchWithRetry(releaseUrl, headers, "Failed to fetch release");
|
|
230
|
+
|
|
231
|
+
const releaseData = (await releaseResponse.json()) as {
|
|
232
|
+
tag_name: string;
|
|
233
|
+
assets: Array<{
|
|
234
|
+
name: string;
|
|
235
|
+
browser_download_url: string;
|
|
236
|
+
}>;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
log.debug(`» found release ${releaseData.tag_name}`);
|
|
240
|
+
|
|
241
|
+
const asset = releaseData.assets.find((a) => a.name === params.assetName);
|
|
242
|
+
if (!asset) {
|
|
243
|
+
throw new Error(`Asset '${params.assetName}' not found in release ${releaseData.tag_name}`);
|
|
244
|
+
}
|
|
245
|
+
const assetUrl = asset.browser_download_url;
|
|
246
|
+
|
|
247
|
+
log.debug(`» downloading asset from ${assetUrl}...`);
|
|
248
|
+
|
|
249
|
+
mkdirSync(installDir, { recursive: true });
|
|
250
|
+
|
|
251
|
+
// determine file extension and download path
|
|
252
|
+
const urlPath = new URL(assetUrl).pathname;
|
|
253
|
+
const fileName = urlPath.split("/").pop() || "asset";
|
|
254
|
+
const downloadPath = join(installDir, fileName);
|
|
255
|
+
|
|
256
|
+
// download the asset
|
|
257
|
+
const assetResponse = await fetchWithRetry(assetUrl, headers, "Failed to download asset");
|
|
258
|
+
|
|
259
|
+
if (!assetResponse.body) throw new Error("Response body is null");
|
|
260
|
+
const fileStream = createWriteStream(downloadPath);
|
|
261
|
+
await pipeline(assetResponse.body, fileStream);
|
|
262
|
+
log.debug(`» downloaded asset to ${downloadPath}`);
|
|
263
|
+
|
|
264
|
+
// determine the executable path
|
|
265
|
+
const cliPath = params.executablePath ? join(installDir, params.executablePath) : downloadPath;
|
|
266
|
+
|
|
267
|
+
if (!existsSync(cliPath)) {
|
|
268
|
+
throw new Error(`Executable not found at ${cliPath}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
chmodSync(cliPath, 0o755);
|
|
272
|
+
log.info(`» installed from GitHub release at ${cliPath}`);
|
|
273
|
+
|
|
274
|
+
return cliPath;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Install a CLI tool from a GitHub release tarball
|
|
279
|
+
* Downloads the tar.gz from GitHub releases, extracts it, and returns the path to the CLI executable
|
|
280
|
+
* The temp directory will be cleaned up by the OS automatically
|
|
281
|
+
*/
|
|
282
|
+
export async function installFromGithubTarball(
|
|
283
|
+
params: InstallFromGithubTarballParams,
|
|
284
|
+
): Promise<string> {
|
|
285
|
+
const tempDir = process.env.TERRAMEND_TEMP_DIR;
|
|
286
|
+
if (!tempDir) throw new Error("TERRAMEND_TEMP_DIR is not set");
|
|
287
|
+
|
|
288
|
+
const cliPath = join(tempDir, params.executablePath);
|
|
289
|
+
|
|
290
|
+
if (existsSync(cliPath)) {
|
|
291
|
+
log.debug(`» using cached binary at ${cliPath}`);
|
|
292
|
+
return cliPath;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
log.info(`» installing ${params.owner}/${params.repo} from GitHub releases...`);
|
|
296
|
+
|
|
297
|
+
// determine platform-specific asset name
|
|
298
|
+
const os = process.platform === "darwin" ? "darwin" : "linux";
|
|
299
|
+
const arch = process.arch === "arm64" ? "arm64" : "x64";
|
|
300
|
+
const assetName = params.assetNamePattern.replace("{os}", os).replace("{arch}", arch);
|
|
301
|
+
|
|
302
|
+
// fetch release from GitHub API (pinned tag or latest)
|
|
303
|
+
const releaseUrl = params.tag
|
|
304
|
+
? `https://api.github.com/repos/${params.owner}/${params.repo}/releases/tags/${params.tag}`
|
|
305
|
+
: `https://api.github.com/repos/${params.owner}/${params.repo}/releases/latest`;
|
|
306
|
+
log.info(`» fetching release from ${releaseUrl}...`);
|
|
307
|
+
|
|
308
|
+
const headers: Record<string, string> = {};
|
|
309
|
+
if (params.githubInstallationToken) {
|
|
310
|
+
headers.Authorization = `Bearer ${params.githubInstallationToken}`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const releaseResponse = await fetchWithRetry(releaseUrl, headers, "Failed to fetch release");
|
|
314
|
+
|
|
315
|
+
const releaseData = (await releaseResponse.json()) as {
|
|
316
|
+
tag_name: string;
|
|
317
|
+
assets: Array<{
|
|
318
|
+
name: string;
|
|
319
|
+
browser_download_url: string;
|
|
320
|
+
}>;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
log.debug(`» found release: ${releaseData.tag_name}`);
|
|
324
|
+
|
|
325
|
+
const asset = releaseData.assets.find((a) => a.name === assetName);
|
|
326
|
+
if (!asset) {
|
|
327
|
+
throw new Error(`Asset '${assetName}' not found in release ${releaseData.tag_name}`);
|
|
328
|
+
}
|
|
329
|
+
const assetUrl = asset.browser_download_url;
|
|
330
|
+
|
|
331
|
+
log.debug(`» downloading asset from ${assetUrl}...`);
|
|
332
|
+
|
|
333
|
+
const tarballPath = join(tempDir, assetName);
|
|
334
|
+
|
|
335
|
+
// download the asset
|
|
336
|
+
const assetResponse = await fetchWithRetry(assetUrl, headers, "Failed to download asset");
|
|
337
|
+
|
|
338
|
+
if (!assetResponse.body) throw new Error("Response body is null");
|
|
339
|
+
const fileStream = createWriteStream(tarballPath);
|
|
340
|
+
await pipeline(assetResponse.body, fileStream);
|
|
341
|
+
log.debug(`» downloaded tarball to ${tarballPath}`);
|
|
342
|
+
|
|
343
|
+
// extract tar.gz
|
|
344
|
+
log.debug(`» extracting tarball...`);
|
|
345
|
+
const extractResult = spawnSync("tar", ["-xzf", tarballPath, "-C", tempDir], {
|
|
346
|
+
stdio: "pipe",
|
|
347
|
+
encoding: "utf-8",
|
|
348
|
+
});
|
|
349
|
+
if (extractResult.status !== 0) {
|
|
350
|
+
throw new Error(
|
|
351
|
+
`Failed to extract tarball: ${extractResult.stderr || extractResult.stdout || "Unknown error"}`,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!existsSync(cliPath)) {
|
|
356
|
+
throw new Error(`Executable not found in extracted tarball at ${cliPath}`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// make the file executable
|
|
360
|
+
chmodSync(cliPath, 0o755);
|
|
361
|
+
|
|
362
|
+
log.info(`» ${params.owner}/${params.repo} installed at ${cliPath}`);
|
|
363
|
+
|
|
364
|
+
return cliPath;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Install a CLI tool from a direct tarball URL.
|
|
369
|
+
* Downloads the tarball, extracts it to a temp directory, and returns the path to the CLI executable.
|
|
370
|
+
*/
|
|
371
|
+
export async function installFromDirectTarball(
|
|
372
|
+
params: InstallFromDirectTarballParams,
|
|
373
|
+
): Promise<string> {
|
|
374
|
+
const tempDir = process.env.TERRAMEND_TEMP_DIR;
|
|
375
|
+
if (!tempDir) throw new Error("TERRAMEND_TEMP_DIR is not set");
|
|
376
|
+
|
|
377
|
+
const extractDir = join(tempDir, "direct-package");
|
|
378
|
+
const cliPath = join(extractDir, params.executablePath);
|
|
379
|
+
|
|
380
|
+
if (existsSync(cliPath)) {
|
|
381
|
+
log.debug(`» using cached binary at ${cliPath}`);
|
|
382
|
+
return cliPath;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
log.info(`» downloading tarball from ${params.url}...`);
|
|
386
|
+
|
|
387
|
+
const tarballPath = join(tempDir, "direct-package.tgz");
|
|
388
|
+
|
|
389
|
+
const response = await fetchWithRetry(params.url, {}, "failed to download tarball");
|
|
390
|
+
if (!response.body) throw new Error("response body is null");
|
|
391
|
+
|
|
392
|
+
const fileStream = createWriteStream(tarballPath);
|
|
393
|
+
await pipeline(response.body, fileStream);
|
|
394
|
+
log.debug(`» downloaded tarball to ${tarballPath}`);
|
|
395
|
+
|
|
396
|
+
mkdirSync(extractDir, { recursive: true });
|
|
397
|
+
|
|
398
|
+
const tarArgs = ["-xzf", tarballPath, "-C", extractDir];
|
|
399
|
+
if (params.stripComponents !== undefined && params.stripComponents > 0) {
|
|
400
|
+
tarArgs.push(`--strip-components=${Math.floor(params.stripComponents)}`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
log.debug(`» extracting tarball...`);
|
|
404
|
+
const extractResult = spawnSync("tar", tarArgs, {
|
|
405
|
+
stdio: "pipe",
|
|
406
|
+
encoding: "utf-8",
|
|
407
|
+
});
|
|
408
|
+
if (extractResult.status !== 0) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
`failed to extract tarball: ${extractResult.stderr || extractResult.stdout || "unknown error"}`,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (!existsSync(cliPath)) {
|
|
415
|
+
throw new Error(`executable not found in extracted tarball at ${cliPath}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
chmodSync(cliPath, 0o755);
|
|
419
|
+
log.info(`» installed at ${cliPath}`);
|
|
420
|
+
|
|
421
|
+
return cliPath;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Install a CLI tool from a curl-based install script
|
|
426
|
+
* Downloads the install script, runs it with HOME set to temp directory, and returns the path to the CLI executable
|
|
427
|
+
* The temp directory will be cleaned up by the OS automatically
|
|
428
|
+
*/
|
|
429
|
+
export async function installFromCurl(params: InstallFromCurlParams): Promise<string> {
|
|
430
|
+
const tempDir = process.env.TERRAMEND_TEMP_DIR;
|
|
431
|
+
if (!tempDir) throw new Error("TERRAMEND_TEMP_DIR is not set");
|
|
432
|
+
|
|
433
|
+
const cliPath = join(tempDir, ".local", "bin", params.executableName);
|
|
434
|
+
|
|
435
|
+
if (existsSync(cliPath)) {
|
|
436
|
+
log.debug(`» using cached binary at ${cliPath}`);
|
|
437
|
+
return cliPath;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
log.info(`» installing ${params.executableName}...`);
|
|
441
|
+
|
|
442
|
+
const installScriptPath = join(tempDir, "install.sh");
|
|
443
|
+
|
|
444
|
+
// Download the install script
|
|
445
|
+
log.debug(`» downloading install script from ${params.installUrl}...`);
|
|
446
|
+
const installScriptResponse = await fetch(params.installUrl);
|
|
447
|
+
if (!installScriptResponse.ok) {
|
|
448
|
+
throw new Error(`Failed to download install script: ${installScriptResponse.status}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (!installScriptResponse.body) throw new Error("Response body is null");
|
|
452
|
+
const fileStream = createWriteStream(installScriptPath);
|
|
453
|
+
await pipeline(installScriptResponse.body, fileStream);
|
|
454
|
+
log.debug(`» downloaded install script to ${installScriptPath}`);
|
|
455
|
+
|
|
456
|
+
// Make install script executable
|
|
457
|
+
chmodSync(installScriptPath, 0o755);
|
|
458
|
+
|
|
459
|
+
log.debug(`» installing to temp directory at ${tempDir}...`);
|
|
460
|
+
|
|
461
|
+
const installResult = spawnSync("bash", [installScriptPath], {
|
|
462
|
+
cwd: tempDir,
|
|
463
|
+
env: {
|
|
464
|
+
// Run the install script with HOME set to temp directory
|
|
465
|
+
// ensuring a fresh install for each run
|
|
466
|
+
HOME: tempDir,
|
|
467
|
+
// XDG_CONFIG_HOME must match HOME so CLI tools find config in the right place
|
|
468
|
+
XDG_CONFIG_HOME: join(tempDir, ".config"),
|
|
469
|
+
SHELL: process.env.SHELL,
|
|
470
|
+
USER: process.env.USER,
|
|
471
|
+
},
|
|
472
|
+
stdio: "pipe",
|
|
473
|
+
encoding: "utf-8",
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
if (installResult.status !== 0) {
|
|
477
|
+
const errorOutput = installResult.stderr || installResult.stdout || "No output";
|
|
478
|
+
throw new Error(
|
|
479
|
+
`Failed to install ${params.executableName}. Install script exited with code ${installResult.status}. Output: ${errorOutput}`,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (!existsSync(cliPath)) {
|
|
484
|
+
throw new Error(`Executable not found at ${cliPath}`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Ensure binary is executable
|
|
488
|
+
chmodSync(cliPath, 0o755);
|
|
489
|
+
log.info(`» ${params.executableName} installed at ${cliPath}`);
|
|
490
|
+
|
|
491
|
+
return cliPath;
|
|
492
|
+
}
|