Files
zesdex/apps/packages/infrastructure/src/tools/search/glob.ts
T

96 lines
2.9 KiB
TypeScript
Raw Normal View History

/**
* Glob: list files matching a glob pattern under a directory root.
* Mirrors `tools/search.rs` Glob tool. Uses a lightweight `**`/`*`/`?`
* matcher rather than the `globset` crate.
*/
import * as fs from "node:fs";
import * as path from "node:path";
import type { JsonValue } from "@zesdex/domain";
import { type Tool, type ToolCtx } from "../mod.ts";
import { argStr, resolvePath } from "../util.ts";
/** Convert a glob pattern to a RegExp. Supports `**`, `*`, `?`, `{a,b}`. */
export function globToRegExp(pattern: string): RegExp {
// Split path separators to handle `**` correctly.
const parts = pattern.split("/");
let re = "";
for (let i = 0; i < parts.length; i++) {
const part = parts[i] ?? "";
if (part === "**") {
re += "(?:.*/)?";
continue;
}
if (i > 0) re += "/";
let out = "";
for (let j = 0; j < part.length; j++) {
const ch = part[j];
if (ch === "*") out += "[^/]*";
else if (ch === "?") out += "[^/]";
else if (ch === ".") out += "\\.";
else if (ch === "(" || ch === ")" || ch === "[" || ch === "]" || ch === "+" || ch === "^" || ch === "$") out += `\\${ch}`;
else out += ch;
}
re += out;
}
return new RegExp(`^${re}$`);
}
/** List files matching a glob pattern. */
export class Glob implements Tool {
name = "glob";
description = "List files matching a glob pattern";
parameters = {
type: "object",
properties: {
pattern: { type: "string", description: "Glob pattern to match files (e.g. '**/*.rs')" },
path: { type: "string", description: "Root path to search from (relative to workspace root)" },
},
required: ["pattern", "path"],
};
run(ctx: ToolCtx, args: JsonValue): string {
const patStr = argStr(args, "pattern");
const rel = argStr(args, "path");
const root = resolvePath(ctx.workspaces, rel);
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
throw new Error(`path '${rel}' is not a valid directory`);
}
let re: RegExp;
try {
re = globToRegExp(patStr);
} catch (e) {
throw new Error(`invalid glob pattern '${patStr}': ${(e as Error).message}`);
}
const matches: string[] = [];
const walk = (dir: string) => {
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === ".git" || entry.name === "node_modules" || entry.name === "target") continue;
walk(full);
}
const relPath = path.relative(root, full);
if (re.test(relPath)) {
matches.push(relPath + (entry.isDirectory() ? "/" : ""));
}
}
};
walk(root);
matches.sort();
if (matches.length === 0) {
return `no files match '${patStr}' in ${rel}`;
}
return matches.join("\n");
}
}