@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.
- package/README.md +283 -0
- package/examples/grep.ts +285 -0
- package/examples/search.ts +224 -0
- package/package.json +86 -0
- package/scripts/cli.ts +114 -0
- package/scripts/postinstall.ts +51 -0
- package/src/download.ts +252 -0
- package/src/ffi.ts +450 -0
- package/src/finder.ts +413 -0
- package/src/git-lifecycle.test.ts +291 -0
- package/src/index.test.ts +357 -0
- package/src/index.ts +83 -0
- package/src/platform.ts +121 -0
- package/src/types.ts +404 -0
package/src/download.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary resolution utilities for fff
|
|
3
|
+
*
|
|
4
|
+
* Resolves the native binary from:
|
|
5
|
+
* 1. Platform-specific npm package (e.g. @ff-labs/fff-bun-darwin-arm64) - primary
|
|
6
|
+
* 2. Local bin/ directory (legacy or manual download)
|
|
7
|
+
* 3. Local dev build (target/release or target/debug)
|
|
8
|
+
* 4. GitHub releases (fallback, requires network)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, mkdirSync, writeFileSync, chmodSync } from "node:fs";
|
|
12
|
+
import { join, dirname } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
import {
|
|
16
|
+
getTriple,
|
|
17
|
+
getLibExtension,
|
|
18
|
+
getLibFilename,
|
|
19
|
+
getNpmPackageName,
|
|
20
|
+
} from "./platform";
|
|
21
|
+
|
|
22
|
+
const GITHUB_REPO = "dmtrKovalenko/fff.nvim";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the current file's directory
|
|
26
|
+
*/
|
|
27
|
+
function getCurrentDir(): string {
|
|
28
|
+
const url = import.meta.url;
|
|
29
|
+
if (url.startsWith("file://")) {
|
|
30
|
+
return dirname(fileURLToPath(url));
|
|
31
|
+
}
|
|
32
|
+
return dirname(url);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get the package root directory
|
|
37
|
+
*/
|
|
38
|
+
function getPackageDir(): string {
|
|
39
|
+
const currentDir = getCurrentDir();
|
|
40
|
+
return dirname(currentDir);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the directory where binaries are stored (legacy/fallback)
|
|
45
|
+
*/
|
|
46
|
+
export function getBinDir(): string {
|
|
47
|
+
return join(getPackageDir(), "bin");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get the full path to the native library in bin/ (legacy/fallback)
|
|
52
|
+
*/
|
|
53
|
+
export function getBinaryPath(): string {
|
|
54
|
+
const binDir = getBinDir();
|
|
55
|
+
return join(binDir, getLibFilename());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if the binary exists in any known location
|
|
60
|
+
*/
|
|
61
|
+
export function binaryExists(): boolean {
|
|
62
|
+
return findBinary() !== null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Try to resolve the binary from the platform-specific npm package.
|
|
67
|
+
*
|
|
68
|
+
* When users install @ff-labs/bun, npm/bun automatically installs the matching
|
|
69
|
+
* optionalDependency (e.g. @ff-labs/fff-bun-darwin-arm64). We resolve the binary
|
|
70
|
+
* path by requiring that package's package.json and looking for the binary
|
|
71
|
+
* in the same directory.
|
|
72
|
+
*/
|
|
73
|
+
function resolveFromNpmPackage(): string | null {
|
|
74
|
+
const packageName = getNpmPackageName();
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// Use createRequire to resolve the platform package's location
|
|
78
|
+
const require = createRequire(join(getPackageDir(), "package.json"));
|
|
79
|
+
const packageJsonPath = require.resolve(`${packageName}/package.json`);
|
|
80
|
+
const packageDir = dirname(packageJsonPath);
|
|
81
|
+
const binaryPath = join(packageDir, getLibFilename());
|
|
82
|
+
|
|
83
|
+
if (existsSync(binaryPath)) {
|
|
84
|
+
return binaryPath;
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// Package not installed - this is expected on unsupported platforms
|
|
88
|
+
// or when installed without optional dependencies
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get the development binary path (for local development)
|
|
96
|
+
*/
|
|
97
|
+
export function getDevBinaryPath(): string | null {
|
|
98
|
+
const packageDir = getPackageDir();
|
|
99
|
+
const workspaceRoot = join(packageDir, "..", "..");
|
|
100
|
+
|
|
101
|
+
const possiblePaths = [
|
|
102
|
+
join(workspaceRoot, "target", "release", getLibFilename()),
|
|
103
|
+
join(workspaceRoot, "target", "debug", getLibFilename()),
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
for (const path of possiblePaths) {
|
|
107
|
+
if (existsSync(path)) {
|
|
108
|
+
return path;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isDevWorkspace(): boolean {
|
|
116
|
+
const packageDir = getPackageDir();
|
|
117
|
+
const workspaceRoot = join(packageDir, "..", "..");
|
|
118
|
+
return existsSync(join(workspaceRoot, "Cargo.toml"));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function findBinary(): string | null {
|
|
122
|
+
if (isDevWorkspace()) {
|
|
123
|
+
// 1. Local bin/ directory (populated by `make prepare-bun`)
|
|
124
|
+
const installedPath = getBinaryPath();
|
|
125
|
+
if (existsSync(installedPath)) return installedPath;
|
|
126
|
+
|
|
127
|
+
// 2. Local dev build (target/release or target/debug)
|
|
128
|
+
const devPath = getDevBinaryPath();
|
|
129
|
+
if (devPath) return devPath;
|
|
130
|
+
|
|
131
|
+
// 3. Fallback to npm package
|
|
132
|
+
const npmPath = resolveFromNpmPackage();
|
|
133
|
+
if (npmPath) return npmPath;
|
|
134
|
+
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Production: npm package first
|
|
139
|
+
// 1. Try platform-specific npm package first
|
|
140
|
+
const npmPath = resolveFromNpmPackage();
|
|
141
|
+
if (npmPath) return npmPath;
|
|
142
|
+
|
|
143
|
+
// 2. Try local bin/ directory (legacy or manual download)
|
|
144
|
+
const installedPath = getBinaryPath();
|
|
145
|
+
if (existsSync(installedPath)) return installedPath;
|
|
146
|
+
|
|
147
|
+
// 3. Try local dev build
|
|
148
|
+
return getDevBinaryPath();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Download the binary from GitHub releases as a fallback.
|
|
153
|
+
* This is only used when the platform npm package is not available.
|
|
154
|
+
*
|
|
155
|
+
* @param tag - The release tag to download (e.g. commit hash), or "latest"
|
|
156
|
+
*/
|
|
157
|
+
export async function downloadBinary(tag?: string): Promise<string> {
|
|
158
|
+
const resolvedTag = tag || "latest";
|
|
159
|
+
const triple = getTriple();
|
|
160
|
+
const ext = getLibExtension();
|
|
161
|
+
|
|
162
|
+
// Resolve "latest" tag via GitHub API
|
|
163
|
+
let releaseTag = resolvedTag;
|
|
164
|
+
if (releaseTag === "latest") {
|
|
165
|
+
console.log("fff: Fetching latest release tag from GitHub...");
|
|
166
|
+
releaseTag = await fetchLatestReleaseTag();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const binaryName = `c-lib-${triple}.${ext}`;
|
|
170
|
+
const baseUrl = `https://github.com/${GITHUB_REPO}/releases/download/${releaseTag}`;
|
|
171
|
+
const binaryUrl = `${baseUrl}/${binaryName}`;
|
|
172
|
+
|
|
173
|
+
console.log(`fff: Downloading native library for ${triple}...`);
|
|
174
|
+
console.log(`fff: Release: ${releaseTag}`);
|
|
175
|
+
|
|
176
|
+
const binaryResponse = await fetch(binaryUrl);
|
|
177
|
+
if (!binaryResponse.ok) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Failed to download binary: ${binaryResponse.status} ${binaryResponse.statusText}\nURL: ${binaryUrl}`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const binaryBuffer = Buffer.from(await binaryResponse.arrayBuffer());
|
|
184
|
+
|
|
185
|
+
const binDir = getBinDir();
|
|
186
|
+
if (!existsSync(binDir)) {
|
|
187
|
+
mkdirSync(binDir, { recursive: true });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const binaryPath = getBinaryPath();
|
|
191
|
+
writeFileSync(binaryPath, binaryBuffer);
|
|
192
|
+
|
|
193
|
+
// Make executable on Unix
|
|
194
|
+
if (process.platform !== "win32") {
|
|
195
|
+
chmodSync(binaryPath, 0o755);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.log(`fff: Binary downloaded to ${binaryPath}`);
|
|
199
|
+
return releaseTag;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Fetch the latest release tag from GitHub
|
|
204
|
+
*/
|
|
205
|
+
async function fetchLatestReleaseTag(): Promise<string> {
|
|
206
|
+
const url = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
|
|
207
|
+
|
|
208
|
+
const response = await fetch(url, {
|
|
209
|
+
headers: {
|
|
210
|
+
Accept: "application/vnd.github.v3+json",
|
|
211
|
+
"User-Agent": "fff-bun-client",
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (!response.ok) {
|
|
216
|
+
const allReleasesUrl = `https://api.github.com/repos/${GITHUB_REPO}/releases`;
|
|
217
|
+
const allResponse = await fetch(allReleasesUrl, {
|
|
218
|
+
headers: {
|
|
219
|
+
Accept: "application/vnd.github.v3+json",
|
|
220
|
+
"User-Agent": "fff-bun-client",
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
if (!allResponse.ok) {
|
|
225
|
+
throw new Error(`Failed to fetch releases: ${allResponse.status}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const releases = (await allResponse.json()) as Array<{ tag_name: string }>;
|
|
229
|
+
if (releases.length === 0) {
|
|
230
|
+
throw new Error("No releases found");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return releases[0].tag_name;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const release = (await response.json()) as { tag_name: string };
|
|
237
|
+
return release.tag_name;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Ensure the binary exists, downloading from GitHub if necessary.
|
|
242
|
+
*/
|
|
243
|
+
export async function ensureBinary(): Promise<string> {
|
|
244
|
+
const existingPath = findBinary();
|
|
245
|
+
if (existingPath) {
|
|
246
|
+
return existingPath;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Fallback: download from GitHub
|
|
250
|
+
await downloadBinary();
|
|
251
|
+
return getBinaryPath();
|
|
252
|
+
}
|
package/src/ffi.ts
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
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
|
+
* The API is instance-based: `ffiCreate` returns an opaque handle that must
|
|
8
|
+
* be passed to all subsequent calls and freed with `ffiDestroy`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { dlopen, FFIType, ptr, CString, read, type Pointer } from "bun:ffi";
|
|
12
|
+
import { findBinary, ensureBinary } from "./download";
|
|
13
|
+
import type { Result } from "./types";
|
|
14
|
+
import { err } from "./types";
|
|
15
|
+
|
|
16
|
+
// Define the FFI symbols
|
|
17
|
+
const ffiDefinition = {
|
|
18
|
+
// Lifecycle
|
|
19
|
+
fff_create: {
|
|
20
|
+
args: [FFIType.cstring],
|
|
21
|
+
returns: FFIType.ptr,
|
|
22
|
+
},
|
|
23
|
+
fff_destroy: {
|
|
24
|
+
args: [FFIType.ptr],
|
|
25
|
+
returns: FFIType.void,
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
// Search
|
|
29
|
+
fff_search: {
|
|
30
|
+
args: [FFIType.ptr, FFIType.cstring, FFIType.cstring],
|
|
31
|
+
returns: FFIType.ptr,
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// Live grep (content search)
|
|
35
|
+
fff_live_grep: {
|
|
36
|
+
args: [FFIType.ptr, FFIType.cstring, FFIType.cstring],
|
|
37
|
+
returns: FFIType.ptr,
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
// File index
|
|
41
|
+
fff_scan_files: {
|
|
42
|
+
args: [FFIType.ptr],
|
|
43
|
+
returns: FFIType.ptr,
|
|
44
|
+
},
|
|
45
|
+
fff_is_scanning: {
|
|
46
|
+
args: [FFIType.ptr],
|
|
47
|
+
returns: FFIType.bool,
|
|
48
|
+
},
|
|
49
|
+
fff_get_scan_progress: {
|
|
50
|
+
args: [FFIType.ptr],
|
|
51
|
+
returns: FFIType.ptr,
|
|
52
|
+
},
|
|
53
|
+
fff_wait_for_scan: {
|
|
54
|
+
args: [FFIType.ptr, FFIType.u64],
|
|
55
|
+
returns: FFIType.ptr,
|
|
56
|
+
},
|
|
57
|
+
fff_restart_index: {
|
|
58
|
+
args: [FFIType.ptr, FFIType.cstring],
|
|
59
|
+
returns: FFIType.ptr,
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// Frecency
|
|
63
|
+
fff_track_access: {
|
|
64
|
+
args: [FFIType.ptr, FFIType.cstring],
|
|
65
|
+
returns: FFIType.ptr,
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// Git
|
|
69
|
+
fff_refresh_git_status: {
|
|
70
|
+
args: [FFIType.ptr],
|
|
71
|
+
returns: FFIType.ptr,
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Query tracking
|
|
75
|
+
fff_track_query: {
|
|
76
|
+
args: [FFIType.ptr, FFIType.cstring, FFIType.cstring],
|
|
77
|
+
returns: FFIType.ptr,
|
|
78
|
+
},
|
|
79
|
+
fff_get_historical_query: {
|
|
80
|
+
args: [FFIType.ptr, FFIType.u64],
|
|
81
|
+
returns: FFIType.ptr,
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// Utilities
|
|
85
|
+
fff_health_check: {
|
|
86
|
+
args: [FFIType.ptr, FFIType.cstring],
|
|
87
|
+
returns: FFIType.ptr,
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// Memory management
|
|
91
|
+
fff_free_result: {
|
|
92
|
+
args: [FFIType.ptr],
|
|
93
|
+
returns: FFIType.void,
|
|
94
|
+
},
|
|
95
|
+
fff_free_string: {
|
|
96
|
+
args: [FFIType.ptr],
|
|
97
|
+
returns: FFIType.void,
|
|
98
|
+
},
|
|
99
|
+
} as const;
|
|
100
|
+
|
|
101
|
+
type FFFLibrary = ReturnType<typeof dlopen<typeof ffiDefinition>>;
|
|
102
|
+
|
|
103
|
+
// Library instance (lazy loaded)
|
|
104
|
+
let lib: FFFLibrary | null = null;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Load the native library
|
|
108
|
+
*/
|
|
109
|
+
function loadLibrary(): FFFLibrary {
|
|
110
|
+
if (lib) return lib;
|
|
111
|
+
|
|
112
|
+
const binaryPath = findBinary();
|
|
113
|
+
if (!binaryPath) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
"fff native library not found. Run `bunx fff download` or build from source with `cargo build --release -p fff-c`"
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
lib = dlopen(binaryPath, ffiDefinition);
|
|
120
|
+
return lib;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Encode a string for FFI (null-terminated)
|
|
125
|
+
*/
|
|
126
|
+
function encodeString(s: string): Uint8Array {
|
|
127
|
+
return new TextEncoder().encode(s + "\0");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Read a C string from a pointer
|
|
132
|
+
* Note: read.ptr() returns number but CString expects Pointer - we cast through unknown
|
|
133
|
+
*/
|
|
134
|
+
function readCString(pointer: Pointer | number | null): string | null {
|
|
135
|
+
if (pointer === null || pointer === 0) return null;
|
|
136
|
+
// CString constructor accepts Pointer, but read.ptr returns number
|
|
137
|
+
// Cast through unknown for runtime compatibility
|
|
138
|
+
return new CString(pointer as unknown as Pointer).toString();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Convert snake_case keys to camelCase recursively
|
|
143
|
+
*/
|
|
144
|
+
function snakeToCamel(obj: unknown): unknown {
|
|
145
|
+
if (obj === null || obj === undefined) return obj;
|
|
146
|
+
if (typeof obj !== "object") return obj;
|
|
147
|
+
if (Array.isArray(obj)) return obj.map(snakeToCamel);
|
|
148
|
+
|
|
149
|
+
const result: Record<string, unknown> = {};
|
|
150
|
+
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
151
|
+
const camelKey = key.replace(/_([a-z])/g, (_, letter) =>
|
|
152
|
+
letter.toUpperCase()
|
|
153
|
+
);
|
|
154
|
+
result[camelKey] = snakeToCamel(value);
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Parse a FffResult from the FFI return value.
|
|
161
|
+
*
|
|
162
|
+
* The result is a pointer to a struct:
|
|
163
|
+
* { success: bool, data: *char, error: *char, handle: *void }
|
|
164
|
+
*
|
|
165
|
+
* Layout (with alignment padding):
|
|
166
|
+
* offset 0: success (bool, 1 byte + 7 padding)
|
|
167
|
+
* offset 8: data pointer (8 bytes)
|
|
168
|
+
* offset 16: error pointer (8 bytes)
|
|
169
|
+
* offset 24: handle pointer (8 bytes)
|
|
170
|
+
*/
|
|
171
|
+
function parseResult<T>(resultPtr: Pointer | null): Result<T> {
|
|
172
|
+
if (resultPtr === null) {
|
|
173
|
+
return err("FFI returned null pointer");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const success = read.u8(resultPtr, 0) !== 0;
|
|
177
|
+
const dataPtr = read.ptr(resultPtr, 8);
|
|
178
|
+
const errorPtr = read.ptr(resultPtr, 16);
|
|
179
|
+
|
|
180
|
+
const library = loadLibrary();
|
|
181
|
+
|
|
182
|
+
if (success) {
|
|
183
|
+
const data = readCString(dataPtr);
|
|
184
|
+
// Free the result
|
|
185
|
+
library.symbols.fff_free_result(resultPtr);
|
|
186
|
+
|
|
187
|
+
if (data === null || data === "") {
|
|
188
|
+
return { ok: true, value: undefined as T };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const parsed = JSON.parse(data);
|
|
193
|
+
// Convert snake_case to camelCase for TypeScript consumers
|
|
194
|
+
const transformed = snakeToCamel(parsed) as T;
|
|
195
|
+
return { ok: true, value: transformed };
|
|
196
|
+
} catch {
|
|
197
|
+
// For simple values like "true" or numbers
|
|
198
|
+
return { ok: true, value: data as T };
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
const errorMsg = readCString(errorPtr) || "Unknown error";
|
|
202
|
+
// Free the result
|
|
203
|
+
library.symbols.fff_free_result(resultPtr);
|
|
204
|
+
return err(errorMsg);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Opaque native handle type. Callers must not inspect or modify this value.
|
|
210
|
+
*/
|
|
211
|
+
export type NativeHandle = Pointer;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Create a new file finder instance.
|
|
215
|
+
*
|
|
216
|
+
* Returns the opaque native handle on success. The handle must be passed to
|
|
217
|
+
* all subsequent FFI calls and freed with `ffiDestroy`.
|
|
218
|
+
*/
|
|
219
|
+
export function ffiCreate(optsJson: string): Result<NativeHandle> {
|
|
220
|
+
const library = loadLibrary();
|
|
221
|
+
const resultPtr = library.symbols.fff_create(ptr(encodeString(optsJson)));
|
|
222
|
+
|
|
223
|
+
if (resultPtr === null) {
|
|
224
|
+
return err("FFI returned null pointer");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const success = read.u8(resultPtr, 0) !== 0;
|
|
228
|
+
const errorPtr = read.ptr(resultPtr, 16);
|
|
229
|
+
const handlePtr = read.ptr(resultPtr, 24);
|
|
230
|
+
|
|
231
|
+
if (success) {
|
|
232
|
+
const handle = handlePtr as unknown as Pointer;
|
|
233
|
+
library.symbols.fff_free_result(resultPtr);
|
|
234
|
+
|
|
235
|
+
if (!handle || handle === (0 as unknown as Pointer)) {
|
|
236
|
+
return err("fff_create returned null handle");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return { ok: true, value: handle };
|
|
240
|
+
} else {
|
|
241
|
+
const errorMsg = readCString(errorPtr) || "Unknown error";
|
|
242
|
+
library.symbols.fff_free_result(resultPtr);
|
|
243
|
+
return err(errorMsg);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Destroy and clean up an instance.
|
|
249
|
+
*/
|
|
250
|
+
export function ffiDestroy(handle: NativeHandle): void {
|
|
251
|
+
const library = loadLibrary();
|
|
252
|
+
library.symbols.fff_destroy(handle);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Perform fuzzy search.
|
|
257
|
+
*/
|
|
258
|
+
export function ffiSearch(
|
|
259
|
+
handle: NativeHandle,
|
|
260
|
+
query: string,
|
|
261
|
+
optsJson: string
|
|
262
|
+
): Result<unknown> {
|
|
263
|
+
const library = loadLibrary();
|
|
264
|
+
const resultPtr = library.symbols.fff_search(
|
|
265
|
+
handle,
|
|
266
|
+
ptr(encodeString(query)),
|
|
267
|
+
ptr(encodeString(optsJson))
|
|
268
|
+
);
|
|
269
|
+
return parseResult<unknown>(resultPtr);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Trigger file scan.
|
|
274
|
+
*/
|
|
275
|
+
export function ffiScanFiles(handle: NativeHandle): Result<void> {
|
|
276
|
+
const library = loadLibrary();
|
|
277
|
+
const resultPtr = library.symbols.fff_scan_files(handle);
|
|
278
|
+
return parseResult<void>(resultPtr);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Check if scanning.
|
|
283
|
+
*/
|
|
284
|
+
export function ffiIsScanning(handle: NativeHandle): boolean {
|
|
285
|
+
const library = loadLibrary();
|
|
286
|
+
return library.symbols.fff_is_scanning(handle) as boolean;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get scan progress.
|
|
291
|
+
*/
|
|
292
|
+
export function ffiGetScanProgress(handle: NativeHandle): Result<unknown> {
|
|
293
|
+
const library = loadLibrary();
|
|
294
|
+
const resultPtr = library.symbols.fff_get_scan_progress(handle);
|
|
295
|
+
return parseResult<unknown>(resultPtr);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Wait for scan to complete.
|
|
300
|
+
*/
|
|
301
|
+
export function ffiWaitForScan(
|
|
302
|
+
handle: NativeHandle,
|
|
303
|
+
timeoutMs: number
|
|
304
|
+
): Result<boolean> {
|
|
305
|
+
const library = loadLibrary();
|
|
306
|
+
const resultPtr = library.symbols.fff_wait_for_scan(
|
|
307
|
+
handle,
|
|
308
|
+
BigInt(timeoutMs)
|
|
309
|
+
);
|
|
310
|
+
const result = parseResult<boolean | string>(resultPtr);
|
|
311
|
+
if (!result.ok) return result;
|
|
312
|
+
// JSON.parse("true") returns boolean true, but we also handle
|
|
313
|
+
// the string case defensively.
|
|
314
|
+
return { ok: true, value: result.value === true || result.value === "true" };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Restart index in new path.
|
|
319
|
+
*/
|
|
320
|
+
export function ffiRestartIndex(
|
|
321
|
+
handle: NativeHandle,
|
|
322
|
+
newPath: string
|
|
323
|
+
): Result<void> {
|
|
324
|
+
const library = loadLibrary();
|
|
325
|
+
const resultPtr = library.symbols.fff_restart_index(
|
|
326
|
+
handle,
|
|
327
|
+
ptr(encodeString(newPath))
|
|
328
|
+
);
|
|
329
|
+
return parseResult<void>(resultPtr);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Track file access.
|
|
334
|
+
*/
|
|
335
|
+
export function ffiTrackAccess(
|
|
336
|
+
handle: NativeHandle,
|
|
337
|
+
filePath: string
|
|
338
|
+
): Result<boolean> {
|
|
339
|
+
const library = loadLibrary();
|
|
340
|
+
const resultPtr = library.symbols.fff_track_access(
|
|
341
|
+
handle,
|
|
342
|
+
ptr(encodeString(filePath))
|
|
343
|
+
);
|
|
344
|
+
const result = parseResult<boolean | string>(resultPtr);
|
|
345
|
+
if (!result.ok) return result;
|
|
346
|
+
return { ok: true, value: result.value === true || result.value === "true" };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Refresh git status.
|
|
351
|
+
*/
|
|
352
|
+
export function ffiRefreshGitStatus(handle: NativeHandle): Result<number> {
|
|
353
|
+
const library = loadLibrary();
|
|
354
|
+
const resultPtr = library.symbols.fff_refresh_git_status(handle);
|
|
355
|
+
const result = parseResult<number | string>(resultPtr);
|
|
356
|
+
if (!result.ok) return result;
|
|
357
|
+
// JSON.parse("3") returns 3 (number), parseInt handles both
|
|
358
|
+
return { ok: true, value: typeof result.value === "number" ? result.value : parseInt(result.value, 10) };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Track query completion.
|
|
363
|
+
*/
|
|
364
|
+
export function ffiTrackQuery(
|
|
365
|
+
handle: NativeHandle,
|
|
366
|
+
query: string,
|
|
367
|
+
filePath: string
|
|
368
|
+
): Result<boolean> {
|
|
369
|
+
const library = loadLibrary();
|
|
370
|
+
const resultPtr = library.symbols.fff_track_query(
|
|
371
|
+
handle,
|
|
372
|
+
ptr(encodeString(query)),
|
|
373
|
+
ptr(encodeString(filePath))
|
|
374
|
+
);
|
|
375
|
+
const result = parseResult<boolean | string>(resultPtr);
|
|
376
|
+
if (!result.ok) return result;
|
|
377
|
+
return { ok: true, value: result.value === true || result.value === "true" };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Get historical query.
|
|
382
|
+
*/
|
|
383
|
+
export function ffiGetHistoricalQuery(
|
|
384
|
+
handle: NativeHandle,
|
|
385
|
+
offset: number
|
|
386
|
+
): Result<string | null> {
|
|
387
|
+
const library = loadLibrary();
|
|
388
|
+
const resultPtr = library.symbols.fff_get_historical_query(
|
|
389
|
+
handle,
|
|
390
|
+
BigInt(offset)
|
|
391
|
+
);
|
|
392
|
+
const result = parseResult<string | null>(resultPtr);
|
|
393
|
+
if (!result.ok) return result;
|
|
394
|
+
if (result.value === null || result.value === "null") return { ok: true, value: null };
|
|
395
|
+
return result as Result<string>;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Health check.
|
|
400
|
+
*
|
|
401
|
+
* `handle` can be null for a limited check (version + git only).
|
|
402
|
+
*/
|
|
403
|
+
export function ffiHealthCheck(
|
|
404
|
+
handle: NativeHandle | null,
|
|
405
|
+
testPath: string
|
|
406
|
+
): Result<unknown> {
|
|
407
|
+
const library = loadLibrary();
|
|
408
|
+
const resultPtr = library.symbols.fff_health_check(
|
|
409
|
+
handle ?? (0 as unknown as Pointer),
|
|
410
|
+
ptr(encodeString(testPath))
|
|
411
|
+
);
|
|
412
|
+
return parseResult<unknown>(resultPtr);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Live grep - search file contents.
|
|
417
|
+
*/
|
|
418
|
+
export function ffiLiveGrep(
|
|
419
|
+
handle: NativeHandle,
|
|
420
|
+
query: string,
|
|
421
|
+
optsJson: string
|
|
422
|
+
): Result<unknown> {
|
|
423
|
+
const library = loadLibrary();
|
|
424
|
+
const resultPtr = library.symbols.fff_live_grep(
|
|
425
|
+
handle,
|
|
426
|
+
ptr(encodeString(query)),
|
|
427
|
+
ptr(encodeString(optsJson))
|
|
428
|
+
);
|
|
429
|
+
return parseResult<unknown>(resultPtr);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Ensure the library is loaded (for preloading).
|
|
434
|
+
*/
|
|
435
|
+
export async function ensureLoaded(): Promise<void> {
|
|
436
|
+
await ensureBinary();
|
|
437
|
+
loadLibrary();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Check if the library is available.
|
|
442
|
+
*/
|
|
443
|
+
export function isAvailable(): boolean {
|
|
444
|
+
try {
|
|
445
|
+
loadLibrary();
|
|
446
|
+
return true;
|
|
447
|
+
} catch {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
}
|