@ff-labs/fff-bun 0.1.0-nightly.00750c2

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.
@@ -0,0 +1,357 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from "bun:test";
2
+ import { FileFinder } from "./index";
3
+ import { findBinary, getDevBinaryPath } from "./download";
4
+ import { getTriple, getLibExtension, getLibFilename } from "./platform";
5
+
6
+ // Cross-platform path normalization helpers
7
+ const normalizePath = (path: string | null | undefined): string | null => {
8
+ if (!path) return null;
9
+ // Convert backslashes to forward slashes for consistent comparison
10
+ return path.replace(/\\/g, "/");
11
+ };
12
+
13
+ const testDir = process.cwd();
14
+
15
+ describe("Platform Detection", () => {
16
+ test("getTriple returns valid triple", () => {
17
+ const triple = getTriple();
18
+ expect(triple).toMatch(
19
+ /^(x86_64|aarch64|arm)-(apple-darwin|unknown-linux-(gnu|musl)|pc-windows-msvc)$/,
20
+ );
21
+ });
22
+
23
+ test("getLibExtension returns correct extension", () => {
24
+ const ext = getLibExtension();
25
+ const platform = process.platform;
26
+
27
+ if (platform === "darwin") {
28
+ expect(ext).toBe("dylib");
29
+ } else if (platform === "win32") {
30
+ expect(ext).toBe("dll");
31
+ } else {
32
+ expect(ext).toBe("so");
33
+ }
34
+ });
35
+
36
+ test("getLibFilename returns correct filename", () => {
37
+ const filename = getLibFilename();
38
+ const ext = getLibExtension();
39
+
40
+ if (process.platform === "win32") {
41
+ expect(filename).toBe(`fff_c.${ext}`);
42
+ } else {
43
+ expect(filename).toBe(`libfff_c.${ext}`);
44
+ }
45
+ });
46
+ });
47
+
48
+ describe("Binary Detection", () => {
49
+ test("getDevBinaryPath finds local build", () => {
50
+ const devPath = getDevBinaryPath();
51
+ expect(devPath).not.toBeNull();
52
+ // Normalize path for cross-platform comparison (Windows uses backslashes)
53
+ const normalizedPath = normalizePath(devPath);
54
+ expect(normalizedPath).toContain("target/release");
55
+ });
56
+
57
+ test("findBinary returns a path", () => {
58
+ const path = findBinary();
59
+ expect(path).not.toBeNull();
60
+ });
61
+ });
62
+
63
+ describe("FileFinder - Health Check", () => {
64
+ test("healthCheckStatic works without an instance", () => {
65
+ const result = FileFinder.healthCheckStatic();
66
+ expect(result.ok).toBe(true);
67
+
68
+ if (result.ok) {
69
+ expect(result.value.version).toBeDefined();
70
+ expect(result.value.git.available).toBe(true);
71
+ expect(result.value.filePicker.initialized).toBe(false);
72
+ }
73
+ });
74
+ });
75
+
76
+ describe("FileFinder - Full Lifecycle", () => {
77
+ let finder: FileFinder;
78
+
79
+ beforeAll(() => {
80
+ const result = FileFinder.create({ basePath: testDir });
81
+ expect(result.ok).toBe(true);
82
+ if (result.ok) {
83
+ finder = result.value;
84
+ }
85
+ });
86
+
87
+ afterAll(() => {
88
+ finder?.destroy();
89
+ });
90
+
91
+ test("create succeeds with valid path", () => {
92
+ expect(finder).toBeDefined();
93
+ expect(finder.isDestroyed).toBe(false);
94
+ });
95
+
96
+ test("isScanning returns a boolean", () => {
97
+ const scanning = finder.isScanning();
98
+ expect(typeof scanning).toBe("boolean");
99
+ });
100
+
101
+ test("getScanProgress returns valid data", () => {
102
+ const result = finder.getScanProgress();
103
+ expect(result.ok).toBe(true);
104
+
105
+ if (result.ok) {
106
+ expect(typeof result.value.scannedFilesCount).toBe("number");
107
+ expect(typeof result.value.isScanning).toBe("boolean");
108
+ }
109
+ });
110
+
111
+ test("waitForScan completes", () => {
112
+ // Small timeout - scan should be fast or already done
113
+ const result = finder.waitForScan(500);
114
+ expect(result.ok).toBe(true);
115
+ });
116
+
117
+ test("search with empty query returns all files", () => {
118
+ // First check scan progress to see if files were indexed
119
+ const progress = finder.getScanProgress();
120
+ if (progress.ok) {
121
+ }
122
+
123
+ const result = finder.search("");
124
+ expect(result.ok).toBe(true);
125
+
126
+ if (result.ok) {
127
+ if (result.value.items.length > 0) {
128
+ // Log first few paths to see format on Windows
129
+ // Items are strings (file paths), not objects
130
+ const samplePaths = result.value.items
131
+ .slice(0, 3)
132
+ .map((item) =>
133
+ normalizePath(typeof item === "string" ? item : item.relativePath),
134
+ );
135
+ }
136
+ // Empty query should return files (frecency-sorted)
137
+ expect(result.value.totalFiles).toBeGreaterThan(0);
138
+ } else {
139
+ }
140
+ });
141
+
142
+ test("search returns a valid result structure", () => {
143
+ const result = finder.search("Cargo.toml");
144
+ expect(result.ok).toBe(true);
145
+
146
+ if (result.ok) {
147
+ expect(typeof result.value.totalMatched).toBe("number");
148
+ expect(typeof result.value.totalFiles).toBe("number");
149
+ expect(Array.isArray(result.value.items)).toBe(true);
150
+ expect(Array.isArray(result.value.scores)).toBe(true);
151
+ }
152
+ });
153
+
154
+ test("search returns empty for non-matching query", () => {
155
+ const result = finder.search("xyznonexistentfilenamexyz123456");
156
+ expect(result.ok).toBe(true);
157
+
158
+ if (result.ok) {
159
+ expect(result.value.totalMatched).toBe(0);
160
+ expect(result.value.items.length).toBe(0);
161
+ }
162
+ });
163
+
164
+ test("search respects pageSize option", () => {
165
+ const result = finder.search("ts", { pageSize: 3 });
166
+ expect(result.ok).toBe(true);
167
+
168
+ if (result.ok) {
169
+ expect(result.value.items.length).toBeLessThanOrEqual(3);
170
+ }
171
+ });
172
+
173
+ test("liveGrep plain text returns matching lines", () => {
174
+ const result = finder.liveGrep("fff-core", {
175
+ mode: "plain",
176
+ });
177
+ expect(result.ok).toBe(true);
178
+
179
+ if (result.ok) {
180
+ if (result.value.items.length > 0) {
181
+ // Log sample match to verify content on Windows
182
+ const first = result.value.items[0];
183
+ const normalizedPath = normalizePath(first.relativePath);
184
+ }
185
+
186
+ expect(result.value.totalMatched).toBeGreaterThan(0);
187
+ expect(result.value.items.length).toBeGreaterThan(0);
188
+
189
+ const first = result.value.items[0];
190
+ expect(typeof first.relativePath).toBe("string");
191
+ // Normalize path for cross-platform validation
192
+ const normalizedFirstPath = normalizePath(first.relativePath);
193
+ expect(normalizedFirstPath).toBeTruthy();
194
+ expect(typeof first.lineNumber).toBe("number");
195
+ expect(first.lineNumber).toBeGreaterThan(0);
196
+ expect(typeof first.lineContent).toBe("string");
197
+ expect(first.lineContent.toLowerCase()).toContain("fff-core");
198
+ expect(Array.isArray(first.matchRanges)).toBe(true);
199
+ expect(first.matchRanges.length).toBeGreaterThan(0);
200
+
201
+ expect(typeof result.value.totalFilesSearched).toBe("number");
202
+ expect(typeof result.value.totalFiles).toBe("number");
203
+ expect(typeof result.value.filteredFileCount).toBe("number");
204
+ } else {
205
+ }
206
+ });
207
+
208
+ test("liveGrep fuzzy mode returns results with scores", () => {
209
+ // Intentional typo: "depdnency" instead of "dependency" to exercise fuzzy matching
210
+ const result = finder.liveGrep("depdnency", {
211
+ mode: "fuzzy",
212
+ });
213
+ expect(result.ok).toBe(true);
214
+
215
+ if (result.ok) {
216
+ expect(result.value.totalMatched).toBeGreaterThan(0);
217
+ expect(result.value.items.length).toBeGreaterThan(0);
218
+
219
+ const first = result.value.items[0];
220
+ expect(typeof first.relativePath).toBe("string");
221
+ // Normalize path for cross-platform validation
222
+ const normalizedFirstPath = normalizePath(first.relativePath);
223
+ expect(normalizedFirstPath).toBeTruthy();
224
+ expect(typeof first.lineNumber).toBe("number");
225
+ expect(typeof first.lineContent).toBe("string");
226
+ // Fuzzy mode should produce a fuzzyScore on each match
227
+ expect(typeof first.fuzzyScore).toBe("number");
228
+ }
229
+ });
230
+
231
+ test("healthCheck shows initialized state", () => {
232
+ const result = finder.healthCheck();
233
+ expect(result.ok).toBe(true);
234
+
235
+ if (result.ok) {
236
+ expect(result.value.filePicker.initialized).toBe(true);
237
+ expect(result.value.filePicker.basePath).toBeDefined();
238
+ // Normalize basePath for cross-platform comparison
239
+ const normalizedBasePath = normalizePath(
240
+ result.value.filePicker.basePath || "",
241
+ );
242
+ const normalizedTestDir = normalizePath(testDir);
243
+ expect(normalizedBasePath).toBe(normalizedTestDir);
244
+ expect(typeof result.value.filePicker.indexedFiles).toBe("number");
245
+ }
246
+ });
247
+
248
+ test("healthCheck detects git repository", () => {
249
+ const result = finder.healthCheck(testDir);
250
+ expect(result.ok).toBe(true);
251
+
252
+ if (result.ok) {
253
+ expect(result.value.git.available).toBe(true);
254
+ expect(typeof result.value.git.repositoryFound).toBe("boolean");
255
+ }
256
+ });
257
+
258
+ test("destroy and re-create works", () => {
259
+ finder.destroy();
260
+ expect(finder.isDestroyed).toBe(true);
261
+
262
+ const result = FileFinder.create({ basePath: testDir });
263
+ expect(result.ok).toBe(true);
264
+ if (result.ok) {
265
+ finder = result.value;
266
+ }
267
+ expect(finder.isDestroyed).toBe(false);
268
+ });
269
+
270
+ test("multiple instances can coexist", () => {
271
+ const result2 = FileFinder.create({ basePath: testDir });
272
+ expect(result2.ok).toBe(true);
273
+
274
+ if (result2.ok) {
275
+ const finder2 = result2.value;
276
+
277
+ // Both should work independently
278
+ const search1 = finder.search("Cargo");
279
+ const search2 = finder2.search("Cargo");
280
+
281
+ expect(search1.ok).toBe(true);
282
+ expect(search2.ok).toBe(true);
283
+
284
+ // Destroying one should not affect the other
285
+ finder2.destroy();
286
+
287
+ const search3 = finder.search("Cargo");
288
+ expect(search3.ok).toBe(true);
289
+ }
290
+ });
291
+ });
292
+
293
+ describe("FileFinder - Error Handling", () => {
294
+ test("search fails on destroyed instance", () => {
295
+ const createResult = FileFinder.create({ basePath: testDir });
296
+ expect(createResult.ok).toBe(true);
297
+ if (!createResult.ok) return;
298
+
299
+ const f = createResult.value;
300
+ f.destroy();
301
+
302
+ const result = f.search("test");
303
+ expect(result.ok).toBe(false);
304
+ if (!result.ok) {
305
+ expect(result.error).toContain("destroyed");
306
+ }
307
+ });
308
+
309
+ test("getScanProgress fails on destroyed instance", () => {
310
+ const createResult = FileFinder.create({ basePath: testDir });
311
+ expect(createResult.ok).toBe(true);
312
+ if (!createResult.ok) return;
313
+
314
+ const f = createResult.value;
315
+ f.destroy();
316
+
317
+ const result = f.getScanProgress();
318
+ expect(result.ok).toBe(false);
319
+ });
320
+
321
+ test("create fails with invalid path", () => {
322
+ // Use a cross-platform invalid path
323
+ const invalidPath =
324
+ process.platform === "win32"
325
+ ? "C:\\nonexistent\\path\\that\\does\\not\\exist"
326
+ : "/nonexistent/path/that/does/not/exist";
327
+
328
+ const result = FileFinder.create({
329
+ basePath: invalidPath,
330
+ });
331
+
332
+ expect(result.ok).toBe(false);
333
+ if (!result.ok) {
334
+ expect(result.error).toContain("Failed");
335
+ }
336
+ });
337
+ });
338
+
339
+ describe("Result Type Helpers", () => {
340
+ test("ok helper creates success result", async () => {
341
+ const { ok } = await import("./types");
342
+ const result = ok(42);
343
+ expect(result.ok).toBe(true);
344
+ if (result.ok) {
345
+ expect(result.value).toBe(42);
346
+ }
347
+ });
348
+
349
+ test("err helper creates error result", async () => {
350
+ const { err } = await import("./types");
351
+ const result = err<number>("something went wrong");
352
+ expect(result.ok).toBe(false);
353
+ if (!result.ok) {
354
+ expect(result.error).toBe("something went wrong");
355
+ }
356
+ });
357
+ });
package/src/index.ts ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * fff - Fast File Finder
3
+ *
4
+ * High-performance fuzzy file finder for Bun, powered by Rust.
5
+ * Perfect for LLM agent tools that need to search through codebases.
6
+ *
7
+ * Each `FileFinder` instance is backed by an independent native file picker.
8
+ * Create as many as you need and destroy them when done.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { FileFinder } from "fff";
13
+ *
14
+ * // Create a file finder instance
15
+ * const result = FileFinder.create({ basePath: "/path/to/project" });
16
+ * if (!result.ok) {
17
+ * console.error(result.error);
18
+ * process.exit(1);
19
+ * }
20
+ * const finder = result.value;
21
+ *
22
+ * // Wait for initial scan
23
+ * finder.waitForScan(5000);
24
+ *
25
+ * // Search for files
26
+ * const search = finder.search("main.ts");
27
+ * if (search.ok) {
28
+ * for (const item of search.value.items) {
29
+ * console.log(item.relativePath);
30
+ * }
31
+ * }
32
+ *
33
+ * // Track file access (for frecency)
34
+ * finder.trackAccess("/path/to/project/src/main.ts");
35
+ *
36
+ * // Cleanup when done
37
+ * finder.destroy();
38
+ * ```
39
+ *
40
+ * @packageDocumentation
41
+ */
42
+
43
+ // Main API
44
+ export { FileFinder } from "./finder";
45
+
46
+ // Types
47
+ export type {
48
+ Result,
49
+ InitOptions,
50
+ SearchOptions,
51
+ FileItem,
52
+ Score,
53
+ Location,
54
+ SearchResult,
55
+ ScanProgress,
56
+ HealthCheck,
57
+ DbHealth,
58
+ GrepMode,
59
+ GrepOptions,
60
+ GrepMatch,
61
+ GrepResult,
62
+ GrepCursor,
63
+ } from "./types";
64
+
65
+ // Result helpers
66
+ export { ok, err } from "./types";
67
+
68
+ // Binary management (for CLI tools)
69
+ export {
70
+ downloadBinary,
71
+ ensureBinary,
72
+ binaryExists,
73
+ getBinaryPath,
74
+ findBinary,
75
+ } from "./download";
76
+
77
+ // Platform utilities
78
+ export {
79
+ getTriple,
80
+ getLibExtension,
81
+ getLibFilename,
82
+ getNpmPackageName,
83
+ } from "./platform";
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Platform detection utilities for downloading the correct binary
3
+ */
4
+
5
+ import { execSync } from "node:child_process";
6
+
7
+ /**
8
+ * Get the platform triple (e.g., "x86_64-unknown-linux-gnu")
9
+ */
10
+ export function getTriple(): string {
11
+ const platform = process.platform;
12
+ const arch = process.arch;
13
+
14
+ let osName: string;
15
+ if (platform === "darwin") {
16
+ osName = "apple-darwin";
17
+ } else if (platform === "linux") {
18
+ osName = detectLinuxLibc();
19
+ } else if (platform === "win32") {
20
+ osName = "pc-windows-msvc";
21
+ } else {
22
+ throw new Error(`Unsupported platform: ${platform}`);
23
+ }
24
+
25
+ const archName = normalizeArch(arch);
26
+ return `${archName}-${osName}`;
27
+ }
28
+
29
+ /**
30
+ * Detect whether we're on musl or glibc Linux
31
+ */
32
+ function detectLinuxLibc(): string {
33
+ try {
34
+ const lddOutput = execSync("ldd --version 2>&1", {
35
+ encoding: "utf-8",
36
+ timeout: 5000,
37
+ });
38
+ if (lddOutput.toLowerCase().includes("musl")) {
39
+ return "unknown-linux-musl";
40
+ }
41
+ } catch {
42
+ // ldd failed, assume glibc
43
+ }
44
+ return "unknown-linux-gnu";
45
+ }
46
+
47
+ /**
48
+ * Normalize architecture name to Rust target format
49
+ */
50
+ function normalizeArch(arch: string): string {
51
+ switch (arch) {
52
+ case "x64":
53
+ case "amd64":
54
+ return "x86_64";
55
+ case "arm64":
56
+ return "aarch64";
57
+ case "arm":
58
+ return "arm";
59
+ default:
60
+ throw new Error(`Unsupported architecture: ${arch}`);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Get the library file extension for the current platform
66
+ */
67
+ export function getLibExtension(): "dylib" | "so" | "dll" {
68
+ switch (process.platform) {
69
+ case "darwin":
70
+ return "dylib";
71
+ case "win32":
72
+ return "dll";
73
+ default:
74
+ return "so";
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Get the library filename prefix (empty on Windows)
80
+ */
81
+ export function getLibPrefix(): string {
82
+ return process.platform === "win32" ? "" : "lib";
83
+ }
84
+
85
+ /**
86
+ * Get the full library filename for the current platform
87
+ */
88
+ export function getLibFilename(): string {
89
+ const prefix = getLibPrefix();
90
+ const ext = getLibExtension();
91
+ return `${prefix}fff_c.${ext}`;
92
+ }
93
+
94
+ /**
95
+ * Map from Rust target triple to npm platform package name
96
+ */
97
+ const TRIPLE_TO_NPM_PACKAGE: Record<string, string> = {
98
+ "aarch64-apple-darwin": "@ff-labs/fff-bun-darwin-arm64",
99
+ "x86_64-apple-darwin": "@ff-labs/fff-bun-darwin-x64",
100
+ "x86_64-unknown-linux-gnu": "@ff-labs/fff-bun-linux-x64-gnu",
101
+ "aarch64-unknown-linux-gnu": "@ff-labs/fff-bun-linux-arm64-gnu",
102
+ "x86_64-unknown-linux-musl": "@ff-labs/fff-bun-linux-x64-musl",
103
+ "aarch64-unknown-linux-musl": "@ff-labs/fff-bun-linux-arm64-musl",
104
+ "x86_64-pc-windows-msvc": "@ff-labs/fff-bun-win32-x64",
105
+ "aarch64-pc-windows-msvc": "@ff-labs/fff-bun-win32-arm64",
106
+ };
107
+
108
+ /**
109
+ * Get the npm package name for the current platform's native binary.
110
+ *
111
+ * @returns Package name like "@ff-labs/fff-bun-darwin-arm64"
112
+ * @throws If the current platform is not supported
113
+ */
114
+ export function getNpmPackageName(): string {
115
+ const triple = getTriple();
116
+ const packageName = TRIPLE_TO_NPM_PACKAGE[triple];
117
+ if (!packageName) {
118
+ throw new Error(`No npm package available for platform: ${triple}`);
119
+ }
120
+ return packageName;
121
+ }