/** * 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"); } }