fix: improve glob tool Windows compatibility and rg resolution (#309)

This commit is contained in:
Sisyphus
2025-12-29 10:10:22 +09:00
committed by GitHub
parent 55a3a6c9eb
commit 6dd98254be
4 changed files with 71 additions and 14 deletions

View File

@@ -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 })
} }

View File

@@ -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

View File

@@ -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) {

View File

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