2026-04-18 02:37:57 +09:00
|
|
|
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 { 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" : "which"
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const result = spawnSync(cmd, [name], { encoding: "utf-8", timeout: 5000 })
|
|
|
|
|
if (result.status === 0 && result.stdout.trim()) {
|
|
|
|
|
return result.stdout.trim().split("\n")[0]
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
2026-04-18 03:01:51 +09:00
|
|
|
return null
|
2026-04-18 02:37:57 +09:00
|
|
|
}
|
|
|
|
|
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 = [
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-18 03:01:51 +09:00
|
|
|
const rgPath = getOpenCodeBundledRg() ?? findExecutable("rg") ?? getInstalledRipgrepPath()
|
|
|
|
|
if (rgPath) {
|
|
|
|
|
cachedCli = { path: rgPath, backend: "rg" }
|
2026-04-18 02:37:57 +09:00
|
|
|
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) {
|
2026-04-18 03:01:51 +09:00
|
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
|
|
|
|
2026-04-18 02:37:57 +09:00
|
|
|
if (current.backend === "grep") {
|
|
|
|
|
log(`[${PUBLISHED_PACKAGE_NAME}] Failed to auto-install ripgrep. Falling back to GNU grep.`, {
|
2026-04-18 03:01:51 +09:00
|
|
|
error: message,
|
2026-04-18 02:37:57 +09:00
|
|
|
grep_path: current.path,
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
log(`[${PUBLISHED_PACKAGE_NAME}] Failed to auto-install ripgrep and GNU grep was not found.`, {
|
2026-04-18 03:01:51 +09:00
|
|
|
error: message,
|
2026-04-18 02:37:57 +09:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return current
|
|
|
|
|
}
|
|
|
|
|
}
|