From 9e83add65b88e7e6d0781a0dd7feaf9ba6675c47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Sat, 25 Apr 2026 19:16:12 +0200 Subject: [PATCH 1/3] fix(ripgrep): time out binary download --- packages/opencode/src/file/ripgrep.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index e31f5373345..bd96eba5551 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -16,6 +16,7 @@ import { withStatics } from "@/util/schema" const log = Log.create({ service: "ripgrep" }) const VERSION = "15.1.0" +const DOWNLOAD_TIMEOUT_MS = 30_000 const PLATFORM = { "arm64-darwin": { platform: "aarch64-apple-darwin", extension: "tar.gz" }, "arm64-linux": { platform: "aarch64-unknown-linux-gnu", extension: "tar.gz" }, @@ -172,6 +173,12 @@ function error(stderr: string, code: number) { return err } +function downloadError(url: string) { + return new Error( + `Timed out downloading ripgrep from ${url}. Install ripgrep with your system package manager and restart opencode, or retry when GitHub releases are reachable.`, + ) +} + function clean(file: string) { return path.normalize(file.replace(/^\.[\\/]/, "")) } @@ -310,6 +317,7 @@ export const layer: Layer.Layer response.arrayBuffer), + Effect.timeoutOrElse({ duration: DOWNLOAD_TIMEOUT_MS, orElse: () => Effect.fail(downloadError(url)) }), Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))), ) if (bytes.byteLength === 0) { From 7106ef69d93dddfa26fe5a97b1089356055fba06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Sat, 25 Apr 2026 19:40:22 +0200 Subject: [PATCH 2/3] fix(ripgrep): report download failures clearly --- packages/opencode/src/file/ripgrep.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index bd96eba5551..58f4d71a042 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -173,12 +173,18 @@ function error(stderr: string, code: number) { return err } -function downloadError(url: string) { +function downloadError(url: string, prefix: "Failed" | "Timed out", cause?: unknown) { return new Error( - `Timed out downloading ripgrep from ${url}. Install ripgrep with your system package manager and restart opencode, or retry when GitHub releases are reachable.`, + `${prefix} downloading ripgrep from ${url}. Install ripgrep with your system package manager and restart opencode, or retry when GitHub releases are reachable.`, + { cause }, ) } +function normalizeDownloadError(url: string, cause: unknown) { + if (cause instanceof Error && cause.message.includes("downloading ripgrep")) return cause + return downloadError(url, "Failed", cause) +} + function clean(file: string) { return path.normalize(file.replace(/^\.[\\/]/, "")) } @@ -317,11 +323,14 @@ export const layer: Layer.Layer response.arrayBuffer), - Effect.timeoutOrElse({ duration: DOWNLOAD_TIMEOUT_MS, orElse: () => Effect.fail(downloadError(url)) }), - Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))), + Effect.timeoutOrElse({ + duration: DOWNLOAD_TIMEOUT_MS, + orElse: () => Effect.fail(downloadError(url, "Timed out")), + }), + Effect.mapError((cause) => normalizeDownloadError(url, cause)), ) if (bytes.byteLength === 0) { - return yield* Effect.fail(new Error(`failed to download ripgrep from ${url}`)) + return yield* Effect.fail(downloadError(url, "Failed")) } yield* fs.writeWithDirs(archive, new Uint8Array(bytes)) From 2a2cc19f84118afae6df1405a8355d62cdf5c969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Sun, 26 Apr 2026 11:58:00 +0200 Subject: [PATCH 3/3] fix(ripgrep): retry after download failure and allow timeout override --- packages/opencode/src/file/ripgrep.ts | 88 ++++++++++++++++----------- 1 file changed, 51 insertions(+), 37 deletions(-) diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index 2127025d201..ffe94c1d7cb 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -16,7 +16,15 @@ import { withStatics } from "@/util/schema" const log = Log.create({ service: "ripgrep" }) const VERSION = "15.1.0" -const DOWNLOAD_TIMEOUT_MS = 30_000 +const DEFAULT_DOWNLOAD_TIMEOUT_MS = 30_000 + +function downloadTimeoutMs() { + const raw = process.env["OPENCODE_RIPGREP_DOWNLOAD_TIMEOUT_MS"] + if (!raw) return DEFAULT_DOWNLOAD_TIMEOUT_MS + const parsed = Number(raw) + if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_DOWNLOAD_TIMEOUT_MS + return parsed +} const PLATFORM = { "arm64-darwin": { platform: "aarch64-apple-darwin", extension: "tar.gz" }, "arm64-linux": { platform: "aarch64-unknown-linux-gnu", extension: "tar.gz" }, @@ -299,46 +307,52 @@ export const layer: Layer.Layer which(process.platform === "win32" ? "rg.exe" : "rg")) - if (system && (yield* fs.isFile(system).pipe(Effect.orDie))) return system + const downloadBinary = Effect.gen(function* () { + const system = yield* Effect.sync(() => which(process.platform === "win32" ? "rg.exe" : "rg")) + if (system && (yield* fs.isFile(system).pipe(Effect.orDie))) return system - const target = path.join(Global.Path.bin, `rg${process.platform === "win32" ? ".exe" : ""}`) - if (yield* fs.isFile(target).pipe(Effect.orDie)) return target + const target = path.join(Global.Path.bin, `rg${process.platform === "win32" ? ".exe" : ""}`) + if (yield* fs.isFile(target).pipe(Effect.orDie)) return target - const platformKey = `${process.arch}-${process.platform}` as keyof typeof PLATFORM - const config = PLATFORM[platformKey] - if (!config) { - return yield* Effect.fail(new Error(`unsupported platform for ripgrep: ${platformKey}`)) - } + const platformKey = `${process.arch}-${process.platform}` as keyof typeof PLATFORM + const config = PLATFORM[platformKey] + if (!config) { + return yield* Effect.fail(new Error(`unsupported platform for ripgrep: ${platformKey}`)) + } - const filename = `ripgrep-${VERSION}-${config.platform}.${config.extension}` - const url = `https://github.com/BurntSushi/ripgrep/releases/download/${VERSION}/${filename}` - const archive = path.join(Global.Path.bin, filename) - - log.info("downloading ripgrep", { url }) - yield* fs.ensureDir(Global.Path.bin).pipe(Effect.orDie) - - const bytes = yield* HttpClientRequest.get(url).pipe( - http.execute, - Effect.flatMap((response) => response.arrayBuffer), - Effect.timeoutOrElse({ - duration: DOWNLOAD_TIMEOUT_MS, - orElse: () => Effect.fail(downloadError(url, "Timed out")), - }), - Effect.mapError((cause) => normalizeDownloadError(url, cause)), - ) - if (bytes.byteLength === 0) { - return yield* Effect.fail(downloadError(url, "Failed")) - } + const filename = `ripgrep-${VERSION}-${config.platform}.${config.extension}` + const url = `https://github.com/BurntSushi/ripgrep/releases/download/${VERSION}/${filename}` + const archive = path.join(Global.Path.bin, filename) + + log.info("downloading ripgrep", { url }) + yield* fs.ensureDir(Global.Path.bin).pipe(Effect.orDie) - yield* fs.writeWithDirs(archive, new Uint8Array(bytes)) - yield* extract(archive, config, target) - yield* fs.remove(archive, { force: true }).pipe(Effect.ignore) - return target - }), - ) + const bytes = yield* HttpClientRequest.get(url).pipe( + http.execute, + Effect.flatMap((response) => response.arrayBuffer), + Effect.timeoutOrElse({ + duration: downloadTimeoutMs(), + orElse: () => Effect.fail(downloadError(url, "Timed out")), + }), + Effect.mapError((cause) => normalizeDownloadError(url, cause)), + ) + if (bytes.byteLength === 0) { + return yield* Effect.fail(downloadError(url, "Failed")) + } + + yield* fs.writeWithDirs(archive, new Uint8Array(bytes)) + yield* extract(archive, config, target) + yield* fs.remove(archive, { force: true }).pipe(Effect.ignore) + return target + }) + + let cached = yield* Effect.cached(downloadBinary) + const filepath = Effect.gen(function* () { + const result = yield* Effect.exit(cached) + if (result._tag === "Success") return result.value + cached = yield* Effect.cached(downloadBinary) + return yield* result + }) const check = Effect.fnUntraced(function* (cwd: string) { if (yield* fs.isDir(cwd).pipe(Effect.orDie)) return