import { spawnSync } from "node:child_process" import { existsSync } from "node:fs" import { dirname, join } from "node:path" import { downloadAndInstallRipgrep, getInstalledRipgrepPath } from "../tools/grep/downloader" import { getDataDir, getOpenCodeCacheDir } from "./data-path" import { log } from "./logger" import { PUBLISHED_PACKAGE_NAME } from "./plugin-identity" export type GrepBackend = "rg" | "grep" export interface ResolvedCli { path: string backend: GrepBackend } export const DEFAULT_RG_THREADS = 4 let cachedCli: ResolvedCli | null = null let autoInstallAttempted = false function findExecutable(name: string): string | null { const isWindows = process.platform === "win32" const cmd = isWindows ? "where.exe" : "which" try { // #3919: Keep Windows executable probes hidden and shell-free in Desktop utility processes. const result = spawnSync(cmd, [name], { encoding: "utf-8", timeout: 5000, windowsHide: isWindows, shell: false, }) const stdout = result.stdout if (result.status === 0 && stdout.trim()) { return stdout.trim().split("\n")[0] } } catch { return null } return null } function getOpenCodeBundledRg(): string | null { const execPath = process.execPath const execDir = dirname(execPath) const isWindows = process.platform === "win32" const rgName = isWindows ? "rg.exe" : "rg" const candidates = [ // #3805: Upstream OpenCode's Global.Path.bin is cache-backed (~/.cache/opencode/bin), // and its auto-downloaded ripgrep + LSP binaries live there. Probe it first so OMO // reuses tools OpenCode already installed instead of triggering a duplicate download. join(getOpenCodeCacheDir(), "bin", rgName), join(getDataDir(), "opencode", "bin", rgName), join(execDir, rgName), join(execDir, "bin", rgName), join(execDir, "..", "bin", rgName), join(execDir, "..", "libexec", rgName), ] for (const candidate of candidates) { if (existsSync(candidate)) { return candidate } } return null } export function resolveGrepCli(): ResolvedCli { if (cachedCli) { return cachedCli } const rgPath = getOpenCodeBundledRg() ?? findExecutable("rg") ?? getInstalledRipgrepPath() if (rgPath) { cachedCli = { path: rgPath, backend: "rg" } return cachedCli } const grep = findExecutable("grep") if (grep) { cachedCli = { path: grep, backend: "grep" } return cachedCli } cachedCli = { path: "rg", backend: "rg" } return cachedCli } export async function resolveGrepCliWithAutoInstall(): Promise { const current = resolveGrepCli() if (current.backend === "rg" && current.path !== "rg") { return current } if (autoInstallAttempted) { return current } autoInstallAttempted = true try { const rgPath = await downloadAndInstallRipgrep() cachedCli = { path: rgPath, backend: "rg" } return cachedCli } catch (error) { const message = error instanceof Error ? error.message : String(error) if (current.backend === "grep") { log(`[${PUBLISHED_PACKAGE_NAME}] Failed to auto-install ripgrep. Falling back to GNU grep.`, { error: message, grep_path: current.path, }) } else { log(`[${PUBLISHED_PACKAGE_NAME}] Failed to auto-install ripgrep and GNU grep was not found.`, { error: message, }) } return current } }