@ff-labs/fff-bun 0.1.0-nightly.044314f

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,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
+ }
package/src/types.ts ADDED
@@ -0,0 +1,430 @@
1
+ /**
2
+ * Result type for all operations - follows the Result pattern
3
+ */
4
+ export type Result<T> =
5
+ | { ok: true; value: T }
6
+ | { ok: false; error: string };
7
+
8
+ /**
9
+ * Helper to create a successful result
10
+ */
11
+ export function ok<T>(value: T): Result<T> {
12
+ return { ok: true, value };
13
+ }
14
+
15
+ /**
16
+ * Helper to create an error result
17
+ */
18
+ export function err<T>(error: string): Result<T> {
19
+ return { ok: false, error };
20
+ }
21
+
22
+ /**
23
+ * Initialization options for the file finder
24
+ */
25
+ export interface InitOptions {
26
+ /** Base directory to index (required) */
27
+ basePath: string;
28
+ /** Path to frecency database (optional, omit to skip frecency initialization) */
29
+ frecencyDbPath?: string;
30
+ /** Path to query history database (optional, omit to skip query tracker initialization) */
31
+ historyDbPath?: string;
32
+ /** Use unsafe no-lock mode for databases (optional, defaults to false) */
33
+ useUnsafeNoLock?: boolean;
34
+ /**
35
+ * Pre-populate mmap caches for all files after the initial scan completes.
36
+ * When enabled, the first grep search will be as fast as subsequent ones
37
+ * at the cost of a longer scan time and higher initial memory usage.
38
+ * (default: false)
39
+ */
40
+ warmupMmapCache?: boolean;
41
+ }
42
+
43
+ /**
44
+ * Search options for fuzzy file search
45
+ */
46
+ export interface SearchOptions {
47
+ /** Maximum threads for parallel search (0 = auto) */
48
+ maxThreads?: number;
49
+ /** Current file path (for deprioritization in results) */
50
+ currentFile?: string;
51
+ /** Combo boost score multiplier (default: 100) */
52
+ comboBoostMultiplier?: number;
53
+ /** Minimum combo count for boost (default: 3) */
54
+ minComboCount?: number;
55
+ /** Page index for pagination (default: 0) */
56
+ pageIndex?: number;
57
+ /** Page size for pagination (default: 100) */
58
+ pageSize?: number;
59
+ }
60
+
61
+ /**
62
+ * A file item in search results
63
+ */
64
+ export interface FileItem {
65
+ /** Absolute path to the file */
66
+ path: string;
67
+ /** Path relative to the indexed directory */
68
+ relativePath: string;
69
+ /** File name only */
70
+ fileName: string;
71
+ /** File size in bytes */
72
+ size: number;
73
+ /** Last modified timestamp (Unix seconds) */
74
+ modified: number;
75
+ /** Frecency score based on access patterns */
76
+ accessFrecencyScore: number;
77
+ /** Frecency score based on modification time */
78
+ modificationFrecencyScore: number;
79
+ /** Combined frecency score */
80
+ totalFrecencyScore: number;
81
+ /** Git status: 'clean', 'modified', 'untracked', 'staged_new', etc. */
82
+ gitStatus: string;
83
+ }
84
+
85
+ /**
86
+ * Score breakdown for a search result
87
+ */
88
+ export interface Score {
89
+ /** Total combined score */
90
+ total: number;
91
+ /** Base fuzzy match score */
92
+ baseScore: number;
93
+ /** Bonus for filename match */
94
+ filenameBonus: number;
95
+ /** Bonus for special filenames (index.ts, main.rs, etc.) */
96
+ specialFilenameBonus: number;
97
+ /** Boost from frecency */
98
+ frecencyBoost: number;
99
+ /** Penalty for distance in path */
100
+ distancePenalty: number;
101
+ /** Penalty if this is the current file */
102
+ currentFilePenalty: number;
103
+ /** Boost from query history combo matching */
104
+ comboMatchBoost: number;
105
+ /** Whether this was an exact match */
106
+ exactMatch: boolean;
107
+ /** Type of match: 'fuzzy', 'exact', 'prefix', etc. */
108
+ matchType: string;
109
+ }
110
+
111
+ /**
112
+ * Location in file (from query like "file.ts:42")
113
+ */
114
+ export type Location =
115
+ | { type: "line"; line: number }
116
+ | { type: "position"; line: number; col: number }
117
+ | {
118
+ type: "range";
119
+ start: { line: number; col: number };
120
+ end: { line: number; col: number };
121
+ };
122
+
123
+ /**
124
+ * Search result from fuzzy file search
125
+ */
126
+ export interface SearchResult {
127
+ /** Matched file items */
128
+ items: FileItem[];
129
+ /** Corresponding scores for each item */
130
+ scores: Score[];
131
+ /** Total number of files that matched */
132
+ totalMatched: number;
133
+ /** Total number of indexed files */
134
+ totalFiles: number;
135
+ /** Location parsed from query (e.g., "file.ts:42:10") */
136
+ location?: Location;
137
+ }
138
+
139
+ /**
140
+ * Scan progress information
141
+ */
142
+ export interface ScanProgress {
143
+ /** Number of files scanned so far */
144
+ scannedFilesCount: number;
145
+ /** Whether a scan is currently in progress */
146
+ isScanning: boolean;
147
+ }
148
+
149
+ /**
150
+ * Database health information
151
+ */
152
+ export interface DbHealth {
153
+ /** Path to the database */
154
+ path: string;
155
+ /** Size of the database on disk in bytes */
156
+ diskSize: number;
157
+ }
158
+
159
+ /**
160
+ * Health check result
161
+ */
162
+ export interface HealthCheck {
163
+ /** Library version */
164
+ version: string;
165
+ /** Git integration status */
166
+ git: {
167
+ /** Whether git2 library is available */
168
+ available: boolean;
169
+ /** Whether a git repository was found */
170
+ repositoryFound: boolean;
171
+ /** Git working directory path */
172
+ workdir?: string;
173
+ /** libgit2 version string */
174
+ libgit2Version: string;
175
+ /** Error message if git detection failed */
176
+ error?: string;
177
+ };
178
+ /** File picker status */
179
+ filePicker: {
180
+ /** Whether the file picker is initialized */
181
+ initialized: boolean;
182
+ /** Base path being indexed */
183
+ basePath?: string;
184
+ /** Whether a scan is in progress */
185
+ isScanning?: boolean;
186
+ /** Number of indexed files */
187
+ indexedFiles?: number;
188
+ /** Error message if there's an issue */
189
+ error?: string;
190
+ };
191
+ /** Frecency database status */
192
+ frecency: {
193
+ /** Whether frecency tracking is initialized */
194
+ initialized: boolean;
195
+ /** Database health information */
196
+ dbHealthcheck?: DbHealth;
197
+ /** Error message if there's an issue */
198
+ error?: string;
199
+ };
200
+ /** Query tracker status */
201
+ queryTracker: {
202
+ /** Whether query tracking is initialized */
203
+ initialized: boolean;
204
+ /** Database health information */
205
+ dbHealthcheck?: DbHealth;
206
+ /** Error message if there's an issue */
207
+ error?: string;
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Internal: Options format sent to Rust FFI
213
+ * @internal
214
+ */
215
+ export interface InitOptionsInternal {
216
+ base_path: string;
217
+ frecency_db_path?: string;
218
+ history_db_path?: string;
219
+ use_unsafe_no_lock: boolean;
220
+ warmup_mmap_cache: boolean;
221
+ }
222
+
223
+ /**
224
+ * Internal: Search options format sent to Rust FFI
225
+ * @internal
226
+ */
227
+ export interface SearchOptionsInternal {
228
+ max_threads?: number;
229
+ current_file?: string;
230
+ combo_boost_multiplier?: number;
231
+ min_combo_count?: number;
232
+ page_index?: number;
233
+ page_size?: number;
234
+ }
235
+
236
+ /**
237
+ * Convert public InitOptions to internal format
238
+ * @internal
239
+ */
240
+ export function toInternalInitOptions(opts: InitOptions): InitOptionsInternal {
241
+ return {
242
+ base_path: opts.basePath,
243
+ frecency_db_path: opts.frecencyDbPath,
244
+ history_db_path: opts.historyDbPath,
245
+ use_unsafe_no_lock: opts.useUnsafeNoLock ?? false,
246
+ warmup_mmap_cache: opts.warmupMmapCache ?? false,
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Convert public SearchOptions to internal format
252
+ * @internal
253
+ */
254
+ export function toInternalSearchOptions(
255
+ opts?: SearchOptions
256
+ ): SearchOptionsInternal {
257
+ return {
258
+ max_threads: opts?.maxThreads,
259
+ current_file: opts?.currentFile,
260
+ combo_boost_multiplier: opts?.comboBoostMultiplier,
261
+ min_combo_count: opts?.minComboCount,
262
+ page_index: opts?.pageIndex,
263
+ page_size: opts?.pageSize,
264
+ };
265
+ }
266
+
267
+ // ============================================================================
268
+ // Grep (live content search) types
269
+ // ============================================================================
270
+
271
+ /**
272
+ * Grep search mode
273
+ */
274
+ export type GrepMode = "plain" | "regex" | "fuzzy";
275
+
276
+ /**
277
+ * Opaque pagination cursor for grep results.
278
+ * Pass this to `GrepOptions.cursor` to fetch the next page.
279
+ * Do not construct or modify this — use the `nextCursor` from a previous `GrepResult`.
280
+ */
281
+ export interface GrepCursor {
282
+ /** @internal */
283
+ readonly __brand: "GrepCursor";
284
+ /** @internal */
285
+ readonly _offset: number;
286
+ }
287
+
288
+ /**
289
+ * @internal Create a GrepCursor from a raw file offset.
290
+ */
291
+ export function createGrepCursor(offset: number): GrepCursor {
292
+ return { __brand: "GrepCursor" as const, _offset: offset };
293
+ }
294
+
295
+ /**
296
+ * Options for live grep (content search)
297
+ *
298
+ * Files are searched sequentially in frecency order (most recently/frequently
299
+ * accessed first). The engine collects matching lines across files until
300
+ * `pageLimit` total matches are reached, then stops and returns a
301
+ * `nextCursor` for fetching the next page.
302
+ */
303
+ export interface GrepOptions {
304
+ /** Maximum file size to search in bytes. Files larger than this are skipped. (default: 10MB) */
305
+ maxFileSize?: number;
306
+ /** Maximum matching lines to collect from a single file (default: 200) */
307
+ maxMatchesPerFile?: number;
308
+ /** Smart case: case-insensitive when the query is all lowercase, case-sensitive otherwise (default: true) */
309
+ smartCase?: boolean;
310
+ /**
311
+ * Pagination cursor from a previous `GrepResult.nextCursor`.
312
+ * Omit (or pass `null`) for the first page.
313
+ */
314
+ cursor?: GrepCursor | null;
315
+ /**
316
+ * Maximum total number of matching lines to return across all files.
317
+ * The engine walks files in frecency order, accumulating matches until this
318
+ * limit is reached, then truncates and stops.
319
+ *
320
+ * Pagination is file-based, not match-based: if a single file produces more
321
+ * matches than the remaining capacity, the excess matches from that file are
322
+ * dropped and the next page resumes from the *next* file. This means some
323
+ * matches at the boundary may be skipped, but it guarantees no duplicates
324
+ * across pages and requires no server-side cursor state.
325
+ *
326
+ * Use `nextCursor` from the result to fetch the next page. (default: 50)
327
+ */
328
+ pageLimit?: number;
329
+ /** Search mode (default: "plain") */
330
+ mode?: GrepMode;
331
+ /**
332
+ * Maximum wall-clock time in milliseconds to spend searching before returning
333
+ * partial results. The engine will still return at least `pageLimit / 2` matches
334
+ * (if available) before honoring the budget. 0 = unlimited. (default: 0)
335
+ */
336
+ timeBudgetMs?: number;
337
+ }
338
+
339
+ /**
340
+ * A single grep match with file and line information
341
+ */
342
+ export interface GrepMatch {
343
+ /** Absolute path to the file */
344
+ path: string;
345
+ /** Path relative to the indexed directory */
346
+ relativePath: string;
347
+ /** File name only */
348
+ fileName: string;
349
+ /** Git status */
350
+ gitStatus: string;
351
+ /** File size in bytes */
352
+ size: number;
353
+ /** Last modified timestamp (Unix seconds) */
354
+ modified: number;
355
+ /** Whether the file is binary */
356
+ isBinary: boolean;
357
+ /** Combined frecency score */
358
+ totalFrecencyScore: number;
359
+ /** Access-based frecency score */
360
+ accessFrecencyScore: number;
361
+ /** Modification-based frecency score */
362
+ modificationFrecencyScore: number;
363
+ /** 1-based line number of the match */
364
+ lineNumber: number;
365
+ /** 0-based byte column of first match start */
366
+ col: number;
367
+ /** Absolute byte offset of the matched line from file start */
368
+ byteOffset: number;
369
+ /** The matched line text (may be truncated) */
370
+ lineContent: string;
371
+ /** Byte offset pairs [start, end] within lineContent for highlighting */
372
+ matchRanges: [number, number][];
373
+ /** Fuzzy match score (only in fuzzy mode) */
374
+ fuzzyScore?: number;
375
+ }
376
+
377
+ /**
378
+ * Result from a grep search
379
+ */
380
+ export interface GrepResult {
381
+ /** Matched items with file and line information. At most `pageLimit` entries. */
382
+ items: GrepMatch[];
383
+ /** Total number of matches collected (equal to items.length unless truncated by pageLimit) */
384
+ totalMatched: number;
385
+ /** Number of files actually opened and searched in this call */
386
+ totalFilesSearched: number;
387
+ /** Total number of indexed files (before any filtering) */
388
+ totalFiles: number;
389
+ /** Number of files eligible for search after filtering out binary files, oversized files, and constraint mismatches */
390
+ filteredFileCount: number;
391
+ /**
392
+ * Cursor for the next page, or `null` if all eligible files have been searched.
393
+ * Pass this as `GrepOptions.cursor` to continue from where this call left off.
394
+ */
395
+ nextCursor: GrepCursor | null;
396
+ /** When regex mode fails to compile the pattern, the engine falls back to literal matching and this field contains the compilation error */
397
+ regexFallbackError?: string;
398
+ }
399
+
400
+ /**
401
+ * Internal: Grep options format sent to Rust FFI
402
+ * @internal
403
+ */
404
+ export interface GrepOptionsInternal {
405
+ max_file_size?: number;
406
+ max_matches_per_file?: number;
407
+ smart_case?: boolean;
408
+ file_offset?: number;
409
+ page_limit?: number;
410
+ mode?: string;
411
+ time_budget_ms?: number;
412
+ }
413
+
414
+ /**
415
+ * Convert public GrepOptions to internal format
416
+ * @internal
417
+ */
418
+ export function toInternalGrepOptions(
419
+ opts?: GrepOptions
420
+ ): GrepOptionsInternal {
421
+ return {
422
+ max_file_size: opts?.maxFileSize,
423
+ max_matches_per_file: opts?.maxMatchesPerFile,
424
+ smart_case: opts?.smartCase,
425
+ file_offset: opts?.cursor?._offset ?? 0,
426
+ page_limit: opts?.pageLimit,
427
+ mode: opts?.mode,
428
+ time_budget_ms: opts?.timeBudgetMs,
429
+ };
430
+ }