From f6ed59bf459defd4db62135d262fc222e909e0b3 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 11 Jun 2025 23:59:51 -0400 Subject: Refactor external tools organization and add file search API endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [OpenCode](https://opencode.ai) Co-Authored-By: OpenCode --- packages/opencode/src/external/ripgrep.ts | 114 ++++++++++++++++++++++++++++++ packages/opencode/src/file/index.ts | 2 +- packages/opencode/src/index.ts | 2 +- packages/opencode/src/ripgrep/index.ts | 111 ----------------------------- packages/opencode/src/server/server.ts | 29 ++++++++ packages/opencode/src/tool/grep.ts | 2 +- packages/opencode/src/util/lazy.ts | 11 +++ 7 files changed, 157 insertions(+), 114 deletions(-) create mode 100644 packages/opencode/src/external/ripgrep.ts delete mode 100644 packages/opencode/src/ripgrep/index.ts create mode 100644 packages/opencode/src/util/lazy.ts diff --git a/packages/opencode/src/external/ripgrep.ts b/packages/opencode/src/external/ripgrep.ts new file mode 100644 index 000000000..876078700 --- /dev/null +++ b/packages/opencode/src/external/ripgrep.ts @@ -0,0 +1,114 @@ +import { App } from "../app/app" +import path from "path" +import { Global } from "../global" +import fs from "fs/promises" +import { z } from "zod" +import { NamedError } from "../util/error" +import { lazy } from "../util/lazy" + +export namespace Ripgrep { + const PLATFORM = { + darwin: { platform: "apple-darwin", extension: "tar.gz" }, + linux: { platform: "unknown-linux-musl", extension: "tar.gz" }, + win32: { platform: "pc-windows-msvc", extension: "zip" }, + } as const + + export const ExtractionFailedError = NamedError.create( + "RipgrepExtractionFailedError", + z.object({ + filepath: z.string(), + stderr: z.string(), + }), + ) + + export const UnsupportedPlatformError = NamedError.create( + "RipgrepUnsupportedPlatformError", + z.object({ + platform: z.string(), + }), + ) + + export const DownloadFailedError = NamedError.create( + "RipgrepDownloadFailedError", + z.object({ + url: z.string(), + status: z.number(), + }), + ) + + const state = lazy(async () => { + let filepath = Bun.which("rg") + if (filepath) return { filepath } + filepath = path.join( + Global.Path.bin, + "rg" + (process.platform === "win32" ? ".exe" : ""), + ) + + const file = Bun.file(filepath) + if (!(await file.exists())) { + const archMap = { x64: "x86_64", arm64: "aarch64" } as const + const arch = archMap[process.arch as keyof typeof archMap] ?? process.arch + + const config = PLATFORM[process.platform as keyof typeof PLATFORM] + if (!config) + throw new UnsupportedPlatformError({ platform: process.platform }) + + const version = "14.1.1" + const filename = `ripgrep-${version}-${arch}-${config.platform}.${config.extension}` + const url = `https://github.com/BurntSushi/ripgrep/releases/download/${version}/${filename}` + + const response = await fetch(url) + if (!response.ok) + throw new DownloadFailedError({ url, status: response.status }) + + const buffer = await response.arrayBuffer() + const archivePath = path.join(Global.Path.bin, filename) + await Bun.write(archivePath, buffer) + if (config.extension === "tar.gz") { + const args = ["tar", "-xzf", archivePath, "--strip-components=1"] + + if (process.platform === "darwin") args.push("--include=*/rg") + if (process.platform === "linux") args.push("--wildcards", "*/rg") + + const proc = Bun.spawn(args, { + cwd: Global.Path.bin, + stderr: "pipe", + stdout: "pipe", + }) + await proc.exited + if (proc.exitCode !== 0) + throw new ExtractionFailedError({ + filepath, + stderr: await Bun.readableStreamToText(proc.stderr), + }) + } + if (config.extension === "zip") { + const proc = Bun.spawn( + ["unzip", "-j", archivePath, "*/rg.exe", "-d", Global.Path.bin], + { + cwd: Global.Path.bin, + stderr: "pipe", + stdout: "ignore", + }, + ) + await proc.exited + if (proc.exitCode !== 0) + throw new ExtractionFailedError({ + filepath: archivePath, + stderr: await Bun.readableStreamToText(proc.stderr), + }) + } + await fs.unlink(archivePath) + if (process.platform !== "win32") await fs.chmod(filepath, 0o755) + } + + return { + filepath, + } + }) + + export async function filepath() { + const { filepath } = await state() + return filepath + } +} diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index cece788dd..b26a890ce 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -1,6 +1,6 @@ export namespace File { const glob = new Bun.Glob("**/*") - export async function search(path: string) { + export async function search(path: string, query: string) { for await (const entry of glob.scan({ cwd: path, onlyFiles: true, diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index a17b9e7fb..84dcf9246 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -26,7 +26,7 @@ const cli = yargs(hideBin(process.argv)) describe: "Print logs to stderr", type: "boolean", }) - .middleware(async (args) => { + .middleware(async () => { await Log.init({ print: process.argv.includes("--print-logs") }) Log.Default.info("opencode", { version: VERSION, diff --git a/packages/opencode/src/ripgrep/index.ts b/packages/opencode/src/ripgrep/index.ts deleted file mode 100644 index d98e9f139..000000000 --- a/packages/opencode/src/ripgrep/index.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { App } from "../app/app" -import path from "path" -import { Global } from "../global" -import fs from "fs/promises" -import { z } from "zod" -import { NamedError } from "../util/error" - -export namespace Ripgrep { - const PLATFORM = { - darwin: { platform: "apple-darwin", extension: "tar.gz" }, - linux: { platform: "unknown-linux-musl", extension: "tar.gz" }, - win32: { platform: "pc-windows-msvc", extension: "zip" }, - } as const - - export const ExtractionFailedError = NamedError.create( - "RipgrepExtractionFailedError", - z.object({ - filepath: z.string(), - stderr: z.string(), - }), - ) - - export const UnsupportedPlatformError = NamedError.create( - "RipgrepUnsupportedPlatformError", - z.object({ - platform: z.string(), - }), - ) - - export const DownloadFailedError = NamedError.create( - "RipgrepDownloadFailedError", - z.object({ - url: z.string(), - status: z.number(), - }), - ) - - const state = App.state("ripgrep", async () => { - const filepath = path.join( - Global.Path.bin, - "rg" + (process.platform === "win32" ? ".exe" : ""), - ) - - const file = Bun.file(filepath) - if (!(await file.exists())) { - const archMap = { x64: "x86_64", arm64: "aarch64" } as const - const arch = archMap[process.arch as keyof typeof archMap] ?? process.arch - - const config = PLATFORM[process.platform as keyof typeof PLATFORM] - if (!config) - throw new UnsupportedPlatformError({ platform: process.platform }) - - const version = "14.1.1" - const filename = `ripgrep-${version}-${arch}-${config.platform}.${config.extension}` - const url = `https://github.com/BurntSushi/ripgrep/releases/download/${version}/${filename}` - - const response = await fetch(url) - if (!response.ok) - throw new DownloadFailedError({ url, status: response.status }) - - const buffer = await response.arrayBuffer() - const archivePath = path.join(Global.Path.bin, filename) - await Bun.write(archivePath, buffer) - if (config.extension === "tar.gz") { - const args = ["tar", "-xzf", archivePath, "--strip-components=1"] - - if (process.platform === "darwin") args.push("--include=*/rg") - if (process.platform === "linux") args.push("--wildcards", "*/rg") - - const proc = Bun.spawn(args, { - cwd: Global.Path.bin, - stderr: "pipe", - stdout: "pipe", - }) - await proc.exited - if (proc.exitCode !== 0) - throw new ExtractionFailedError({ - filepath, - stderr: await Bun.readableStreamToText(proc.stderr), - }) - } - if (config.extension === "zip") { - const proc = Bun.spawn( - ["unzip", "-j", archivePath, "*/rg.exe", "-d", Global.Path.bin], - { - cwd: Global.Path.bin, - stderr: "pipe", - stdout: "ignore", - }, - ) - await proc.exited - if (proc.exitCode !== 0) - throw new ExtractionFailedError({ - filepath: archivePath, - stderr: await Bun.readableStreamToText(proc.stderr), - }) - } - await fs.unlink(archivePath) - if (process.platform !== "win32") await fs.chmod(filepath, 0o755) - } - - return { - filepath, - } - }) - - export async function filepath() { - const { filepath } = await state() - return filepath - } -} diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 1ba5afdef..acb2905c4 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -12,6 +12,7 @@ import { App } from "../app/app" import { Global } from "../global" import { mapValues } from "remeda" import { NamedError } from "../util/error" +import { Fzf } from "../external/fzf" const ERRORS = { 400: { @@ -427,6 +428,34 @@ export namespace Server { }) }, ) + .post( + "/file_search", + describeRoute({ + description: "Search for files", + responses: { + 200: { + description: "Search for files", + content: { + "application/json": { + schema: resolver(z.string().array()), + }, + }, + }, + }, + }), + zValidator( + "json", + z.object({ + query: z.string(), + }), + ), + async (c) => { + const body = c.req.valid("json") + const app = App.info() + const result = await Fzf.search(app.path.cwd, body.query) + return c.json(result) + }, + ) return result } diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts index 62a32756c..20199a5c6 100644 --- a/packages/opencode/src/tool/grep.ts +++ b/packages/opencode/src/tool/grep.ts @@ -1,7 +1,7 @@ import { z } from "zod" import { Tool } from "./tool" import { App } from "../app/app" -import { Ripgrep } from "../ripgrep" +import { Ripgrep } from "../external/ripgrep" import DESCRIPTION from "./grep.txt" diff --git a/packages/opencode/src/util/lazy.ts b/packages/opencode/src/util/lazy.ts new file mode 100644 index 000000000..8b7ded89f --- /dev/null +++ b/packages/opencode/src/util/lazy.ts @@ -0,0 +1,11 @@ +export function lazy(fn: () => T) { + let value: T | undefined + let loaded = false + + return (): T => { + if (loaded) return value as T + value = fn() + return value as T + } +} + -- cgit v1.2.3