@oh-my-pi/pi-natives 18.8.5 → 18.8.7

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.
@@ -1,6 +1,5 @@
1
-
2
1
  // Always null on disk. Standalone binary builds replace this module in memory
3
- // with the target's manifest and addon archive (scripts/embed-native.ts).
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
@@ -730,6 +730,13 @@ export declare class VcsRepo {
730
730
  */
731
731
  export declare function __ompInstallTokioRuntime(): void
732
732
 
733
+ /**
734
+ * Points the addon at the directory holding downloaded wasm grammars
735
+ * (`<dir>/<file>`). The loader (`native/loader-state.js`) calls this once
736
+ * with `<natives dir>/grammars`; grammars then load lazily from it.
737
+ */
738
+ export declare function __ompSetGrammarDir(dir: string): void
739
+
733
740
  /**
734
741
  * Release version stamped into this `.node` after linking.
735
742
  *
@@ -863,6 +870,11 @@ export interface AstFindResult {
863
870
  limitReached: boolean
864
871
  /** Non-fatal parse or pattern errors collected during the run. */
865
872
  parseErrors?: Array<string>
873
+ /**
874
+ * Languages whose on-demand grammar is not installed; their files were
875
+ * skipped (see `wasmGrammarFor`).
876
+ */
877
+ missingGrammars?: Array<string>
866
878
  }
867
879
 
868
880
  /**
@@ -1035,6 +1047,11 @@ export interface AstReplaceResult {
1035
1047
  limitReached: boolean
1036
1048
  /** Parse or pattern errors when not failing the whole operation. */
1037
1049
  parseErrors?: Array<string>
1050
+ /**
1051
+ * Languages whose on-demand grammar is not installed; their files were
1052
+ * skipped (see `wasmGrammarFor`).
1053
+ */
1054
+ missingGrammars?: Array<string>
1038
1055
  }
1039
1056
 
