@gajae-code/natives 0.18.11 → 0.19.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/native/diagnostic-artifact-embedded.json +5 -0
- package/native/diagnostic-artifact.json +7 -7
- package/native/diagnostic-loader.js +121 -8
- package/native/index.d.ts +86 -13
- package/native/index.js +3 -1
- package/native/loader-state.d.ts +3 -0
- package/native/loader-state.js +96 -12
- package/package.json +8 -7
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "gjc.diagnostic-artifact",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"artifacts": {
|
|
5
|
-
"pi_natives.darwin-arm64.node": "
|
|
6
|
-
"pi_natives.darwin-x64-baseline.node": "
|
|
7
|
-
"pi_natives.linux-arm64.node": "
|
|
8
|
-
"pi_natives.linux-x64-baseline.node": "
|
|
9
|
-
"pi_natives.linux-x64-modern.node": "
|
|
10
|
-
"pi_natives.win32-x64-baseline.node": "
|
|
5
|
+
"pi_natives.darwin-arm64.node": "bcb20db50a480de66eed5448d63d530ce708ab84d77ab1cb1620132772690af5",
|
|
6
|
+
"pi_natives.darwin-x64-baseline.node": "3de5a046531495e0a0afdff4d6f0bcd4c8639fcd30fd6cff8ea9ec2efd43b63f",
|
|
7
|
+
"pi_natives.linux-arm64.node": "3eb72a1956e59f3134d45690c61c79a24e0eddcb9627b238deb32540ec0433ce",
|
|
8
|
+
"pi_natives.linux-x64-baseline.node": "d6282ea56f7c47bf659fbb1aef7f8df3fb798c9b828abc5dc3b6cf4334f3c651",
|
|
9
|
+
"pi_natives.linux-x64-modern.node": "8d1fd3d3bc98cc479c75cea158b6059dbed4dbd7a614a6a8c9ad60a12b7a4dae",
|
|
10
|
+
"pi_natives.win32-x64-baseline.node": "7d9ed748b9b6eb6e14a2c87afccbe1590498512f2830fb1e813aa3529b715bb2"
|
|
11
11
|
}
|
|
12
12
|
}
|
|
@@ -19,10 +19,12 @@
|
|
|
19
19
|
* - proves the trusted namespace before reading: every component is owned by the
|
|
20
20
|
* current user or by root, is not a symlink, and grants no group/other write,
|
|
21
21
|
* so an account that owns none of the path cannot swap the artifact out;
|
|
22
|
-
* - proves the bytes on a retained descriptor: SHA-256 equal to the
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* - proves the bytes on a retained descriptor: SHA-256 equal to the shipped
|
|
23
|
+
* `diagnostic-artifact.json` digest, a compiled-binary digest captured from the
|
|
24
|
+
* verified embed input, or (in a source checkout only) the gitignored local
|
|
25
|
+
* manifest when adjacent build metadata explicitly identifies a local build.
|
|
26
|
+
* Each manifest belongs to the package and is checked before use. An addon's own
|
|
27
|
+
* `nativeBuildInfo()` is self-reported by code that has already been executed, so
|
|
26
28
|
* it is only a consistency check afterwards, never the trust anchor;
|
|
27
29
|
* - activates the verified bytes through that same retained descriptor
|
|
28
30
|
* (`process.dlopen` on `/dev/fd/<fd>`), so a pathname replaced between
|
|
@@ -75,6 +77,8 @@ import * as fs from "node:fs";
|
|
|
75
77
|
import { createRequire } from "node:module";
|
|
76
78
|
import * as os from "node:os";
|
|
77
79
|
import * as nodePath from "node:path";
|
|
80
|
+
import { fileURLToPath } from "node:url";
|
|
81
|
+
import compiledArtifacts from "./diagnostic-artifact-embedded.json" with { type: "json" };
|
|
78
82
|
import trustedArtifacts from "./diagnostic-artifact.json" with { type: "json" };
|
|
79
83
|
import packageManifest from "../package.json" with { type: "json" };
|
|
80
84
|
|
|
@@ -127,17 +131,126 @@ function cachedArtifactPath() {
|
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
/**
|
|
130
|
-
* The package-owned expected digest
|
|
131
|
-
*
|
|
134
|
+
* The package-owned expected digest, or null when this package has no trusted
|
|
135
|
+
* record. Compiled binaries prefer the digest recorded at their embed boundary;
|
|
136
|
+
* source checkouts may use the gitignored local manifest for an explicitly local
|
|
137
|
+
* build. Installed packages use the shipped record.
|
|
132
138
|
*
|
|
139
|
+
* @param {string} file
|
|
133
140
|
* @returns {string | null}
|
|
134
141
|
*/
|
|
135
|
-
function trustedDigest() {
|
|
142
|
+
function trustedDigest(file) {
|
|
143
|
+
const compiledDigest = compiledArtifactDigest(file);
|
|
144
|
+
if (compiledDigest !== undefined) return compiledDigest;
|
|
145
|
+
const localDigest = sourceLocalBuildDigest(file);
|
|
146
|
+
if (localDigest !== undefined) return localDigest;
|
|
136
147
|
if (trustedArtifacts?.version !== packageManifest.version) return null;
|
|
137
148
|
const digest = trustedArtifacts?.artifacts?.[ARTIFACT_BASENAME];
|
|
138
149
|
return typeof digest === "string" && /^[0-9a-f]{64}$/.test(digest) ? digest : null;
|
|
139
150
|
}
|
|
140
151
|
|
|
152
|
+
/**
|
|
153
|
+
* A compiled binary carries the digest of the exact addon verified before embedding.
|
|
154
|
+
* An empty stub falls back to the shipped record; a populated but stale or malformed
|
|
155
|
+
* compiled record fails closed.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} file
|
|
158
|
+
* @returns {string | null | undefined} Undefined means no compiled digest is present.
|
|
159
|
+
*/
|
|
160
|
+
function compiledArtifactDigest(file) {
|
|
161
|
+
if (!runningFromCompiledBinary() || compiledArtifacts?.version === "") return undefined;
|
|
162
|
+
if (compiledArtifacts?.schema !== "gjc.diagnostic-artifact" || compiledArtifacts?.version !== packageManifest.version) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
const digest = compiledArtifacts?.artifacts?.[nodePath.basename(file)];
|
|
166
|
+
if (digest === undefined) return undefined;
|
|
167
|
+
return typeof digest === "string" && /^[0-9a-f]{64}$/.test(digest) ? digest : null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Read one bounded JSON file only after proving its path and retained descriptor are
|
|
172
|
+
* in the same trusted namespace used for the native artifact.
|
|
173
|
+
*
|
|
174
|
+
* @param {string} file
|
|
175
|
+
* @returns {Record<string, unknown> | null}
|
|
176
|
+
*/
|
|
177
|
+
function readTrustedLocalJson(file) {
|
|
178
|
+
let fd = -1;
|
|
179
|
+
try {
|
|
180
|
+
if (!trustedNamespace(file)) return null;
|
|
181
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
182
|
+
const stat = fs.fstatSync(fd);
|
|
183
|
+
if (!stat.isFile() || stat.size === 0 || stat.size > 1 << 20) return null;
|
|
184
|
+
if ((stat.mode & 0o022) !== 0) return null;
|
|
185
|
+
if (stat.uid !== process.geteuid?.() && stat.uid !== 0) return null;
|
|
186
|
+
if (inspectAclOnDescriptor(fd) !== "absent") return null;
|
|
187
|
+
const bytes = Buffer.allocUnsafe(stat.size);
|
|
188
|
+
let offset = 0;
|
|
189
|
+
while (offset < bytes.length) {
|
|
190
|
+
const read = fs.readSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
191
|
+
if (read === 0) return null;
|
|
192
|
+
offset += read;
|
|
193
|
+
}
|
|
194
|
+
if (fs.fstatSync(fd).size !== stat.size) return null;
|
|
195
|
+
const value = JSON.parse(bytes.toString("utf8"));
|
|
196
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
} finally {
|
|
200
|
+
if (fd >= 0) closeDescriptor(fd);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* An unversioned local build is accepted only when the package-owned record proves
|
|
206
|
+
* these exact bytes for the current package version. This is the migration path for
|
|
207
|
+
* builds produced before local sidecars carried their version.
|
|
208
|
+
*
|
|
209
|
+
* @param {unknown} record
|
|
210
|
+
* @param {string} version
|
|
211
|
+
* @param {string} artifact
|
|
212
|
+
* @param {string} digest
|
|
213
|
+
* @returns {boolean}
|
|
214
|
+
*/
|
|
215
|
+
function trustedRecordHasDigest(record, version, artifact, digest) {
|
|
216
|
+
if (typeof record !== "object" || record === null || Array.isArray(record)) return false;
|
|
217
|
+
const candidate = /** @type {Record<string, unknown>} */ (record);
|
|
218
|
+
if (candidate.schema !== "gjc.diagnostic-artifact" || candidate.version !== version) return false;
|
|
219
|
+
const artifacts = candidate.artifacts;
|
|
220
|
+
if (typeof artifacts !== "object" || artifacts === null || Array.isArray(artifacts)) return false;
|
|
221
|
+
return /** @type {Record<string, unknown>} */ (artifacts)[artifact] === digest;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* A source-mode local build has an explicit profile marker and a gitignored digest
|
|
226
|
+
* record. Never consult these files for a compiled binary, an installed platform
|
|
227
|
+
* package, or a release/CI addon.
|
|
228
|
+
*
|
|
229
|
+
* @param {string} file
|
|
230
|
+
* @returns {string | null | undefined} Undefined means this is not a local source build.
|
|
231
|
+
*/
|
|
232
|
+
function sourceLocalBuildDigest(file) {
|
|
233
|
+
if (runningFromCompiledBinary()) return undefined;
|
|
234
|
+
const sourceAddon = nodePath.join(nodePath.dirname(fileURLToPath(import.meta.url)), ARTIFACT_BASENAME);
|
|
235
|
+
if (nodePath.resolve(file) !== sourceAddon) return undefined;
|
|
236
|
+
const buildMetadata = readTrustedLocalJson(`${sourceAddon}.build.json`);
|
|
237
|
+
if (buildMetadata?.profile !== "local") return undefined;
|
|
238
|
+
|
|
239
|
+
const localManifest = readTrustedLocalJson(
|
|
240
|
+
nodePath.join(nodePath.dirname(sourceAddon), "diagnostic-artifact.local.json"),
|
|
241
|
+
);
|
|
242
|
+
if (localManifest?.schema !== "gjc.diagnostic-artifact") return null;
|
|
243
|
+
if (localManifest.version !== packageManifest.version) return null;
|
|
244
|
+
const artifacts = localManifest.artifacts;
|
|
245
|
+
if (typeof artifacts !== "object" || artifacts === null || Array.isArray(artifacts)) return null;
|
|
246
|
+
const digest = /** @type {Record<string, unknown>} */ (artifacts)[ARTIFACT_BASENAME];
|
|
247
|
+
if (typeof digest !== "string" || !/^[0-9a-f]{64}$/.test(digest)) return null;
|
|
248
|
+
if (buildMetadata.version !== undefined) {
|
|
249
|
+
return buildMetadata.version === packageManifest.version ? digest : null;
|
|
250
|
+
}
|
|
251
|
+
return trustedRecordHasDigest(trustedArtifacts, packageManifest.version, ARTIFACT_BASENAME, digest) ? digest : null;
|
|
252
|
+
}
|
|
253
|
+
|
|
141
254
|
/**
|
|
142
255
|
* Select the single applicable layout. Exactly one distribution shape applies per
|
|
143
256
|
* install, and a failure of the selected layout is never a reason to try another.
|
|
@@ -674,7 +787,7 @@ function consumeHandle(handle) {
|
|
|
674
787
|
* @returns {{ ok: true, token: string } | { ok: false, reason: "unsupported" }}
|
|
675
788
|
*/
|
|
676
789
|
export function verifyDiagnosticArtifact(file) {
|
|
677
|
-
const expectedDigest = trustedDigest();
|
|
790
|
+
const expectedDigest = trustedDigest(file);
|
|
678
791
|
if (expectedDigest === null) return unsupported;
|
|
679
792
|
let fd = -1;
|
|
680
793
|
try {
|
package/native/index.d.ts
CHANGED
|
@@ -687,7 +687,7 @@ export declare function __piNativesPublishOutcomeV1(): void
|
|
|
687
687
|
* `packages/natives/native/index.js` (which derives the name from
|
|
688
688
|
* `package.json#version`).
|
|
689
689
|
*/
|
|
690
|
-
export declare function
|
|
690
|
+
export declare function __piNativesV0_19_0(): void
|
|
691
691
|
|
|
692
692
|
/**
|
|
693
693
|
* Apply conservative pre-execution rewrites to a bash command.
|
|
@@ -795,6 +795,59 @@ export interface AstFindResult {
|
|
|
795
795
|
*/
|
|
796
796
|
export declare function astGrep(options: AstFindOptions): Promise<AstFindResult>
|
|
797
797
|
|
|
798
|
+
/**
|
|
799
|
+
* Match ast-grep patterns against an in-memory source string; returns a
|
|
800
|
+
* promise resolved on a worker thread.
|
|
801
|
+
*
|
|
802
|
+
* This is the file-free counterpart to [`ast_grep`]: callers that already hold
|
|
803
|
+
* the source (streaming buffers, generated code, editor contents) avoid a
|
|
804
|
+
* temp-file round trip. `lang` is required since there is no path to infer it
|
|
805
|
+
* from.
|
|
806
|
+
*/
|
|
807
|
+
export declare function astMatch(options: AstMatchOptions): Promise<AstMatchResult>
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Options for `astMatch`: run ast-grep patterns against an in-memory source
|
|
811
|
+
* string instead of files on disk.
|
|
812
|
+
*/
|
|
813
|
+
export interface AstMatchOptions {
|
|
814
|
+
/** Source code to match against (parsed in memory, never read from disk). */
|
|
815
|
+
source: string
|
|
816
|
+
/**
|
|
817
|
+
* Language of `source` (required): an alias ("ts", "rust", "python") or a
|
|
818
|
+
* file extension ("scss"), resolved like a path's extension.
|
|
819
|
+
*/
|
|
820
|
+
lang: string
|
|
821
|
+
/** ast-grep patterns to search for (OR across patterns). */
|
|
822
|
+
patterns: Array<string>
|
|
823
|
+
/** Rule selector for multi-rule ast-grep configurations. */
|
|
824
|
+
selector?: string
|
|
825
|
+
/** Pattern strictness; defaults to smart matching when omitted. */
|
|
826
|
+
strictness?: AstMatchStrictness
|
|
827
|
+
/** Maximum matches to return after `offset` (default applies when omitted). */
|
|
828
|
+
limit?: number
|
|
829
|
+
/** Number of leading matches to skip before applying `limit`. */
|
|
830
|
+
offset?: number
|
|
831
|
+
/** When true, include meta-variable bindings per match. */
|
|
832
|
+
includeMeta?: boolean
|
|
833
|
+
/** Optional cancellation handle (library-specific). */
|
|
834
|
+
signal?: unknown
|
|
835
|
+
/** Wall-clock timeout for the worker task in milliseconds. */
|
|
836
|
+
timeoutMs?: number
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/** Result of an in-memory `astMatch` run. */
|
|
840
|
+
export interface AstMatchResult {
|
|
841
|
+
/** Page of matches after sort, offset, and limit. */
|
|
842
|
+
matches: Array<AstFindMatch>
|
|
843
|
+
/** Total matches found before paging (can exceed `matches.length`). */
|
|
844
|
+
totalMatches: number
|
|
845
|
+
/** True when results were truncated by `limit`. */
|
|
846
|
+
limitReached: boolean
|
|
847
|
+
/** Non-fatal parse or pattern-compile errors collected during the run. */
|
|
848
|
+
parseErrors?: Array<string>
|
|
849
|
+
}
|
|
850
|
+
|
|
798
851
|
/** ast-grep pattern strictness (controls how patterns match syntax). */
|
|
799
852
|
export declare enum AstMatchStrictness {
|
|
800
853
|
/** Match at the concrete syntax tree level. */
|
|
@@ -1189,10 +1242,11 @@ export declare function encodeSixel(bytes: Uint8Array, targetWidthPx: number, ta
|
|
|
1189
1242
|
|
|
1190
1243
|
/**
|
|
1191
1244
|
* Remove a directory tree only when a fresh descriptor-relative snapshot
|
|
1192
|
-
* exactly equals the persisted snapshot. POSIX
|
|
1193
|
-
* verified root to its deterministic `.removing` sibling
|
|
1194
|
-
*
|
|
1195
|
-
* replay
|
|
1245
|
+
* exactly equals the persisted snapshot. POSIX removal no-replace detaches the
|
|
1246
|
+
* verified root to its deterministic `.removing` sibling before scrubbing. On
|
|
1247
|
+
* Windows, callers that need retirement semantics must request a detach-only
|
|
1248
|
+
* pass, then replay exact removal at the returned sibling; ordinary Windows
|
|
1249
|
+
* removal never vacates the caller's path before scrubbing completes.
|
|
1196
1250
|
*/
|
|
1197
1251
|
export declare function exactRemoveDirectoryTree(path: string, snapshot: NativeDirectoryTreeSnapshot, parentIdentity?: NativeDirectoryParentIdentity | undefined | null, detachOnly?: boolean | undefined | null): NativeExactUnlinkResult
|
|
1198
1252
|
|
|
@@ -1239,9 +1293,10 @@ export declare function exactReplacePath(sourcePath: string, destinationPath: st
|
|
|
1239
1293
|
* single-link ownership; hard-linked or symlinked source/destination are
|
|
1240
1294
|
* rejected) before anything is mutated. On Windows a pre-mutation
|
|
1241
1295
|
* `STATUS_SHARING_VIOLATION` on the destination open is reported as a
|
|
1242
|
-
* distinct `sharing_violation` code
|
|
1243
|
-
*
|
|
1244
|
-
*
|
|
1296
|
+
* distinct `sharing_violation` code, while `STATUS_ACCESS_DENIED` on a
|
|
1297
|
+
* retained open or rename is reported as `access_denied`. Pre-mutation
|
|
1298
|
+
* failures include the original `windows_error_code`; only known-safe rename
|
|
1299
|
+
* operations retry transient access-denied or sharing-violation statuses.
|
|
1245
1300
|
*/
|
|
1246
1301
|
export declare function exactReplaceRetained(sourcePath: string, destinationPath: string, backupName: string, expectedSource: NativeExactFileIdentity, expectedDestination: NativeExactFileIdentity): NativeExactUnlinkResult
|
|
1247
1302
|
|
|
@@ -1294,6 +1349,17 @@ export declare function exactUnlinkDirect(path: string, identity: NativeExactFil
|
|
|
1294
1349
|
*/
|
|
1295
1350
|
export declare function exactUnlinkDirectDetached(path: string, identity: NativeExactFileIdentity): boolean
|
|
1296
1351
|
|
|
1352
|
+
/**
|
|
1353
|
+
* Delete only the empty regular file opened by the caller's retained
|
|
1354
|
+
* descriptor.
|
|
1355
|
+
*
|
|
1356
|
+
* This is used when a newly created staging file cannot be statted through the
|
|
1357
|
+
* host API. Native code derives identity from the open descriptor and verifies
|
|
1358
|
+
* the public entry still names that object before using the platform's exact
|
|
1359
|
+
* unlink protocol.
|
|
1360
|
+
*/
|
|
1361
|
+
export declare function exactUnlinkOpenFile(path: string, callerFd: number, quarantineName: string): NativeExactUnlinkResult
|
|
1362
|
+
|
|
1297
1363
|
/**
|
|
1298
1364
|
* Execute a brush shell command.
|
|
1299
1365
|
*
|
|
@@ -2105,7 +2171,7 @@ export type NativeCanonicalDirectoryIdentity =
|
|
|
2105
2171
|
ok: false;
|
|
2106
2172
|
platform?: never;
|
|
2107
2173
|
canonicalPath?: never;
|
|
2108
|
-
code: "not_found" | "not_directory" | "not_utf8" | "network_unsupported" | "identity_unavailable" | "io_error";
|
|
2174
|
+
code: "access_denied" | "not_found" | "not_directory" | "not_utf8" | "network_unsupported" | "identity_unavailable" | "io_error";
|
|
2109
2175
|
}
|
|
2110
2176
|
|
|
2111
2177
|
/** Bytes copied out of one read-only broker publication observation. */
|
|
@@ -2321,6 +2387,7 @@ export type NativeOwnerOnlySecurityResult =
|
|
|
2321
2387
|
| {
|
|
2322
2388
|
ok: false;
|
|
2323
2389
|
code:
|
|
2390
|
+
| "access_denied"
|
|
2324
2391
|
| "not_found"
|
|
2325
2392
|
| "not_directory"
|
|
2326
2393
|
| "network_unsupported"
|
|
@@ -2578,16 +2645,22 @@ export interface PtyStartOptions {
|
|
|
2578
2645
|
export declare function ptyTimeoutCount(): bigint
|
|
2579
2646
|
|
|
2580
2647
|
/**
|
|
2581
|
-
* Rasterize SVG/SVGZ bytes into a
|
|
2648
|
+
* Rasterize bounded SVG/SVGZ bytes into a PNG without resolving local files.
|
|
2649
|
+
*
|
|
2650
|
+
* DTD/entity declarations and SVG image elements are rejected rather than
|
|
2651
|
+
* allowing unbounded expansion or silently returning an incomplete preview.
|
|
2582
2652
|
*
|
|
2583
2653
|
* Conversion runs on the native blocking pool so parsing and rendering do not
|
|
2584
2654
|
* stall the JavaScript event loop.
|
|
2585
2655
|
*
|
|
2656
|
+
* When both cell dimensions are provided, the PNG is padded on a transparent
|
|
2657
|
+
* canvas to whole terminal cells without embedding or decoding another image.
|
|
2658
|
+
*
|
|
2586
2659
|
* # Errors
|
|
2587
|
-
* Returns an error for invalid SVG data, zero
|
|
2588
|
-
* failure, or PNG encoding failure.
|
|
2660
|
+
* Returns an error for invalid SVG data, mismatched/zero cell dimensions,
|
|
2661
|
+
* zero/oversized limits, allocation failure, or PNG encoding failure.
|
|
2589
2662
|
*/
|
|
2590
|
-
export declare function rasterizeSvg(input: Uint8Array, maxWidthPx: number, maxHeightPx: number): Promise<Uint8Array>
|
|
2663
|
+
export declare function rasterizeSvg(input: Uint8Array, maxWidthPx: number, maxHeightPx: number, cellWidthPx?: number | undefined | null, cellHeightPx?: number | undefined | null): Promise<Uint8Array>
|
|
2591
2664
|
|
|
2592
2665
|
/**
|
|
2593
2666
|
* Read an image from the system clipboard.
|
package/native/index.js
CHANGED
|
@@ -35,12 +35,13 @@ export const TtyWriter = nativeBindings.TtyWriter;
|
|
|
35
35
|
// functions
|
|
36
36
|
export const __gjcInstallTokioRuntime = nativeBindings.__gjcInstallTokioRuntime;
|
|
37
37
|
export const __piNativesPublishOutcomeV1 = nativeBindings.__piNativesPublishOutcomeV1;
|
|
38
|
-
export const
|
|
38
|
+
export const __piNativesV0_19_0 = nativeBindings.__piNativesV0_19_0;
|
|
39
39
|
export const applyBashFixups = nativeBindings.applyBashFixups;
|
|
40
40
|
export const applyOwnerOnlyFdSecurity = nativeBindings.applyOwnerOnlyFdSecurity;
|
|
41
41
|
export const applyOwnerOnlyPathSecurity = nativeBindings.applyOwnerOnlyPathSecurity;
|
|
42
42
|
export const astEdit = nativeBindings.astEdit;
|
|
43
43
|
export const astGrep = nativeBindings.astGrep;
|
|
44
|
+
export const astMatch = nativeBindings.astMatch;
|
|
44
45
|
export const canonicalExistingDirectoryIdentity = nativeBindings.canonicalExistingDirectoryIdentity;
|
|
45
46
|
export const computerScreenshot = nativeBindings.computerScreenshot;
|
|
46
47
|
export const copyToClipboard = nativeBindings.copyToClipboard;
|
|
@@ -64,6 +65,7 @@ export const exactSwapManagedLink = nativeBindings.exactSwapManagedLink;
|
|
|
64
65
|
export const exactUnlink = nativeBindings.exactUnlink;
|
|
65
66
|
export const exactUnlinkDirect = nativeBindings.exactUnlinkDirect;
|
|
66
67
|
export const exactUnlinkDirectDetached = nativeBindings.exactUnlinkDirectDetached;
|
|
68
|
+
export const exactUnlinkOpenFile = nativeBindings.exactUnlinkOpenFile;
|
|
67
69
|
export const executeShell = nativeBindings.executeShell;
|
|
68
70
|
export const extractSegments = nativeBindings.extractSegments;
|
|
69
71
|
export const fuzzyFind = nativeBindings.fuzzyFind;
|
package/native/loader-state.d.ts
CHANGED
|
@@ -88,7 +88,9 @@ export function loadFromCandidates<T>(input: LoadFromCandidatesInput<T>): LoadFr
|
|
|
88
88
|
export interface CachedEmbeddedExtractionIsFreshInput {
|
|
89
89
|
targetPath: string;
|
|
90
90
|
embeddedPath: string;
|
|
91
|
+
expectedDigest: string | null | undefined;
|
|
91
92
|
sizeOf: (path: string) => number | null;
|
|
93
|
+
digestOf: (path: string) => string | null;
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
export function cachedEmbeddedExtractionIsFresh(input: CachedEmbeddedExtractionIsFreshInput): boolean;
|
|
@@ -136,6 +138,7 @@ export interface LoadNativeOptions {
|
|
|
136
138
|
acquireStagedCandidateLease?: (candidate: string) => () => void;
|
|
137
139
|
requireCandidate?: (candidate: string) => Record<string, unknown>;
|
|
138
140
|
validateCandidate?: (bindings: Record<string, unknown>, candidate: string) => void;
|
|
141
|
+
loadNativeLibrary?: (runtime: string) => bigint | number | null;
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
export function loadNative(options?: LoadNativeOptions): Record<string, unknown>;
|
package/native/loader-state.js
CHANGED
|
@@ -5,6 +5,7 @@ import * as fs from "node:fs";
|
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
8
|
+
import embeddedDiagnosticArtifacts from "./diagnostic-artifact-embedded.json" with { type: "json" };
|
|
8
9
|
import packageJson from "../package.json" with { type: "json" };
|
|
9
10
|
import { embeddedAddon } from "./embedded-addon.js";
|
|
10
11
|
|
|
@@ -32,13 +33,14 @@ import { embeddedAddon } from "./embedded-addon.js";
|
|
|
32
33
|
* post-build `--reset` stub) is the authoritative compiled-mode signal.
|
|
33
34
|
*/
|
|
34
35
|
|
|
35
|
-
const SUPPORTED_PLATFORMS = ["linux-x64", "linux-arm64", "darwin-x64", "darwin-arm64", "win32-x64"];
|
|
36
|
+
const SUPPORTED_PLATFORMS = ["linux-x64", "linux-arm64", "darwin-x64", "darwin-arm64", "win32-x64", "win32-arm64"];
|
|
36
37
|
const OPTIONAL_PACKAGE_BY_PLATFORM_TAG = {
|
|
37
38
|
"darwin-arm64": "@gajae-code/natives-darwin-arm64",
|
|
38
39
|
"darwin-x64": "@gajae-code/natives-darwin-x64",
|
|
39
40
|
"linux-arm64": "@gajae-code/natives-linux-arm64",
|
|
40
41
|
"linux-x64": "@gajae-code/natives-linux-x64",
|
|
41
42
|
"win32-x64": "@gajae-code/natives-win32-x64",
|
|
43
|
+
"win32-arm64": "@gajae-code/natives-win32-arm64",
|
|
42
44
|
};
|
|
43
45
|
const WINDOWS_GENERIC_READ = 0x80000000;
|
|
44
46
|
const WINDOWS_FILE_SHARE_READ = 0x00000001;
|
|
@@ -138,6 +140,32 @@ function validateStagedCandidate(ctx, candidate) {
|
|
|
138
140
|
}
|
|
139
141
|
}
|
|
140
142
|
|
|
143
|
+
function preloadSiblingNativeRuntime(ctx, candidate, loadLibrary) {
|
|
144
|
+
if (process.platform !== "win32") return;
|
|
145
|
+
// Staging copies only the .node into the versioned cache and drops the
|
|
146
|
+
// package directory from the candidate list. The DLL stays beside the
|
|
147
|
+
// recorded source addon, not beside that cache copy or bun.exe.
|
|
148
|
+
const sourcePath = ctx.stagedSourceSnapshots?.get(candidate)?.sourcePath;
|
|
149
|
+
const runtimes = [
|
|
150
|
+
typeof sourcePath === "string" ? path.join(path.dirname(sourcePath), "libunwind.dll") : null,
|
|
151
|
+
path.join(path.dirname(candidate), "libunwind.dll"),
|
|
152
|
+
path.join(path.dirname(process.execPath), "libunwind.dll"),
|
|
153
|
+
];
|
|
154
|
+
const seen = new Set();
|
|
155
|
+
for (const runtime of runtimes) {
|
|
156
|
+
if (!runtime || seen.has(runtime) || !fs.existsSync(runtime)) continue;
|
|
157
|
+
seen.add(runtime);
|
|
158
|
+
const handle = loadLibrary(runtime);
|
|
159
|
+
if (handle !== 0 && handle !== 0n && handle != null) return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function loadSiblingNativeLibrary(runtime) {
|
|
164
|
+
const widePath = new Uint16Array(runtime.length + 1);
|
|
165
|
+
for (let index = 0; index < runtime.length; index++) widePath[index] = runtime.charCodeAt(index);
|
|
166
|
+
return windowsKernel32Bindings().symbols.LoadLibraryW(ptr(widePath));
|
|
167
|
+
}
|
|
168
|
+
|
|
141
169
|
function windowsKernel32Bindings() {
|
|
142
170
|
if (windowsKernel32) return windowsKernel32;
|
|
143
171
|
windowsKernel32 = dlopen("kernel32.dll", {
|
|
@@ -147,6 +175,7 @@ function windowsKernel32Bindings() {
|
|
|
147
175
|
},
|
|
148
176
|
CloseHandle: { args: ["i64"], returns: "u32" },
|
|
149
177
|
GetLastError: { args: [], returns: "u32" },
|
|
178
|
+
LoadLibraryW: { args: ["ptr"], returns: "ptr" },
|
|
150
179
|
});
|
|
151
180
|
return windowsKernel32;
|
|
152
181
|
}
|
|
@@ -368,18 +397,60 @@ export function loadFromCandidates({ candidates, requireCandidate, validateCandi
|
|
|
368
397
|
/**
|
|
369
398
|
* Decide whether a previously extracted embedded addon may be reused. A cached
|
|
370
399
|
* extraction from an earlier build of the same version carries the same version
|
|
371
|
-
* sentinel
|
|
372
|
-
*
|
|
373
|
-
* a
|
|
374
|
-
*
|
|
400
|
+
* sentinel and may have the same byte size, so freshness also requires its digest
|
|
401
|
+
* to match the package-owned embedded build record. `sizeOf` returns a path's
|
|
402
|
+
* byte size; `digestOf` returns a SHA-256 hex digest. Both return `null` when the
|
|
403
|
+
* path cannot be inspected.
|
|
404
|
+
* @param {{ targetPath: string; embeddedPath: string; expectedDigest: string | null | undefined; sizeOf: (path: string) => number | null; digestOf: (path: string) => string | null }} input
|
|
375
405
|
* @returns {boolean}
|
|
376
406
|
*/
|
|
377
|
-
export function cachedEmbeddedExtractionIsFresh({ targetPath, embeddedPath, sizeOf }) {
|
|
407
|
+
export function cachedEmbeddedExtractionIsFresh({ targetPath, embeddedPath, expectedDigest, sizeOf, digestOf }) {
|
|
408
|
+
if (typeof expectedDigest !== "string" || !/^[0-9a-f]{64}$/.test(expectedDigest)) return false;
|
|
378
409
|
const cachedSize = sizeOf(targetPath);
|
|
379
410
|
if (cachedSize === null) return false;
|
|
380
411
|
const embeddedSize = sizeOf(embeddedPath);
|
|
381
412
|
if (embeddedSize === null) return false;
|
|
382
|
-
|
|
413
|
+
if (cachedSize !== embeddedSize) return false;
|
|
414
|
+
return digestOf(targetPath) === expectedDigest;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function sha256File(file) {
|
|
418
|
+
let fd = -1;
|
|
419
|
+
try {
|
|
420
|
+
const noFollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
421
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
|
|
422
|
+
const before = fs.fstatSync(fd);
|
|
423
|
+
if (!before.isFile()) return null;
|
|
424
|
+
const hash = createHash("sha256");
|
|
425
|
+
const buffer = Buffer.allocUnsafe(1 << 20);
|
|
426
|
+
let offset = 0;
|
|
427
|
+
while (offset < before.size) {
|
|
428
|
+
const read = fs.readSync(fd, buffer, 0, Math.min(buffer.length, before.size - offset), offset);
|
|
429
|
+
if (read === 0) return null;
|
|
430
|
+
hash.update(buffer.subarray(0, read));
|
|
431
|
+
offset += read;
|
|
432
|
+
}
|
|
433
|
+
const after = fs.fstatSync(fd);
|
|
434
|
+
if (
|
|
435
|
+
after.dev !== before.dev ||
|
|
436
|
+
after.ino !== before.ino ||
|
|
437
|
+
after.size !== before.size ||
|
|
438
|
+
after.mtimeMs !== before.mtimeMs
|
|
439
|
+
) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
return hash.digest("hex");
|
|
443
|
+
} catch {
|
|
444
|
+
return null;
|
|
445
|
+
} finally {
|
|
446
|
+
if (fd >= 0) {
|
|
447
|
+
try {
|
|
448
|
+
fs.closeSync(fd);
|
|
449
|
+
} catch {
|
|
450
|
+
// A failed close cannot make an unverified digest admissible.
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
383
454
|
}
|
|
384
455
|
|
|
385
456
|
// =========================================================================
|
|
@@ -549,12 +620,16 @@ function maybeExtractEmbeddedAddons(ctx, errors) {
|
|
|
549
620
|
const extracted = [];
|
|
550
621
|
for (const embeddedFile of embeddedAddonCandidates(ctx.selectedVariant)) {
|
|
551
622
|
const targetPath = path.join(ctx.versionedDir, embeddedFile.filename);
|
|
623
|
+
const expectedDigest =
|
|
624
|
+
embeddedDiagnosticArtifacts?.schema === "gjc.diagnostic-artifact" &&
|
|
625
|
+
embeddedDiagnosticArtifacts?.version === embeddedAddon.version
|
|
626
|
+
? embeddedDiagnosticArtifacts?.artifacts?.[embeddedFile.filename]
|
|
627
|
+
: undefined;
|
|
552
628
|
if (fs.existsSync(targetPath)) {
|
|
553
629
|
// Guard against intra-version drift: a cached extraction written by an earlier
|
|
554
|
-
// build of the same version carries the same version sentinel
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
// embedded payload size and re-extract otherwise.
|
|
630
|
+
// build of the same version carries the same version sentinel and may even have
|
|
631
|
+
// the same byte size. The embedded digest is the source of truth, so reuse the
|
|
632
|
+
// cached file only when its bytes match that exact build.
|
|
558
633
|
const sizeOf = candidate => {
|
|
559
634
|
try {
|
|
560
635
|
return fs.statSync(candidate).size;
|
|
@@ -562,7 +637,15 @@ function maybeExtractEmbeddedAddons(ctx, errors) {
|
|
|
562
637
|
return null;
|
|
563
638
|
}
|
|
564
639
|
};
|
|
565
|
-
if (
|
|
640
|
+
if (
|
|
641
|
+
cachedEmbeddedExtractionIsFresh({
|
|
642
|
+
targetPath,
|
|
643
|
+
embeddedPath: embeddedFile.filePath,
|
|
644
|
+
expectedDigest,
|
|
645
|
+
sizeOf,
|
|
646
|
+
digestOf: sha256File,
|
|
647
|
+
})
|
|
648
|
+
) {
|
|
566
649
|
extracted.push(targetPath);
|
|
567
650
|
continue;
|
|
568
651
|
}
|
|
@@ -910,6 +993,7 @@ export function loadNative(options = {}) {
|
|
|
910
993
|
}
|
|
911
994
|
try {
|
|
912
995
|
validateStagedCandidate(ctx, candidate);
|
|
996
|
+
preloadSiblingNativeRuntime(ctx, candidate, options.loadNativeLibrary ?? loadSiblingNativeLibrary);
|
|
913
997
|
const bindings = options.requireCandidate ? options.requireCandidate(candidate) : require_(candidate);
|
|
914
998
|
if (typeof bindings.__gjcInstallTokioRuntime === "function") {
|
|
915
999
|
bindings.__gjcInstallTokioRuntime();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gajae-code/natives",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Native Rust bindings for grep, clipboard, image processing, syntax highlighting, PTY, and shell operations via N-API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://gajae-code.com",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "bun scripts/build-native.ts",
|
|
33
33
|
"check": "biome check . && bun run check:types",
|
|
34
|
-
"check:types": "tsc -p tsconfig.json --noEmit",
|
|
34
|
+
"check:types": "bun ../coding-agent/scripts/generate-docs-index.ts && tsc -p tsconfig.json --noEmit",
|
|
35
35
|
"lint": "biome lint .",
|
|
36
36
|
"test": "bun test",
|
|
37
37
|
"fix": "biome check --write --unsafe .",
|
|
@@ -61,15 +61,16 @@
|
|
|
61
61
|
"native/diagnostic-loader.d.ts",
|
|
62
62
|
"native/diagnostic-loader.js",
|
|
63
63
|
"native/diagnostic-artifact.json",
|
|
64
|
+
"native/diagnostic-artifact-embedded.json",
|
|
64
65
|
"native/embedded-addon.js",
|
|
65
66
|
"README.md"
|
|
66
67
|
],
|
|
67
68
|
"optionalDependencies": {
|
|
68
|
-
"@gajae-code/natives-darwin-arm64": "0.
|
|
69
|
-
"@gajae-code/natives-darwin-x64": "0.
|
|
70
|
-
"@gajae-code/natives-linux-arm64": "0.
|
|
71
|
-
"@gajae-code/natives-linux-x64": "0.
|
|
72
|
-
"@gajae-code/natives-win32-x64": "0.
|
|
69
|
+
"@gajae-code/natives-darwin-arm64": "0.19.0",
|
|
70
|
+
"@gajae-code/natives-darwin-x64": "0.19.0",
|
|
71
|
+
"@gajae-code/natives-linux-arm64": "0.19.0",
|
|
72
|
+
"@gajae-code/natives-linux-x64": "0.19.0",
|
|
73
|
+
"@gajae-code/natives-win32-x64": "0.19.0"
|
|
73
74
|
},
|
|
74
75
|
"exports": {
|
|
75
76
|
".": {
|