Files
harness-tools/extensions/file-search/src/args.ts
T

123 lines
3.6 KiB
TypeScript

/**
* Pure CLI argument construction for the fd and rg tools.
*
* Everything here is synchronous and side-effect free so the exact argv
* passed to pi.exec can be asserted in tests. Patterns are always placed
* after a `--` separator so user-controlled input can never be parsed as a
* flag, and paths are normalized (leading `@`, `~` expansion) before use.
*/
import { homedir } from "node:os";
import { join } from "node:path";
export const FD_DEFAULT_LIMIT = 1000;
export const FD_MAX_LIMIT = 10_000;
export const FD_MAX_DEPTH_LIMIT = 64;
export const RG_DEFAULT_COUNT_LIMIT = 100;
export const RG_MAX_COUNT_LIMIT = 1000;
export const RG_MAX_CONTEXT = 20;
/** Some models prefix path arguments with @; built-in tools strip it, so do we. */
export function normalizeSearchPath(raw: string) {
let path = raw.trim();
if (path.startsWith("@")) path = path.slice(1);
if (path === "~") return homedir();
if (path.startsWith("~/")) return join(homedir(), path.slice(2));
return path;
}
function clamp(value: number, minimum: number, maximum: number) {
return Math.min(maximum, Math.max(minimum, Math.floor(value)));
}
function optionalPath(raw: string | undefined) {
if (raw === undefined) return undefined;
const normalized = normalizeSearchPath(raw);
return normalized === "" ? undefined : normalized;
}
export type FdEntryType = "file" | "directory" | "symlink";
export interface FdToolParams {
pattern?: string;
path?: string;
type?: FdEntryType;
extension?: string;
glob?: boolean;
hidden?: boolean;
max_depth?: number;
limit?: number;
}
const FD_TYPE_FLAGS: Record<FdEntryType, string> = {
file: "f",
directory: "d",
symlink: "l",
};
export function buildFdArgs(params: FdToolParams) {
const args = ["--color=never"];
if (params.hidden) args.push("--hidden");
if (params.glob) args.push("--glob");
if (params.type) args.push("--type", FD_TYPE_FLAGS[params.type]);
if (params.extension) {
args.push("--extension", params.extension.replace(/^\.+/, ""));
}
if (params.max_depth !== undefined) {
args.push(
"--max-depth",
String(clamp(params.max_depth, 1, FD_MAX_DEPTH_LIMIT)),
);
}
args.push(
"--max-results",
String(clamp(params.limit ?? FD_DEFAULT_LIMIT, 1, FD_MAX_LIMIT)),
);
// An empty pattern matches everything, which keeps `path` usable without a pattern.
args.push("--", params.pattern ?? "");
const path = optionalPath(params.path);
if (path) args.push(path);
return args;
}
export interface RgToolParams {
pattern: string;
path?: string;
glob?: string;
file_type?: string;
case_sensitive?: boolean;
fixed_strings?: boolean;
hidden?: boolean;
context?: number;
limit?: number;
}
export function buildRgArgs(params: RgToolParams) {
const args = [
"--line-number",
"--color=never",
"--no-heading",
"--with-filename",
];
if (params.case_sensitive === true) args.push("--case-sensitive");
else if (params.case_sensitive === false) args.push("--ignore-case");
else args.push("--smart-case");
if (params.fixed_strings) args.push("--fixed-strings");
if (params.hidden) args.push("--hidden");
if (params.context !== undefined) {
args.push("--context", String(clamp(params.context, 0, RG_MAX_CONTEXT)));
}
if (params.glob) args.push("--glob", params.glob);
if (params.file_type) args.push("--type", params.file_type);
args.push(
"--max-count",
String(
clamp(params.limit ?? RG_DEFAULT_COUNT_LIMIT, 1, RG_MAX_COUNT_LIMIT),
),
);
args.push("--", params.pattern);
const path = optionalPath(params.path);
if (path) args.push(path);
return args;
}