178 lines
4.5 KiB
TypeScript
178 lines
4.5 KiB
TypeScript
import { spawn } from "bun"
|
|
import {
|
|
resolveGrepCli,
|
|
type GrepBackend,
|
|
DEFAULT_TIMEOUT_MS,
|
|
DEFAULT_LIMIT,
|
|
DEFAULT_MAX_DEPTH,
|
|
DEFAULT_MAX_OUTPUT_BYTES,
|
|
RG_FILES_FLAGS,
|
|
} from "./constants"
|
|
import type { GlobOptions, GlobResult, FileMatch } from "./types"
|
|
import { stat } from "node:fs/promises"
|
|
|
|
export interface ResolvedCli {
|
|
path: string
|
|
backend: GrepBackend
|
|
}
|
|
|
|
function buildRgArgs(options: GlobOptions): string[] {
|
|
const args: string[] = [
|
|
...RG_FILES_FLAGS,
|
|
`--max-depth=${Math.min(options.maxDepth ?? DEFAULT_MAX_DEPTH, DEFAULT_MAX_DEPTH)}`,
|
|
]
|
|
|
|
if (options.hidden) args.push("--hidden")
|
|
if (options.noIgnore) args.push("--no-ignore")
|
|
|
|
args.push(`--glob=${options.pattern}`)
|
|
|
|
return args
|
|
}
|
|
|
|
function buildFindArgs(options: GlobOptions): string[] {
|
|
const args: string[] = ["."]
|
|
|
|
const maxDepth = Math.min(options.maxDepth ?? DEFAULT_MAX_DEPTH, DEFAULT_MAX_DEPTH)
|
|
args.push("-maxdepth", String(maxDepth))
|
|
|
|
args.push("-type", "f")
|
|
args.push("-name", options.pattern)
|
|
|
|
if (!options.hidden) {
|
|
args.push("-not", "-path", "*/.*")
|
|
}
|
|
|
|
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> {
|
|
try {
|
|
const stats = await stat(filePath)
|
|
return stats.mtime.getTime()
|
|
} catch {
|
|
return 0
|
|
}
|
|
}
|
|
|
|
export async function runRgFiles(
|
|
options: GlobOptions,
|
|
resolvedCli?: ResolvedCli
|
|
): Promise<GlobResult> {
|
|
const cli = resolvedCli ?? resolveGrepCli()
|
|
const timeout = Math.min(options.timeout ?? DEFAULT_TIMEOUT_MS, DEFAULT_TIMEOUT_MS)
|
|
const limit = Math.min(options.limit ?? DEFAULT_LIMIT, DEFAULT_LIMIT)
|
|
|
|
const isRg = cli.backend === "rg"
|
|
const isWindows = process.platform === "win32"
|
|
|
|
let command: string[]
|
|
let cwd: string | undefined
|
|
|
|
if (isRg) {
|
|
const args = buildRgArgs(options)
|
|
const paths = options.paths?.length ? options.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 proc = spawn(command, {
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
cwd,
|
|
})
|
|
|
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
const id = setTimeout(() => {
|
|
proc.kill()
|
|
reject(new Error(`Glob search timeout after ${timeout}ms`))
|
|
}, timeout)
|
|
proc.exited.then(() => clearTimeout(id))
|
|
})
|
|
|
|
try {
|
|
const stdout = await Promise.race([new Response(proc.stdout).text(), timeoutPromise])
|
|
const stderr = await new Response(proc.stderr).text()
|
|
const exitCode = await proc.exited
|
|
|
|
if (exitCode > 1 && stderr.trim()) {
|
|
return {
|
|
files: [],
|
|
totalFiles: 0,
|
|
truncated: false,
|
|
error: stderr.trim(),
|
|
}
|
|
}
|
|
|
|
const truncatedOutput = stdout.length >= DEFAULT_MAX_OUTPUT_BYTES
|
|
const outputToProcess = truncatedOutput ? stdout.substring(0, DEFAULT_MAX_OUTPUT_BYTES) : stdout
|
|
|
|
const lines = outputToProcess.trim().split("\n").filter(Boolean)
|
|
|
|
const files: FileMatch[] = []
|
|
let truncated = false
|
|
|
|
for (const line of lines) {
|
|
if (files.length >= limit) {
|
|
truncated = true
|
|
break
|
|
}
|
|
|
|
let filePath: string
|
|
if (isRg) {
|
|
filePath = line
|
|
} else if (isWindows) {
|
|
filePath = line.trim()
|
|
} else {
|
|
filePath = `${cwd}/${line}`
|
|
}
|
|
|
|
const mtime = await getFileMtime(filePath)
|
|
files.push({ path: filePath, mtime })
|
|
}
|
|
|
|
files.sort((a, b) => b.mtime - a.mtime)
|
|
|
|
return {
|
|
files,
|
|
totalFiles: files.length,
|
|
truncated: truncated || truncatedOutput,
|
|
}
|
|
} catch (e) {
|
|
return {
|
|
files: [],
|
|
totalFiles: 0,
|
|
truncated: false,
|
|
error: e instanceof Error ? e.message : String(e),
|
|
}
|
|
}
|
|
}
|