@prometheus-ai/natives 0.5.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.
@@ -0,0 +1,598 @@
1
+ import * as childProcess from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import * as os from "node:os";
5
+ import * as path from "node:path";
6
+ import * as zlib from "node:zlib";
7
+ import packageJson from "../package.json" with { type: "json" };
8
+ import { embeddedAddon } from "./embedded-addon.js";
9
+
10
+ /**
11
+ * Native addon loader for `@prometheus-ai/natives`.
12
+ *
13
+ * Owns every step between "Node imports `native/index.js`" and "the right
14
+ * `prometheus_natives.<platform>-<arch>*.node` is required, validated, and returned":
15
+ * platform/variant detection, candidate-path resolution, on-disk staging from
16
+ * `node_modules` (Windows update safety), embedded-addon extraction (Bun
17
+ * standalone binaries), version-sentinel validation, and the aggregated error
18
+ * surface for diagnostic-friendly failures.
19
+ *
20
+ * `native/index.js` is reduced to one `loadNative()` call plus the generated
21
+ * surface-area exports between `MARKER_START`/`MARKER_END` (rewritten by
22
+ * `scripts/gen-enums.ts`); everything else lives here so the pure helpers stay
23
+ * unit-testable without triggering the side-effectful module-load path.
24
+ *
25
+ * Background (issue #823): `bun build --compile --define PROMETHEUS_COMPILED=true`
26
+ * substitutes the bare identifier `PROMETHEUS_COMPILED`, NOT `process.env.PROMETHEUS_COMPILED`,
27
+ * so a runtime read of the env var returns `undefined`. Older CommonJS loader
28
+ * code also saw the original build-host absolute path in `__filename`; ESM
29
+ * `import.meta.url` is rewritten to the bunfs URL. The embedded-addon
30
+ * presence (true iff the build pipeline ran `embed:native`, false in the
31
+ * post-build `--reset` stub) is the authoritative compiled-mode signal.
32
+ */
33
+
34
+ const SUPPORTED_PLATFORMS = ["linux-x64", "linux-arm64", "darwin-x64", "darwin-arm64", "win32-x64"];
35
+
36
+ function getNativesDir() {
37
+ const xdgDataHome = process.env.XDG_DATA_HOME;
38
+ if (xdgDataHome && fs.existsSync(path.join(xdgDataHome, "prometheus"))) {
39
+ return path.join(xdgDataHome, "prometheus", "natives");
40
+ }
41
+ return path.join(os.homedir(), ".prometheus", "natives");
42
+ }
43
+
44
+ function resolveLeafPackageDir(platformTag) {
45
+ try {
46
+ const require_ = createRequire(import.meta.url);
47
+ return path.dirname(require_.resolve(`@prometheus-ai/natives-${platformTag}/package.json`));
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ // =========================================================================
54
+ // Pure helpers — re-exported for unit tests in `packages/natives/test/`.
55
+ // =========================================================================
56
+
57
+ /**
58
+ * @param {{
59
+ * embeddedAddon: { platformTag: string; version: string; files: unknown[] } | null | undefined;
60
+ * env: Record<string, string | undefined>;
61
+ * importMetaUrl: string | null | undefined;
62
+ * }} input
63
+ * @returns {boolean}
64
+ */
65
+ export function detectCompiledBinary({ embeddedAddon, env, importMetaUrl }) {
66
+ if (embeddedAddon) return true;
67
+ if (env && env.PROMETHEUS_COMPILED) return true;
68
+ if (typeof importMetaUrl === "string") {
69
+ if (importMetaUrl.includes("$bunfs")) return true;
70
+ if (importMetaUrl.includes("~BUN")) return true;
71
+ if (importMetaUrl.includes("%7EBUN")) return true;
72
+ }
73
+ return false;
74
+ }
75
+
76
+ /**
77
+ * @param {{ tag: string; arch: string; variant: "modern" | "baseline" | null | undefined }} input
78
+ * @returns {string[]}
79
+ */
80
+ export function getAddonFilenames({ tag, arch, variant }) {
81
+ const defaultFilename = `prometheus_natives.${tag}.node`;
82
+ if (arch !== "x64" || !variant) return [defaultFilename];
83
+ const baselineFilename = `prometheus_natives.${tag}-baseline.node`;
84
+ const modernFilename = `prometheus_natives.${tag}-modern.node`;
85
+ if (variant === "modern") {
86
+ return [modernFilename, baselineFilename, defaultFilename];
87
+ }
88
+ return [baselineFilename, defaultFilename];
89
+ }
90
+
91
+ /**
92
+ * Decide whether the loader should mirror the package's `native/<filename>.node`
93
+ * into the per-version cache directory (`~/.prometheus/natives/<version>/`) before loading.
94
+ *
95
+ * Windows-only safety net for `bun install -g` updates: when a previous `prometheus`
96
+ * process is running, bun cannot overwrite the locked `.node` inside
97
+ * `node_modules/@prometheus-ai/natives/native/`, leaving an old binary next to a
98
+ * newer `index.js` and producing `<sym> is not a function` crashes on the next
99
+ * launch. Staging into the version-pinned cache:
100
+ * 1. Gives every package version its own filesystem path, so concurrent prometheus
101
+ * processes never collide on the same file.
102
+ * 2. Makes the running process keep its handle on the cache copy, freeing bun
103
+ * to overwrite the `node_modules` copy on subsequent updates.
104
+ * Disabled on non-Windows (no file-lock problem), in workspace dev (`nativeDir`
105
+ * is not inside a `node_modules` segment), and for compiled binaries (handled
106
+ * by `maybeExtractEmbeddedAddon`).
107
+ *
108
+ * @param {{ platform: NodeJS.Platform | string; isCompiledBinary: boolean; nativeDir: string }} input
109
+ * @returns {boolean}
110
+ */
111
+ export function shouldStageNodeModulesAddon({ platform, isCompiledBinary, nativeDir }) {
112
+ if (platform !== "win32") return false;
113
+ if (isCompiledBinary) return false;
114
+ // Check both separators independently of the host's `path.sep`: this helper
115
+ // is shared by the loader (running on Windows with `\`) and the test suite
116
+ // (typically running on POSIX hosts when CI executes the regression test).
117
+ return nativeDir.includes("\\node_modules\\") || nativeDir.includes("/node_modules/");
118
+ }
119
+
120
+ /**
121
+ * @param {{
122
+ * addonFilenames: string[];
123
+ * isCompiledBinary: boolean;
124
+ * stageFromNodeModules?: boolean;
125
+ * nativeDir: string;
126
+ * leafPackageDir?: string | null;
127
+ * execDir: string;
128
+ * versionedDir: string;
129
+ * userDataDir: string;
130
+ * }} input
131
+ * @returns {string[]}
132
+ */
133
+ export function resolveLoaderCandidates({
134
+ addonFilenames,
135
+ isCompiledBinary,
136
+ stageFromNodeModules = false,
137
+ nativeDir,
138
+ leafPackageDir = null,
139
+ execDir,
140
+ versionedDir,
141
+ userDataDir,
142
+ }) {
143
+ const baseReleaseCandidates = addonFilenames.flatMap(filename => [
144
+ path.join(nativeDir, filename),
145
+ path.join(execDir, filename),
146
+ ]);
147
+ const leafCandidates = leafPackageDir ? addonFilenames.map(filename => path.join(leafPackageDir, filename)) : [];
148
+ const compiledCandidates = addonFilenames.flatMap(filename => [
149
+ path.join(versionedDir, filename),
150
+ path.join(userDataDir, filename),
151
+ ]);
152
+ const stagedCandidates = stageFromNodeModules ? addonFilenames.map(filename => path.join(versionedDir, filename)) : [];
153
+ let releaseCandidates;
154
+ if (isCompiledBinary) {
155
+ releaseCandidates = [...compiledCandidates, ...baseReleaseCandidates];
156
+ } else if (stageFromNodeModules) {
157
+ releaseCandidates = [...stagedCandidates, ...leafCandidates, ...baseReleaseCandidates];
158
+ } else {
159
+ releaseCandidates = [...leafCandidates, ...baseReleaseCandidates];
160
+ }
161
+ return [...new Set(releaseCandidates)];
162
+ }
163
+
164
+ // =========================================================================
165
+ // Side-effectful loader. Everything below runs only when `loadNative()` is
166
+ // called from `native/index.js` — tests that only import the pure helpers
167
+ // above pay nothing for variant detection, subprocess spawns, or fs probes.
168
+ // =========================================================================
169
+
170
+ function runCommand(command, args) {
171
+ try {
172
+ const result = childProcess.spawnSync(command, args, { encoding: "utf-8" });
173
+ if (result.error) return null;
174
+ if (result.status !== 0) return null;
175
+ return (result.stdout || "").trim();
176
+ } catch {
177
+ return null;
178
+ }
179
+ }
180
+
181
+ function getVariantOverride() {
182
+ const value = process.env.PROMETHEUS_NATIVE_VARIANT;
183
+ if (!value) return null;
184
+ if (value === "modern" || value === "baseline") return value;
185
+ return null;
186
+ }
187
+
188
+ function detectAvx2Support() {
189
+ if (process.arch !== "x64") {
190
+ return false;
191
+ }
192
+
193
+ if (process.platform === "linux") {
194
+ try {
195
+ const cpuInfo = fs.readFileSync("/proc/cpuinfo", "utf8");
196
+ return /\bavx2\b/i.test(cpuInfo);
197
+ } catch {
198
+ return false;
199
+ }
200
+ }
201
+
202
+ if (process.platform === "darwin") {
203
+ const leaf7 = runCommand("sysctl", ["-n", "machdep.cpu.leaf7_features"]);
204
+ if (leaf7 && /\bAVX2\b/i.test(leaf7)) {
205
+ return true;
206
+ }
207
+ const features = runCommand("sysctl", ["-n", "machdep.cpu.features"]);
208
+ return Boolean(features && /\bAVX2\b/i.test(features));
209
+ }
210
+
211
+ if (process.platform === "win32") {
212
+ const output = runCommand("powershell.exe", [
213
+ "-NoProfile",
214
+ "-NonInteractive",
215
+ "-Command",
216
+ "[System.Runtime.Intrinsics.X86.Avx2]::IsSupported",
217
+ ]);
218
+ return output && output.toLowerCase() === "true";
219
+ }
220
+
221
+ return false;
222
+ }
223
+
224
+ function resolveCpuVariant(override) {
225
+ if (process.arch !== "x64") return null;
226
+ if (override) return override;
227
+ return detectAvx2Support() ? "modern" : "baseline";
228
+ }
229
+
230
+ function selectEmbeddedAddonFile(selectedVariant) {
231
+ if (!embeddedAddon) return null;
232
+ const defaultFile = embeddedAddon.files.find(file => file.variant === "default") || null;
233
+ if (process.arch !== "x64") return defaultFile || embeddedAddon.files[0] || null;
234
+ if (selectedVariant === "modern") {
235
+ return (
236
+ embeddedAddon.files.find(file => file.variant === "modern") ||
237
+ embeddedAddon.files.find(file => file.variant === "baseline") ||
238
+ null
239
+ );
240
+ }
241
+ return embeddedAddon.files.find(file => file.variant === "baseline") || null;
242
+ }
243
+
244
+ function readTarString(buffer, offset, length) {
245
+ const end = Math.min(offset + length, buffer.length);
246
+ let stringEnd = offset;
247
+ while (stringEnd < end && buffer[stringEnd] !== 0) stringEnd++;
248
+ return buffer.toString("utf8", offset, stringEnd);
249
+ }
250
+
251
+ function readTarOctal(buffer, offset, length) {
252
+ const value = readTarString(buffer, offset, length).trim();
253
+ if (!value) return 0;
254
+ const parsed = Number.parseInt(value, 8);
255
+ if (!Number.isFinite(parsed)) {
256
+ throw new Error(`Invalid tar octal value: ${value}`);
257
+ }
258
+ return parsed;
259
+ }
260
+
261
+ function isZeroTarBlock(buffer, offset) {
262
+ for (let index = 0; index < 512; index++) {
263
+ if (buffer[offset + index] !== 0) return false;
264
+ }
265
+ return true;
266
+ }
267
+
268
+ function getTarEntryName(header) {
269
+ const name = readTarString(header, 0, 100);
270
+ const prefix = readTarString(header, 345, 155);
271
+ return prefix ? `${prefix}/${name}` : name;
272
+ }
273
+
274
+ function isSafeEmbeddedAddonFilename(filename) {
275
+ return filename.length > 0 && path.basename(filename) === filename && !filename.includes("/") && !filename.includes("\\");
276
+ }
277
+
278
+ function isEmbeddedAddonFileCurrent(targetPath, file) {
279
+ try {
280
+ const stat = fs.statSync(targetPath);
281
+ if (!stat.isFile()) return false;
282
+ return typeof file.size !== "number" || stat.size === file.size;
283
+ } catch (err) {
284
+ if (err && err.code === "ENOENT") return false;
285
+ throw err;
286
+ }
287
+ }
288
+
289
+ function writeEmbeddedAddonFile(targetPath, content) {
290
+ const tempPath = `${targetPath}.tmp.${process.pid}.${Date.now()}`;
291
+ try {
292
+ fs.writeFileSync(tempPath, content, { mode: 0o755 });
293
+ fs.renameSync(tempPath, targetPath);
294
+ } catch (err) {
295
+ try {
296
+ fs.unlinkSync(tempPath);
297
+ } catch {
298
+ // Best-effort cleanup only.
299
+ }
300
+ throw err;
301
+ }
302
+ }
303
+
304
+ export function extractEmbeddedAddonArchive({ archivePath, files, targetDir }) {
305
+ const pending = new Map();
306
+ for (const file of files) {
307
+ if (!isSafeEmbeddedAddonFilename(file.filename)) {
308
+ throw new Error(`Unsafe embedded addon filename: ${file.filename}`);
309
+ }
310
+ const targetPath = path.join(targetDir, file.filename);
311
+ if (!isEmbeddedAddonFileCurrent(targetPath, file)) {
312
+ pending.set(file.filename, file);
313
+ }
314
+ }
315
+ if (pending.size === 0) return [];
316
+
317
+ const archive = zlib.gunzipSync(fs.readFileSync(archivePath));
318
+ const writtenPaths = [];
319
+ let offset = 0;
320
+
321
+ while (offset + 512 <= archive.length) {
322
+ if (isZeroTarBlock(archive, offset)) break;
323
+ const header = archive.subarray(offset, offset + 512);
324
+ const filename = getTarEntryName(header);
325
+ const size = readTarOctal(header, 124, 12);
326
+ const typeflag = header[156] === 0 ? "0" : String.fromCharCode(header[156]);
327
+ offset += 512;
328
+
329
+ if (offset + size > archive.length) {
330
+ throw new Error(`Truncated embedded addon archive entry: ${filename}`);
331
+ }
332
+
333
+ if (!isSafeEmbeddedAddonFilename(filename)) {
334
+ throw new Error(`Unsafe embedded addon archive entry: ${filename}`);
335
+ }
336
+ if (typeflag !== "0") {
337
+ throw new Error(`Unsupported embedded addon archive entry type ${typeflag}: ${filename}`);
338
+ }
339
+
340
+ const file = pending.get(filename);
341
+ if (file) {
342
+ if (typeof file.size === "number" && file.size !== size) {
343
+ throw new Error(`Embedded addon size mismatch for ${filename}: expected ${file.size}, got ${size}`);
344
+ }
345
+ const targetPath = path.join(targetDir, filename);
346
+ writeEmbeddedAddonFile(targetPath, archive.subarray(offset, offset + size));
347
+ pending.delete(filename);
348
+ writtenPaths.push(targetPath);
349
+ }
350
+
351
+ offset += Math.ceil(size / 512) * 512;
352
+ }
353
+
354
+ if (pending.size > 0) {
355
+ throw new Error(`Embedded addon archive missing: ${[...pending.keys()].join(", ")}`);
356
+ }
357
+
358
+ return writtenPaths;
359
+ }
360
+
361
+ function maybeExtractEmbeddedAddon(ctx, errors) {
362
+ if (!ctx.isCompiledBinary || !embeddedAddon) return null;
363
+ if (embeddedAddon.platformTag !== ctx.platformTag || embeddedAddon.version !== ctx.packageVersion) return null;
364
+
365
+ const selectedEmbeddedFile = selectEmbeddedAddonFile(ctx.selectedVariant);
366
+ if (!selectedEmbeddedFile) return null;
367
+ const targetPath = path.join(ctx.versionedDir, selectedEmbeddedFile.filename);
368
+
369
+ try {
370
+ fs.mkdirSync(ctx.versionedDir, { recursive: true });
371
+ } catch (err) {
372
+ const message = err instanceof Error ? err.message : String(err);
373
+ errors.push(`embedded addon dir: ${message}`);
374
+ return null;
375
+ }
376
+
377
+ if (embeddedAddon.archive) {
378
+ try {
379
+ extractEmbeddedAddonArchive({
380
+ archivePath: embeddedAddon.archive.filePath,
381
+ files: embeddedAddon.files,
382
+ targetDir: ctx.versionedDir,
383
+ });
384
+ if (isEmbeddedAddonFileCurrent(targetPath, selectedEmbeddedFile)) {
385
+ return targetPath;
386
+ }
387
+ errors.push(`embedded addon archive (${embeddedAddon.archive.filename}): missing ${selectedEmbeddedFile.filename}`);
388
+ return null;
389
+ } catch (err) {
390
+ const message = err instanceof Error ? err.message : String(err);
391
+ errors.push(`embedded addon archive (${embeddedAddon.archive.filename}): ${message}`);
392
+ return null;
393
+ }
394
+ }
395
+
396
+ if (isEmbeddedAddonFileCurrent(targetPath, selectedEmbeddedFile)) {
397
+ return targetPath;
398
+ }
399
+ if (!selectedEmbeddedFile.filePath) {
400
+ errors.push(`embedded addon metadata missing file path for ${selectedEmbeddedFile.filename}`);
401
+ return null;
402
+ }
403
+
404
+ try {
405
+ const buffer = fs.readFileSync(selectedEmbeddedFile.filePath);
406
+ fs.writeFileSync(targetPath, buffer);
407
+ return targetPath;
408
+ } catch (err) {
409
+ const message = err instanceof Error ? err.message : String(err);
410
+ errors.push(`embedded addon write (${selectedEmbeddedFile.filename}): ${message}`);
411
+ return null;
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Mirror `leafPackageDir ?? nativeDir` addon binaries to
417
+ * `versionedDir/<filename>.node` on Windows installs so the running process
418
+ * cache path, never on the `node_modules` copy that bun must overwrite on
419
+ * update. No-op on non-Windows, in workspace dev, and for compiled binaries —
420
+ * see `shouldStageNodeModulesAddon` for the gating rules.
421
+ */
422
+ function maybeStageNodeModulesAddon(ctx, errors) {
423
+ if (!ctx.stageFromNodeModules) return null;
424
+
425
+ let stagedPath = null;
426
+ for (const filename of ctx.addonFilenames) {
427
+ const sourcePath = path.join(ctx.leafPackageDir ?? ctx.nativeDir, filename);
428
+ const targetPath = path.join(ctx.versionedDir, filename);
429
+
430
+ if (fs.existsSync(targetPath)) {
431
+ stagedPath = stagedPath || targetPath;
432
+ continue;
433
+ }
434
+ if (!fs.existsSync(sourcePath)) continue;
435
+
436
+ try {
437
+ fs.mkdirSync(ctx.versionedDir, { recursive: true });
438
+ } catch (err) {
439
+ const message = err instanceof Error ? err.message : String(err);
440
+ errors.push(`staged addon dir: ${message}`);
441
+ continue;
442
+ }
443
+
444
+ try {
445
+ // `copyFileSync` is atomic on Windows (CopyFileW) and avoids holding
446
+ // two large buffers in JS for the read/write dance.
447
+ fs.copyFileSync(sourcePath, targetPath);
448
+ stagedPath = stagedPath || targetPath;
449
+ } catch (err) {
450
+ const message = err instanceof Error ? err.message : String(err);
451
+ errors.push(`staged addon copy (${filename}): ${message}`);
452
+ }
453
+ }
454
+ return stagedPath;
455
+ }
456
+
457
+ function validateLoadedBindings(ctx, bindings, candidate) {
458
+ // In workspace dev (running out of `packages/natives/native/` rather than a
459
+ // `node_modules` install or a compiled bundle) the local `.node` only gains
460
+ // the renamed sentinel after `bun --cwd=packages/natives run build`. Skip
461
+ // validation there so a stale post-pull dev tree boots while the rebuild
462
+ // completes; install and compiled-binary paths still validate.
463
+ if (ctx.isWorkspaceLoad) return;
464
+ if (typeof bindings[ctx.versionSentinelExport] === "function") return;
465
+ throw new Error(
466
+ `Loaded ${candidate} but it does not expose the @prometheus-ai/natives@${ctx.packageVersion} ` +
467
+ `version sentinel \`${ctx.versionSentinelExport}\`. The .node file on disk is from a different ` +
468
+ "release than this loader — reinstall to re-sync.",
469
+ );
470
+ }
471
+
472
+ function buildHelpMessage(ctx) {
473
+ if (ctx.isCompiledBinary) {
474
+ const expectedPaths = ctx.addonFilenames.map(filename => ` ${path.join(ctx.versionedDir, filename)}`).join("\n");
475
+ const downloadHints = ctx.addonFilenames
476
+ .map(filename => {
477
+ const downloadUrl = `https://github.com/uttamtrivedi/Prometheus/releases/latest/download/${filename}`;
478
+ const targetPath = path.join(ctx.versionedDir, filename);
479
+ return ` curl -fsSL "${downloadUrl}" -o "${targetPath}"`;
480
+ })
481
+ .join("\n");
482
+ return (
483
+ `The compiled binary should extract one of:\n${expectedPaths}\n\n` +
484
+ `If missing, delete ${ctx.versionedDir} and re-run, or download manually:\n${downloadHints}`
485
+ );
486
+ }
487
+ return (
488
+ "If installed via npm/bun, try reinstalling: bun install @prometheus-ai/natives\n" +
489
+ "If developing locally, build with: bun --cwd=packages/natives run build\n" +
490
+ "Optional x64 variants: TARGET_VARIANT=baseline|modern bun --cwd=packages/natives run build"
491
+ );
492
+ }
493
+
494
+ /**
495
+ * Initialize the loader context: resolves every path, variant, and policy
496
+ * decision once so the inner load loop stays a pure require/validate pipeline.
497
+ * Called from `loadNative()` rather than at module scope so importing pure
498
+ * helpers from this file doesn't trigger AVX2 detection or filesystem probes.
499
+ */
500
+ function initLoaderContext() {
501
+ const platformTag = `${process.platform}-${process.arch}`;
502
+ const packageVersion = packageJson.version;
503
+ const nativeDir = path.join(import.meta.dir, "..", "native");
504
+ const execDir = path.dirname(process.execPath);
505
+ const versionedDir = path.join(getNativesDir(), packageVersion);
506
+ const userDataDir =
507
+ process.platform === "win32"
508
+ ? path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "prometheus")
509
+ : path.join(os.homedir(), ".local", "bin");
510
+
511
+ const isCompiledBinary = detectCompiledBinary({
512
+ embeddedAddon,
513
+ env: process.env,
514
+ importMetaUrl: import.meta.url,
515
+ });
516
+ const leafPackageDir = isCompiledBinary ? null : resolveLeafPackageDir(platformTag);
517
+ const stageFromNodeModules = shouldStageNodeModulesAddon({
518
+ platform: process.platform,
519
+ isCompiledBinary,
520
+ nativeDir,
521
+ });
522
+
523
+ const selectedVariant = resolveCpuVariant(getVariantOverride());
524
+ const addonFilenames = getAddonFilenames({ tag: platformTag, arch: process.arch, variant: selectedVariant });
525
+ const addonLabel = selectedVariant ? `${platformTag} (${selectedVariant})` : platformTag;
526
+
527
+ const candidates = resolveLoaderCandidates({
528
+ addonFilenames,
529
+ isCompiledBinary,
530
+ stageFromNodeModules,
531
+ nativeDir,
532
+ leafPackageDir,
533
+ execDir,
534
+ versionedDir,
535
+ userDataDir,
536
+ });
537
+
538
+ // Version sentinel emitted by the Rust addon under a `js_name` that encodes
539
+ // the package version (`__prometheusNativesV{major}_{minor}_{patch}`).
540
+ // `scripts/release.ts` bumps the name in `crates/prometheus-natives/src/lib.rs` in
541
+ // lock-step with the version, so a `.node` from a different release
542
+ // physically cannot expose the symbol this loader is looking for. That
543
+ // turns the silent `<sym> is not a function` crash from a Windows
544
+ // locked-file update into an actionable load-time error.
545
+ const versionSentinelExport = `__prometheusNativesV${packageVersion.replace(/[^A-Za-z0-9]/g, "_")}`;
546
+ const isWorkspaceLoad =
547
+ !isCompiledBinary && !nativeDir.includes("\\node_modules\\") && !nativeDir.includes("/node_modules/");
548
+
549
+ return {
550
+ platformTag,
551
+ packageVersion,
552
+ nativeDir,
553
+ leafPackageDir,
554
+ versionedDir,
555
+ isCompiledBinary,
556
+ stageFromNodeModules,
557
+ selectedVariant,
558
+ addonFilenames,
559
+ addonLabel,
560
+ candidates,
561
+ versionSentinelExport,
562
+ isWorkspaceLoad,
563
+ };
564
+ }
565
+
566
+ export function loadNative() {
567
+ const ctx = initLoaderContext();
568
+ const require_ = createRequire(import.meta.url);
569
+
570
+ const errors = [];
571
+ const embeddedCandidate = maybeExtractEmbeddedAddon(ctx, errors);
572
+ const stagedCandidate = embeddedCandidate ? null : maybeStageNodeModulesAddon(ctx, errors);
573
+ const prepended = [embeddedCandidate, stagedCandidate].filter(c => typeof c === "string");
574
+ const runtimeCandidates = prepended.length > 0 ? [...prepended, ...ctx.candidates] : ctx.candidates;
575
+
576
+ for (const candidate of runtimeCandidates) {
577
+ try {
578
+ const bindings = require_(candidate);
579
+ validateLoadedBindings(ctx, bindings, candidate);
580
+ return bindings;
581
+ } catch (err) {
582
+ const message = err instanceof Error ? err.message : String(err);
583
+ errors.push(`${candidate}: ${message}`);
584
+ }
585
+ }
586
+
587
+ if (!SUPPORTED_PLATFORMS.includes(ctx.platformTag)) {
588
+ throw new Error(
589
+ `Unsupported platform: ${ctx.platformTag}\n` +
590
+ `Supported platforms: ${SUPPORTED_PLATFORMS.join(", ")}\n` +
591
+ "If you need support for this platform, please open an issue.",
592
+ );
593
+ }
594
+ const details = errors.map(error => `- ${error}`).join("\n");
595
+ throw new Error(
596
+ `Failed to load prometheus_natives native addon for ${ctx.addonLabel}.\n\nTried:\n${details}\n\n${buildHelpMessage(ctx)}`,
597
+ );
598
+ }
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@prometheus-ai/natives",
3
+ "version": "0.5.0",
4
+ "description": "Native Rust bindings for grep, clipboard, image processing, syntax highlighting, PTY, and shell operations via N-API",
5
+ "type": "module",
6
+ "homepage": "https://prometheus.trivlab.com",
7
+ "author": "Uttam Trivedi",
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/uttamtrivedi/Prometheus.git",
12
+ "directory": "packages/natives"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/uttamtrivedi/Prometheus/issues"
16
+ },
17
+ "keywords": [
18
+ "napi",
19
+ "rust",
20
+ "native",
21
+ "grep",
22
+ "text-processing",
23
+ "clipboard",
24
+ "image",
25
+ "pty",
26
+ "shell",
27
+ "syntax-highlighting"
28
+ ],
29
+ "main": "./native/index.js",
30
+ "types": "./native/index.d.ts",
31
+ "scripts": {
32
+ "build": "bun scripts/build-native.ts",
33
+ "check": "biome check . && bun run check:types",
34
+ "check:types": "tsgo -p tsconfig.json --noEmit",
35
+ "lint": "biome lint .",
36
+ "test": "bun test --parallel",
37
+ "fix": "biome check --write --unsafe .",
38
+ "fmt": "biome format --write .",
39
+ "embed:native": "bun scripts/embed-native.ts",
40
+ "gen:npm": "bun scripts/gen-npm-packages.ts",
41
+ "bench": "bun bench/grep.ts"
42
+ },
43
+ "devDependencies": {
44
+ "@napi-rs/cli": "3.7.0",
45
+ "@types/bun": "^1.3.14"
46
+ },
47
+ "engines": {
48
+ "bun": ">=1.3.14"
49
+ },
50
+ "napi": {
51
+ "binaryName": "prometheus_natives",
52
+ "triples": {}
53
+ },
54
+ "files": [
55
+ "native/index.js",
56
+ "native/index.d.ts",
57
+ "native/loader-state.js",
58
+ "native/loader-state.d.ts",
59
+ "native/embedded-addon.js",
60
+ "README.md"
61
+ ],
62
+ "exports": {
63
+ ".": {
64
+ "types": "./native/index.d.ts",
65
+ "import": "./native/index.js"
66
+ }
67
+ },
68
+ "optionalDependencies": {
69
+ "@prometheus-ai/natives-linux-x64": "0.5.0",
70
+ "@prometheus-ai/natives-linux-arm64": "0.5.0",
71
+ "@prometheus-ai/natives-darwin-x64": "0.5.0",
72
+ "@prometheus-ai/natives-darwin-arm64": "0.5.0",
73
+ "@prometheus-ai/natives-win32-x64": "0.5.0"
74
+ }
75
+ }