From 6c691a1afa2bac18dbffd9f8ac38abbb7353bd99 Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Sun, 24 May 2026 01:23:57 +0900 Subject: [PATCH] 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. --- src/shared/ripgrep-cli.test.ts | 56 ++++++++++++++++++++++++++++++++++ src/shared/ripgrep-cli.ts | 6 +++- 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 src/shared/ripgrep-cli.test.ts diff --git a/src/shared/ripgrep-cli.test.ts b/src/shared/ripgrep-cli.test.ts new file mode 100644 index 000000000..d6fae4b7c --- /dev/null +++ b/src/shared/ripgrep-cli.test.ts @@ -0,0 +1,56 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test" +import { mkdirSync, rmSync, writeFileSync } from "node:fs" +import { join } from "node:path" +import { tmpdir } from "node:os" + +describe("resolveGrepCli OpenCode cache fallback (#3805)", () => { + let tempCache: string + let tempData: string + let originalCache: string | undefined + let originalData: string | undefined + + beforeEach(() => { + const stamp = `omo-ripgrep-cli-${process.pid}-${Date.now()}` + tempCache = join(tmpdir(), `${stamp}-cache`) + tempData = join(tmpdir(), `${stamp}-data`) + mkdirSync(tempCache, { recursive: true }) + mkdirSync(tempData, { recursive: true }) + originalCache = process.env.XDG_CACHE_HOME + originalData = process.env.XDG_DATA_HOME + process.env.XDG_CACHE_HOME = tempCache + process.env.XDG_DATA_HOME = tempData + }) + + afterEach(() => { + if (originalCache === undefined) delete process.env.XDG_CACHE_HOME + else process.env.XDG_CACHE_HOME = originalCache + if (originalData === undefined) delete process.env.XDG_DATA_HOME + else process.env.XDG_DATA_HOME = originalData + try { + rmSync(tempCache, { recursive: true, force: true }) + rmSync(tempData, { recursive: true, force: true }) + } catch { + // best-effort cleanup + } + }) + + it("prefers ~/.cache/opencode/bin/rg over ~/.local/share/opencode/bin/rg", async () => { + const rgName = process.platform === "win32" ? "rg.exe" : "rg" + const cacheBinDir = join(tempCache, "opencode", "bin") + const dataBinDir = join(tempData, "opencode", "bin") + mkdirSync(cacheBinDir, { recursive: true }) + mkdirSync(dataBinDir, { recursive: true }) + const cacheRg = join(cacheBinDir, rgName) + const dataRg = join(dataBinDir, rgName) + writeFileSync(cacheRg, "") + writeFileSync(dataRg, "") + + // Reset the module cache so the singleton cachedCli is fresh. + delete require.cache[require.resolve("./ripgrep-cli")] + const { resolveGrepCli } = await import("./ripgrep-cli") + + const resolved = resolveGrepCli() + expect(resolved.backend).toBe("rg") + expect(resolved.path).toBe(cacheRg) + }) +}) diff --git a/src/shared/ripgrep-cli.ts b/src/shared/ripgrep-cli.ts index 3ee9b8a22..5a21c7dab 100644 --- a/src/shared/ripgrep-cli.ts +++ b/src/shared/ripgrep-cli.ts @@ -2,7 +2,7 @@ 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 } from "./data-path" +import { getDataDir, getOpenCodeCacheDir } from "./data-path" import { log } from "./logger" import { PUBLISHED_PACKAGE_NAME } from "./plugin-identity" @@ -48,6 +48,10 @@ function getOpenCodeBundledRg(): string | null { 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),