135 lines
5.8 KiB
TypeScript
135 lines
5.8 KiB
TypeScript
import path from "path"
|
|
import { Context, Effect, Layer, Stream } from "effect"
|
|
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
|
|
import { ChildProcess } from "effect/unstable/process"
|
|
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
|
|
import { CrossSpawnSpawner } from "../cross-spawn-spawner"
|
|
import { LayerNode } from "../effect/layer-node"
|
|
import { httpClient } from "../effect/layer-node-platform"
|
|
import { FSUtil } from "../fs-util"
|
|
import { Global } from "../global"
|
|
import { which } from "../util/which"
|
|
|
|
export namespace RipgrepBinary {
|
|
const VERSION = "15.1.0"
|
|
const PLATFORM = {
|
|
"arm64-darwin": { platform: "aarch64-apple-darwin", extension: "tar.gz" },
|
|
"arm64-linux": { platform: "aarch64-unknown-linux-gnu", extension: "tar.gz" },
|
|
"x64-darwin": { platform: "x86_64-apple-darwin", extension: "tar.gz" },
|
|
"x64-linux": { platform: "x86_64-unknown-linux-musl", extension: "tar.gz" },
|
|
"arm64-win32": { platform: "aarch64-pc-windows-msvc", extension: "zip" },
|
|
"ia32-win32": { platform: "i686-pc-windows-msvc", extension: "zip" },
|
|
"x64-win32": { platform: "x86_64-pc-windows-msvc", extension: "zip" },
|
|
} as const
|
|
|
|
interface Interface {
|
|
readonly filepath: Effect.Effect<string, Error>
|
|
}
|
|
|
|
export class Service extends Context.Service<Service, Interface>()("@opencode/RipgrepBinary") {}
|
|
|
|
export const layer = Layer.effect(
|
|
Service,
|
|
Effect.gen(function* () {
|
|
const fs = yield* FSUtil.Service
|
|
const http = HttpClient.filterStatusOk(yield* HttpClient.HttpClient)
|
|
const spawner = yield* ChildProcessSpawner
|
|
|
|
const run = Effect.fnUntraced(function* (command: string, args: string[]) {
|
|
const handle = yield* spawner.spawn(ChildProcess.make(command, args, { extendEnv: true, stdin: "ignore" }))
|
|
const [stdout, stderr, code] = yield* Effect.all(
|
|
[
|
|
Stream.mkString(Stream.decodeText(handle.stdout)),
|
|
Stream.mkString(Stream.decodeText(handle.stderr)),
|
|
handle.exitCode,
|
|
],
|
|
{ concurrency: "unbounded" },
|
|
)
|
|
return { stdout, stderr, code }
|
|
}, Effect.scoped)
|
|
|
|
const extract = Effect.fnUntraced(function* (
|
|
archive: string,
|
|
config: (typeof PLATFORM)[keyof typeof PLATFORM],
|
|
target: string,
|
|
) {
|
|
const dir = yield* fs.makeTempDirectoryScoped({ directory: Global.Path.bin, prefix: "ripgrep-" })
|
|
|
|
if (config.extension === "zip") {
|
|
const shell = (yield* Effect.sync(() => which("powershell.exe") ?? which("pwsh.exe"))) ?? "powershell.exe"
|
|
const result = yield* run(shell, [
|
|
"-NoProfile",
|
|
"-NonInteractive",
|
|
"-Command",
|
|
`$global:ProgressPreference = 'SilentlyContinue'; Expand-Archive -LiteralPath '${archive.replaceAll("'", "''")}' -DestinationPath '${dir.replaceAll("'", "''")}' -Force`,
|
|
])
|
|
if (result.code !== 0)
|
|
throw new Error(
|
|
result.stderr.trim() || result.stdout.trim() || `ripgrep extraction failed with code ${result.code}`,
|
|
)
|
|
}
|
|
|
|
if (config.extension === "tar.gz") {
|
|
const result = yield* run("tar", ["-xzf", archive, "-C", dir])
|
|
if (result.code !== 0)
|
|
throw new Error(
|
|
result.stderr.trim() || result.stdout.trim() || `ripgrep extraction failed with code ${result.code}`,
|
|
)
|
|
}
|
|
|
|
const extracted = path.join(
|
|
dir,
|
|
`ripgrep-${VERSION}-${config.platform}`,
|
|
process.platform === "win32" ? "rg.exe" : "rg",
|
|
)
|
|
if (!(yield* fs.isFile(extracted))) throw new Error(`ripgrep archive did not contain executable: ${extracted}`)
|
|
|
|
yield* fs.copyFile(extracted, target)
|
|
if (process.platform !== "win32") yield* fs.chmod(target, 0o755)
|
|
}, Effect.scoped)
|
|
|
|
return Service.of({
|
|
filepath: yield* Effect.cached(
|
|
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 platformKey = `${process.arch}-${process.platform}` as keyof typeof PLATFORM
|
|
const config = PLATFORM[platformKey]
|
|
if (!config) throw 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)
|
|
|
|
yield* Effect.logInfo("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.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))),
|
|
)
|
|
if (bytes.byteLength === 0) throw new Error(`failed to download ripgrep from ${url}`)
|
|
|
|
yield* fs.writeWithDirs(archive, new Uint8Array(bytes))
|
|
yield* extract(archive, config, target)
|
|
yield* fs.remove(archive, { force: true }).pipe(Effect.ignore)
|
|
return target
|
|
}),
|
|
),
|
|
})
|
|
}),
|
|
)
|
|
|
|
export const defaultLayer = layer.pipe(
|
|
Layer.provide(FetchHttpClient.layer),
|
|
Layer.provide(FSUtil.defaultLayer),
|
|
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
|
)
|
|
|
|
export const node = LayerNode.make(layer, [FSUtil.node, httpClient, CrossSpawnSpawner.node])
|
|
}
|