Files
oh-my-opencode/src/shared/ripgrep-cli.ts
T
Yeachan-Heo 6c691a1afa fix(grep): probe OpenCode cache-backed bin for auto-downloaded rg (#3805)
Upstream OpenCode's Global.Path.bin resolves to ~/.cache/opencode/bin
(XDG cache), where its ripgrep auto-installer and many LSP servers land.
OMO's getOpenCodeBundledRg only checked the data-dir variant
(~/.local/share/opencode/bin), so when OpenCode had already downloaded
rg into its cache directory OMO would skip it and either redownload via
its own fallback installer or fall back to system grep.

Probe the cache-backed bin path first so OMO reuses tools OpenCode has
already installed.
2026-05-24 02:02:31 +09:00

126 lines
3.3 KiB
TypeScript

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<ResolvedCli> {
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
}
}