1040
1057
  export interface AxNode {
@@ -3679,6 +3696,39 @@ export declare function warmBlockParse(options: BlockParseOptions): Promise<unde
3679
3696
  */
3680
3697
  export declare function warmHighlighter(): Promise<undefined>
3681
3698
 
3699
+ /**
3700
+ * Wasm grammar backing `lang` (alias) or the language of `path`; `null` for
3701
+ * built-in or unknown languages.
3702
+ */
3703
+ export declare function wasmGrammarFor(query: WasmGrammarQuery): WasmGrammarInfo | null
3704
+
3705
+ /** A grammar the host downloads on first use. */
3706
+ export interface WasmGrammarInfo {
3707
+ /** Canonical language name, e.g. `verilog`, `csharp`. */
3708
+ language: string
3709
+ /** stencil-hq/wasm-grammars release tag hosting the grammar, e.g. `v1`. */
3710
+ release: string
3711
+ /**
3712
+ * File name inside the grammar directory; the release asset is
3713
+ * `<file>.zst`.
3714
+ */
3715
+ file: string
3716
+ /** Lowercase hex SHA-256 of the decompressed `.wasm`. */
3717
+ sha256: string
3718
+ /** Byte size of the decompressed `.wasm`. */
3719
+ size: number
3720
+ /** Whether the grammar is downloaded (or already loaded). */
3721
+ installed: boolean
3722
+ }
3723
+
3724
+ /** Language to look up: an alias, or a path to infer it from. */
3725
+ export interface WasmGrammarQuery {
3726
+ /** Language alias (e.g. `kotlin`, `sv`); wins over `path`. */
3727
+ lang?: string
3728
+ /** File whose extension selects the language. */
3729
+ path?: string
3730
+ }
3731
+
3682
3732
  /** Profiling results returned to JavaScript. */
3683
3733
  export interface WorkProfile {
3684
3734
  /** Folded stack format for flamegraph tools. */
package/native/index.js CHANGED
@@ -46,6 +46,7 @@ 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");
@@ -142,6 +143,7 @@ export const vectorIndexTopK = nativeBindings.vectorIndexTopK ?? missingNativeEx
142
143
  export const visibleWidth = nativeBindings.visibleWidth ?? missingNativeExport("visibleWidth");
143
144
  export const warmBlockParse = nativeBindings.warmBlockParse ?? missingNativeExport("warmBlockParse");
144
145
  export const warmHighlighter = nativeBindings.warmHighlighter ?? missingNativeExport("warmHighlighter");
146
+ export const wasmGrammarFor = nativeBindings.wasmGrammarFor ?? missingNativeExport("wasmGrammarFor");
145
147
  export const wrapTextWithAnsi = nativeBindings.wrapTextWithAnsi ?? missingNativeExport("wrapTextWithAnsi");
146
148
 
147
149
  // 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";
@@ -450,36 +449,6 @@ function selectEmbeddedAddonFile(selectedVariant) {
450
449
  return embeddedAddon.files.find(file => file.variant === "baseline") || null;
451
450
  }
452
451
 
453
- function readTarString(buffer, offset, length) {
454
- const end = Math.min(offset + length, buffer.length);
455
- let stringEnd = offset;
456
- while (stringEnd < end && buffer[stringEnd] !== 0) stringEnd++;
457
- return buffer.toString("utf8", offset, stringEnd);
458
- }
459
-
460
- function readTarOctal(buffer, offset, length) {
461
- const value = readTarString(buffer, offset, length).trim();
462
- if (!value) return 0;
463
- const parsed = Number.parseInt(value, 8);
464
- if (!Number.isFinite(parsed)) {
465
- throw new Error(`Invalid tar octal value: ${value}`);
466
- }
467
- return parsed;
468
- }
469
-
470
- function isZeroTarBlock(buffer, offset) {
471
- for (let index = 0; index < 512; index++) {
472
- if (buffer[offset + index] !== 0) return false;
473
- }
474
- return true;
475
- }
476
-
477
- function getTarEntryName(header) {
478
- const name = readTarString(header, 0, 100);
479
- const prefix = readTarString(header, 345, 155);
480
- return prefix ? `${prefix}/${name}` : name;
481
- }
482
-
483
452
  function isSafeEmbeddedAddonFilename(filename) {
484
453
  return filename.length > 0 && path.basename(filename) === filename && !filename.includes("/") && !filename.includes("\\");
485
454
  }
@@ -488,7 +457,7 @@ function isEmbeddedAddonFileCurrent(targetPath, file) {
488
457
  try {
489
458
  const stat = fs.statSync(targetPath);
490
459
  if (!stat.isFile()) return false;
491
- return typeof file.size !== "number" || stat.size === file.size;
460
+ return stat.size === file.size;
492
461
  } catch (err) {
493
462
  if (err && err.code === "ENOENT") return false;
494
463
  throw err;
@@ -510,60 +479,23 @@ function writeEmbeddedAddonFile(targetPath, content) {
510
479
  }
511
480
  }
512
481
 
513
- export function extractEmbeddedAddonArchive({ archivePath, files, targetDir }) {
514
- const pending = new Map();
482
+ export function extractEmbeddedAddons({ files, targetDir }) {
515
483
  for (const file of files) {
516
484
  if (!isSafeEmbeddedAddonFilename(file.filename)) {
517
485
  throw new Error(`Unsafe embedded addon filename: ${file.filename}`);
518
486
  }
519
- const targetPath = path.join(targetDir, file.filename);
520
- if (!isEmbeddedAddonFileCurrent(targetPath, file)) {
521
- pending.set(file.filename, file);
522
- }
523
487
  }
524
- if (pending.size === 0) return [];
525
-
526
- const archive = zlib.gunzipSync(fs.readFileSync(archivePath));
527
488
  const writtenPaths = [];
528
- let offset = 0;
529
-
530
- while (offset + 512 <= archive.length) {
531
- if (isZeroTarBlock(archive, offset)) break;
532
- const header = archive.subarray(offset, offset + 512);
533
- const filename = getTarEntryName(header);
534
- const size = readTarOctal(header, 124, 12);
535
- const typeflag = header[156] === 0 ? "0" : String.fromCharCode(header[156]);
536
- offset += 512;
537
-
538
- if (offset + size > archive.length) {
539
- throw new Error(`Truncated embedded addon archive entry: ${filename}`);
540
- }
541
-
542
- if (!isSafeEmbeddedAddonFilename(filename)) {
543
- throw new Error(`Unsafe embedded addon archive entry: ${filename}`);
544
- }
545
- if (typeflag !== "0") {
546
- throw new Error(`Unsupported embedded addon archive entry type ${typeflag}: ${filename}`);
547
- }
548
-
549
- const file = pending.get(filename);
550
- if (file) {
551
- if (typeof file.size === "number" && file.size !== size) {
552
- throw new Error(`Embedded addon size mismatch for ${filename}: expected ${file.size}, got ${size}`);
553
- }
554
- const targetPath = path.join(targetDir, filename);
555
- writeEmbeddedAddonFile(targetPath, archive.subarray(offset, offset + size));
556
- pending.delete(filename);
557
- 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}`);
558
495
  }
559
-
560
- offset += Math.ceil(size / 512) * 512;
561
- }
562
-
563
- if (pending.size > 0) {
564
- throw new Error(`Embedded addon archive missing: ${[...pending.keys()].join(", ")}`);
496
+ writeEmbeddedAddonFile(targetPath, content);
497
+ writtenPaths.push(targetPath);
565
498
  }
566
-
567
499
  return writtenPaths;
568
500
  }
569
501
 
@@ -584,40 +516,12 @@ function maybeExtractEmbeddedAddon(ctx, errors) {
584
516
  return null;
585
517
  }
586
518
 
587
- if (embeddedAddon.archive) {
588
- try {
589
- extractEmbeddedAddonArchive({
590
- archivePath: embeddedAddon.archive.filePath,
591
- files: embeddedAddon.files,
592
- targetDir: ctx.versionedDir,
593
- });
594
- if (isEmbeddedAddonFileCurrent(targetPath, selectedEmbeddedFile)) {
595
- return targetPath;
596
- }
597
- errors.push(`embedded addon archive (${embeddedAddon.archive.filename}): missing ${selectedEmbeddedFile.filename}`);
598
- return null;
599
- } catch (err) {
600
- const message = err instanceof Error ? err.message : String(err);
601
- errors.push(`embedded addon archive (${embeddedAddon.archive.filename}): ${message}`);
602
- return null;
603
- }
604
- }
605
-
606
- if (isEmbeddedAddonFileCurrent(targetPath, selectedEmbeddedFile)) {
607
- return targetPath;
608
- }
609
- if (!selectedEmbeddedFile.filePath) {
610
- errors.push(`embedded addon metadata missing file path for ${selectedEmbeddedFile.filename}`);
611
- return null;
612
- }
613
-
614
519
  try {
615
- const buffer = fs.readFileSync(selectedEmbeddedFile.filePath);
616
- fs.writeFileSync(targetPath, buffer);
520
+ extractEmbeddedAddons({ files: embeddedAddon.files, targetDir: ctx.versionedDir });
617
521
  return targetPath;
618
522
  } catch (err) {
619
523
  const message = err instanceof Error ? err.message : String(err);
620
- errors.push(`embedded addon write (${selectedEmbeddedFile.filename}): ${message}`);
524
+ errors.push(`embedded addon extraction (${selectedEmbeddedFile.filename}): ${message}`);
621
525
  return null;
622
526
  }
623
527
  }
@@ -832,6 +736,18 @@ function installNativeTokioRuntime(bindings) {
832
736
  }
833
737
  }
834
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
+
835
751
 
836
752
  function buildHelpMessage(ctx) {
837
753
  if (ctx.isCompiledBinary) {
@@ -957,6 +873,7 @@ export function loadNative() {
957
873
  const bindings = require_(candidate);
958
874
  validateLoadedBindings(ctx, bindings, candidate);
959
875
  installNativeTokioRuntime(bindings);
876
+ configureGrammarDir(bindings, ctx.nativesDir);
960
877
  loadedAddon = describeLoadedAddon(bindings, candidate, ctx);
961
878
  cleanupStaleNativeVersions({ nativesDir: ctx.nativesDir, currentVersion: ctx.packageVersion });
962
879
  startupMarker("native:loadNative:done");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-natives",
3
- "version": "18.8.5",
3
+ "version": "18.8.7",
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://omp.sh",
@@ -112,11 +112,11 @@
112
112
  }
113
113
  },
114
114
  "optionalDependencies": {
115
- "@oh-my-pi/pi-natives-linux-x64": "18.8.5",
116
- "@oh-my-pi/pi-natives-linux-arm64": "18.8.5",
117
- "@oh-my-pi/pi-natives-darwin-x64": "18.8.5",
118
- "@oh-my-pi/pi-natives-darwin-arm64": "18.8.5",
119
- "@oh-my-pi/pi-natives-win32-x64": "18.8.5",
120
- "@oh-my-pi/pi-natives-win32-arm64": "18.8.5"
115
+ "@oh-my-pi/pi-natives-linux-x64": "18.8.7",
116
+ "@oh-my-pi/pi-natives-linux-arm64": "18.8.7",
117
+ "@oh-my-pi/pi-natives-darwin-x64": "18.8.7",
118
+ "@oh-my-pi/pi-natives-darwin-arm64": "18.8.7",
119
+ "@oh-my-pi/pi-natives-win32-x64": "18.8.7",
120
+ "@oh-my-pi/pi-natives-win32-arm64": "18.8.7"
121
121
  }
122
122
  }