@linxiraos/pi-natives 1.1.26 → 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.
@@ -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");
package/native/vcs.d.ts CHANGED
@@ -81,6 +81,12 @@ export declare function detachGitDir(
81
81
  signal?: AbortSignal,
82
82
  ): Promise<"no-git" | "independent" | "detached">;
83
83
 
84
+ /** Run Git's checkout hook in a new linked worktree and return its output and exit status. */
85
+ export declare function runPostCheckoutHook(
86
+ worktreeRoot: string,
87
+ head: string,
88
+ ): Promise<{ exitCode: number; stdout: Uint8Array; stderr: Uint8Array }>;
89
+
84
90
  /** Join patch fragments, preserving each part's trailing newline. */
85
91
  export declare function joinPatches(parts: string[]): string;
86
92
 
package/native/vcs.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { $ } from "bun";
1
2
  import * as fs from "node:fs";
2
3
  import { loadNative } from "./loader-state.js";
3
4
 
@@ -95,6 +96,19 @@ export function detachGitDir(worktreeRoot, sourceCommonDir, signal) {
95
96
  return api().vcsDetachGitDir(worktreeRoot, sourceCommonDir, signal);
96
97
  }
97
98
 
99
+ /**
100
+ * Run Git's checkout hook in a newly created linked worktree, using the same
101
+ * null old HEAD and branch-switch flag as `git worktree add`.
102
+ */
103
+ export async function runPostCheckoutHook(worktreeRoot, head) {
104
+ const nullOid = "0".repeat(head.length);
105
+ const result = await $`git hook run --ignore-missing post-checkout -- ${nullOid} ${head} 1`
106
+ .cwd(worktreeRoot)
107
+ .quiet()
108
+ .nothrow();
109
+ return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
110
+ }
111
+
98
112
  /** Join patch fragments, preserving each part's trailing newline. */
99
113
  export function joinPatches(parts) {
100
114
  return api().vcsJoinPatches(parts);
@@ -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.26",
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.26",
115
- "@linxiraos/pi-natives-linux-arm64": "1.1.26",
116
- "@linxiraos/pi-natives-darwin-x64": "1.1.26",
117
- "@linxiraos/pi-natives-darwin-arm64": "1.1.26",
118
- "@linxiraos/pi-natives-win32-x64": "1.1.26",
119
- "@linxiraos/pi-natives-win32-arm64": "1.1.26"
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
  }