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),