@linxiraos/pi-natives 1.1.27 → 1.2.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 CHANGED
@@ -11,6 +11,7 @@ Native Rust functionality via N-API.
11
11
  - **WebRTC**: Native Opus media, SDP offer/answer negotiation, and data-channel events for live sessions
12
12
  - **File locking**: Process-owned cross-process locks with in-memory kernel names on Linux/Windows and `flock(2)` sidecars on other Unix platforms
13
13
  - **PDF**: In-memory PDF-to-Markdown extraction with OCR-page classification via `pdf-inspector`
14
+ - **Binary patches**: `applyBinaryPatch(oldPath, patchPath, outPath)` applies an HDiffPatch single-stream patch (HDIFFSF20, zstd or uncompressed), streaming from files; callers verify digests
14
15
 
15
16
  General-purpose image processing (decode/resize/encode for files and buffers)
16
17
  lives in [`Bun.Image`](https://bun.com/docs/runtime/image) on the JS side; this
@@ -77,7 +78,7 @@ crates/pi-natives/ # Rust source (workspace member)
77
78
  native/ # Core loader files and local/CI native build outputs
78
79
  index.js # Public native export surface
79
80
  loader-state.js # Platform, ISA variant, and addon resolution
80
- embedded-addon.js # Standalone binary embed stub/generated metadata
81
+ embedded-addon.js # Null embed stub; binary builds replace it in memory
81
82
  pi_natives.<platform>-<arch>-modern.node # x64 modern ISA (local/CI artifact)
82
83
  pi_natives.<platform>-<arch>-baseline.node # x64 baseline ISA (local/CI artifact)
83
84
  pi_natives.<platform>-<arch>.node # non-x64 build artifact
@@ -1,6 +1,5 @@
1
-
2
- // AUTOGENERATED FILE -- DO NOT EDIT DIRECTLY
3
- // See scripts/embed-native.ts
1
+ // Always null on disk. Standalone binary builds replace this module in memory
2
+ // with the target's manifest and one zstd frame per addon (scripts/embed-native.ts).
4
3
 
5
4
  /** @typedef {"modern" | "baseline" | "default"} EmbeddedAddonVariant */
6
5
 
@@ -8,15 +7,8 @@
8
7
  * @typedef {Object} EmbeddedAddonFile
9
8
  * @property {EmbeddedAddonVariant} variant
10
9
  * @property {string} filename
11
- * @property {number} size
12
- * @property {string=} filePath
13
- */
14
-
15
- /**
16
- * @typedef {Object} EmbeddedAddonArchive
17
- * @property {"tar.gz"} format
18
- * @property {string} filename
19
- * @property {string} filePath
10
+ * @property {number} size Decompressed `.node` size in bytes.
11
+ * @property {string} zstdPath Embedded zstd frame holding the addon bytes.
20
12
  */
21
13
 
22
14
  /**
@@ -24,7 +16,6 @@
24
16
  * @property {string} platformTag
25
17
  * @property {string} version
26
18
  * @property {EmbeddedAddonFile[]} files
27
- * @property {EmbeddedAddonArchive=} archive
28
19
  */
29
20
 
30
21
  /** @type {EmbeddedAddon|null} */
package/native/index.d.ts CHANGED
@@ -38,6 +38,11 @@ export declare class DesktopSession {
38
38
  * behind it.
39
39
  */
40
40
  get capabilities(): DesktopCapabilities
41
+ /**
42
+ * Lock and display-sleep state read on the calling thread, never queued
43
+ * behind the worker, so it answers at once even while a request is stuck.
44
+ */
45
+ get screenState(): DesktopScreenState
41
46
  listDisplays(): Promise<Array<DesktopDisplay>>
42
47
  listWindows(): Promise<Array<DesktopWindow>>
43
48
  listApplications(options?: ApplicationQuery | undefined | null): Promise<Array<Application>>
@@ -730,6 +735,13 @@ export declare class VcsRepo {
730
735
  */
731
736
  export declare function __ompInstallTokioRuntime(): void
732
737
 
738
+ /**
739
+ * Points the addon at the directory holding downloaded wasm grammars
740
+ * (`<dir>/<file>`). The loader (`native/loader-state.js`) calls this once
741
+ * with `<natives dir>/grammars`; grammars then load lazily from it.
742
+ */
743
+ export declare function __ompSetGrammarDir(dir: string): void
744
+
733
745
  /**
734
746
  * Release version stamped into this `.node` after linking.
735
747
  *
@@ -782,6 +794,15 @@ export interface ApplicationQuery {
782
794
  runningOnly?: boolean
783
795
  }
784
796
 
797
+ /**
798
+ * Applies an `HDiffPatch` single-stream patch to `oldPath`, writing `outPath`.
799
+ *
800
+ * Accepts HDIFFSF20 patches, zstd-compressed or uncompressed; `outPath` is
801
+ * created or truncated. Resolves to the result's size in bytes. Rejects on I/O
802
+ * failure, a damaged or unsupported patch, or a patch made for other old data.
803
+ */
804
+ export declare function applyBinaryPatch(oldPath: string, patchPath: string, outPath: string): Promise<number>
805
+
785
806
  /**
786
807
  * Apply ast-grep rewrite rules to matching files; honors `dryRun` and returns
787
808
  * a promise.
@@ -863,6 +884,11 @@ export interface AstFindResult {
863
884
  limitReached: boolean
864
885
  /** Non-fatal parse or pattern errors collected during the run. */
865
886
  parseErrors?: Array<string>
887
+ /**
888
+ * Languages whose on-demand grammar is not installed; their files were
889
+ * skipped (see `wasmGrammarFor`).
890
+ */
891
+ missingGrammars?: Array<string>
866
892
  }
867
893
 
868
894
  /**
@@ -1035,6 +1061,11 @@ export interface AstReplaceResult {
1035
1061
  limitReached: boolean
1036
1062
  /** Parse or pattern errors when not failing the whole operation. */
1037
1063
  parseErrors?: Array<string>
1064
+ /**
1065
+ * Languages whose on-demand grammar is not installed; their files were
1066
+ * skipped (see `wasmGrammarFor`).
1067
+ */
1068
+ missingGrammars?: Array<string>
1038
1069
  }
1039
1070
 
1040
1071
  export interface AxNode {
@@ -1217,6 +1248,16 @@ export interface DesktopCapabilities {
1217
1248
  inputPermission: string
1218
1249
  axPermission: string
1219
1250
  displayCount: number
1251
+ /**
1252
+ * The user session is locked (lock screen up). Only macOS detects this;
1253
+ * other backends report `false`.
1254
+ */
1255
+ screenLocked: boolean
1256
+ /**
1257
+ * The display is asleep, so nothing can be captured until it wakes. Only
1258
+ * macOS detects this; other backends report `false`.
1259
+ */
1260
+ displayAsleep: boolean
1220
1261
  }
1221
1262
 
1222
1263
  export interface DesktopCapture {
@@ -1242,6 +1283,12 @@ export interface DesktopCapture {
1242
1283
  displays: Array<DesktopDisplay>
1243
1284
  backend: string
1244
1285
  displayServer?: string
1286
+ /**
1287
+ * The user session was locked when this frame was taken: a display
1288
+ * capture shows the lock screen, and a window capture shows the window's
1289
+ * last frame behind it. Only macOS detects this; others report `false`.
1290
+ */
1291
+ screenLocked: boolean
1245
1292
  }
1246
1293
 
1247
1294
  export interface DesktopControlState {
@@ -1290,6 +1337,15 @@ export interface DesktopPoint {
1290
1337
  y: number
1291
1338
  }
1292
1339
 
1340
+ /**
1341
+ * Lock and display-sleep state of the user session, read live per call.
1342
+ * Only macOS detects either; other platforms report both as `false`.
1343
+ */
1344
+ export interface DesktopScreenState {
1345
+ screenLocked: boolean
1346
+ displayAsleep: boolean
1347
+ }
1348
+
1293
1349
  export interface DesktopSessionOptions {
1294
1350
  display?: string
1295
1351
  }
@@ -1908,6 +1964,11 @@ export interface GrepMatch {
1908
1964
  contextAfter?: Array<ContextLine>
1909
1965
  /** Whether the line was truncated. */
1910
1966
  truncated?: boolean
1967
+ /**
1968
+ * 1-indexed character column where the first match on a truncated line
1969
+ * starts; the truncated `line` shows a window around it.
1970
+ */
1971
+ column?: number
1911
1972
  /** Per-file match count (count mode only). */
1912
1973
  matchCount?: number
1913
1974
  }
@@ -2351,6 +2412,11 @@ export interface Match {
2351
2412
  contextAfter?: Array<ContextLine>
2352
2413
  /** Whether the line was truncated. */
2353
2414
  truncated?: boolean
2415
+ /**
2416
+ * 1-indexed character column where the first match on a truncated line
2417
+ * starts; the truncated `line` shows a window around it.
2418
+ */
2419
+ column?: number
2354
2420
  }
2355
2421
 
2356
2422
  /**
@@ -2729,14 +2795,23 @@ export interface PtyStartOptions {
2729
2795
  /**
2730
2796
  * Rasterize SVG/SVGZ bytes into a bounded PNG without resolving local files.
2731
2797
  *
2732
- * Conversion runs on the native blocking pool so parsing and rendering do not
2733
- * stall the JavaScript event loop.
2798
+ * The image is drawn at `scale` times the SVG's intrinsic size (default 1;
2799
+ * above 1 renders vector content crisply at display resolution), then shrunk
2800
+ * as needed to fit `max_width_px` x `max_height_px` with its aspect ratio
2801
+ * kept. Conversion runs on the native blocking pool so parsing and rendering
2802
+ * do not stall the JavaScript event loop.
2803
+ *
2804
+ * With `cell`, the limits round down to whole cells and the canvas pads with
2805
+ * transparency, right and bottom, to whole cells: a terminal placing the PNG
2806
+ * over `width / cell.width_px` columns and `height / cell.height_px` rows
2807
+ * shows it 1:1 instead of resampling it.
2734
2808
  *
2735
2809
  * # Errors
2736
- * Returns an error for invalid SVG data, zero/oversized limits, allocation
2737
- * failure, or PNG encoding failure.
2810
+ * Returns an error for invalid SVG data, zero/oversized limits, a zero cell
2811
+ * size, a scale that is not finite and positive, allocation failure, or PNG
2812
+ * encoding failure.
2738
2813
  */
2739
- export declare function rasterizeSvg(input: Uint8Array, maxWidthPx: number, maxHeightPx: number): Promise<Uint8Array>
2814
+ export declare function rasterizeSvg(input: Uint8Array, maxWidthPx: number, maxHeightPx: number, scale?: number | undefined | null, cell?: SvgCell | undefined | null): Promise<Uint8Array>
2740
2815
 
2741
2816
  /**
2742
2817
  * Read an image from the system clipboard.
@@ -3384,12 +3459,20 @@ export interface SummarySegment {
3384
3459
  }
3385
3460
 
3386
3461
  /**
3387
- * Check if a language is supported for highlighting.
3388
- * Returns true if the language has either direct support or a fallback
3389
- * mapping.
3462
+ * Check if a language resolves to a grammar (by name, extension or alias),
3463
+ * i.e. whether `highlightCode` colors it rather than echoing it.
3390
3464
  */
3391
3465
  export declare function supportsLanguage(lang: string): boolean
3392
3466
 
3467
+ /**
3468
+ * Terminal cell size in device pixels, for [`rasterize_svg`] canvases a
3469
+ * terminal shows over whole cells.
3470
+ */
3471
+ export interface SvgCell {
3472
+ widthPx: number
3473
+ heightPx: number
3474
+ }
3475
+
3393
3476
  /** Options for [`TextPredictor::new`]. */
3394
3477
  export interface TextPredictorOptions {
3395
3478
  /** Engine: `ngram`, `smollm`, or `apple`. */
@@ -3661,6 +3744,39 @@ export declare function warmBlockParse(options: BlockParseOptions): Promise<unde
3661
3744
  */
3662
3745
  export declare function warmHighlighter(): Promise<undefined>
3663
3746
 
3747
+ /**
3748
+ * Wasm grammar backing `lang` (alias) or the language of `path`; `null` for
3749
+ * built-in or unknown languages.
3750
+ */
3751
+ export declare function wasmGrammarFor(query: WasmGrammarQuery): WasmGrammarInfo | null
3752
+
3753
+ /** A grammar the host downloads on first use. */
3754
+ export interface WasmGrammarInfo {
3755
+ /** Canonical language name, e.g. `verilog`, `csharp`. */
3756
+ language: string
3757
+ /** stencil-hq/wasm-grammars release tag hosting the grammar, e.g. `v1`. */
3758
+ release: string
3759
+ /**
3760
+ * File name inside the grammar directory; the release asset is
3761
+ * `<file>.zst`.
3762
+ */
3763
+ file: string
3764
+ /** Lowercase hex SHA-256 of the decompressed `.wasm`. */
3765
+ sha256: string
3766
+ /** Byte size of the decompressed `.wasm`. */
3767
+ size: number
3768
+ /** Whether the grammar is downloaded (or already loaded). */
3769
+ installed: boolean
3770
+ }
3771
+
3772
+ /** Language to look up: an alias, or a path to infer it from. */
3773
+ export interface WasmGrammarQuery {
3774
+ /** Language alias (e.g. `kotlin`, `sv`); wins over `path`. */
3775
+ lang?: string
3776
+ /** File whose extension selects the language. */
3777
+ path?: string
3778
+ }
3779
+
3664
3780
  /** Profiling results returned to JavaScript. */
3665
3781
  export interface WorkProfile {
3666
3782
  /** Folded stack format for flamegraph tools. */
package/native/index.js CHANGED
@@ -46,10 +46,12 @@ export const VcsRepo = nativeBindings.VcsRepo;
46
46
 
47
47
  // functions
48
48
  export const __ompInstallTokioRuntime = nativeBindings.__ompInstallTokioRuntime ?? missingNativeExport("__ompInstallTokioRuntime");
49
+ export const __ompSetGrammarDir = nativeBindings.__ompSetGrammarDir ?? missingNativeExport("__ompSetGrammarDir");
49
50
  export const __piNativesBuildVersion = nativeBindings.__piNativesBuildVersion;
50
51
  export const appleFmAvailability = nativeBindings.appleFmAvailability ?? missingNativeExport("appleFmAvailability");
51
52
  export const appleFmCancel = nativeBindings.appleFmCancel ?? missingNativeExport("appleFmCancel");
52
53
  export const appleFmGenerate = nativeBindings.appleFmGenerate ?? missingNativeExport("appleFmGenerate");
54
+ export const applyBinaryPatch = nativeBindings.applyBinaryPatch ?? missingNativeExport("applyBinaryPatch");
53
55
  export const astEdit = nativeBindings.astEdit ?? missingNativeExport("astEdit");
54
56
  export const astGrep = nativeBindings.astGrep ?? missingNativeExport("astGrep");
55
57
  export const astMatch = nativeBindings.astMatch ?? missingNativeExport("astMatch");
@@ -142,6 +144,7 @@ export const vectorIndexTopK = nativeBindings.vectorIndexTopK ?? missingNativeEx
142
144
  export const visibleWidth = nativeBindings.visibleWidth ?? missingNativeExport("visibleWidth");
143
145
  export const warmBlockParse = nativeBindings.warmBlockParse ?? missingNativeExport("warmBlockParse");
144
146
  export const warmHighlighter = nativeBindings.warmHighlighter ?? missingNativeExport("warmHighlighter");
147
+ export const wasmGrammarFor = nativeBindings.wasmGrammarFor ?? missingNativeExport("wasmGrammarFor");
145
148
  export const wrapTextWithAnsi = nativeBindings.wrapTextWithAnsi ?? missingNativeExport("wrapTextWithAnsi");
146
149
 
147
150
  // string/numeric enums (napi-rs string_enum produces TS-only const enum)
@@ -1,21 +1,17 @@
1
1
  export interface EmbeddedAddonFile {
2
2
  variant: "modern" | "baseline" | "default";
3
+ /** Basename of the extracted `.node` file. */
3
4
  filename: string;
4
- size?: number;
5
- filePath?: string;
6
- }
7
-
8
- export interface EmbeddedAddonArchive {
9
- format: "tar.gz";
10
- filename: string;
11
- filePath: string;
5
+ /** Decompressed addon size in bytes. */
6
+ size: number;
7
+ /** Embedded zstd frame holding the addon bytes. */
8
+ zstdPath: string;
12
9
  }
13
10
 
14
11
  export interface EmbeddedAddon {
15
12
  platformTag: string;
16
13
  version: string;
17
14
  files: EmbeddedAddonFile[];
18
- archive?: EmbeddedAddonArchive;
19
15
  }
20
16
 
21
17
  export interface DetectCompiledBinaryInput {
@@ -90,13 +86,13 @@ export function cleanupStaleNativeVersions(input: CleanupStaleNativeVersionsInpu
90
86
 
91
87
  export function prepareNativeVersionDir(versionedDir: string): void;
92
88
 
93
- export interface ExtractEmbeddedAddonArchiveInput {
94
- archivePath: string;
89
+ export interface ExtractEmbeddedAddonsInput {
95
90
  files: EmbeddedAddonFile[];
96
91
  targetDir: string;
97
92
  }
98
93
 
99
- export function extractEmbeddedAddonArchive(input: ExtractEmbeddedAddonArchiveInput): string[];
94
+ /** Decompress every manifest addon missing or wrong-sized in `targetDir`; returns the paths written. */
95
+ export function extractEmbeddedAddons(input: ExtractEmbeddedAddonsInput): string[];
100
96
 
101
97
  export interface SelectCpuVariantInput {
102
98
  arch: string;
@@ -3,7 +3,6 @@ import * as fs from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
- import * as zlib from "node:zlib";
7
6
  import packageJson from "../package.json" with { type: "json" };
8
7
  import { embeddedAddon } from "./embedded-addon.js";
9
8
  import { bindingsHaveReleaseIdentity, bindingsReleaseVersion, containsVersionStamp } from "./version-sentinel.js";
@@ -57,11 +56,19 @@ function startupMarker(text) {
57
56
  }
58
57
 
59
58
  function getNativesDir() {
59
+ // Match pi-utils directory overrides without depending on pi-utils.
60
+ const override = process.env.PI_NATIVES_DIR?.trim();
61
+ if (override) {
62
+ let dir = override;
63
+ if (dir === "~") dir = os.homedir();
64
+ else if (dir.startsWith("~/") || dir.startsWith("~\\")) dir = os.homedir() + dir.slice(1);
65
+ if (path.isAbsolute(dir)) return path.normalize(dir);
66
+ }
60
67
  const xdgDataHome = process.env.XDG_DATA_HOME;
61
68
  if (xdgDataHome && fs.existsSync(path.join(xdgDataHome, "omp"))) {
62
69
  return path.join(xdgDataHome, "omp", "natives");
63
70
  }
64
- return path.join(os.homedir(), ".omp", "natives");
71
+ return path.join(os.homedir(), ".zeta", "natives");
65
72
  }
66
73
 
67
74
  function resolveLeafPackageDir(platformTag) {
@@ -112,9 +119,9 @@ export function getAddonFilenames({ tag, arch, variant }) {
112
119
 
113
120
  /**
114
121
  * Decide whether the loader should mirror the package's `native/<filename>.node`
115
- * into the per-version cache directory (`~/.omp/natives/<version>/`) before loading.
122
+ * into the per-version cache directory (`~/.zeta/natives/<version>/`) before loading.
116
123
  *
117
- * Windows-only safety net for `bun install -g` updates: when a previous `omp`
124
+ * Windows-only safety net for `bun install -g` updates: when a previous `zetacode`
118
125
  * process is running, bun cannot overwrite the locked `.node` inside
119
126
  * `node_modules/@linxiraos/pi-natives/native/`, leaving an old binary next to a
120
127
  * newer `index.js` and producing `<sym> is not a function` crashes on the next
@@ -442,36 +449,6 @@ function selectEmbeddedAddonFile(selectedVariant) {
442
449
  return embeddedAddon.files.find(file => file.variant === "baseline") || null;
443
450
  }
444
451
 
445
- function readTarString(buffer, offset, length) {
446
- const end = Math.min(offset + length, buffer.length);
447
- let stringEnd = offset;
448
- while (stringEnd < end && buffer[stringEnd] !== 0) stringEnd++;
449
- return buffer.toString("utf8", offset, stringEnd);
450
- }
451
-
452
- function readTarOctal(buffer, offset, length) {
453
- const value = readTarString(buffer, offset, length).trim();
454
- if (!value) return 0;
455
- const parsed = Number.parseInt(value, 8);
456
- if (!Number.isFinite(parsed)) {
457
- throw new Error(`Invalid tar octal value: ${value}`);
458
- }
459
- return parsed;
460
- }
461
-
462
- function isZeroTarBlock(buffer, offset) {
463
- for (let index = 0; index < 512; index++) {
464
- if (buffer[offset + index] !== 0) return false;
465
- }
466
- return true;
467
- }
468
-
469
- function getTarEntryName(header) {
470
- const name = readTarString(header, 0, 100);
471
- const prefix = readTarString(header, 345, 155);
472
- return prefix ? `${prefix}/${name}` : name;
473
- }
474
-
475
452
  function isSafeEmbeddedAddonFilename(filename) {
476
453
  return filename.length > 0 && path.basename(filename) === filename && !filename.includes("/") && !filename.includes("\\");
477
454
  }
@@ -480,7 +457,7 @@ function isEmbeddedAddonFileCurrent(targetPath, file) {
480
457
  try {
481
458
  const stat = fs.statSync(targetPath);
482
459
  if (!stat.isFile()) return false;
483
- return typeof file.size !== "number" || stat.size === file.size;
460
+ return stat.size === file.size;
484
461
  } catch (err) {
485
462
  if (err && err.code === "ENOENT") return false;
486
463
  throw err;
@@ -502,60 +479,23 @@ function writeEmbeddedAddonFile(targetPath, content) {
502
479
  }
503
480
  }
504
481
 
505
- export function extractEmbeddedAddonArchive({ archivePath, files, targetDir }) {
506
- const pending = new Map();
482
+ export function extractEmbeddedAddons({ files, targetDir }) {
507
483
  for (const file of files) {
508
484
  if (!isSafeEmbeddedAddonFilename(file.filename)) {
509
485
  throw new Error(`Unsafe embedded addon filename: ${file.filename}`);
510
486
  }
511
- const targetPath = path.join(targetDir, file.filename);
512
- if (!isEmbeddedAddonFileCurrent(targetPath, file)) {
513
- pending.set(file.filename, file);
514
- }
515
487
  }
516
- if (pending.size === 0) return [];
517
-
518
- const archive = zlib.gunzipSync(fs.readFileSync(archivePath));
519
488
  const writtenPaths = [];
520
- let offset = 0;
521
-
522
- while (offset + 512 <= archive.length) {
523
- if (isZeroTarBlock(archive, offset)) break;
524
- const header = archive.subarray(offset, offset + 512);
525
- const filename = getTarEntryName(header);
526
- const size = readTarOctal(header, 124, 12);
527
- const typeflag = header[156] === 0 ? "0" : String.fromCharCode(header[156]);
528
- offset += 512;
529
-
530
- if (offset + size > archive.length) {
531
- throw new Error(`Truncated embedded addon archive entry: ${filename}`);
532
- }
533
-
534
- if (!isSafeEmbeddedAddonFilename(filename)) {
535
- throw new Error(`Unsafe embedded addon archive entry: ${filename}`);
536
- }
537
- if (typeflag !== "0") {
538
- throw new Error(`Unsupported embedded addon archive entry type ${typeflag}: ${filename}`);
539
- }
540
-
541
- const file = pending.get(filename);
542
- if (file) {
543
- if (typeof file.size === "number" && file.size !== size) {
544
- throw new Error(`Embedded addon size mismatch for ${filename}: expected ${file.size}, got ${size}`);
545
- }
546
- const targetPath = path.join(targetDir, filename);
547
- writeEmbeddedAddonFile(targetPath, archive.subarray(offset, offset + size));
548
- pending.delete(filename);
549
- writtenPaths.push(targetPath);
489
+ for (const file of files) {
490
+ const targetPath = path.join(targetDir, file.filename);
491
+ if (isEmbeddedAddonFileCurrent(targetPath, file)) continue;
492
+ const content = Bun.zstdDecompressSync(fs.readFileSync(file.zstdPath));
493
+ if (content.length !== file.size) {
494
+ throw new Error(`Embedded addon size mismatch for ${file.filename}: expected ${file.size}, got ${content.length}`);
550
495
  }
551
-
552
- offset += Math.ceil(size / 512) * 512;
553
- }
554
-
555
- if (pending.size > 0) {
556
- throw new Error(`Embedded addon archive missing: ${[...pending.keys()].join(", ")}`);
496
+ writeEmbeddedAddonFile(targetPath, content);
497
+ writtenPaths.push(targetPath);
557
498
  }
558
-
559
499
  return writtenPaths;
560
500
  }
561
501
 
@@ -576,40 +516,12 @@ function maybeExtractEmbeddedAddon(ctx, errors) {
576
516
  return null;
577
517
  }
578
518
 
579
- if (embeddedAddon.archive) {
580
- try {
581
- extractEmbeddedAddonArchive({
582
- archivePath: embeddedAddon.archive.filePath,
583
- files: embeddedAddon.files,
584
- targetDir: ctx.versionedDir,
585
- });
586
- if (isEmbeddedAddonFileCurrent(targetPath, selectedEmbeddedFile)) {
587
- return targetPath;
588
- }
589
- errors.push(`embedded addon archive (${embeddedAddon.archive.filename}): missing ${selectedEmbeddedFile.filename}`);
590
- return null;
591
- } catch (err) {
592
- const message = err instanceof Error ? err.message : String(err);
593
- errors.push(`embedded addon archive (${embeddedAddon.archive.filename}): ${message}`);
594
- return null;
595
- }
596
- }
597
-
598
- if (isEmbeddedAddonFileCurrent(targetPath, selectedEmbeddedFile)) {
599
- return targetPath;
600
- }
601
- if (!selectedEmbeddedFile.filePath) {
602
- errors.push(`embedded addon metadata missing file path for ${selectedEmbeddedFile.filename}`);
603
- return null;
604
- }
605
-
606
519
  try {
607
- const buffer = fs.readFileSync(selectedEmbeddedFile.filePath);
608
- fs.writeFileSync(targetPath, buffer);
520
+ extractEmbeddedAddons({ files: embeddedAddon.files, targetDir: ctx.versionedDir });
609
521
  return targetPath;
610
522
  } catch (err) {
611
523
  const message = err instanceof Error ? err.message : String(err);
612
- errors.push(`embedded addon write (${selectedEmbeddedFile.filename}): ${message}`);
524
+ errors.push(`embedded addon extraction (${selectedEmbeddedFile.filename}): ${message}`);
613
525
  return null;
614
526
  }
615
527
  }
@@ -824,6 +736,18 @@ function installNativeTokioRuntime(bindings) {
824
736
  }
825
737
  }
826
738
 
739
+ /**
740
+ * Point the addon at `<nativesDir>/grammars`, where the agent downloads the
741
+ * tree-sitter grammars that load as WebAssembly on demand (pi-utils
742
+ * `getNativeGrammarsDir()` resolves the same path). Version cleanup never
743
+ * touches it: only release-version directories are removed. Older addons
744
+ * predating the export link every grammar and need no directory.
745
+ */
746
+ function configureGrammarDir(bindings, nativesDir) {
747
+ const setGrammarDir = bindings.__ompSetGrammarDir;
748
+ if (typeof setGrammarDir === "function") setGrammarDir(path.join(nativesDir, "grammars"));
749
+ }
750
+
827
751
 
828
752
  function buildHelpMessage(ctx) {
829
753
  if (ctx.isCompiledBinary) {
@@ -949,6 +873,7 @@ export function loadNative() {
949
873
  const bindings = require_(candidate);
950
874
  validateLoadedBindings(ctx, bindings, candidate);
951
875
  installNativeTokioRuntime(bindings);
876
+ configureGrammarDir(bindings, ctx.nativesDir);
952
877
  loadedAddon = describeLoadedAddon(bindings, candidate, ctx);
953
878
  cleanupStaleNativeVersions({ nativesDir: ctx.nativesDir, currentVersion: ctx.packageVersion });
954
879
  startupMarker("native:loadNative:done");
@@ -3,11 +3,7 @@
3
3
  * pipeline, and the post-link stamp tool (`scripts/stamp-native-version.ts`).
4
4
  *
5
5
  * Kept in its own module so `scripts/embed-native.ts` can reuse them without
6
- * importing `loader-state.js` — which pulls in the generated
7
- * `embedded-addon.js` and its `with { type: "file" }` archive import. That
8
- * chain fails to resolve when the archive is missing, which would break
9
- * `gen:native:reset` on an inconsistent tree (populated manifest, deleted
10
- * archive) before it can restore the checked-in null stub.
6
+ * importing `loader-state.js` and the rest of the runtime loader.
11
7
  *
12
8
  * Current addons carry a fixed-size stamp slot (`VERSION_STAMP_MAGIC` + the
13
9
  * release version + NUL padding) written after linking and reported by
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linxiraos/pi-natives",
3
- "version": "1.1.27",
3
+ "version": "1.2.0",
4
4
  "description": "Native Rust bindings for PDF conversion, audio, WebRTC, grep, clipboard, image processing, syntax highlighting, PTY, and shell operations via N-API",
5
5
  "type": "module",
6
6
  "homepage": "https://linxira-os.github.io/zeta/",
@@ -37,8 +37,6 @@
37
37
  "test": "bun test --parallel",
38
38
  "fix": "oxlint --fix --fix-suggestions . && bun run fmt",
39
39
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'",
40
- "gen:native": "bun scripts/embed-native.ts",
41
- "gen:native:reset": "bun scripts/embed-native.ts --reset",
42
40
  "gen:npm": "bun scripts/gen-npm-packages.ts",
43
41
  "bench": "bun bench/grep.ts",
44
42
  "bench:text": "bun bench/text.ts"
@@ -111,11 +109,11 @@
111
109
  }
112
110
  },
113
111
  "optionalDependencies": {
114
- "@linxiraos/pi-natives-linux-x64": "1.1.27",
115
- "@linxiraos/pi-natives-linux-arm64": "1.1.27",
116
- "@linxiraos/pi-natives-darwin-x64": "1.1.27",
117
- "@linxiraos/pi-natives-darwin-arm64": "1.1.27",
118
- "@linxiraos/pi-natives-win32-x64": "1.1.27",
119
- "@linxiraos/pi-natives-win32-arm64": "1.1.27"
112
+ "@linxiraos/pi-natives-linux-x64": "1.2.0",
113
+ "@linxiraos/pi-natives-linux-arm64": "1.2.0",
114
+ "@linxiraos/pi-natives-darwin-x64": "1.2.0",
115
+ "@linxiraos/pi-natives-darwin-arm64": "1.2.0",
116
+ "@linxiraos/pi-natives-win32-x64": "1.2.0",
117
+ "@linxiraos/pi-natives-win32-arm64": "1.2.0"
120
118
  }
121
119
  }