@ff-labs/bun 0.1.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/README.md +200 -0
- package/bin/libfff_c.dylib +0 -0
- package/package.json +74 -0
- package/scripts/cli.cjs +16 -0
- package/scripts/cli.ts +131 -0
- package/scripts/postinstall.ts +37 -0
- package/src/download.ts +316 -0
- package/src/ffi.ts +377 -0
- package/src/finder.ts +328 -0
- package/src/index.test.ts +263 -0
- package/src/index.ts +69 -0
- package/src/platform.ts +92 -0
- package/src/types.ts +260 -0
package/src/finder.ts
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileFinder - High-level API for the fff file finder
|
|
3
|
+
*
|
|
4
|
+
* This class provides a type-safe, ergonomic API for file finding operations.
|
|
5
|
+
* All methods return Result types for explicit error handling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
ffiInit,
|
|
10
|
+
ffiDestroy,
|
|
11
|
+
ffiSearch,
|
|
12
|
+
ffiScanFiles,
|
|
13
|
+
ffiIsScanning,
|
|
14
|
+
ffiGetScanProgress,
|
|
15
|
+
ffiWaitForScan,
|
|
16
|
+
ffiRestartIndex,
|
|
17
|
+
ffiTrackAccess,
|
|
18
|
+
ffiRefreshGitStatus,
|
|
19
|
+
ffiTrackQuery,
|
|
20
|
+
ffiGetHistoricalQuery,
|
|
21
|
+
ffiHealthCheck,
|
|
22
|
+
ffiShortenPath,
|
|
23
|
+
ensureLoaded,
|
|
24
|
+
isAvailable,
|
|
25
|
+
} from "./ffi";
|
|
26
|
+
|
|
27
|
+
import type {
|
|
28
|
+
Result,
|
|
29
|
+
InitOptions,
|
|
30
|
+
SearchOptions,
|
|
31
|
+
SearchResult,
|
|
32
|
+
ScanProgress,
|
|
33
|
+
HealthCheck,
|
|
34
|
+
} from "./types";
|
|
35
|
+
|
|
36
|
+
import { err, toInternalInitOptions, toInternalSearchOptions } from "./types";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* FileFinder - Fast file finder with fuzzy search
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* import { FileFinder } from "fff";
|
|
44
|
+
*
|
|
45
|
+
* // Initialize
|
|
46
|
+
* const result = FileFinder.init({ basePath: "/path/to/project" });
|
|
47
|
+
* if (!result.ok) {
|
|
48
|
+
* console.error(result.error);
|
|
49
|
+
* process.exit(1);
|
|
50
|
+
* }
|
|
51
|
+
*
|
|
52
|
+
* // Wait for initial scan
|
|
53
|
+
* FileFinder.waitForScan(5000);
|
|
54
|
+
*
|
|
55
|
+
* // Search for files
|
|
56
|
+
* const search = FileFinder.search("main.ts");
|
|
57
|
+
* if (search.ok) {
|
|
58
|
+
* for (const item of search.value.items) {
|
|
59
|
+
* console.log(item.relativePath);
|
|
60
|
+
* }
|
|
61
|
+
* }
|
|
62
|
+
*
|
|
63
|
+
* // Cleanup
|
|
64
|
+
* FileFinder.destroy();
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export class FileFinder {
|
|
68
|
+
private static initialized = false;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Initialize the file finder with the given options.
|
|
72
|
+
*
|
|
73
|
+
* @param options - Initialization options
|
|
74
|
+
* @returns Result indicating success or failure
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* // Basic initialization
|
|
79
|
+
* FileFinder.init({ basePath: "/path/to/project" });
|
|
80
|
+
*
|
|
81
|
+
* // With custom database paths
|
|
82
|
+
* FileFinder.init({
|
|
83
|
+
* basePath: "/path/to/project",
|
|
84
|
+
* frecencyDbPath: "/custom/frecency.mdb",
|
|
85
|
+
* historyDbPath: "/custom/history.mdb",
|
|
86
|
+
* });
|
|
87
|
+
*
|
|
88
|
+
* // Minimal mode (no databases)
|
|
89
|
+
* FileFinder.init({ basePath: "/path/to/project", skipDatabases: true });
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
static init(options: InitOptions): Result<void> {
|
|
93
|
+
const internalOpts = toInternalInitOptions(options);
|
|
94
|
+
const result = ffiInit(JSON.stringify(internalOpts));
|
|
95
|
+
|
|
96
|
+
if (result.ok) {
|
|
97
|
+
this.initialized = true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Destroy and clean up all resources.
|
|
105
|
+
*
|
|
106
|
+
* Call this when you're done using the file finder to free memory
|
|
107
|
+
* and stop background file watching.
|
|
108
|
+
*/
|
|
109
|
+
static destroy(): Result<void> {
|
|
110
|
+
const result = ffiDestroy();
|
|
111
|
+
if (result.ok) {
|
|
112
|
+
this.initialized = false;
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Search for files matching the query.
|
|
119
|
+
*
|
|
120
|
+
* The query supports fuzzy matching and special syntax:
|
|
121
|
+
* - `foo bar` - Match files containing "foo" and "bar"
|
|
122
|
+
* - `src/` - Match files in src directory
|
|
123
|
+
* - `file.ts:42` - Match file.ts with line 42
|
|
124
|
+
* - `file.ts:42:10` - Match file.ts with line 42, column 10
|
|
125
|
+
*
|
|
126
|
+
* @param query - Search query string
|
|
127
|
+
* @param options - Search options
|
|
128
|
+
* @returns Search results with matched files and scores
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```typescript
|
|
132
|
+
* const result = FileFinder.search("main.ts", { pageSize: 10 });
|
|
133
|
+
* if (result.ok) {
|
|
134
|
+
* console.log(`Found ${result.value.totalMatched} files`);
|
|
135
|
+
* for (const item of result.value.items) {
|
|
136
|
+
* console.log(item.relativePath);
|
|
137
|
+
* }
|
|
138
|
+
* }
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
static search(query: string, options?: SearchOptions): Result<SearchResult> {
|
|
142
|
+
if (!this.initialized) {
|
|
143
|
+
return err("FileFinder not initialized. Call FileFinder.init() first.");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const internalOpts = toInternalSearchOptions(options);
|
|
147
|
+
const result = ffiSearch(query, JSON.stringify(internalOpts));
|
|
148
|
+
|
|
149
|
+
if (!result.ok) {
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// The FFI returns the search result already parsed
|
|
154
|
+
return result as Result<SearchResult>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Trigger a rescan of the indexed directory.
|
|
159
|
+
*
|
|
160
|
+
* This is useful after major file system changes that the
|
|
161
|
+
* background watcher might have missed.
|
|
162
|
+
*/
|
|
163
|
+
static scanFiles(): Result<void> {
|
|
164
|
+
if (!this.initialized) {
|
|
165
|
+
return err("FileFinder not initialized. Call FileFinder.init() first.");
|
|
166
|
+
}
|
|
167
|
+
return ffiScanFiles();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Check if a scan is currently in progress.
|
|
172
|
+
*/
|
|
173
|
+
static isScanning(): boolean {
|
|
174
|
+
if (!this.initialized) return false;
|
|
175
|
+
return ffiIsScanning();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get the current scan progress.
|
|
180
|
+
*/
|
|
181
|
+
static getScanProgress(): Result<ScanProgress> {
|
|
182
|
+
if (!this.initialized) {
|
|
183
|
+
return err("FileFinder not initialized. Call FileFinder.init() first.");
|
|
184
|
+
}
|
|
185
|
+
return ffiGetScanProgress() as Result<ScanProgress>;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Wait for the initial file scan to complete.
|
|
190
|
+
*
|
|
191
|
+
* @param timeoutMs - Maximum time to wait in milliseconds (default: 5000)
|
|
192
|
+
* @returns true if scan completed, false if timed out
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* FileFinder.init({ basePath: "/path/to/project" });
|
|
197
|
+
* const completed = FileFinder.waitForScan(10000);
|
|
198
|
+
* if (!completed.ok || !completed.value) {
|
|
199
|
+
* console.warn("Scan did not complete in time");
|
|
200
|
+
* }
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
static waitForScan(timeoutMs: number = 5000): Result<boolean> {
|
|
204
|
+
if (!this.initialized) {
|
|
205
|
+
return err("FileFinder not initialized. Call FileFinder.init() first.");
|
|
206
|
+
}
|
|
207
|
+
return ffiWaitForScan(timeoutMs);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Change the indexed directory to a new path.
|
|
212
|
+
*
|
|
213
|
+
* This stops the current file watcher and starts indexing the new directory.
|
|
214
|
+
*
|
|
215
|
+
* @param newPath - New directory path to index
|
|
216
|
+
*/
|
|
217
|
+
static restartIndex(newPath: string): Result<void> {
|
|
218
|
+
if (!this.initialized) {
|
|
219
|
+
return err("FileFinder not initialized. Call FileFinder.init() first.");
|
|
220
|
+
}
|
|
221
|
+
return ffiRestartIndex(newPath);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Track file access for frecency scoring.
|
|
226
|
+
*
|
|
227
|
+
* Call this when a user opens a file to improve future search rankings.
|
|
228
|
+
*
|
|
229
|
+
* @param filePath - Absolute path to the accessed file
|
|
230
|
+
*/
|
|
231
|
+
static trackAccess(filePath: string): Result<boolean> {
|
|
232
|
+
if (!this.initialized) {
|
|
233
|
+
return { ok: true, value: false };
|
|
234
|
+
}
|
|
235
|
+
return ffiTrackAccess(filePath);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Refresh the git status cache.
|
|
240
|
+
*
|
|
241
|
+
* @returns Number of files with updated git status
|
|
242
|
+
*/
|
|
243
|
+
static refreshGitStatus(): Result<number> {
|
|
244
|
+
if (!this.initialized) {
|
|
245
|
+
return err("FileFinder not initialized. Call FileFinder.init() first.");
|
|
246
|
+
}
|
|
247
|
+
return ffiRefreshGitStatus();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Track query completion for smart suggestions.
|
|
252
|
+
*
|
|
253
|
+
* Call this when a user selects a file from search results.
|
|
254
|
+
* This helps improve future search rankings for similar queries.
|
|
255
|
+
*
|
|
256
|
+
* @param query - The search query that was used
|
|
257
|
+
* @param selectedFilePath - The file path that was selected
|
|
258
|
+
*/
|
|
259
|
+
static trackQuery(query: string, selectedFilePath: string): Result<boolean> {
|
|
260
|
+
if (!this.initialized) {
|
|
261
|
+
return { ok: true, value: false };
|
|
262
|
+
}
|
|
263
|
+
return ffiTrackQuery(query, selectedFilePath);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get a historical query by offset.
|
|
268
|
+
*
|
|
269
|
+
* @param offset - Offset from most recent (0 = most recent)
|
|
270
|
+
* @returns The historical query string, or null if not found
|
|
271
|
+
*/
|
|
272
|
+
static getHistoricalQuery(offset: number): Result<string | null> {
|
|
273
|
+
if (!this.initialized) {
|
|
274
|
+
return { ok: true, value: null };
|
|
275
|
+
}
|
|
276
|
+
return ffiGetHistoricalQuery(offset);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get health check information.
|
|
281
|
+
*
|
|
282
|
+
* Useful for debugging and verifying the file finder is working correctly.
|
|
283
|
+
*
|
|
284
|
+
* @param testPath - Optional path to test git repository detection
|
|
285
|
+
*/
|
|
286
|
+
static healthCheck(testPath?: string): Result<HealthCheck> {
|
|
287
|
+
return ffiHealthCheck(testPath || "") as Result<HealthCheck>;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Shorten a file path for display.
|
|
292
|
+
*
|
|
293
|
+
* @param path - Path to shorten
|
|
294
|
+
* @param maxSize - Maximum length
|
|
295
|
+
* @param strategy - Shortening strategy: 'middle_number', 'beginning', or 'end'
|
|
296
|
+
*/
|
|
297
|
+
static shortenPath(
|
|
298
|
+
path: string,
|
|
299
|
+
maxSize: number,
|
|
300
|
+
strategy: "middle_number" | "beginning" | "end" = "middle_number"
|
|
301
|
+
): Result<string> {
|
|
302
|
+
return ffiShortenPath(path, maxSize, strategy);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Check if the native library is available.
|
|
307
|
+
*/
|
|
308
|
+
static isAvailable(): boolean {
|
|
309
|
+
return isAvailable();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Ensure the native library is loaded.
|
|
314
|
+
*
|
|
315
|
+
* This will download the binary if needed and load it.
|
|
316
|
+
* Useful for preloading before first use.
|
|
317
|
+
*/
|
|
318
|
+
static async ensureLoaded(): Promise<void> {
|
|
319
|
+
return ensureLoaded();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Check if the file finder is initialized.
|
|
324
|
+
*/
|
|
325
|
+
static isInitialized(): boolean {
|
|
326
|
+
return this.initialized;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
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
|
+
// Use a single shared instance to avoid thread cleanup issues
|
|
7
|
+
const testDir = process.cwd();
|
|
8
|
+
|
|
9
|
+
describe("Platform Detection", () => {
|
|
10
|
+
test("getTriple returns valid triple", () => {
|
|
11
|
+
const triple = getTriple();
|
|
12
|
+
expect(triple).toMatch(
|
|
13
|
+
/^(x86_64|aarch64|arm)-(apple-darwin|unknown-linux-(gnu|musl)|pc-windows-msvc)$/,
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("getLibExtension returns correct extension", () => {
|
|
18
|
+
const ext = getLibExtension();
|
|
19
|
+
const platform = process.platform;
|
|
20
|
+
|
|
21
|
+
if (platform === "darwin") {
|
|
22
|
+
expect(ext).toBe("dylib");
|
|
23
|
+
} else if (platform === "win32") {
|
|
24
|
+
expect(ext).toBe("dll");
|
|
25
|
+
} else {
|
|
26
|
+
expect(ext).toBe("so");
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("getLibFilename returns correct filename", () => {
|
|
31
|
+
const filename = getLibFilename();
|
|
32
|
+
const ext = getLibExtension();
|
|
33
|
+
|
|
34
|
+
if (process.platform === "win32") {
|
|
35
|
+
expect(filename).toBe(`fff_c.${ext}`);
|
|
36
|
+
} else {
|
|
37
|
+
expect(filename).toBe(`libfff_c.${ext}`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("Binary Detection", () => {
|
|
43
|
+
test("getDevBinaryPath finds local build", () => {
|
|
44
|
+
const devPath = getDevBinaryPath();
|
|
45
|
+
expect(devPath).not.toBeNull();
|
|
46
|
+
expect(devPath).toContain("target/release");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("findBinary returns a path", () => {
|
|
50
|
+
const path = findBinary();
|
|
51
|
+
expect(path).not.toBeNull();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("FileFinder - Availability", () => {
|
|
56
|
+
test("isAvailable returns true when binary exists", () => {
|
|
57
|
+
const available = FileFinder.isAvailable();
|
|
58
|
+
expect(available).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("FileFinder - Health Check (before init)", () => {
|
|
63
|
+
test("healthCheck works before initialization", () => {
|
|
64
|
+
// Make sure we start fresh
|
|
65
|
+
FileFinder.destroy();
|
|
66
|
+
|
|
67
|
+
const result = FileFinder.healthCheck();
|
|
68
|
+
expect(result.ok).toBe(true);
|
|
69
|
+
|
|
70
|
+
if (result.ok) {
|
|
71
|
+
expect(result.value.version).toBeDefined();
|
|
72
|
+
expect(result.value.git.available).toBe(true);
|
|
73
|
+
expect(result.value.filePicker.initialized).toBe(false);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("FileFinder - Full Lifecycle", () => {
|
|
79
|
+
// Single beforeAll/afterAll for the entire test suite to avoid repeated init/destroy
|
|
80
|
+
beforeAll(() => {
|
|
81
|
+
FileFinder.destroy(); // Clean any previous state
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterAll(() => {
|
|
85
|
+
FileFinder.destroy();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("init succeeds with valid path", () => {
|
|
89
|
+
const result = FileFinder.init({
|
|
90
|
+
basePath: testDir,
|
|
91
|
+
skipDatabases: true,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(result.ok).toBe(true);
|
|
95
|
+
expect(FileFinder.isInitialized()).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("isScanning returns a boolean", () => {
|
|
99
|
+
const scanning = FileFinder.isScanning();
|
|
100
|
+
expect(typeof scanning).toBe("boolean");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("getScanProgress returns valid data", () => {
|
|
104
|
+
const result = FileFinder.getScanProgress();
|
|
105
|
+
expect(result.ok).toBe(true);
|
|
106
|
+
|
|
107
|
+
if (result.ok) {
|
|
108
|
+
expect(typeof result.value.scannedFilesCount).toBe("number");
|
|
109
|
+
expect(typeof result.value.isScanning).toBe("boolean");
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("waitForScan completes", () => {
|
|
114
|
+
// Small timeout - scan should be fast or already done
|
|
115
|
+
const result = FileFinder.waitForScan(500);
|
|
116
|
+
expect(result.ok).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("search with empty query returns all files", () => {
|
|
120
|
+
const result = FileFinder.search("");
|
|
121
|
+
expect(result.ok).toBe(true);
|
|
122
|
+
|
|
123
|
+
if (result.ok) {
|
|
124
|
+
// Empty query should return files (frecency-sorted)
|
|
125
|
+
expect(result.value.totalFiles).toBeGreaterThan(0);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("search returns a valid result structure", () => {
|
|
130
|
+
const result = FileFinder.search("Cargo.toml");
|
|
131
|
+
expect(result.ok).toBe(true);
|
|
132
|
+
|
|
133
|
+
if (result.ok) {
|
|
134
|
+
expect(typeof result.value.totalMatched).toBe("number");
|
|
135
|
+
expect(typeof result.value.totalFiles).toBe("number");
|
|
136
|
+
expect(Array.isArray(result.value.items)).toBe(true);
|
|
137
|
+
expect(Array.isArray(result.value.scores)).toBe(true);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("search returns empty for non-matching query", () => {
|
|
142
|
+
const result = FileFinder.search("xyznonexistentfilenamexyz123456");
|
|
143
|
+
expect(result.ok).toBe(true);
|
|
144
|
+
|
|
145
|
+
if (result.ok) {
|
|
146
|
+
expect(result.value.totalMatched).toBe(0);
|
|
147
|
+
expect(result.value.items.length).toBe(0);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("search respects pageSize option", () => {
|
|
152
|
+
const result = FileFinder.search("ts", { pageSize: 3 });
|
|
153
|
+
expect(result.ok).toBe(true);
|
|
154
|
+
|
|
155
|
+
if (result.ok) {
|
|
156
|
+
expect(result.value.items.length).toBeLessThanOrEqual(3);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("healthCheck shows initialized state", () => {
|
|
161
|
+
const result = FileFinder.healthCheck();
|
|
162
|
+
expect(result.ok).toBe(true);
|
|
163
|
+
|
|
164
|
+
if (result.ok) {
|
|
165
|
+
expect(result.value.filePicker.initialized).toBe(true);
|
|
166
|
+
expect(result.value.filePicker.basePath).toBeDefined();
|
|
167
|
+
expect(typeof result.value.filePicker.indexedFiles).toBe("number");
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("healthCheck detects git repository", () => {
|
|
172
|
+
const result = FileFinder.healthCheck(testDir);
|
|
173
|
+
expect(result.ok).toBe(true);
|
|
174
|
+
|
|
175
|
+
if (result.ok) {
|
|
176
|
+
expect(result.value.git.available).toBe(true);
|
|
177
|
+
expect(typeof result.value.git.repositoryFound).toBe("boolean");
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("destroy and re-init works", () => {
|
|
182
|
+
FileFinder.destroy();
|
|
183
|
+
expect(FileFinder.isInitialized()).toBe(false);
|
|
184
|
+
|
|
185
|
+
const result = FileFinder.init({
|
|
186
|
+
basePath: testDir,
|
|
187
|
+
skipDatabases: true,
|
|
188
|
+
});
|
|
189
|
+
expect(result.ok).toBe(true);
|
|
190
|
+
expect(FileFinder.isInitialized()).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("FileFinder - Utilities (stateless)", () => {
|
|
195
|
+
test("shortenPath shortens long paths", () => {
|
|
196
|
+
const longPath = "/very/long/path/to/some/deeply/nested/file.ts";
|
|
197
|
+
const result = FileFinder.shortenPath(longPath, 20, "middle_number");
|
|
198
|
+
|
|
199
|
+
expect(result.ok).toBe(true);
|
|
200
|
+
if (result.ok) {
|
|
201
|
+
expect(result.value.length).toBeLessThanOrEqual(25);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("shortenPath handles short paths", () => {
|
|
206
|
+
const shortPath = "file.ts";
|
|
207
|
+
const result = FileFinder.shortenPath(shortPath, 50, "middle_number");
|
|
208
|
+
|
|
209
|
+
expect(result.ok).toBe(true);
|
|
210
|
+
if (result.ok) {
|
|
211
|
+
expect(result.value).toBe(shortPath);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("FileFinder - Error Handling", () => {
|
|
217
|
+
test("search fails when not initialized", () => {
|
|
218
|
+
FileFinder.destroy();
|
|
219
|
+
|
|
220
|
+
const result = FileFinder.search("test");
|
|
221
|
+
expect(result.ok).toBe(false);
|
|
222
|
+
if (!result.ok) {
|
|
223
|
+
expect(result.error).toContain("not initialized");
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("getScanProgress fails when not initialized", () => {
|
|
228
|
+
const result = FileFinder.getScanProgress();
|
|
229
|
+
expect(result.ok).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("init fails with invalid path", () => {
|
|
233
|
+
const result = FileFinder.init({
|
|
234
|
+
basePath: "/nonexistent/path/that/does/not/exist",
|
|
235
|
+
skipDatabases: true,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
expect(result.ok).toBe(false);
|
|
239
|
+
if (!result.ok) {
|
|
240
|
+
expect(result.error).toContain("Failed");
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("Result Type Helpers", () => {
|
|
246
|
+
test("ok helper creates success result", async () => {
|
|
247
|
+
const { ok } = await import("./types");
|
|
248
|
+
const result = ok(42);
|
|
249
|
+
expect(result.ok).toBe(true);
|
|
250
|
+
if (result.ok) {
|
|
251
|
+
expect(result.value).toBe(42);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("err helper creates error result", async () => {
|
|
256
|
+
const { err } = await import("./types");
|
|
257
|
+
const result = err<number>("something went wrong");
|
|
258
|
+
expect(result.ok).toBe(false);
|
|
259
|
+
if (!result.ok) {
|
|
260
|
+
expect(result.error).toBe("something went wrong");
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { FileFinder } from "fff";
|
|
10
|
+
*
|
|
11
|
+
* // Initialize with a directory
|
|
12
|
+
* const result = FileFinder.init({ basePath: "/path/to/project" });
|
|
13
|
+
* if (!result.ok) {
|
|
14
|
+
* console.error(result.error);
|
|
15
|
+
* process.exit(1);
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* // Wait for initial scan
|
|
19
|
+
* FileFinder.waitForScan(5000);
|
|
20
|
+
*
|
|
21
|
+
* // Search for files
|
|
22
|
+
* const search = FileFinder.search("main.ts");
|
|
23
|
+
* if (search.ok) {
|
|
24
|
+
* for (const item of search.value.items) {
|
|
25
|
+
* console.log(item.relativePath);
|
|
26
|
+
* }
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* // Track file access (for frecency)
|
|
30
|
+
* FileFinder.trackAccess("/path/to/project/src/main.ts");
|
|
31
|
+
*
|
|
32
|
+
* // Cleanup when done
|
|
33
|
+
* FileFinder.destroy();
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @packageDocumentation
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
// Main API
|
|
40
|
+
export { FileFinder } from "./finder";
|
|
41
|
+
|
|
42
|
+
// Types
|
|
43
|
+
export type {
|
|
44
|
+
Result,
|
|
45
|
+
InitOptions,
|
|
46
|
+
SearchOptions,
|
|
47
|
+
FileItem,
|
|
48
|
+
Score,
|
|
49
|
+
Location,
|
|
50
|
+
SearchResult,
|
|
51
|
+
ScanProgress,
|
|
52
|
+
HealthCheck,
|
|
53
|
+
DbHealth,
|
|
54
|
+
} from "./types";
|
|
55
|
+
|
|
56
|
+
// Result helpers
|
|
57
|
+
export { ok, err } from "./types";
|
|
58
|
+
|
|
59
|
+
// Binary management (for CLI tools)
|
|
60
|
+
export {
|
|
61
|
+
downloadBinary,
|
|
62
|
+
ensureBinary,
|
|
63
|
+
binaryExists,
|
|
64
|
+
getBinaryPath,
|
|
65
|
+
findBinary,
|
|
66
|
+
} from "./download";
|
|
67
|
+
|
|
68
|
+
// Platform utilities
|
|
69
|
+
export { getTriple, getLibExtension, getLibFilename } from "./platform";
|