@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.
- package/README.md +11 -4
- package/native/desktop-adapter.d.ts +3 -27
- package/native/desktop-adapter.js +32 -400
- package/native/embedded-addon.js +4 -13
- package/native/index.d.ts +295 -20
- package/native/index.js +6 -0
- package/native/loader-state.d.ts +8 -12
- package/native/loader-state.js +36 -111
- package/native/vcs.d.ts +6 -0
- package/native/vcs.js +14 -0
- package/native/version-sentinel.js +1 -5
- package/package.json +7 -9
package/native/loader-state.js
CHANGED
|
@@ -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(), ".
|
|
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 (`~/.
|
|
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 `
|
|
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
|
|
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
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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`
|
|
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.
|
|
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.
|
|
115
|
-
"@linxiraos/pi-natives-linux-arm64": "1.
|
|
116
|
-
"@linxiraos/pi-natives-darwin-x64": "1.
|
|
117
|
-
"@linxiraos/pi-natives-darwin-arm64": "1.
|
|
118
|
-
"@linxiraos/pi-natives-win32-x64": "1.
|
|
119
|
-
"@linxiraos/pi-natives-win32-arm64": "1.
|
|
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
|
}
|