@edxeth/pi-fff 0.7.2-edxeth.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/query.ts ADDED
@@ -0,0 +1,87 @@
1
+ import path from "node:path";
2
+
3
+ export function normalizePathConstraint(
4
+ pathConstraint: string,
5
+ cwd = process.cwd(),
6
+ ): string | null {
7
+ let trimmed = pathConstraint.trim();
8
+ if (!trimmed) return trimmed;
9
+
10
+ if (path.isAbsolute(trimmed)) {
11
+ const relative = path.relative(cwd, trimmed).replaceAll(path.sep, "/");
12
+ if (relative === "") return null;
13
+ if (relative.startsWith("../") || relative === ".." || path.isAbsolute(relative)) {
14
+ throw new Error(
15
+ `Path constraint must be relative to the workspace: ${pathConstraint}`,
16
+ );
17
+ }
18
+ trimmed = relative;
19
+ }
20
+
21
+ if (trimmed === "." || trimmed === "./") return null;
22
+ // Strip a leading `./` so `./**/*.rs` and `**/*.rs` behave identically.
23
+ if (trimmed.startsWith("./")) trimmed = trimmed.slice(2);
24
+
25
+ // FFF's glob matcher can treat a hidden directory root glob such as
26
+ // `.agents/**` as empty, while the tool contract says this means "inside
27
+ // this directory". Collapse simple trailing recursive directory globs to the
28
+ // directory-prefix constraint understood by the parser. Keep real file globs
29
+ // such as `src/**/*.ts` unchanged.
30
+ const recursiveDir = trimmed.match(/^(.*)\/\*\*(?:\/\*)?$/);
31
+ if (recursiveDir) {
32
+ const dir = recursiveDir[1];
33
+ if (dir && !/[*?[{]/.test(dir)) return `${dir}/`;
34
+ }
35
+
36
+ // Already signals path-constraint syntax to the parser.
37
+ if (trimmed.startsWith("/") || trimmed.endsWith("/")) return trimmed;
38
+ // Globs (`*.ts`, `src/**/*.cc`, `{src,lib}`) are handled by the parser.
39
+ if (/[*?[{]/.test(trimmed)) return trimmed;
40
+ // Filename with extension (`main.rs`, `config.json`) → FilePath constraint.
41
+ const lastSegment = trimmed.split("/").pop() ?? "";
42
+ if (/\.[a-zA-Z][a-zA-Z0-9]{0,9}$/.test(lastSegment)) return trimmed;
43
+ // Bare directory prefix → append `/` so the parser sees a PathSegment.
44
+ return `${trimmed}/`;
45
+ }
46
+
47
+ // Exclusions are emitted as `!<constraint>` tokens, which the Rust parser
48
+ // understands (crates/fff-query-parser/src/parser.rs). We normalize each one
49
+ // the same way as the include path so bare dirs become PathSegment excludes.
50
+ // Tolerate callers passing already-negated forms like `!src/` by stripping
51
+ // the leading `!` before normalizing so we never double-negate (`!!src/`).
52
+ export function normalizeExcludes(
53
+ exclude: string | string[] | undefined,
54
+ cwd = process.cwd(),
55
+ ): string[] {
56
+ if (!exclude) return [];
57
+ const list = Array.isArray(exclude) ? exclude : [exclude];
58
+ const out: string[] = [];
59
+ for (const raw of list) {
60
+ const parts = raw
61
+ .split(/[,\s]+/)
62
+ .map((s) => s.trim())
63
+ .filter(Boolean);
64
+ for (const p of parts) {
65
+ const stripped = p.startsWith("!") ? p.slice(1) : p;
66
+ const normalized = normalizePathConstraint(stripped, cwd);
67
+ if (normalized) out.push(`!${normalized}`);
68
+ }
69
+ }
70
+ return out;
71
+ }
72
+
73
+ export function buildQuery(
74
+ path: string | undefined,
75
+ pattern: string,
76
+ exclude?: string | string[],
77
+ cwd = process.cwd(),
78
+ ): string {
79
+ const parts: string[] = [];
80
+ if (path) {
81
+ const pathConstraint = normalizePathConstraint(path, cwd);
82
+ if (pathConstraint) parts.push(pathConstraint);
83
+ }
84
+ parts.push(...normalizeExcludes(exclude, cwd));
85
+ parts.push(pattern);
86
+ return parts.join(" ");
87
+ }