@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/download.ts
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary download utilities for fff
|
|
3
|
+
*
|
|
4
|
+
* Downloads prebuilt binaries from GitHub releases based on commit hash.
|
|
5
|
+
* The release tag corresponds to the short commit SHA (7 characters).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync, writeFileSync, chmodSync } from "node:fs";
|
|
9
|
+
import { join, dirname } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { getTriple, getLibExtension, getLibFilename } from "./platform";
|
|
13
|
+
|
|
14
|
+
const GITHUB_REPO = "dmtrKovalenko/fff.nvim";
|
|
15
|
+
const GITHUB_API = "https://api.github.com";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the current file's directory
|
|
19
|
+
*/
|
|
20
|
+
function getCurrentDir(): string {
|
|
21
|
+
const url = import.meta.url;
|
|
22
|
+
if (url.startsWith("file://")) {
|
|
23
|
+
return dirname(fileURLToPath(url));
|
|
24
|
+
}
|
|
25
|
+
return dirname(url);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the package root directory
|
|
30
|
+
*/
|
|
31
|
+
function getPackageDir(): string {
|
|
32
|
+
const currentDir = getCurrentDir();
|
|
33
|
+
return dirname(currentDir);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get the path to package.json
|
|
38
|
+
*/
|
|
39
|
+
function getPackageJsonPath(): string {
|
|
40
|
+
return join(getPackageDir(), "package.json");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the directory where binaries are stored
|
|
45
|
+
*/
|
|
46
|
+
export function getBinDir(): string {
|
|
47
|
+
return join(getPackageDir(), "bin");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get the full path to the native library
|
|
52
|
+
*/
|
|
53
|
+
export function getBinaryPath(): string {
|
|
54
|
+
const binDir = getBinDir();
|
|
55
|
+
return join(binDir, getLibFilename());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if the binary exists
|
|
60
|
+
*/
|
|
61
|
+
export function binaryExists(): boolean {
|
|
62
|
+
return existsSync(getBinaryPath());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Read package.json
|
|
67
|
+
*/
|
|
68
|
+
async function readPackageJson(): Promise<Record<string, unknown>> {
|
|
69
|
+
try {
|
|
70
|
+
return await Bun.file(getPackageJsonPath()).json();
|
|
71
|
+
} catch {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Write package.json
|
|
78
|
+
*/
|
|
79
|
+
async function writePackageJson(pkg: Record<string, unknown>): Promise<void> {
|
|
80
|
+
const content = JSON.stringify(pkg, null, 2) + "\n";
|
|
81
|
+
writeFileSync(getPackageJsonPath(), content);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get the installed binary hash from package.json
|
|
86
|
+
*/
|
|
87
|
+
export async function getInstalledHash(): Promise<string | null> {
|
|
88
|
+
const pkg = await readPackageJson();
|
|
89
|
+
return (pkg.nativeBinaryHash as string) || null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Update the installed hash in package.json
|
|
94
|
+
*/
|
|
95
|
+
async function setInstalledHash(hash: string): Promise<void> {
|
|
96
|
+
const pkg = await readPackageJson();
|
|
97
|
+
pkg.nativeBinaryHash = hash;
|
|
98
|
+
await writePackageJson(pkg);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get the development binary path (for local development)
|
|
103
|
+
*/
|
|
104
|
+
export function getDevBinaryPath(): string | null {
|
|
105
|
+
const packageDir = getPackageDir();
|
|
106
|
+
const workspaceRoot = join(packageDir, "..", "..");
|
|
107
|
+
|
|
108
|
+
const possiblePaths = [
|
|
109
|
+
join(workspaceRoot, "target", "release", getLibFilename()),
|
|
110
|
+
join(workspaceRoot, "target", "debug", getLibFilename()),
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
for (const path of possiblePaths) {
|
|
114
|
+
if (existsSync(path)) {
|
|
115
|
+
return path;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Find the binary, checking both installed and dev paths
|
|
124
|
+
*/
|
|
125
|
+
export function findBinary(): string | null {
|
|
126
|
+
const installedPath = getBinaryPath();
|
|
127
|
+
if (existsSync(installedPath)) {
|
|
128
|
+
return installedPath;
|
|
129
|
+
}
|
|
130
|
+
return getDevBinaryPath();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Fetch the latest release tag from GitHub
|
|
135
|
+
*/
|
|
136
|
+
async function fetchLatestReleaseTag(): Promise<string> {
|
|
137
|
+
const url = `${GITHUB_API}/repos/${GITHUB_REPO}/releases/latest`;
|
|
138
|
+
|
|
139
|
+
const response = await fetch(url, {
|
|
140
|
+
headers: {
|
|
141
|
+
"Accept": "application/vnd.github.v3+json",
|
|
142
|
+
"User-Agent": "fff-bun-client",
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
// If no "latest" release, try getting the most recent prerelease
|
|
148
|
+
const allReleasesUrl = `${GITHUB_API}/repos/${GITHUB_REPO}/releases`;
|
|
149
|
+
const allResponse = await fetch(allReleasesUrl, {
|
|
150
|
+
headers: {
|
|
151
|
+
"Accept": "application/vnd.github.v3+json",
|
|
152
|
+
"User-Agent": "fff-bun-client",
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (!allResponse.ok) {
|
|
157
|
+
throw new Error(`Failed to fetch releases: ${allResponse.status}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const releases = await allResponse.json() as Array<{ tag_name: string }>;
|
|
161
|
+
if (releases.length === 0) {
|
|
162
|
+
throw new Error("No releases found");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return releases[0].tag_name;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const release = await response.json() as { tag_name: string };
|
|
169
|
+
return release.tag_name;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Resolve the hash to use for downloading
|
|
174
|
+
* If "latest", fetches the latest release tag from GitHub
|
|
175
|
+
*/
|
|
176
|
+
async function resolveHash(hash: string): Promise<string> {
|
|
177
|
+
if (hash === "latest") {
|
|
178
|
+
console.log("fff: Fetching latest release tag...");
|
|
179
|
+
return await fetchLatestReleaseTag();
|
|
180
|
+
}
|
|
181
|
+
return hash;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Download and verify checksum for a binary
|
|
186
|
+
*/
|
|
187
|
+
async function downloadWithChecksum(
|
|
188
|
+
binaryUrl: string,
|
|
189
|
+
checksumUrl: string,
|
|
190
|
+
): Promise<Buffer> {
|
|
191
|
+
// Download binary
|
|
192
|
+
const binaryResponse = await fetch(binaryUrl);
|
|
193
|
+
if (!binaryResponse.ok) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`Failed to download binary: ${binaryResponse.status} ${binaryResponse.statusText}\nURL: ${binaryUrl}`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const binaryBuffer = Buffer.from(await binaryResponse.arrayBuffer());
|
|
200
|
+
|
|
201
|
+
// Try to download and verify checksum
|
|
202
|
+
try {
|
|
203
|
+
const checksumResponse = await fetch(checksumUrl);
|
|
204
|
+
if (checksumResponse.ok) {
|
|
205
|
+
const checksumText = await checksumResponse.text();
|
|
206
|
+
// Format: "hash filename" or just "hash"
|
|
207
|
+
const expectedHash = checksumText.trim().split(/\s+/)[0];
|
|
208
|
+
|
|
209
|
+
const actualHash = createHash("sha256").update(binaryBuffer).digest("hex");
|
|
210
|
+
|
|
211
|
+
if (actualHash !== expectedHash) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Checksum mismatch!\nExpected: ${expectedHash}\nActual: ${actualHash}`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
console.log("fff: Checksum verified ✓");
|
|
217
|
+
}
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (error instanceof Error && error.message.includes("Checksum mismatch")) {
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
// Checksum file not found, continue without verification
|
|
223
|
+
console.log("fff: Checksum file not available, skipping verification");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return binaryBuffer;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Download the binary from GitHub releases
|
|
231
|
+
* @param hash - The commit hash (release tag) to download, or "latest"
|
|
232
|
+
*/
|
|
233
|
+
export async function downloadBinary(hash?: string): Promise<string> {
|
|
234
|
+
const currentHash = await getInstalledHash();
|
|
235
|
+
const packageHash = hash || currentHash || "latest";
|
|
236
|
+
const resolvedHash = await resolveHash(packageHash);
|
|
237
|
+
|
|
238
|
+
const triple = getTriple();
|
|
239
|
+
const ext = getLibExtension();
|
|
240
|
+
|
|
241
|
+
// Binary name format: c-lib-{triple}.{ext}
|
|
242
|
+
const binaryName = `c-lib-${triple}.${ext}`;
|
|
243
|
+
const baseUrl = `https://github.com/${GITHUB_REPO}/releases/download/${resolvedHash}`;
|
|
244
|
+
const binaryUrl = `${baseUrl}/${binaryName}`;
|
|
245
|
+
const checksumUrl = `${baseUrl}/${binaryName}.sha256`;
|
|
246
|
+
|
|
247
|
+
console.log(`fff: Downloading native library for ${triple}...`);
|
|
248
|
+
console.log(`fff: Release: ${resolvedHash}`);
|
|
249
|
+
console.log(`fff: URL: ${binaryUrl}`);
|
|
250
|
+
|
|
251
|
+
const binaryBuffer = await downloadWithChecksum(binaryUrl, checksumUrl);
|
|
252
|
+
|
|
253
|
+
const binDir = getBinDir();
|
|
254
|
+
if (!existsSync(binDir)) {
|
|
255
|
+
mkdirSync(binDir, { recursive: true });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const binaryPath = getBinaryPath();
|
|
259
|
+
writeFileSync(binaryPath, binaryBuffer);
|
|
260
|
+
|
|
261
|
+
// Save the hash to package.json
|
|
262
|
+
await setInstalledHash(resolvedHash);
|
|
263
|
+
|
|
264
|
+
// Make executable on Unix
|
|
265
|
+
if (process.platform !== "win32") {
|
|
266
|
+
chmodSync(binaryPath, 0o755);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
console.log(`fff: Binary downloaded to ${binaryPath}`);
|
|
270
|
+
return resolvedHash;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Check if an update is available
|
|
275
|
+
*/
|
|
276
|
+
export async function checkForUpdate(): Promise<{
|
|
277
|
+
currentHash: string | null;
|
|
278
|
+
latestHash: string;
|
|
279
|
+
updateAvailable: boolean;
|
|
280
|
+
}> {
|
|
281
|
+
const currentHash = await getInstalledHash();
|
|
282
|
+
const latestHash = await fetchLatestReleaseTag();
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
currentHash,
|
|
286
|
+
latestHash,
|
|
287
|
+
updateAvailable: currentHash !== latestHash,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Ensure the binary exists, downloading if necessary
|
|
293
|
+
*/
|
|
294
|
+
export async function ensureBinary(): Promise<string> {
|
|
295
|
+
const existingPath = findBinary();
|
|
296
|
+
if (existingPath) {
|
|
297
|
+
return existingPath;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
await downloadBinary();
|
|
301
|
+
return getBinaryPath();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Download binary, with fallback to cargo build instructions
|
|
306
|
+
*/
|
|
307
|
+
export async function downloadOrBuild(): Promise<void> {
|
|
308
|
+
try {
|
|
309
|
+
await downloadBinary();
|
|
310
|
+
} catch (error) {
|
|
311
|
+
console.error(`fff: Failed to download binary: ${error}`);
|
|
312
|
+
console.error(`fff: You can build from source instead:`);
|
|
313
|
+
console.error(` cargo build --release -p fff-c`);
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
}
|
package/src/ffi.ts
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun FFI bindings for the fff-c native library
|
|
3
|
+
*
|
|
4
|
+
* This module uses Bun's native FFI to call into the Rust C library.
|
|
5
|
+
* All functions follow the Result pattern for error handling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { dlopen, FFIType, ptr, CString, read, type Pointer } from "bun:ffi";
|
|
9
|
+
import { findBinary, ensureBinary } from "./download";
|
|
10
|
+
import type { Result } from "./types";
|
|
11
|
+
import { err } from "./types";
|
|
12
|
+
|
|
13
|
+
// Define the FFI symbols
|
|
14
|
+
const ffiDefinition = {
|
|
15
|
+
// Lifecycle
|
|
16
|
+
fff_init: {
|
|
17
|
+
args: [FFIType.cstring],
|
|
18
|
+
returns: FFIType.ptr,
|
|
19
|
+
},
|
|
20
|
+
fff_destroy: {
|
|
21
|
+
args: [],
|
|
22
|
+
returns: FFIType.ptr,
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
// Search
|
|
26
|
+
fff_search: {
|
|
27
|
+
args: [FFIType.cstring, FFIType.cstring],
|
|
28
|
+
returns: FFIType.ptr,
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
// File index
|
|
32
|
+
fff_scan_files: {
|
|
33
|
+
args: [],
|
|
34
|
+
returns: FFIType.ptr,
|
|
35
|
+
},
|
|
36
|
+
fff_is_scanning: {
|
|
37
|
+
args: [],
|
|
38
|
+
returns: FFIType.bool,
|
|
39
|
+
},
|
|
40
|
+
fff_get_scan_progress: {
|
|
41
|
+
args: [],
|
|
42
|
+
returns: FFIType.ptr,
|
|
43
|
+
},
|
|
44
|
+
fff_wait_for_scan: {
|
|
45
|
+
args: [FFIType.u64],
|
|
46
|
+
returns: FFIType.ptr,
|
|
47
|
+
},
|
|
48
|
+
fff_restart_index: {
|
|
49
|
+
args: [FFIType.cstring],
|
|
50
|
+
returns: FFIType.ptr,
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// Frecency
|
|
54
|
+
fff_track_access: {
|
|
55
|
+
args: [FFIType.cstring],
|
|
56
|
+
returns: FFIType.ptr,
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
// Git
|
|
60
|
+
fff_refresh_git_status: {
|
|
61
|
+
args: [],
|
|
62
|
+
returns: FFIType.ptr,
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Query tracking
|
|
66
|
+
fff_track_query: {
|
|
67
|
+
args: [FFIType.cstring, FFIType.cstring],
|
|
68
|
+
returns: FFIType.ptr,
|
|
69
|
+
},
|
|
70
|
+
fff_get_historical_query: {
|
|
71
|
+
args: [FFIType.u64],
|
|
72
|
+
returns: FFIType.ptr,
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// Utilities
|
|
76
|
+
fff_health_check: {
|
|
77
|
+
args: [FFIType.cstring],
|
|
78
|
+
returns: FFIType.ptr,
|
|
79
|
+
},
|
|
80
|
+
fff_shorten_path: {
|
|
81
|
+
args: [FFIType.cstring, FFIType.u64, FFIType.cstring],
|
|
82
|
+
returns: FFIType.ptr,
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// Memory management
|
|
86
|
+
fff_free_result: {
|
|
87
|
+
args: [FFIType.ptr],
|
|
88
|
+
returns: FFIType.void,
|
|
89
|
+
},
|
|
90
|
+
fff_free_string: {
|
|
91
|
+
args: [FFIType.ptr],
|
|
92
|
+
returns: FFIType.void,
|
|
93
|
+
},
|
|
94
|
+
} as const;
|
|
95
|
+
|
|
96
|
+
type FFFLibrary = ReturnType<typeof dlopen<typeof ffiDefinition>>;
|
|
97
|
+
|
|
98
|
+
// Library instance (lazy loaded)
|
|
99
|
+
let lib: FFFLibrary | null = null;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Load the native library
|
|
103
|
+
*/
|
|
104
|
+
function loadLibrary(): FFFLibrary {
|
|
105
|
+
if (lib) return lib;
|
|
106
|
+
|
|
107
|
+
const binaryPath = findBinary();
|
|
108
|
+
if (!binaryPath) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
"fff native library not found. Run `bunx fff download` or build from source with `cargo build --release -p fff-c`"
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
lib = dlopen(binaryPath, ffiDefinition);
|
|
115
|
+
return lib;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Encode a string for FFI (null-terminated)
|
|
120
|
+
*/
|
|
121
|
+
function encodeString(s: string): Uint8Array {
|
|
122
|
+
return new TextEncoder().encode(s + "\0");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Read a C string from a pointer
|
|
127
|
+
* Note: read.ptr() returns number but CString expects Pointer - we cast through unknown
|
|
128
|
+
*/
|
|
129
|
+
function readCString(pointer: Pointer | number | null): string | null {
|
|
130
|
+
if (pointer === null || pointer === 0) return null;
|
|
131
|
+
// CString constructor accepts Pointer, but read.ptr returns number
|
|
132
|
+
// Cast through unknown for runtime compatibility
|
|
133
|
+
return new CString(pointer as unknown as Pointer).toString();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Convert snake_case keys to camelCase recursively
|
|
138
|
+
*/
|
|
139
|
+
function snakeToCamel(obj: unknown): unknown {
|
|
140
|
+
if (obj === null || obj === undefined) return obj;
|
|
141
|
+
if (typeof obj !== "object") return obj;
|
|
142
|
+
if (Array.isArray(obj)) return obj.map(snakeToCamel);
|
|
143
|
+
|
|
144
|
+
const result: Record<string, unknown> = {};
|
|
145
|
+
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
146
|
+
const camelKey = key.replace(/_([a-z])/g, (_, letter) =>
|
|
147
|
+
letter.toUpperCase()
|
|
148
|
+
);
|
|
149
|
+
result[camelKey] = snakeToCamel(value);
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Parse a FffResult from the FFI return value
|
|
156
|
+
* The result is a pointer to a struct: { success: bool, data: *char, error: *char }
|
|
157
|
+
*/
|
|
158
|
+
function parseResult<T>(resultPtr: Pointer | null): Result<T> {
|
|
159
|
+
if (resultPtr === null) {
|
|
160
|
+
return err("FFI returned null pointer");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Read the struct fields
|
|
164
|
+
// FffResult layout: bool (1 byte + 7 padding) + pointer (8 bytes) + pointer (8 bytes)
|
|
165
|
+
// offset 0: success (bool, 1 byte)
|
|
166
|
+
// offset 8: data pointer (8 bytes)
|
|
167
|
+
// offset 16: error pointer (8 bytes)
|
|
168
|
+
const success = read.u8(resultPtr, 0) !== 0;
|
|
169
|
+
const dataPtr = read.ptr(resultPtr, 8);
|
|
170
|
+
const errorPtr = read.ptr(resultPtr, 16);
|
|
171
|
+
|
|
172
|
+
const library = loadLibrary();
|
|
173
|
+
|
|
174
|
+
if (success) {
|
|
175
|
+
const data = readCString(dataPtr);
|
|
176
|
+
// Free the result
|
|
177
|
+
library.symbols.fff_free_result(resultPtr);
|
|
178
|
+
|
|
179
|
+
if (data === null || data === "") {
|
|
180
|
+
return { ok: true, value: undefined as T };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const parsed = JSON.parse(data);
|
|
185
|
+
// Convert snake_case to camelCase for TypeScript consumers
|
|
186
|
+
const transformed = snakeToCamel(parsed) as T;
|
|
187
|
+
return { ok: true, value: transformed };
|
|
188
|
+
} catch {
|
|
189
|
+
// For simple values like "true" or numbers
|
|
190
|
+
return { ok: true, value: data as T };
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
const errorMsg = readCString(errorPtr) || "Unknown error";
|
|
194
|
+
// Free the result
|
|
195
|
+
library.symbols.fff_free_result(resultPtr);
|
|
196
|
+
return err(errorMsg);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Initialize the file finder
|
|
202
|
+
*/
|
|
203
|
+
export function ffiInit(optsJson: string): Result<void> {
|
|
204
|
+
const library = loadLibrary();
|
|
205
|
+
const resultPtr = library.symbols.fff_init(ptr(encodeString(optsJson)));
|
|
206
|
+
return parseResult<void>(resultPtr);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Destroy and clean up resources
|
|
211
|
+
*/
|
|
212
|
+
export function ffiDestroy(): Result<void> {
|
|
213
|
+
const library = loadLibrary();
|
|
214
|
+
const resultPtr = library.symbols.fff_destroy();
|
|
215
|
+
return parseResult<void>(resultPtr);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Perform fuzzy search
|
|
220
|
+
*/
|
|
221
|
+
export function ffiSearch(query: string, optsJson: string): Result<unknown> {
|
|
222
|
+
const library = loadLibrary();
|
|
223
|
+
const resultPtr = library.symbols.fff_search(
|
|
224
|
+
ptr(encodeString(query)),
|
|
225
|
+
ptr(encodeString(optsJson))
|
|
226
|
+
);
|
|
227
|
+
return parseResult<unknown>(resultPtr);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Trigger file scan
|
|
232
|
+
*/
|
|
233
|
+
export function ffiScanFiles(): Result<void> {
|
|
234
|
+
const library = loadLibrary();
|
|
235
|
+
const resultPtr = library.symbols.fff_scan_files();
|
|
236
|
+
return parseResult<void>(resultPtr);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Check if scanning
|
|
241
|
+
*/
|
|
242
|
+
export function ffiIsScanning(): boolean {
|
|
243
|
+
const library = loadLibrary();
|
|
244
|
+
return library.symbols.fff_is_scanning() as boolean;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get scan progress
|
|
249
|
+
*/
|
|
250
|
+
export function ffiGetScanProgress(): Result<unknown> {
|
|
251
|
+
const library = loadLibrary();
|
|
252
|
+
const resultPtr = library.symbols.fff_get_scan_progress();
|
|
253
|
+
return parseResult<unknown>(resultPtr);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Wait for scan to complete
|
|
258
|
+
*/
|
|
259
|
+
export function ffiWaitForScan(timeoutMs: number): Result<boolean> {
|
|
260
|
+
const library = loadLibrary();
|
|
261
|
+
const resultPtr = library.symbols.fff_wait_for_scan(BigInt(timeoutMs));
|
|
262
|
+
const result = parseResult<string>(resultPtr);
|
|
263
|
+
if (!result.ok) return result;
|
|
264
|
+
return { ok: true, value: result.value === "true" };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Restart index in new path
|
|
269
|
+
*/
|
|
270
|
+
export function ffiRestartIndex(newPath: string): Result<void> {
|
|
271
|
+
const library = loadLibrary();
|
|
272
|
+
const resultPtr = library.symbols.fff_restart_index(
|
|
273
|
+
ptr(encodeString(newPath))
|
|
274
|
+
);
|
|
275
|
+
return parseResult<void>(resultPtr);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Track file access
|
|
280
|
+
*/
|
|
281
|
+
export function ffiTrackAccess(filePath: string): Result<boolean> {
|
|
282
|
+
const library = loadLibrary();
|
|
283
|
+
const resultPtr = library.symbols.fff_track_access(
|
|
284
|
+
ptr(encodeString(filePath))
|
|
285
|
+
);
|
|
286
|
+
const result = parseResult<string>(resultPtr);
|
|
287
|
+
if (!result.ok) return result;
|
|
288
|
+
return { ok: true, value: result.value === "true" };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Refresh git status
|
|
293
|
+
*/
|
|
294
|
+
export function ffiRefreshGitStatus(): Result<number> {
|
|
295
|
+
const library = loadLibrary();
|
|
296
|
+
const resultPtr = library.symbols.fff_refresh_git_status();
|
|
297
|
+
const result = parseResult<string>(resultPtr);
|
|
298
|
+
if (!result.ok) return result;
|
|
299
|
+
return { ok: true, value: parseInt(result.value, 10) };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Track query completion
|
|
304
|
+
*/
|
|
305
|
+
export function ffiTrackQuery(
|
|
306
|
+
query: string,
|
|
307
|
+
filePath: string
|
|
308
|
+
): Result<boolean> {
|
|
309
|
+
const library = loadLibrary();
|
|
310
|
+
const resultPtr = library.symbols.fff_track_query(
|
|
311
|
+
ptr(encodeString(query)),
|
|
312
|
+
ptr(encodeString(filePath))
|
|
313
|
+
);
|
|
314
|
+
const result = parseResult<string>(resultPtr);
|
|
315
|
+
if (!result.ok) return result;
|
|
316
|
+
return { ok: true, value: result.value === "true" };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Get historical query
|
|
321
|
+
*/
|
|
322
|
+
export function ffiGetHistoricalQuery(offset: number): Result<string | null> {
|
|
323
|
+
const library = loadLibrary();
|
|
324
|
+
const resultPtr = library.symbols.fff_get_historical_query(BigInt(offset));
|
|
325
|
+
const result = parseResult<string>(resultPtr);
|
|
326
|
+
if (!result.ok) return result;
|
|
327
|
+
if (result.value === "null") return { ok: true, value: null };
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Health check
|
|
333
|
+
*/
|
|
334
|
+
export function ffiHealthCheck(testPath: string): Result<unknown> {
|
|
335
|
+
const library = loadLibrary();
|
|
336
|
+
const resultPtr = library.symbols.fff_health_check(
|
|
337
|
+
ptr(encodeString(testPath))
|
|
338
|
+
);
|
|
339
|
+
return parseResult<unknown>(resultPtr);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Shorten path
|
|
344
|
+
*/
|
|
345
|
+
export function ffiShortenPath(
|
|
346
|
+
path: string,
|
|
347
|
+
maxSize: number,
|
|
348
|
+
strategy: string
|
|
349
|
+
): Result<string> {
|
|
350
|
+
const library = loadLibrary();
|
|
351
|
+
const resultPtr = library.symbols.fff_shorten_path(
|
|
352
|
+
ptr(encodeString(path)),
|
|
353
|
+
BigInt(maxSize),
|
|
354
|
+
ptr(encodeString(strategy))
|
|
355
|
+
);
|
|
356
|
+
return parseResult<string>(resultPtr);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Ensure the library is loaded (for preloading)
|
|
361
|
+
*/
|
|
362
|
+
export async function ensureLoaded(): Promise<void> {
|
|
363
|
+
await ensureBinary();
|
|
364
|
+
loadLibrary();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Check if the library is available
|
|
369
|
+
*/
|
|
370
|
+
export function isAvailable(): boolean {
|
|
371
|
+
try {
|
|
372
|
+
loadLibrary();
|
|
373
|
+
return true;
|
|
374
|
+
} catch {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
}
|