From 0ec7deb5fc7613efdf85e82cf593e08fd1310177 Mon Sep 17 00:00:00 2001 From: TechTide AI Date: Thu, 21 May 2026 18:30:42 -0400 Subject: [PATCH 1/2] feat: implement glob in TauriFSAdapter using recursive listDir walk Replace the TODO stub with a working implementation that recursively walks directories via the existing listDir IPC call and matches file paths against a simple glob-to-regex converter supporting *, **, and ? wildcards. No new dependencies required. --- src/adapters/tauri-fs-adapter.ts | 61 +++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/src/adapters/tauri-fs-adapter.ts b/src/adapters/tauri-fs-adapter.ts index a636b8f..ea0740e 100644 --- a/src/adapters/tauri-fs-adapter.ts +++ b/src/adapters/tauri-fs-adapter.ts @@ -1,6 +1,44 @@ import type { FSAdapter, DirEntry } from "../../core/services/fs-adapter"; import { FilesystemClient } from "@/lib/filesystem-client"; +/** + * Convert a simple glob pattern to a RegExp. + * + * Supports: + * - `*` → match anything except path separators + * - `**` → match anything including path separators (recursive) + * - `?` → match a single character + * + * This intentionally does NOT cover brace expansion or advanced glob + * features — just the patterns needed for file discovery. + */ +function globToRegExp(pattern: string): RegExp { + let re = ""; + let i = 0; + while (i < pattern.length) { + const ch = pattern[i]; + if (ch === "*" && pattern[i + 1] === "*") { + // `**/` or trailing `**` + re += ".*"; + i += 2; + if (pattern[i] === "/") i++; // consume trailing slash + } else if (ch === "*") { + re += "[^/]*"; + i++; + } else if (ch === "?") { + re += "[^/]"; + i++; + } else if (ch === ".") { + re += "\\."; + i++; + } else { + re += ch; + i++; + } + } + return new RegExp(`^${re}$`); +} + /** * Tauri/frontend implementation of FSAdapter. * Uses FilesystemClient directly for absolute path operations. @@ -28,10 +66,25 @@ export class TauriFSAdapter implements FSAdapter { return entries.map((entry) => entry.name); } - async glob(_pattern: string, _cwd: string): Promise { - // TODO: Implement glob using Tauri fs if needed - // For now, skill discovery doesn't use glob - throw new Error("glob not implemented in TauriFSAdapter"); + async glob(pattern: string, cwd: string): Promise { + const regex = globToRegExp(pattern); + const results: string[] = []; + + const walk = async (dir: string, prefix: string): Promise => { + const entries = await this.fsClient.listDir(dir); + for (const entry of entries) { + const relative = prefix ? `${prefix}/${entry.name}` : entry.name; + if (entry.isFile && regex.test(relative)) { + results.push(relative); + } + if (entry.isDirectory) { + await walk(this.fsClient.joinPath(dir, entry.name), relative); + } + } + }; + + await walk(cwd, ""); + return results; } async mkdir(path: string, _recursive?: boolean): Promise { From 3a598324a8d237380f029aeb9a5dd7e8e5aa395f Mon Sep 17 00:00:00 2001 From: TechTide AI Date: Tue, 23 Jun 2026 01:21:56 -0400 Subject: [PATCH 2/2] fix: escape regex metacharacters in TauriFSAdapter glob translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit globToRegExp only escaped '.', so glob patterns containing regex metacharacters (e.g. 'a+b/*.ts', '(x).js') were translated into invalid or unintended RegExps — '+' became a quantifier, '(...)' a capture group, and unbalanced '(', '[', '{' could throw. Escape the full metacharacter class so non-wildcard characters match literally, and export the helper. Add a vitest covering metacharacter inputs and the existing *, **, ? wildcard behavior. --- src/adapters/tauri-fs-adapter.test.ts | 73 +++++++++++++++++++++++++++ src/adapters/tauri-fs-adapter.ts | 19 +++++-- 2 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 src/adapters/tauri-fs-adapter.test.ts diff --git a/src/adapters/tauri-fs-adapter.test.ts b/src/adapters/tauri-fs-adapter.test.ts new file mode 100644 index 0000000..1bea01b --- /dev/null +++ b/src/adapters/tauri-fs-adapter.test.ts @@ -0,0 +1,73 @@ +// @vitest-environment node +import { describe, it, expect } from "vitest"; +import { globToRegExp } from "./tauri-fs-adapter"; + +describe("globToRegExp", () => { + describe("wildcards", () => { + it("matches `*` against a single path segment only", () => { + const re = globToRegExp("*.ts"); + expect(re.test("index.ts")).toBe(true); + expect(re.test("index.js")).toBe(false); + // `*` must not cross a path separator + expect(re.test("src/index.ts")).toBe(false); + }); + + it("matches `**` recursively across path separators", () => { + const re = globToRegExp("**/*.ts"); + expect(re.test("index.ts")).toBe(true); + expect(re.test("src/index.ts")).toBe(true); + expect(re.test("src/lib/util.ts")).toBe(true); + expect(re.test("src/util.js")).toBe(false); + }); + + it("matches `?` against exactly one non-separator character", () => { + const re = globToRegExp("src/index.?s"); + expect(re.test("src/index.ts")).toBe(true); + expect(re.test("src/index.js")).toBe(true); + expect(re.test("src/index.tsx")).toBe(false); + expect(re.test("src/index./s")).toBe(false); + }); + }); + + describe("regex metacharacters are treated literally", () => { + it("does not crash and matches literally on `+` (a regex quantifier)", () => { + // `a+b/*.ts` — the `+` must be a literal plus, not a quantifier. + const re = globToRegExp("a+b/*.ts"); + expect(re.test("a+b/index.ts")).toBe(true); + // Without escaping, `a+` would match `aaa`; assert it does NOT. + expect(re.test("aaab/index.ts")).toBe(false); + expect(re.test("ab/index.ts")).toBe(false); + }); + + it("treats parentheses as literal, not a capture group", () => { + // `(x).js` — without escaping this becomes a capture group + `.` wildcard. + const re = globToRegExp("(x).js"); + expect(re.test("(x).js")).toBe(true); + // The `.` must be literal: `xXjs` (where X is any char) must NOT match. + expect(re.test("xZjs")).toBe(false); + expect(re.test("x.js")).toBe(false); + }); + + it("does not throw on characters that would form invalid regex", () => { + // Unbalanced/structural metacharacters would throw if passed raw to RegExp. + const tricky = ["a(b.ts", "a)b.ts", "a[b.ts", "a]b.ts", "a{b.ts", "a$b^c.ts", "a|b.ts"]; + for (const pattern of tricky) { + expect(() => globToRegExp(pattern)).not.toThrow(); + // Each pattern should match itself literally. + expect(globToRegExp(pattern).test(pattern)).toBe(true); + } + }); + + it("escapes `.` so it is not a wildcard", () => { + const re = globToRegExp("file.ts"); + expect(re.test("file.ts")).toBe(true); + expect(re.test("fileXts")).toBe(false); + }); + + it("anchors the full string", () => { + const re = globToRegExp("*.ts"); + expect(re.test("a.ts.bak")).toBe(false); + expect(re.test("prefix-a.ts")).toBe(true); + }); + }); +}); diff --git a/src/adapters/tauri-fs-adapter.ts b/src/adapters/tauri-fs-adapter.ts index ea0740e..9700fd0 100644 --- a/src/adapters/tauri-fs-adapter.ts +++ b/src/adapters/tauri-fs-adapter.ts @@ -11,8 +11,13 @@ import { FilesystemClient } from "@/lib/filesystem-client"; * * This intentionally does NOT cover brace expansion or advanced glob * features — just the patterns needed for file discovery. + * + * All non-wildcard characters are treated literally: any regex + * metacharacter (e.g. `.`, `+`, `(`, `)`, `[`, `]`, `{`, `}`, `^`, `$`, + * `|`, `\`) is escaped so patterns like `a+b/*.ts` or `(x).js` are matched + * as literal text instead of producing an invalid or unintended RegExp. */ -function globToRegExp(pattern: string): RegExp { +export function globToRegExp(pattern: string): RegExp { let re = ""; let i = 0; while (i < pattern.length) { @@ -28,11 +33,15 @@ function globToRegExp(pattern: string): RegExp { } else if (ch === "?") { re += "[^/]"; i++; - } else if (ch === ".") { - re += "\\."; - i++; } else { - re += ch; + // Escape every regex metacharacter so the glob segment is matched + // literally. `/` is not special in a RegExp body but is harmless to + // leave unescaped; everything in this class needs a backslash. + if (/[.+^${}()|[\]\\]/.test(ch)) { + re += "\\" + ch; + } else { + re += ch; + } i++; } }