Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions src/adapters/tauri-fs-adapter.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
70 changes: 66 additions & 4 deletions src/adapters/tauri-fs-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,53 @@
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.
*
* 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.
*/
export 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 {
// 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++;
}
}
return new RegExp(`^${re}$`);
}

/**
* Tauri/frontend implementation of FSAdapter.
* Uses FilesystemClient directly for absolute path operations.
Expand Down Expand Up @@ -28,10 +75,25 @@ export class TauriFSAdapter implements FSAdapter {
return entries.map((entry) => entry.name);
}

async glob(_pattern: string, _cwd: string): Promise<string[]> {
// 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<string[]> {
const regex = globToRegExp(pattern);
const results: string[] = [];

const walk = async (dir: string, prefix: string): Promise<void> => {
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<void> {
Expand Down