@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.
- package/README.md +283 -0
- package/examples/grep.ts +283 -0
- package/examples/search.ts +222 -0
- package/package.json +86 -0
- package/scripts/cli.ts +114 -0
- package/scripts/postinstall.ts +51 -0
- package/src/download.ts +230 -0
- package/src/ffi.ts +374 -0
- package/src/finder.ts +380 -0
- package/src/index.test.ts +230 -0
- package/src/index.ts +74 -0
- package/src/platform.ts +121 -0
- package/src/types.ts +430 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Interactive file finder demo
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bunx fff-demo [directory]
|
|
7
|
+
* bun examples/search.ts [directory]
|
|
8
|
+
*
|
|
9
|
+
* Indexes the specified directory (or cwd) and provides an interactive
|
|
10
|
+
* search prompt with detailed metadata about results.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { FileFinder } from "../src/index";
|
|
14
|
+
import * as readline from "readline";
|
|
15
|
+
|
|
16
|
+
const RESET = "\x1b[0m";
|
|
17
|
+
const BOLD = "\x1b[1m";
|
|
18
|
+
const DIM = "\x1b[2m";
|
|
19
|
+
const GREEN = "\x1b[32m";
|
|
20
|
+
const YELLOW = "\x1b[33m";
|
|
21
|
+
const BLUE = "\x1b[34m";
|
|
22
|
+
const MAGENTA = "\x1b[35m";
|
|
23
|
+
const CYAN = "\x1b[36m";
|
|
24
|
+
const RED = "\x1b[31m";
|
|
25
|
+
|
|
26
|
+
function formatGitStatus(status: string): string {
|
|
27
|
+
switch (status) {
|
|
28
|
+
case "modified":
|
|
29
|
+
return `${YELLOW}M${RESET}`;
|
|
30
|
+
case "untracked":
|
|
31
|
+
return `${GREEN}?${RESET}`;
|
|
32
|
+
case "added":
|
|
33
|
+
return `${GREEN}A${RESET}`;
|
|
34
|
+
case "deleted":
|
|
35
|
+
return `${RED}D${RESET}`;
|
|
36
|
+
case "renamed":
|
|
37
|
+
return `${BLUE}R${RESET}`;
|
|
38
|
+
case "clear":
|
|
39
|
+
case "current":
|
|
40
|
+
return `${DIM} ${RESET}`;
|
|
41
|
+
default:
|
|
42
|
+
return `${DIM}${status.charAt(0)}${RESET}`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatScore(score: number): string {
|
|
47
|
+
if (score >= 100) return `${GREEN}${score}${RESET}`;
|
|
48
|
+
if (score >= 50) return `${YELLOW}${score}${RESET}`;
|
|
49
|
+
if (score > 0) return `${DIM}${score}${RESET}`;
|
|
50
|
+
return `${DIM}0${RESET}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function formatSize(bytes: number): string {
|
|
54
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
55
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`;
|
|
56
|
+
return `${(bytes / 1024 / 1024).toFixed(1)}M`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function formatTime(unixSeconds: number): string {
|
|
60
|
+
if (unixSeconds === 0) return "unknown";
|
|
61
|
+
const date = new Date(unixSeconds * 1000);
|
|
62
|
+
const now = new Date();
|
|
63
|
+
const diffMs = now.getTime() - date.getTime();
|
|
64
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
65
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
66
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
67
|
+
|
|
68
|
+
if (diffMins < 1) return "just now";
|
|
69
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
70
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
71
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
72
|
+
return date.toLocaleDateString();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function main() {
|
|
76
|
+
const targetDir = process.argv[2] || process.cwd();
|
|
77
|
+
|
|
78
|
+
console.log(`${BOLD}${CYAN}fff - Fast File Finder Demo${RESET}\n`);
|
|
79
|
+
|
|
80
|
+
// Check library availability
|
|
81
|
+
if (!FileFinder.isAvailable()) {
|
|
82
|
+
console.error(`${RED}Error: Native library not found.${RESET}`);
|
|
83
|
+
console.error("Build with: cargo build --release -p fff-c");
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Initialize
|
|
88
|
+
console.log(`${DIM}Initializing index for: ${targetDir}${RESET}`);
|
|
89
|
+
const initResult = FileFinder.init({
|
|
90
|
+
basePath: targetDir,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (!initResult.ok) {
|
|
94
|
+
console.error(`${RED}Init failed: ${initResult.error}${RESET}`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Wait for scan with progress
|
|
99
|
+
process.stdout.write(`${DIM}Scanning files...${RESET}`);
|
|
100
|
+
const startTime = Date.now();
|
|
101
|
+
let lastCount = 0;
|
|
102
|
+
|
|
103
|
+
while (FileFinder.isScanning()) {
|
|
104
|
+
const progress = FileFinder.getScanProgress();
|
|
105
|
+
if (progress.ok && progress.value.scannedFilesCount !== lastCount) {
|
|
106
|
+
lastCount = progress.value.scannedFilesCount;
|
|
107
|
+
process.stdout.write(`\r${DIM}Scanning files... ${lastCount}${RESET} `);
|
|
108
|
+
}
|
|
109
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const scanTime = Date.now() - startTime;
|
|
113
|
+
const finalProgress = FileFinder.getScanProgress();
|
|
114
|
+
const totalFiles = finalProgress.ok ? finalProgress.value.scannedFilesCount : 0;
|
|
115
|
+
|
|
116
|
+
console.log(`\r${GREEN}✓${RESET} Indexed ${BOLD}${totalFiles}${RESET} files in ${scanTime}ms\n`);
|
|
117
|
+
|
|
118
|
+
// Show index info
|
|
119
|
+
const health = FileFinder.healthCheck();
|
|
120
|
+
if (health.ok) {
|
|
121
|
+
console.log(`${DIM}Version:${RESET} ${health.value.version}`);
|
|
122
|
+
console.log(`${DIM}Base path:${RESET} ${health.value.filePicker.basePath}`);
|
|
123
|
+
if (health.value.git.repositoryFound) {
|
|
124
|
+
console.log(`${DIM}Git root:${RESET} ${health.value.git.workdir}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Interactive search loop
|
|
129
|
+
const rl = readline.createInterface({
|
|
130
|
+
input: process.stdin,
|
|
131
|
+
output: process.stdout,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
console.log(`${BOLD}Enter a search query${RESET} (or 'q' to quit, empty for all files):\n`);
|
|
135
|
+
|
|
136
|
+
const prompt = () => {
|
|
137
|
+
rl.question(`${CYAN}search>${RESET} `, (query) => {
|
|
138
|
+
if (query.toLowerCase() === "q" || query.toLowerCase() === "quit") {
|
|
139
|
+
console.log(`\n${DIM}Goodbye!${RESET}`);
|
|
140
|
+
FileFinder.destroy();
|
|
141
|
+
rl.close();
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const searchStart = Date.now();
|
|
146
|
+
const result = FileFinder.search(query, { pageSize: 15 });
|
|
147
|
+
const searchTime = Date.now() - searchStart;
|
|
148
|
+
|
|
149
|
+
if (!result.ok) {
|
|
150
|
+
console.log(`${RED}Search error: ${result.error}${RESET}\n`);
|
|
151
|
+
prompt();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const { items, scores, totalMatched, totalFiles } = result.value;
|
|
156
|
+
|
|
157
|
+
console.log();
|
|
158
|
+
console.log(
|
|
159
|
+
`${DIM}Found ${BOLD}${totalMatched}${RESET}${DIM} matches in ${totalFiles} files (${searchTime}ms)${RESET}`
|
|
160
|
+
);
|
|
161
|
+
console.log();
|
|
162
|
+
|
|
163
|
+
if (items.length === 0) {
|
|
164
|
+
console.log(`${DIM}No matches found.${RESET}\n`);
|
|
165
|
+
prompt();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Header
|
|
170
|
+
console.log(
|
|
171
|
+
`${DIM} Git │ Score │ Size │ Modified │ Path${RESET}`
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Results
|
|
175
|
+
for (let i = 0; i < items.length; i++) {
|
|
176
|
+
const item = items[i];
|
|
177
|
+
const score = scores[i];
|
|
178
|
+
|
|
179
|
+
const gitStatus = formatGitStatus(item.gitStatus);
|
|
180
|
+
const totalScore = formatScore(score.total);
|
|
181
|
+
const size = formatSize(item.size).padStart(6);
|
|
182
|
+
const modified = formatTime(item.modified).padEnd(10);
|
|
183
|
+
const path = item.relativePath;
|
|
184
|
+
|
|
185
|
+
console.log(
|
|
186
|
+
` ${gitStatus} │ ${totalScore.padStart(5)} │ ${size} │ ${modified} │ ${path}`
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Show score breakdown for top results
|
|
190
|
+
if (i < 3 && score.total > 0) {
|
|
191
|
+
const breakdown: string[] = [];
|
|
192
|
+
if (score.baseScore > 0) breakdown.push(`base:${score.baseScore}`);
|
|
193
|
+
if (score.filenameBonus > 0) breakdown.push(`filename:+${score.filenameBonus}`);
|
|
194
|
+
if (score.frecencyBoost > 0) breakdown.push(`frecency:+${score.frecencyBoost}`);
|
|
195
|
+
if (score.comboMatchBoost > 0) breakdown.push(`combo:+${score.comboMatchBoost}`);
|
|
196
|
+
if (score.distancePenalty < 0) breakdown.push(`distance:${score.distancePenalty}`);
|
|
197
|
+
if (score.exactMatch) breakdown.push(`${GREEN}exact${RESET}`);
|
|
198
|
+
|
|
199
|
+
if (breakdown.length > 0) {
|
|
200
|
+
console.log(`${DIM} │ │ │ │ └─ ${breakdown.join(", ")}${RESET}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (totalMatched > items.length) {
|
|
206
|
+
console.log(
|
|
207
|
+
`${DIM} │ │ │ │ ... and ${totalMatched - items.length} more${RESET}`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log();
|
|
212
|
+
prompt();
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
prompt();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
main().catch((err) => {
|
|
220
|
+
console.error(`${RED}Fatal error: ${err.message}${RESET}`);
|
|
221
|
+
process.exit(1);
|
|
222
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ff-labs/fff-bun",
|
|
3
|
+
"version": "0.1.0-nightly.044314f",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "High-performance fuzzy file finder for Bun - perfect for LLM agent tools",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "src/index.ts",
|
|
8
|
+
"types": "src/index.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./src/index.ts",
|
|
12
|
+
"types": "./src/index.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"fff": "./scripts/cli.ts",
|
|
17
|
+
"fff-demo": "./examples/search.ts",
|
|
18
|
+
"fff-grep": "./examples/grep.ts"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src",
|
|
22
|
+
"bin",
|
|
23
|
+
"scripts",
|
|
24
|
+
"examples"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"postinstall": "bun ./scripts/postinstall.ts",
|
|
28
|
+
"download": "bun ./scripts/cli.ts download",
|
|
29
|
+
"test": "bun test src/",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"demo": "bun ./examples/search.ts"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"bun": ">=1.0.0"
|
|
35
|
+
},
|
|
36
|
+
"os": [
|
|
37
|
+
"darwin",
|
|
38
|
+
"linux",
|
|
39
|
+
"win32"
|
|
40
|
+
],
|
|
41
|
+
"cpu": [
|
|
42
|
+
"x64",
|
|
43
|
+
"arm64"
|
|
44
|
+
],
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/dmtrKovalenko/fff.nvim.git",
|
|
48
|
+
"directory": "packages/fff"
|
|
49
|
+
},
|
|
50
|
+
"keywords": [
|
|
51
|
+
"file-finder",
|
|
52
|
+
"fuzzy-search",
|
|
53
|
+
"bun",
|
|
54
|
+
"ffi",
|
|
55
|
+
"llm-tools",
|
|
56
|
+
"agent-tools",
|
|
57
|
+
"fast",
|
|
58
|
+
"rust"
|
|
59
|
+
],
|
|
60
|
+
"author": "Dmitry Kovalenko",
|
|
61
|
+
"license": "MIT",
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"access": "public"
|
|
64
|
+
},
|
|
65
|
+
"bugs": {
|
|
66
|
+
"url": "https://github.com/dmtrKovalenko/fff.nvim/issues"
|
|
67
|
+
},
|
|
68
|
+
"homepage": "https://github.com/dmtrKovalenko/fff.nvim#readme",
|
|
69
|
+
"optionalDependencies": {
|
|
70
|
+
"@ff-labs/fff-bun-darwin-arm64": "0.1.0-nightly.044314f",
|
|
71
|
+
"@ff-labs/fff-bun-darwin-x64": "0.1.0-nightly.044314f",
|
|
72
|
+
"@ff-labs/fff-bun-linux-x64-gnu": "0.1.0-nightly.044314f",
|
|
73
|
+
"@ff-labs/fff-bun-linux-arm64-gnu": "0.1.0-nightly.044314f",
|
|
74
|
+
"@ff-labs/fff-bun-linux-x64-musl": "0.1.0-nightly.044314f",
|
|
75
|
+
"@ff-labs/fff-bun-linux-arm64-musl": "0.1.0-nightly.044314f",
|
|
76
|
+
"@ff-labs/fff-bun-win32-x64": "0.1.0-nightly.044314f",
|
|
77
|
+
"@ff-labs/fff-bun-win32-arm64": "0.1.0-nightly.044314f"
|
|
78
|
+
},
|
|
79
|
+
"devDependencies": {
|
|
80
|
+
"@types/bun": "^1.3.8",
|
|
81
|
+
"typescript": "^5.0.0"
|
|
82
|
+
},
|
|
83
|
+
"peerDependencies": {
|
|
84
|
+
"bun": ">=1.0.0"
|
|
85
|
+
}
|
|
86
|
+
}
|
package/scripts/cli.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* CLI tool for fff package management
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bunx fff download [tag] - Download native binary from GitHub
|
|
7
|
+
* bunx fff info - Show platform and binary info
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
downloadBinary,
|
|
12
|
+
getBinaryPath,
|
|
13
|
+
findBinary,
|
|
14
|
+
} from "../src/download";
|
|
15
|
+
import { getTriple, getLibExtension, getLibFilename, getNpmPackageName } from "../src/platform";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
const command = args[0];
|
|
21
|
+
|
|
22
|
+
interface PackageJson {
|
|
23
|
+
version: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function getPackageInfo(): Promise<PackageJson> {
|
|
27
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const packageJsonPath = join(currentDir, "..", "package.json");
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
return await Bun.file(packageJsonPath).json();
|
|
32
|
+
} catch {
|
|
33
|
+
return { version: "unknown" };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function main() {
|
|
38
|
+
switch (command) {
|
|
39
|
+
case "download": {
|
|
40
|
+
const tag = args[1];
|
|
41
|
+
console.log("fff: Downloading native library from GitHub...");
|
|
42
|
+
try {
|
|
43
|
+
const resolvedTag = await downloadBinary(tag);
|
|
44
|
+
console.log(`fff: Download complete! (${resolvedTag})`);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error("fff: Download failed:", error);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
case "info": {
|
|
53
|
+
const pkg = await getPackageInfo();
|
|
54
|
+
let npmPackage: string;
|
|
55
|
+
try {
|
|
56
|
+
npmPackage = getNpmPackageName();
|
|
57
|
+
} catch {
|
|
58
|
+
npmPackage = "unsupported";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log("fff - Fast File Finder");
|
|
62
|
+
console.log(`Package version: ${pkg.version}`);
|
|
63
|
+
console.log("");
|
|
64
|
+
console.log("Platform Information:");
|
|
65
|
+
console.log(` Triple: ${getTriple()}`);
|
|
66
|
+
console.log(` Extension: ${getLibExtension()}`);
|
|
67
|
+
console.log(` Library name: ${getLibFilename()}`);
|
|
68
|
+
console.log(` npm package: ${npmPackage}`);
|
|
69
|
+
console.log("");
|
|
70
|
+
console.log("Binary Status:");
|
|
71
|
+
const existing = findBinary();
|
|
72
|
+
if (existing) {
|
|
73
|
+
console.log(` Found: ${existing}`);
|
|
74
|
+
} else {
|
|
75
|
+
console.log(` Not found`);
|
|
76
|
+
console.log(` Expected path: ${getBinaryPath()}`);
|
|
77
|
+
console.log(` Try: bun add ${npmPackage}`);
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
case "version":
|
|
83
|
+
case "--version":
|
|
84
|
+
case "-v": {
|
|
85
|
+
const pkg = await getPackageInfo();
|
|
86
|
+
console.log(pkg.version);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case "help":
|
|
91
|
+
case "--help":
|
|
92
|
+
case "-h":
|
|
93
|
+
default: {
|
|
94
|
+
const pkg = await getPackageInfo();
|
|
95
|
+
console.log(`fff - Fast File Finder CLI v${pkg.version}`);
|
|
96
|
+
console.log("");
|
|
97
|
+
console.log("Usage:");
|
|
98
|
+
console.log(" bunx fff download [tag] Download native binary from GitHub (fallback)");
|
|
99
|
+
console.log(" bunx fff info Show platform and binary info");
|
|
100
|
+
console.log(" bunx fff version Show version");
|
|
101
|
+
console.log(" bunx fff help Show this help message");
|
|
102
|
+
console.log("");
|
|
103
|
+
console.log("Examples:");
|
|
104
|
+
console.log(" bunx fff download Download latest binary from GitHub");
|
|
105
|
+
console.log(" bunx fff download abc1234 Download specific release tag");
|
|
106
|
+
console.log("");
|
|
107
|
+
console.log("Note: Binaries are normally provided via platform-specific npm packages.");
|
|
108
|
+
console.log("The download command is a fallback for when those aren't available.");
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
main();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Postinstall script - ensures the native binary is available
|
|
4
|
+
*
|
|
5
|
+
* Resolution order:
|
|
6
|
+
* 1. Platform-specific npm package (installed via optionalDependencies)
|
|
7
|
+
* 2. Local dev build (target/release or target/debug)
|
|
8
|
+
* 3. Fallback: download from GitHub releases
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { findBinary, downloadBinary } from "../src/download";
|
|
12
|
+
import { getNpmPackageName } from "../src/platform";
|
|
13
|
+
|
|
14
|
+
async function main() {
|
|
15
|
+
// Check if binary is already available (npm package or dev build)
|
|
16
|
+
const existing = findBinary();
|
|
17
|
+
if (existing) {
|
|
18
|
+
console.log(`fff: Native library found at ${existing}`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Binary not found via npm package - try downloading from GitHub as fallback
|
|
23
|
+
let packageName: string;
|
|
24
|
+
try {
|
|
25
|
+
packageName = getNpmPackageName();
|
|
26
|
+
} catch {
|
|
27
|
+
packageName = "unknown";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log(
|
|
31
|
+
`fff: Platform package ${packageName} not found, falling back to GitHub download...`
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const tag = await downloadBinary();
|
|
36
|
+
console.log(`fff: Native library installed successfully! (${tag})`);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error("fff: Failed to download native library:", error);
|
|
39
|
+
console.error("");
|
|
40
|
+
console.error("fff: You can build from source instead:");
|
|
41
|
+
console.error(" cargo build --release -p fff-c");
|
|
42
|
+
console.error("");
|
|
43
|
+
console.error(
|
|
44
|
+
"fff: Or run `bunx fff download` after fixing network issues."
|
|
45
|
+
);
|
|
46
|
+
// Don't exit with error - allow install to complete
|
|
47
|
+
// The error will surface when the user tries to use the library
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
main();
|
package/src/download.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
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 { getTriple, getLibExtension, getLibFilename, getNpmPackageName } from "./platform";
|
|
16
|
+
|
|
17
|
+
const GITHUB_REPO = "dmtrKovalenko/fff.nvim";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the current file's directory
|
|
21
|
+
*/
|
|
22
|
+
function getCurrentDir(): string {
|
|
23
|
+
const url = import.meta.url;
|
|
24
|
+
if (url.startsWith("file://")) {
|
|
25
|
+
return dirname(fileURLToPath(url));
|
|
26
|
+
}
|
|
27
|
+
return dirname(url);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get the package root directory
|
|
32
|
+
*/
|
|
33
|
+
function getPackageDir(): string {
|
|
34
|
+
const currentDir = getCurrentDir();
|
|
35
|
+
return dirname(currentDir);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get the directory where binaries are stored (legacy/fallback)
|
|
40
|
+
*/
|
|
41
|
+
export function getBinDir(): string {
|
|
42
|
+
return join(getPackageDir(), "bin");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get the full path to the native library in bin/ (legacy/fallback)
|
|
47
|
+
*/
|
|
48
|
+
export function getBinaryPath(): string {
|
|
49
|
+
const binDir = getBinDir();
|
|
50
|
+
return join(binDir, getLibFilename());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if the binary exists in any known location
|
|
55
|
+
*/
|
|
56
|
+
export function binaryExists(): boolean {
|
|
57
|
+
return findBinary() !== null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Try to resolve the binary from the platform-specific npm package.
|
|
62
|
+
*
|
|
63
|
+
* When users install @ff-labs/bun, npm/bun automatically installs the matching
|
|
64
|
+
* optionalDependency (e.g. @ff-labs/fff-bun-darwin-arm64). We resolve the binary
|
|
65
|
+
* path by requiring that package's package.json and looking for the binary
|
|
66
|
+
* in the same directory.
|
|
67
|
+
*/
|
|
68
|
+
function resolveFromNpmPackage(): string | null {
|
|
69
|
+
const packageName = getNpmPackageName();
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
// Use createRequire to resolve the platform package's location
|
|
73
|
+
const require = createRequire(join(getPackageDir(), "package.json"));
|
|
74
|
+
const packageJsonPath = require.resolve(`${packageName}/package.json`);
|
|
75
|
+
const packageDir = dirname(packageJsonPath);
|
|
76
|
+
const binaryPath = join(packageDir, getLibFilename());
|
|
77
|
+
|
|
78
|
+
if (existsSync(binaryPath)) {
|
|
79
|
+
return binaryPath;
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// Package not installed - this is expected on unsupported platforms
|
|
83
|
+
// or when installed without optional dependencies
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get the development binary path (for local development)
|
|
91
|
+
*/
|
|
92
|
+
export function getDevBinaryPath(): string | null {
|
|
93
|
+
const packageDir = getPackageDir();
|
|
94
|
+
const workspaceRoot = join(packageDir, "..", "..");
|
|
95
|
+
|
|
96
|
+
const possiblePaths = [
|
|
97
|
+
join(workspaceRoot, "target", "release", getLibFilename()),
|
|
98
|
+
join(workspaceRoot, "target", "debug", getLibFilename()),
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
for (const path of possiblePaths) {
|
|
102
|
+
if (existsSync(path)) {
|
|
103
|
+
return path;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Find the binary, checking all known locations in priority order:
|
|
112
|
+
* 1. Platform-specific npm package (primary distribution method)
|
|
113
|
+
* 2. Local bin/ directory (legacy postinstall download)
|
|
114
|
+
* 3. Local dev build (cargo build output)
|
|
115
|
+
*/
|
|
116
|
+
export function findBinary(): string | null {
|
|
117
|
+
// 1. Try platform-specific npm package first
|
|
118
|
+
const npmPath = resolveFromNpmPackage();
|
|
119
|
+
if (npmPath) return npmPath;
|
|
120
|
+
|
|
121
|
+
// 2. Try local bin/ directory (legacy or manual download)
|
|
122
|
+
const installedPath = getBinaryPath();
|
|
123
|
+
if (existsSync(installedPath)) return installedPath;
|
|
124
|
+
|
|
125
|
+
// 3. Try local dev build
|
|
126
|
+
return getDevBinaryPath();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Download the binary from GitHub releases as a fallback.
|
|
131
|
+
* This is only used when the platform npm package is not available.
|
|
132
|
+
*
|
|
133
|
+
* @param tag - The release tag to download (e.g. commit hash), or "latest"
|
|
134
|
+
*/
|
|
135
|
+
export async function downloadBinary(tag?: string): Promise<string> {
|
|
136
|
+
const resolvedTag = tag || "latest";
|
|
137
|
+
const triple = getTriple();
|
|
138
|
+
const ext = getLibExtension();
|
|
139
|
+
|
|
140
|
+
// Resolve "latest" tag via GitHub API
|
|
141
|
+
let releaseTag = resolvedTag;
|
|
142
|
+
if (releaseTag === "latest") {
|
|
143
|
+
console.log("fff: Fetching latest release tag from GitHub...");
|
|
144
|
+
releaseTag = await fetchLatestReleaseTag();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const binaryName = `c-lib-${triple}.${ext}`;
|
|
148
|
+
const baseUrl = `https://github.com/${GITHUB_REPO}/releases/download/${releaseTag}`;
|
|
149
|
+
const binaryUrl = `${baseUrl}/${binaryName}`;
|
|
150
|
+
|
|
151
|
+
console.log(`fff: Downloading native library for ${triple}...`);
|
|
152
|
+
console.log(`fff: Release: ${releaseTag}`);
|
|
153
|
+
|
|
154
|
+
const binaryResponse = await fetch(binaryUrl);
|
|
155
|
+
if (!binaryResponse.ok) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Failed to download binary: ${binaryResponse.status} ${binaryResponse.statusText}\nURL: ${binaryUrl}`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const binaryBuffer = Buffer.from(await binaryResponse.arrayBuffer());
|
|
162
|
+
|
|
163
|
+
const binDir = getBinDir();
|
|
164
|
+
if (!existsSync(binDir)) {
|
|
165
|
+
mkdirSync(binDir, { recursive: true });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const binaryPath = getBinaryPath();
|
|
169
|
+
writeFileSync(binaryPath, binaryBuffer);
|
|
170
|
+
|
|
171
|
+
// Make executable on Unix
|
|
172
|
+
if (process.platform !== "win32") {
|
|
173
|
+
chmodSync(binaryPath, 0o755);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(`fff: Binary downloaded to ${binaryPath}`);
|
|
177
|
+
return releaseTag;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Fetch the latest release tag from GitHub
|
|
182
|
+
*/
|
|
183
|
+
async function fetchLatestReleaseTag(): Promise<string> {
|
|
184
|
+
const url = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
|
|
185
|
+
|
|
186
|
+
const response = await fetch(url, {
|
|
187
|
+
headers: {
|
|
188
|
+
"Accept": "application/vnd.github.v3+json",
|
|
189
|
+
"User-Agent": "fff-bun-client",
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
const allReleasesUrl = `https://api.github.com/repos/${GITHUB_REPO}/releases`;
|
|
195
|
+
const allResponse = await fetch(allReleasesUrl, {
|
|
196
|
+
headers: {
|
|
197
|
+
"Accept": "application/vnd.github.v3+json",
|
|
198
|
+
"User-Agent": "fff-bun-client",
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (!allResponse.ok) {
|
|
203
|
+
throw new Error(`Failed to fetch releases: ${allResponse.status}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const releases = await allResponse.json() as Array<{ tag_name: string }>;
|
|
207
|
+
if (releases.length === 0) {
|
|
208
|
+
throw new Error("No releases found");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return releases[0].tag_name;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const release = await response.json() as { tag_name: string };
|
|
215
|
+
return release.tag_name;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Ensure the binary exists, downloading from GitHub if necessary.
|
|
220
|
+
*/
|
|
221
|
+
export async function ensureBinary(): Promise<string> {
|
|
222
|
+
const existingPath = findBinary();
|
|
223
|
+
if (existingPath) {
|
|
224
|
+
return existingPath;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Fallback: download from GitHub
|
|
228
|
+
await downloadBinary();
|
|
229
|
+
return getBinaryPath();
|
|
230
|
+
}
|