fix: improve glob tool Windows compatibility and rg resolution (#309)
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { spawn } from "bun"
|
import { spawn } from "bun"
|
||||||
import {
|
import {
|
||||||
resolveGrepCli,
|
resolveGrepCli,
|
||||||
|
type GrepBackend,
|
||||||
DEFAULT_TIMEOUT_MS,
|
DEFAULT_TIMEOUT_MS,
|
||||||
DEFAULT_LIMIT,
|
DEFAULT_LIMIT,
|
||||||
DEFAULT_MAX_DEPTH,
|
DEFAULT_MAX_DEPTH,
|
||||||
@@ -10,6 +11,11 @@ import {
|
|||||||
import type { GlobOptions, GlobResult, FileMatch } from "./types"
|
import type { GlobOptions, GlobResult, FileMatch } from "./types"
|
||||||
import { stat } from "node:fs/promises"
|
import { stat } from "node:fs/promises"
|
||||||
|
|
||||||
|
export interface ResolvedCli {
|
||||||
|
path: string
|
||||||
|
backend: GrepBackend
|
||||||
|
}
|
||||||
|
|
||||||
function buildRgArgs(options: GlobOptions): string[] {
|
function buildRgArgs(options: GlobOptions): string[] {
|
||||||
const args: string[] = [
|
const args: string[] = [
|
||||||
...RG_FILES_FLAGS,
|
...RG_FILES_FLAGS,
|
||||||
@@ -40,6 +46,25 @@ function buildFindArgs(options: GlobOptions): string[] {
|
|||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildPowerShellCommand(options: GlobOptions): string[] {
|
||||||
|
const maxDepth = Math.min(options.maxDepth ?? DEFAULT_MAX_DEPTH, DEFAULT_MAX_DEPTH)
|
||||||
|
const paths = options.paths?.length ? options.paths : ["."]
|
||||||
|
const searchPath = paths[0] || "."
|
||||||
|
|
||||||
|
const escapedPath = searchPath.replace(/'/g, "''")
|
||||||
|
const escapedPattern = options.pattern.replace(/'/g, "''")
|
||||||
|
|
||||||
|
let psCommand = `Get-ChildItem -Path '${escapedPath}' -File -Recurse -Depth ${maxDepth - 1} -Filter '${escapedPattern}'`
|
||||||
|
|
||||||
|
if (options.hidden) {
|
||||||
|
psCommand += " -Force"
|
||||||
|
}
|
||||||
|
|
||||||
|
psCommand += " -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName"
|
||||||
|
|
||||||
|
return ["powershell", "-NoProfile", "-Command", psCommand]
|
||||||
|
}
|
||||||
|
|
||||||
async function getFileMtime(filePath: string): Promise<number> {
|
async function getFileMtime(filePath: string): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const stats = await stat(filePath)
|
const stats = await stat(filePath)
|
||||||
@@ -49,25 +74,40 @@ async function getFileMtime(filePath: string): Promise<number> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runRgFiles(options: GlobOptions): Promise<GlobResult> {
|
export async function runRgFiles(
|
||||||
const cli = resolveGrepCli()
|
options: GlobOptions,
|
||||||
|
resolvedCli?: ResolvedCli
|
||||||
|
): Promise<GlobResult> {
|
||||||
|
const cli = resolvedCli ?? resolveGrepCli()
|
||||||
const timeout = Math.min(options.timeout ?? DEFAULT_TIMEOUT_MS, DEFAULT_TIMEOUT_MS)
|
const timeout = Math.min(options.timeout ?? DEFAULT_TIMEOUT_MS, DEFAULT_TIMEOUT_MS)
|
||||||
const limit = Math.min(options.limit ?? DEFAULT_LIMIT, DEFAULT_LIMIT)
|
const limit = Math.min(options.limit ?? DEFAULT_LIMIT, DEFAULT_LIMIT)
|
||||||
|
|
||||||
const isRg = cli.backend === "rg"
|
const isRg = cli.backend === "rg"
|
||||||
const args = isRg ? buildRgArgs(options) : buildFindArgs(options)
|
const isWindows = process.platform === "win32"
|
||||||
|
|
||||||
|
let command: string[]
|
||||||
|
let cwd: string | undefined
|
||||||
|
|
||||||
const paths = options.paths?.length ? options.paths : ["."]
|
|
||||||
if (isRg) {
|
if (isRg) {
|
||||||
|
const args = buildRgArgs(options)
|
||||||
|
const paths = options.paths?.length ? options.paths : ["."]
|
||||||
args.push(...paths)
|
args.push(...paths)
|
||||||
|
command = [cli.path, ...args]
|
||||||
|
cwd = undefined
|
||||||
|
} else if (isWindows) {
|
||||||
|
command = buildPowerShellCommand(options)
|
||||||
|
cwd = undefined
|
||||||
|
} else {
|
||||||
|
const args = buildFindArgs(options)
|
||||||
|
const paths = options.paths?.length ? options.paths : ["."]
|
||||||
|
cwd = paths[0] || "."
|
||||||
|
command = [cli.path, ...args]
|
||||||
}
|
}
|
||||||
|
|
||||||
const cwd = paths[0] || "."
|
const proc = spawn(command, {
|
||||||
|
|
||||||
const proc = spawn([cli.path, ...args], {
|
|
||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
stderr: "pipe",
|
stderr: "pipe",
|
||||||
cwd: isRg ? undefined : cwd,
|
cwd,
|
||||||
})
|
})
|
||||||
|
|
||||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||||
@@ -106,7 +146,15 @@ export async function runRgFiles(options: GlobOptions): Promise<GlobResult> {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePath = isRg ? line : `${cwd}/${line}`
|
let filePath: string
|
||||||
|
if (isRg) {
|
||||||
|
filePath = line
|
||||||
|
} else if (isWindows) {
|
||||||
|
filePath = line.trim()
|
||||||
|
} else {
|
||||||
|
filePath = `${cwd}/${line}`
|
||||||
|
}
|
||||||
|
|
||||||
const mtime = await getFileMtime(filePath)
|
const mtime = await getFileMtime(filePath)
|
||||||
files.push({ path: filePath, mtime })
|
files.push({ path: filePath, mtime })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export { resolveGrepCli, type GrepBackend } from "../grep/constants"
|
export { resolveGrepCli, resolveGrepCliWithAutoInstall, type GrepBackend } from "../grep/constants"
|
||||||
|
|
||||||
export const DEFAULT_TIMEOUT_MS = 60_000
|
export const DEFAULT_TIMEOUT_MS = 60_000
|
||||||
export const DEFAULT_LIMIT = 100
|
export const DEFAULT_LIMIT = 100
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||||
import { runRgFiles } from "./cli"
|
import { runRgFiles } from "./cli"
|
||||||
|
import { resolveGrepCliWithAutoInstall } from "./constants"
|
||||||
import { formatGlobResult } from "./utils"
|
import { formatGlobResult } from "./utils"
|
||||||
|
|
||||||
export const glob: ToolDefinition = tool({
|
export const glob: ToolDefinition = tool({
|
||||||
@@ -21,12 +22,16 @@ export const glob: ToolDefinition = tool({
|
|||||||
},
|
},
|
||||||
execute: async (args) => {
|
execute: async (args) => {
|
||||||
try {
|
try {
|
||||||
|
const cli = await resolveGrepCliWithAutoInstall()
|
||||||
const paths = args.path ? [args.path] : undefined
|
const paths = args.path ? [args.path] : undefined
|
||||||
|
|
||||||
const result = await runRgFiles({
|
const result = await runRgFiles(
|
||||||
pattern: args.pattern,
|
{
|
||||||
paths,
|
pattern: args.pattern,
|
||||||
})
|
paths,
|
||||||
|
},
|
||||||
|
cli
|
||||||
|
)
|
||||||
|
|
||||||
return formatGlobResult(result)
|
return formatGlobResult(result)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs"
|
|||||||
import { join, dirname } from "node:path"
|
import { join, dirname } from "node:path"
|
||||||
import { spawnSync } from "node:child_process"
|
import { spawnSync } from "node:child_process"
|
||||||
import { getInstalledRipgrepPath, downloadAndInstallRipgrep } from "./downloader"
|
import { getInstalledRipgrepPath, downloadAndInstallRipgrep } from "./downloader"
|
||||||
|
import { getDataDir } from "../../shared/data-path"
|
||||||
|
|
||||||
export type GrepBackend = "rg" | "grep"
|
export type GrepBackend = "rg" | "grep"
|
||||||
|
|
||||||
@@ -36,6 +37,9 @@ function getOpenCodeBundledRg(): string | null {
|
|||||||
const rgName = isWindows ? "rg.exe" : "rg"
|
const rgName = isWindows ? "rg.exe" : "rg"
|
||||||
|
|
||||||
const candidates = [
|
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, rgName),
|
||||||
join(execDir, "bin", rgName),
|
join(execDir, "bin", rgName),
|
||||||
join(execDir, "..", "bin", rgName),
|
join(execDir, "..", "bin", rgName),
|
||||||
|
|||||||
Reference in New Issue
Block a user