refactor(tools): break glob->grep sibling-tool coupling
Hoist shared ripgrep CLI resolution helpers (resolveGrepCli, resolveGrepCliWithAutoInstall, GrepBackend, DEFAULT_RG_THREADS, ResolvedCli) out of src/tools/grep/constants.ts into src/shared/ripgrep-cli.ts so they no longer straddle two sibling tool directories. Before: src/tools/glob/constants.ts re-exported from src/tools/grep/constants.ts, violating the project's "tools should not import from sibling tools" rule enforced by .sisyphus/rules/modular-code-enforcement.md. After: both src/tools/glob/ and src/tools/grep/ consume the shared helpers from src/shared/ripgrep-cli.ts. src/tools/grep/constants.ts keeps only the grep-specific UI-exposed constants.
This commit is contained in:
@@ -0,0 +1,124 @@
|
|||||||
|
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 {
|
||||||
|
// Command execution failed
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
const bundledRg = getOpenCodeBundledRg()
|
||||||
|
if (bundledRg) {
|
||||||
|
cachedCli = { path: bundledRg, backend: "rg" }
|
||||||
|
return cachedCli
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemRg = findExecutable("rg")
|
||||||
|
if (systemRg) {
|
||||||
|
cachedCli = { path: systemRg, backend: "rg" }
|
||||||
|
return cachedCli
|
||||||
|
}
|
||||||
|
|
||||||
|
const installedRg = getInstalledRipgrepPath()
|
||||||
|
if (installedRg) {
|
||||||
|
cachedCli = { path: installedRg, 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) {
|
||||||
|
if (current.backend === "grep") {
|
||||||
|
log(`[${PUBLISHED_PACKAGE_NAME}] Failed to auto-install ripgrep. Falling back to GNU grep.`, {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
grep_path: current.path,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
log(`[${PUBLISHED_PACKAGE_NAME}] Failed to auto-install ripgrep and GNU grep was not found.`, {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export { resolveGrepCli, resolveGrepCliWithAutoInstall, type GrepBackend, DEFAULT_RG_THREADS } from "../grep/constants"
|
export { resolveGrepCli, resolveGrepCliWithAutoInstall, type GrepBackend, DEFAULT_RG_THREADS } from "../../shared/ripgrep-cli"
|
||||||
|
|
||||||
export const DEFAULT_TIMEOUT_MS = 60_000
|
export const DEFAULT_TIMEOUT_MS = 60_000
|
||||||
export const DEFAULT_LIMIT = 100
|
export const DEFAULT_LIMIT = 100
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ import {
|
|||||||
resolveGrepCli,
|
resolveGrepCli,
|
||||||
type ResolvedCli,
|
type ResolvedCli,
|
||||||
type GrepBackend,
|
type GrepBackend,
|
||||||
|
DEFAULT_RG_THREADS,
|
||||||
|
} from "../../shared/ripgrep-cli"
|
||||||
|
import {
|
||||||
DEFAULT_MAX_DEPTH,
|
DEFAULT_MAX_DEPTH,
|
||||||
DEFAULT_MAX_FILESIZE,
|
DEFAULT_MAX_FILESIZE,
|
||||||
DEFAULT_MAX_COUNT,
|
DEFAULT_MAX_COUNT,
|
||||||
DEFAULT_MAX_COLUMNS,
|
DEFAULT_MAX_COLUMNS,
|
||||||
DEFAULT_TIMEOUT_MS,
|
DEFAULT_TIMEOUT_MS,
|
||||||
DEFAULT_MAX_OUTPUT_BYTES,
|
DEFAULT_MAX_OUTPUT_BYTES,
|
||||||
DEFAULT_RG_THREADS,
|
|
||||||
RG_SAFETY_FLAGS,
|
RG_SAFETY_FLAGS,
|
||||||
GREP_SAFETY_FLAGS,
|
GREP_SAFETY_FLAGS,
|
||||||
} from "./constants"
|
} from "./constants"
|
||||||
|
|||||||
@@ -1,126 +1,3 @@
|
|||||||
import { existsSync } from "node:fs"
|
|
||||||
import { join, dirname } from "node:path"
|
|
||||||
import { spawnSync } from "node:child_process"
|
|
||||||
import { getInstalledRipgrepPath, downloadAndInstallRipgrep } from "./downloader"
|
|
||||||
import { getDataDir } from "../../shared/data-path"
|
|
||||||
import { log } from "../../shared/logger"
|
|
||||||
import { PUBLISHED_PACKAGE_NAME } from "../../shared/plugin-identity"
|
|
||||||
|
|
||||||
export type GrepBackend = "rg" | "grep"
|
|
||||||
|
|
||||||
export interface ResolvedCli {
|
|
||||||
path: string
|
|
||||||
backend: GrepBackend
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
// Command execution failed
|
|
||||||
}
|
|
||||||
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 = [
|
|
||||||
// OpenCode XDG data path (highest priority - where OpenCode installs rg)
|
|
||||||
join(getDataDir(), "opencode", "bin", rgName),
|
|
||||||
// Legacy paths relative to execPath
|
|
||||||
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 bundledRg = getOpenCodeBundledRg()
|
|
||||||
if (bundledRg) {
|
|
||||||
cachedCli = { path: bundledRg, backend: "rg" }
|
|
||||||
return cachedCli
|
|
||||||
}
|
|
||||||
|
|
||||||
const systemRg = findExecutable("rg")
|
|
||||||
if (systemRg) {
|
|
||||||
cachedCli = { path: systemRg, backend: "rg" }
|
|
||||||
return cachedCli
|
|
||||||
}
|
|
||||||
|
|
||||||
const installedRg = getInstalledRipgrepPath()
|
|
||||||
if (installedRg) {
|
|
||||||
cachedCli = { path: installedRg, 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) {
|
|
||||||
if (current.backend === "grep") {
|
|
||||||
log(`[${PUBLISHED_PACKAGE_NAME}] Failed to auto-install ripgrep. Falling back to GNU grep.`, {
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
grep_path: current.path,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
log(`[${PUBLISHED_PACKAGE_NAME}] Failed to auto-install ripgrep and GNU grep was not found.`, {
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return current
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_MAX_DEPTH = 20
|
export const DEFAULT_MAX_DEPTH = 20
|
||||||
export const DEFAULT_MAX_FILESIZE = "10M"
|
export const DEFAULT_MAX_FILESIZE = "10M"
|
||||||
export const DEFAULT_MAX_COUNT = 500
|
export const DEFAULT_MAX_COUNT = 500
|
||||||
@@ -128,7 +5,6 @@ export const DEFAULT_MAX_COLUMNS = 1000
|
|||||||
export const DEFAULT_CONTEXT = 2
|
export const DEFAULT_CONTEXT = 2
|
||||||
export const DEFAULT_TIMEOUT_MS = 60_000
|
export const DEFAULT_TIMEOUT_MS = 60_000
|
||||||
export const DEFAULT_MAX_OUTPUT_BYTES = 256 * 1024
|
export const DEFAULT_MAX_OUTPUT_BYTES = 256 * 1024
|
||||||
export const DEFAULT_RG_THREADS = 4
|
|
||||||
|
|
||||||
export const RG_SAFETY_FLAGS = [
|
export const RG_SAFETY_FLAGS = [
|
||||||
"--no-follow",
|
"--no-follow",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { resolve } from "node:path"
|
import { resolve } from "node:path"
|
||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||||
|
import { resolveGrepCliWithAutoInstall } from "../../shared/ripgrep-cli"
|
||||||
import { runRg, runRgCount } from "./cli"
|
import { runRg, runRgCount } from "./cli"
|
||||||
import { resolveGrepCliWithAutoInstall } from "./constants"
|
|
||||||
import { formatGrepResult, formatCountResult } from "./result-formatter"
|
import { formatGrepResult, formatCountResult } from "./result-formatter"
|
||||||
|
|
||||||
export function createGrepTools(ctx: PluginInput): Record<string, ToolDefinition> {
|
export function createGrepTools(ctx: PluginInput): Record<string, ToolDefinition> {
|
||||||
|
|||||||
Reference in New Issue
Block a user