@sayknow-cli/natives 0.4.6 → 0.5.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/index.d.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  /* auto-generated by NAPI-RS */
2
2
  /* eslint-disable */
3
+ /**
4
+ * macOS computer-use controller.
5
+ *
6
+ * This declaration and the named JS export are available on every platform so
7
+ * consumers can import them portably; the native controller itself is built
8
+ * only on macOS.
9
+ */
3
10
  export declare class ComputerController {
4
11
  constructor()
5
12
  screenshot(): ComputerScreenshot
@@ -120,7 +127,7 @@ export declare class NotificationServer {
120
127
  * Create a server for `session_id` authenticated by `token`.
121
128
  *
122
129
  * `state_root` (when given) is where the endpoint discovery file is written
123
- * (e.g. `<repo>/.gjc/state`). `resolver_available` defaults to `true`.
130
+ * (e.g. `<repo>/.skc/state`). `resolver_available` defaults to `true`.
124
131
  */
125
132
  constructor(sessionId: string, token: string, stateRoot?: string | undefined | null, resolverAvailable?: boolean | undefined | null)
126
133
  /** Register the reply callback. Must be called before [`Self::start`]. */
@@ -201,6 +208,11 @@ export declare class NotificationServer {
201
208
  * Fails if not started or `frame_json` is not a valid `ServerMessage`.
202
209
  */
203
210
  pushFrame(frameJson: string): void
211
+ /**
212
+ * Deliver a frame through every authenticated connection and wait for each
213
+ * socket writer to settle within `timeout_ms`.
214
+ */
215
+ pushFrameAndWait(frameJson: string, timeoutMs: number): Promise<boolean>
204
216
  /**
205
217
  * Broadcast a TypeScript-constructed turn frame without re-parsing JSON.
206
218
  * External frames must continue through [`Self::push_frame`] for serde
@@ -300,6 +312,15 @@ export declare class Process {
300
312
  get ppid(): number | null
301
313
  /** Launch arguments for this process. */
302
314
  args(): Array<string>
315
+ /**
316
+ * Send `signal` only to this pinned process reference.
317
+ *
318
+ * On Linux this uses the owned pidfd; on Windows it uses the owned process
319
+ * handle. It deliberately never discovers descendants or signals a process
320
+ * group. Returns `false` when the pinned process has already exited or the
321
+ * operating system rejects delivery.
322
+ */
323
+ signalRoot(signal: number): boolean
303
324
  /**
304
325
  * Send `signal` to this process and its descendants, children first.
305
326
  *
@@ -377,7 +398,7 @@ export declare class RecoveryFsRoot {
377
398
  */
378
399
  appendManaged(relativePath: string, data: Uint8Array, expectedDev: string, expectedIno: string, expectedSize: string, expectedMtimeNs: string, expectedCtimeNs: string, expectedSha256: string): RecoveryFsResult
379
400
  /** Remove one exact managed regular file through retained authority. */
380
- removeManaged(relativePath: string, expectedDev: string, expectedIno: string, expectedSize: string, expectedMtimeNs: string, expectedCtimeNs: string, expectedSha256: string): RecoveryFsResult
401
+ removeManaged(relativePath: string, expectedDev: string, expectedIno: string, expectedSize: string, expectedMtimeNs: string, expectedCtimeNs: string, expectedSha256: string): RecoveryFsRetainedCleanupResult
381
402
  /**
382
403
  * Create each absent directory component beneath the retained root with
383
404
  * owner-only security. Existing components are re-opened no-follow.
@@ -388,22 +409,22 @@ export declare class RecoveryFsRoot {
388
409
  * retained root. The source identity is rechecked after the no-replace
389
410
  * rename, and the move is rolled back on a mismatch.
390
411
  */
391
- renameManagedFileNoReplace(sourceRelativePath: string, destinationRelativePath: string, expectedDev: string, expectedIno: string, expectedSize: string, expectedMtimeNs: string, expectedCtimeNs: string, expectedSha256: string): RecoveryFsResult
412
+ renameManagedFileNoReplace(sourceRelativePath: string, destinationRelativePath: string, expectedDev: string, expectedIno: string, expectedSize: string, expectedMtimeNs: string, expectedCtimeNs: string, expectedSha256: string): RecoveryFsPublishResult
392
413
  /** Snapshot a managed directory tree entirely through the retained root. */
393
414
  snapshotManagedTree(relativePath: string): NativeDirectoryTreeResult
394
415
  /**
395
416
  * Move an exact managed directory tree to an absent name through retained
396
417
  * authority.
397
418
  */
398
- renameManagedTreeNoReplace(sourceRelativePath: string, destinationRelativePath: string, expected: NativeDirectoryTreeSnapshot): RecoveryFsResult
419
+ renameManagedTreeNoReplace(sourceRelativePath: string, destinationRelativePath: string, expected: NativeDirectoryTreeSnapshot): RecoveryFsPublishResult
399
420
  /** Remove an exact managed directory tree through retained authority. */
400
- removeManagedTree(relativePath: string, expected: NativeDirectoryTreeSnapshot): RecoveryFsResult
421
+ removeManagedTree(relativePath: string, expected: NativeDirectoryTreeSnapshot): RecoveryFsRetainedCleanupResult
401
422
  /**
402
423
  * Atomically install an already-created regular file at an absent name.
403
424
  * Both names remain relative to this retained root and are never resolved
404
425
  * through a pathname after their parent descriptors are acquired.
405
426
  */
406
- install(sourceRelativePath: string, destinationRelativePath: string): RecoveryFsResult
427
+ install(sourceRelativePath: string, destinationRelativePath: string): RecoveryFsPublishResult
407
428
  /**
408
429
  * Synchronize the retained root directory, making a preceding create or
409
430
  * install durable when the filesystem supports directory fsync.
@@ -443,6 +464,15 @@ export declare class Shell {
443
464
  abort(): Promise<void>
444
465
  }
445
466
 
467
+ /**
468
+ * Publish-result wire-contract sentinel.
469
+ *
470
+ * The loader requires this in addition to the release sentinel, so a
471
+ * same-version modern artifact built before the retained-publish contract
472
+ * cannot be selected over a compatible baseline.
473
+ */
474
+ export declare function __piNativesPublishOutcomeV1(): void
475
+
446
476
  /**
447
477
  * Version sentinel — exists solely so the JS loader can prove at load time
448
478
  * that the `.node` file on disk is from the same package release as the
@@ -461,7 +491,7 @@ export declare class Shell {
461
491
  * `packages/natives/native/index.js` (which derives the name from
462
492
  * `package.json#version`).
463
493
  */
464
- export declare function __piNativesV0_4_6(): void
494
+ export declare function __piNativesV0_5_0(): void
465
495
 
466
496
  /**
467
497
  * Apply conservative pre-execution rewrites to a bash command.
@@ -1711,6 +1741,18 @@ export interface NativeExactUnlinkResult {
1711
1741
  retainedUnknownPath?: string
1712
1742
  }
1713
1743
 
1744
+ /** Dedicated result for an atomic no-replace namespace publication. */
1745
+ export interface NativeNoReplaceResult {
1746
+ ok: boolean
1747
+ code?: string
1748
+ mutationState: string
1749
+ durabilityState: string
1750
+ reason: string
1751
+ primitive: string
1752
+ phase: string
1753
+ diagnostic: NativePublishDiagnostic
1754
+ }
1755
+
1714
1756
  /** Result of applying or checking owner-only path security. */
1715
1757
  export type NativeOwnerOnlySecurityResult =
1716
1758
  | {
@@ -1856,6 +1898,8 @@ export interface PresentationLease {
1856
1898
  registrationEpoch: number
1857
1899
  }
1858
1900
 
1901
+ export declare function probeWindowsJobMemory(): WindowsJobMemoryProbeResult
1902
+
1859
1903
  /** Current state of a process reference. */
1860
1904
  export declare enum ProcessStatus {
1861
1905
  /** The referenced process is still running. */
@@ -1941,6 +1985,38 @@ export interface RecoveryFsIdentity {
1941
1985
  sha256?: string
1942
1986
  }
1943
1987
 
1988
+ /** Bounded, path-free diagnostic evidence for one retained publication. */
1989
+ export interface RecoveryFsPublishDiagnostic {
1990
+ schemaVersion: number
1991
+ collectionState: string
1992
+ osCode?: number
1993
+ syncFailures?: Array<RecoveryFsPublishSyncFailure>
1994
+ }
1995
+
1996
+ /**
1997
+ * Explicit mutation and durability outcome for retained no-replace
1998
+ * publication.
1999
+ */
2000
+ export interface RecoveryFsPublishResult {
2001
+ ok: boolean
2002
+ code?: string
2003
+ identity?: RecoveryFsIdentity
2004
+ mutationState: string
2005
+ durabilityState: string
2006
+ reason: string
2007
+ primitive: string
2008
+ phase: string
2009
+ diagnostic: RecoveryFsPublishDiagnostic
2010
+ }
2011
+
2012
+ /** Bounded, path-free diagnostic evidence for one retained publication. */
2013
+ export interface RecoveryFsPublishSyncFailure {
2014
+ phase: string
2015
+ parentRole: string
2016
+ osCode?: number
2017
+ kind: string
2018
+ }
2019
+
1944
2020
  export interface RecoveryFsResult {
1945
2021
  ok: boolean
1946
2022
  code?: string
@@ -1948,7 +2024,28 @@ export interface RecoveryFsResult {
1948
2024
  data?: Uint8Array
1949
2025
  }
1950
2026
 
1951
- export declare function renameNoReplacePath(sourcePath: string, destinationPath: string): NativeExactUnlinkResult
2027
+ /**
2028
+ * Fail-closed outcome for a removal whose detached object remains retained.
2029
+ * `recovery_path` identifies evidence only; it grants no authority to replay
2030
+ * or delete the retained object.
2031
+ */
2032
+ export interface RecoveryFsRetainedCleanupResult {
2033
+ ok: boolean
2034
+ code?: string
2035
+ recoveryPath?: string
2036
+ identity?: RecoveryFsIdentity
2037
+ treeSnapshot?: NativeDirectoryTreeSnapshot
2038
+ }
2039
+
2040
+ export declare function renameNoReplacePath(sourcePath: string, destinationPath: string): NativeNoReplaceResult
2041
+
2042
+ /**
2043
+ * Repair an owner-only ACL on a retained expected path.
2044
+ *
2045
+ * Its no-follow handle must still identify the expected object before repair
2046
+ * and again after final ACL verification.
2047
+ */
2048
+ export declare function repairOwnerOnlyPathSecurityExpected(path: string, kind: "directory" | "file", expectedDev: bigint, expectedIno: bigint): NativeOwnerOnlySecurityResult
1952
2049
 
1953
2050
  /** A client reply forwarded to the TypeScript host for gate resolution. */
1954
2051
  export interface ReplyEvent {
@@ -2178,6 +2275,12 @@ export declare function verifyOwnerOnlyFdSecurity(path: string, kind: "directory
2178
2275
 
2179
2276
  export declare function verifyOwnerOnlyPathSecurity(path: string, kind: "directory" | "file"): NativeOwnerOnlySecurityResult
2180
2277
 
2278
+ /**
2279
+ * Verify owner-only ACL security without mutation only when the retained
2280
+ * no-follow handle identifies the expected object before and after inspection.
2281
+ */
2282
+ export declare function verifyOwnerOnlyPathSecurityExpected(path: string, kind: "directory" | "file", expectedDev: bigint, expectedIno: bigint): NativeOwnerOnlySecurityResult
2283
+
2181
2284
  /**
2182
2285
  * Calculate visible width of text, excluding ANSI escape sequences.
2183
2286
  *
@@ -2188,6 +2291,21 @@ export declare function visibleWidth(text: string, tabWidth: number): number
2188
2291
  /** Calculate visible widths of many strings, excluding ANSI escape sequences. */
2189
2292
  export declare function visibleWidths(lines: Array<string>, tabWidth: number): Array<number>
2190
2293
 
2294
+ export interface WindowsJobMemoryProbeResult {
2295
+ kind: string
2296
+ platform: string
2297
+ isInJob?: boolean
2298
+ jobMemoryLimitBytes?: string
2299
+ jobMemoryUsedBytes?: string
2300
+ peakJobMemoryUsedBytes?: string
2301
+ processMemoryLimitBytes?: string
2302
+ processPrivateUsageBytes?: string
2303
+ processWorkingSetBytes?: string
2304
+ peakProcessWorkingSetBytes?: string
2305
+ call?: string
2306
+ code?: string
2307
+ }
2308
+
2191
2309
  /** Profiling results returned to JavaScript. */
2192
2310
  export interface WorkProfile {
2193
2311
  /** Folded stack format for flamegraph tools. */
@@ -2209,3 +2327,19 @@ export interface WorkProfile {
2209
2327
  * Returns UTF-16 lines with active SGR codes carried across line boundaries.
2210
2328
  */
2211
2329
  export declare function wrapTextWithAnsi(text: string, width: number, tabWidth: number): Array<string>
2330
+
2331
+ /** Bounded, path-free evidence for a parent-directory durability failure. */
2332
+ export interface NativePublishSyncFailure {
2333
+ phase: string
2334
+ parentRole: string
2335
+ osCode: number
2336
+ kind: string
2337
+ }
2338
+
2339
+ /** Bounded, path-free evidence for one atomic publication. */
2340
+ export interface NativePublishDiagnostic {
2341
+ schemaVersion: number
2342
+ collectionState: string
2343
+ osCode?: number
2344
+ syncFailures?: Array<NativePublishSyncFailure>
2345
+ }
package/native/index.js CHANGED
@@ -29,7 +29,8 @@ export const RecoveryFsRoot = nativeBindings.RecoveryFsRoot;
29
29
  export const Shell = nativeBindings.Shell;
30
30
 
31
31
  // functions
32
- export const __piNativesV0_4_6 = nativeBindings.__piNativesV0_4_6;
32
+ export const __piNativesPublishOutcomeV1 = nativeBindings.__piNativesPublishOutcomeV1;
33
+ export const __piNativesV0_5_0 = nativeBindings.__piNativesV0_5_0;
33
34
  export const applyBashFixups = nativeBindings.applyBashFixups;
34
35
  export const applyOwnerOnlyFdSecurity = nativeBindings.applyOwnerOnlyFdSecurity;
35
36
  export const applyOwnerOnlyPathSecurity = nativeBindings.applyOwnerOnlyPathSecurity;
@@ -74,9 +75,11 @@ export const nativeBuildInfo = nativeBindings.nativeBuildInfo;
74
75
  export const openRecoveryFsRoot = nativeBindings.openRecoveryFsRoot;
75
76
  export const parseKey = nativeBindings.parseKey;
76
77
  export const parseKittySequence = nativeBindings.parseKittySequence;
78
+ export const probeWindowsJobMemory = nativeBindings.probeWindowsJobMemory;
77
79
  export const ptyTimeoutCount = nativeBindings.ptyTimeoutCount;
78
80
  export const readImageFromClipboard = nativeBindings.readImageFromClipboard;
79
81
  export const renameNoReplacePath = nativeBindings.renameNoReplacePath;
82
+ export const repairOwnerOnlyPathSecurityExpected = nativeBindings.repairOwnerOnlyPathSecurityExpected;
80
83
  export const retainBrokerPublication = nativeBindings.retainBrokerPublication;
81
84
  export const search = nativeBindings.search;
82
85
  export const sliceWithWidth = nativeBindings.sliceWithWidth;
@@ -87,6 +90,7 @@ export const truncateLinesToWidth = nativeBindings.truncateLinesToWidth;
87
90
  export const truncateToWidth = nativeBindings.truncateToWidth;
88
91
  export const verifyOwnerOnlyFdSecurity = nativeBindings.verifyOwnerOnlyFdSecurity;
89
92
  export const verifyOwnerOnlyPathSecurity = nativeBindings.verifyOwnerOnlyPathSecurity;
93
+ export const verifyOwnerOnlyPathSecurityExpected = nativeBindings.verifyOwnerOnlyPathSecurityExpected;
90
94
  export const visibleWidth = nativeBindings.visibleWidth;
91
95
  export const visibleWidths = nativeBindings.visibleWidths;
92
96
  export const wrapTextWithAnsi = nativeBindings.wrapTextWithAnsi;
@@ -26,6 +26,15 @@ export interface GetAddonFilenamesInput {
26
26
 
27
27
  export function getAddonFilenames(input: GetAddonFilenamesInput): string[];
28
28
 
29
+ export function getOptionalPackageNames(platformTag: string): string[];
30
+
31
+ export interface ResolveOptionalPackageNativeDirsInput {
32
+ packageNames: string[];
33
+ requireResolve: (id: string) => string;
34
+ }
35
+
36
+ export function resolveOptionalPackageNativeDirs(input: ResolveOptionalPackageNativeDirsInput): string[];
37
+
29
38
  export interface ShouldStageNodeModulesAddonInput {
30
39
  platform: NodeJS.Platform | string;
31
40
  isCompiledBinary: boolean;
@@ -38,8 +47,9 @@ export interface ResolveLoaderCandidatesInput {
38
47
  addonFilenames: string[];
39
48
  isCompiledBinary: boolean;
40
49
  stageFromNodeModules?: boolean;
50
+ isWorkspaceLoad?: boolean;
51
+ optionalPackageNativeDirs?: string[];
41
52
  nativeDir: string;
42
- platformNativeDir?: string | null;
43
53
  execDir: string;
44
54
  versionedDir: string;
45
55
  userDataDir: string;
@@ -47,4 +57,56 @@ export interface ResolveLoaderCandidatesInput {
47
57
 
48
58
  export function resolveLoaderCandidates(input: ResolveLoaderCandidatesInput): string[];
49
59
 
50
- export function loadNative(): Record<string, unknown>;
60
+ export interface LoadFromCandidatesInput<T> {
61
+ candidates: string[];
62
+ requireCandidate: (candidate: string) => T;
63
+ validateCandidate: (bindings: T, candidate: string) => void;
64
+ describeCandidate: (candidate: string) => string;
65
+ }
66
+
67
+ export interface LoadFromCandidatesResult<T> {
68
+ bindings: T | null;
69
+ errors: string[];
70
+ }
71
+
72
+ export function loadFromCandidates<T>(input: LoadFromCandidatesInput<T>): LoadFromCandidatesResult<T>;
73
+
74
+ export interface CachedEmbeddedExtractionIsFreshInput {
75
+ targetPath: string;
76
+ embeddedPath: string;
77
+ sizeOf: (path: string) => number | null;
78
+ }
79
+
80
+ export function cachedEmbeddedExtractionIsFresh(input: CachedEmbeddedExtractionIsFreshInput): boolean;
81
+
82
+ export function validateLoadedBindings(
83
+ ctx: { versionSentinelExport: string; packageVersion: string },
84
+ bindings: Record<string, unknown>,
85
+ candidate: string,
86
+ ): void;
87
+
88
+ export interface LoaderContext {
89
+ isCompiledBinary: boolean;
90
+ platformTag: string;
91
+ packageVersion?: string;
92
+ addonLabel?: string;
93
+ addonFilenames?: string[];
94
+ versionedDir?: string;
95
+ candidates?: string[];
96
+ selectedVariant?: "modern" | "baseline" | null;
97
+ }
98
+
99
+ export function embeddedAddonIsAuthoritative(
100
+ ctx: LoaderContext,
101
+ addon?: EmbeddedAddon | null,
102
+ ): boolean;
103
+
104
+ export interface LoadNativeOptions {
105
+ context?: LoaderContext;
106
+ extractEmbeddedAddons?: (ctx: LoaderContext) => string[];
107
+ stageNodeModulesAddon?: () => string | null;
108
+ requireCandidate?: (candidate: string) => Record<string, unknown>;
109
+ validateCandidate?: (bindings: Record<string, unknown>) => void;
110
+ }
111
+
112
+ export function loadNative(options?: LoadNativeOptions): Record<string, unknown>;
@@ -31,6 +31,14 @@ import { embeddedAddon } from "./embedded-addon.js";
31
31
  */
32
32
 
33
33
  const SUPPORTED_PLATFORMS = ["linux-x64", "linux-arm64", "darwin-x64", "darwin-arm64", "win32-x64"];
34
+ const OPTIONAL_PACKAGE_BY_PLATFORM_TAG = {
35
+ "darwin-arm64": "@sayknow-cli/natives-darwin-arm64",
36
+ "darwin-x64": "@sayknow-cli/natives-darwin-x64",
37
+ "linux-arm64": "@sayknow-cli/natives-linux-arm64",
38
+ "linux-x64": "@sayknow-cli/natives-linux-x64",
39
+ "win32-x64": "@sayknow-cli/natives-win32-x64",
40
+ };
41
+
34
42
 
35
43
  function getNativesDir() {
36
44
  const xdgDataHome = process.env.XDG_DATA_HOME;
@@ -78,6 +86,32 @@ export function getAddonFilenames({ tag, arch, variant }) {
78
86
  return [baselineFilename, defaultFilename];
79
87
  }
80
88
 
89
+ /**
90
+ * @param {string} platformTag
91
+ * @returns {string[]}
92
+ */
93
+ export function getOptionalPackageNames(platformTag) {
94
+ const packageName = OPTIONAL_PACKAGE_BY_PLATFORM_TAG[platformTag];
95
+ return packageName ? [packageName] : [];
96
+ }
97
+
98
+ /**
99
+ * @param {{ packageNames: string[]; requireResolve: (id: string) => string }} input
100
+ * @returns {string[]}
101
+ */
102
+ export function resolveOptionalPackageNativeDirs({ packageNames, requireResolve }) {
103
+ const dirs = [];
104
+ for (const packageName of packageNames) {
105
+ try {
106
+ const manifestPath = requireResolve(`${packageName}/package.json`);
107
+ dirs.push(path.join(path.dirname(manifestPath), "native"));
108
+ } catch {
109
+ // Optional dependency is absent on non-matching platforms or older installs.
110
+ }
111
+ }
112
+ return dirs;
113
+ }
114
+
81
115
  /**
82
116
  * Decide whether the loader should mirror the package's `native/<filename>.node`
83
117
  * into the per-version cache directory (`~/.skc/natives/<version>/`) before loading.
@@ -112,8 +146,9 @@ export function shouldStageNodeModulesAddon({ platform, isCompiledBinary, native
112
146
  * addonFilenames: string[];
113
147
  * isCompiledBinary: boolean;
114
148
  * stageFromNodeModules?: boolean;
149
+ * isWorkspaceLoad?: boolean;
150
+ * optionalPackageNativeDirs?: string[];
115
151
  * nativeDir: string;
116
- * platformNativeDir?: string | null;
117
152
  * execDir: string;
118
153
  * versionedDir: string;
119
154
  * userDataDir: string;
@@ -124,18 +159,25 @@ export function resolveLoaderCandidates({
124
159
  addonFilenames,
125
160
  isCompiledBinary,
126
161
  stageFromNodeModules = false,
162
+ isWorkspaceLoad = false,
163
+ optionalPackageNativeDirs = [],
127
164
  nativeDir,
128
- platformNativeDir,
129
165
  execDir,
130
166
  versionedDir,
131
167
  userDataDir,
132
168
  }) {
133
- const nestedPlatformNativeDir = path.join(nativeDir, "..", "node_modules", "@sayknow-cli", `natives-${process.platform}-${process.arch}`, "native");
134
- const sourceNativeDirs = [nativeDir, platformNativeDir, nestedPlatformNativeDir].filter(dir => typeof dir === "string" && dir.length > 0);
135
- const baseReleaseCandidates = addonFilenames.flatMap(filename => [
136
- ...sourceNativeDirs.map(dir => path.join(dir, filename)),
169
+ const workspaceCandidates = addonFilenames.map(filename => path.join(nativeDir, filename));
170
+ const optionalPackageCandidates = optionalPackageNativeDirs.flatMap(optionalNativeDir =>
171
+ addonFilenames.map(filename => path.join(optionalNativeDir, filename)),
172
+ );
173
+ const legacyReleaseCandidates = addonFilenames.flatMap(filename => [
174
+ path.join(nativeDir, filename),
137
175
  path.join(execDir, filename),
138
176
  ]);
177
+ const legacyExecCandidates = addonFilenames.map(filename => path.join(execDir, filename));
178
+ const baseReleaseCandidates = isWorkspaceLoad
179
+ ? [...workspaceCandidates, ...optionalPackageCandidates, ...legacyExecCandidates]
180
+ : [...optionalPackageCandidates, ...legacyReleaseCandidates];
139
181
  const compiledCandidates = addonFilenames.flatMap(filename => [
140
182
  path.join(versionedDir, filename),
141
183
  path.join(userDataDir, filename),
@@ -152,6 +194,52 @@ export function resolveLoaderCandidates({
152
194
  return [...new Set(releaseCandidates)];
153
195
  }
154
196
 
197
+ /**
198
+ * Deterministically try candidate paths in order using injected operations.
199
+ * This leaves file loading and compatibility policy at the call site while
200
+ * making fallback behavior testable without a native addon on disk.
201
+ *
202
+ * @template T
203
+ * @param {{
204
+ * candidates: string[];
205
+ * requireCandidate: (candidate: string) => T;
206
+ * validateCandidate: (bindings: T, candidate: string) => void;
207
+ * describeCandidate: (candidate: string) => string;
208
+ * }} input
209
+ * @returns {{ bindings: T | null; errors: string[] }}
210
+ */
211
+ export function loadFromCandidates({ candidates, requireCandidate, validateCandidate, describeCandidate }) {
212
+ const errors = [];
213
+ for (const candidate of candidates) {
214
+ try {
215
+ const bindings = requireCandidate(candidate);
216
+ validateCandidate(bindings, candidate);
217
+ return { bindings, errors };
218
+ } catch (err) {
219
+ const message = err instanceof Error ? err.message : String(err);
220
+ errors.push(`${describeCandidate(candidate)}: ${message}`);
221
+ }
222
+ }
223
+ return { bindings: null, errors };
224
+ }
225
+
226
+ /**
227
+ * Decide whether a previously extracted embedded addon may be reused. A cached
228
+ * extraction from an earlier build of the same version carries the same version
229
+ * sentinel yet can expose a different native surface, so it is fresh only when
230
+ * its byte size matches the embedded payload. `sizeOf` returns the byte size of
231
+ * a path, or `null` when it cannot be inspected.
232
+ * @param {{ targetPath: string; embeddedPath: string; sizeOf: (path: string) => number | null }} input
233
+ * @returns {boolean}
234
+ */
235
+ export function cachedEmbeddedExtractionIsFresh({ targetPath, embeddedPath, sizeOf }) {
236
+ const cachedSize = sizeOf(targetPath);
237
+ if (cachedSize === null) return false;
238
+ const embeddedSize = sizeOf(embeddedPath);
239
+ if (embeddedSize === null) return false;
240
+ return cachedSize === embeddedSize;
241
+ }
242
+
155
243
  // =========================================================================
156
244
  // Side-effectful loader. Everything below runs only when `loadNative()` is
157
245
  // called from `native/index.js` — tests that only import the pure helpers
@@ -218,70 +306,79 @@ function resolveCpuVariant(override) {
218
306
  return detectAvx2Support() ? "modern" : "baseline";
219
307
  }
220
308
 
221
- function selectEmbeddedAddonFile(selectedVariant) {
222
- if (!embeddedAddon) return null;
223
- const defaultFile = embeddedAddon.files.find(file => file.variant === "default") || null;
224
- if (process.arch !== "x64") return defaultFile || embeddedAddon.files[0] || null;
225
- if (selectedVariant === "modern") {
226
- return (
227
- embeddedAddon.files.find(file => file.variant === "modern") ||
228
- embeddedAddon.files.find(file => file.variant === "baseline") ||
229
- null
230
- );
231
- }
232
- return embeddedAddon.files.find(file => file.variant === "baseline") || null;
309
+ function embeddedAddonCandidates(selectedVariant) {
310
+ if (!embeddedAddon) return [];
311
+ const files = embeddedAddon.files;
312
+ const candidates = process.arch !== "x64"
313
+ ? [files.find(file => file.variant === "default"), ...files]
314
+ : selectedVariant === "modern"
315
+ ? [files.find(file => file.variant === "modern"), files.find(file => file.variant === "baseline")]
316
+ : [files.find(file => file.variant === "baseline")];
317
+ return [...new Set(candidates.filter(Boolean))];
233
318
  }
234
319
 
235
- function maybeExtractEmbeddedAddon(ctx, errors) {
236
- if (!ctx.isCompiledBinary || !embeddedAddon) return null;
237
- if (embeddedAddon.platformTag !== ctx.platformTag || embeddedAddon.version !== ctx.packageVersion) return null;
238
-
239
- const selectedEmbeddedFile = selectEmbeddedAddonFile(ctx.selectedVariant);
240
- if (!selectedEmbeddedFile) return null;
241
- const targetPath = path.join(ctx.versionedDir, selectedEmbeddedFile.filename);
242
- if (fs.existsSync(targetPath)) return targetPath;
320
+ function maybeExtractEmbeddedAddons(ctx, errors) {
321
+ if (!ctx.isCompiledBinary || !embeddedAddon) return [];
322
+ if (embeddedAddon.platformTag !== ctx.platformTag || embeddedAddon.version !== ctx.packageVersion) return [];
243
323
 
244
- try {
245
- fs.mkdirSync(ctx.versionedDir, { recursive: true });
246
- } catch (err) {
247
- const message = err instanceof Error ? err.message : String(err);
248
- errors.push(`embedded addon dir: ${message}`);
249
- return null;
250
- }
324
+ const extracted = [];
325
+ for (const embeddedFile of embeddedAddonCandidates(ctx.selectedVariant)) {
326
+ const targetPath = path.join(ctx.versionedDir, embeddedFile.filename);
327
+ if (fs.existsSync(targetPath)) {
328
+ // Guard against intra-version drift: a cached extraction written by an earlier
329
+ // build of the same version carries the same version sentinel but can expose a
330
+ // different native surface (e.g. a symbol added mid-cycle). The embedded addon
331
+ // is the source of truth, so reuse the cached file only when it matches the
332
+ // embedded payload size and re-extract otherwise.
333
+ const sizeOf = candidate => {
334
+ try {
335
+ return fs.statSync(candidate).size;
336
+ } catch {
337
+ return null;
338
+ }
339
+ };
340
+ if (cachedEmbeddedExtractionIsFresh({ targetPath, embeddedPath: embeddedFile.filePath, sizeOf })) {
341
+ extracted.push(targetPath);
342
+ continue;
343
+ }
344
+ }
251
345
 
252
- try {
253
- const buffer = fs.readFileSync(selectedEmbeddedFile.filePath);
254
- const tempPath = `${targetPath}.tmp.${process.pid}`;
255
- fs.writeFileSync(tempPath, buffer);
256
- fs.renameSync(tempPath, targetPath);
257
- return targetPath;
258
- } catch (err) {
259
- const message = err instanceof Error ? err.message : String(err);
260
- errors.push(`embedded addon write (${selectedEmbeddedFile.filename}): ${message}`);
261
- return null;
346
+ try {
347
+ fs.mkdirSync(ctx.versionedDir, { recursive: true });
348
+ const buffer = fs.readFileSync(embeddedFile.filePath);
349
+ const tempPath = `${targetPath}.tmp.${process.pid}`;
350
+ fs.writeFileSync(tempPath, buffer);
351
+ fs.renameSync(tempPath, targetPath);
352
+ extracted.push(targetPath);
353
+ } catch (err) {
354
+ const message = err instanceof Error ? err.message : String(err);
355
+ errors.push(`embedded addon write (${embeddedFile.filename}): ${message}`);
356
+ }
262
357
  }
358
+ return extracted;
263
359
  }
264
360
 
265
361
  /**
266
- * Mirror `nativeDir/<filename>.node` to `versionedDir/<filename>.node` on Windows
267
- * installs so the running process keeps its OS-level handle on a versioned
268
- * cache path, never on the `node_modules` copy that bun must overwrite on
269
- * update. No-op on non-Windows, in workspace dev, and for compiled binaries —
270
- * see `shouldStageNodeModulesAddon` for the gating rules.
362
+ * Mirror the optional-package or legacy bundled `native/<filename>.node` into
363
+ * `versionedDir/<filename>.node` on Windows installs so the running process
364
+ * keeps its OS-level handle on a versioned cache path, never on the
365
+ * `node_modules` copy that bun must overwrite on update. No-op on non-Windows,
366
+ * in workspace dev, and for compiled binaries — see `shouldStageNodeModulesAddon`.
271
367
  */
272
368
  function maybeStageNodeModulesAddon(ctx, errors) {
273
369
  if (!ctx.stageFromNodeModules) return null;
274
370
 
275
371
  let stagedPath = null;
372
+ const sourceDirs = [...ctx.optionalPackageNativeDirs, ctx.nativeDir];
276
373
  for (const filename of ctx.addonFilenames) {
277
- const sourcePath = path.join(ctx.nativeDir, filename);
278
374
  const targetPath = path.join(ctx.versionedDir, filename);
279
375
 
280
376
  if (fs.existsSync(targetPath)) {
281
377
  stagedPath = stagedPath || targetPath;
282
378
  continue;
283
379
  }
284
- if (!fs.existsSync(sourcePath)) continue;
380
+ const sourcePath = sourceDirs.map(sourceDir => path.join(sourceDir, filename)).find(candidate => fs.existsSync(candidate));
381
+ if (!sourcePath) continue;
285
382
 
286
383
  try {
287
384
  fs.mkdirSync(ctx.versionedDir, { recursive: true });
@@ -304,19 +401,26 @@ function maybeStageNodeModulesAddon(ctx, errors) {
304
401
  return stagedPath;
305
402
  }
306
403
 
307
- function validateLoadedBindings(ctx, bindings, candidate) {
308
- // In workspace dev (running out of `packages/natives/native/` rather than a
309
- // `node_modules` install or a compiled bundle) the local `.node` only gains
310
- // the renamed sentinel after `bun --cwd=packages/natives run build`. Skip
311
- // validation there so a stale post-pull dev tree boots while the rebuild
312
- // completes; install and compiled-binary paths still validate.
313
- if (ctx.isWorkspaceLoad) return;
314
- if (typeof bindings[ctx.versionSentinelExport] === "function") return;
315
- throw new Error(
316
- `Loaded ${candidate} but it does not expose the @sayknow-cli/natives@${ctx.packageVersion} ` +
317
- `version sentinel \`${ctx.versionSentinelExport}\`. The .node file on disk is from a different ` +
318
- "release than this loader — reinstall to re-sync.",
319
- );
404
+ export function validateLoadedBindings(ctx, bindings, candidate) {
405
+ if (typeof bindings[ctx.versionSentinelExport] !== "function") {
406
+ throw new Error(
407
+ `Loaded ${candidate} but it does not expose the @sayknow-cli/natives@${ctx.packageVersion} ` +
408
+ `version sentinel \`${ctx.versionSentinelExport}\`. The .node file on disk is from a different ` +
409
+ "release than this loader — reinstall to re-sync.",
410
+ );
411
+ }
412
+ if (typeof bindings.__piNativesPublishOutcomeV1 !== "function") {
413
+ throw new Error(
414
+ `Loaded ${candidate} but it lacks retained-publish capability sentinel ` +
415
+ "`__piNativesPublishOutcomeV1`; trying the next compatible artifact.",
416
+ );
417
+ }
418
+ if (typeof bindings.renameNoReplacePath !== "function") {
419
+ throw new Error(`Loaded ${candidate} but it lacks required atomic publish capability \`renameNoReplacePath\`.`);
420
+ }
421
+ if (typeof bindings.probeWindowsJobMemory !== "function") {
422
+ throw new Error(`Loaded ${candidate} but it lacks required memory probe capability \`probeWindowsJobMemory\`.`);
423
+ }
320
424
  }
321
425
 
322
426
  function buildHelpMessage(ctx) {
@@ -347,11 +451,10 @@ function buildHelpMessage(ctx) {
347
451
  * Called from `loadNative()` rather than at module scope so importing pure
348
452
  * helpers from this file doesn't trigger AVX2 detection or filesystem probes.
349
453
  */
350
- function initLoaderContext() {
454
+ function initLoaderContext(require_) {
351
455
  const platformTag = `${process.platform}-${process.arch}`;
352
456
  const packageVersion = packageJson.version;
353
457
  const nativeDir = path.join(import.meta.dir, "..", "native");
354
- const platformNativeDir = path.join(nativeDir, "..", "..", `natives-${platformTag}`, "native");
355
458
  const execDir = path.dirname(process.execPath);
356
459
  const versionedDir = path.join(getNativesDir(), packageVersion);
357
460
  const userDataDir =
@@ -369,17 +472,25 @@ function initLoaderContext() {
369
472
  isCompiledBinary,
370
473
  nativeDir,
371
474
  });
475
+ const isWorkspaceLoad =
476
+ !isCompiledBinary && !nativeDir.includes("\\node_modules\\") && !nativeDir.includes("/node_modules/");
372
477
 
373
478
  const selectedVariant = resolveCpuVariant(getVariantOverride());
374
479
  const addonFilenames = getAddonFilenames({ tag: platformTag, arch: process.arch, variant: selectedVariant });
375
480
  const addonLabel = selectedVariant ? `${platformTag} (${selectedVariant})` : platformTag;
481
+ const optionalPackageNativeDirs = resolveOptionalPackageNativeDirs({
482
+ packageNames: getOptionalPackageNames(platformTag),
483
+ requireResolve: id => require_.resolve(id),
484
+ });
485
+
376
486
 
377
487
  const candidates = resolveLoaderCandidates({
378
488
  addonFilenames,
379
489
  isCompiledBinary,
380
490
  stageFromNodeModules,
491
+ isWorkspaceLoad,
492
+ optionalPackageNativeDirs,
381
493
  nativeDir,
382
- platformNativeDir,
383
494
  execDir,
384
495
  versionedDir,
385
496
  userDataDir,
@@ -393,8 +504,6 @@ function initLoaderContext() {
393
504
  // turns the silent `<sym> is not a function` crash from a Windows
394
505
  // locked-file update into an actionable load-time error.
395
506
  const versionSentinelExport = `__piNativesV${packageVersion.replace(/[^A-Za-z0-9]/g, "_")}`;
396
- const isWorkspaceLoad =
397
- !isCompiledBinary && !nativeDir.includes("\\node_modules\\") && !nativeDir.includes("/node_modules/");
398
507
 
399
508
  return {
400
509
  platformTag,
@@ -405,33 +514,41 @@ function initLoaderContext() {
405
514
  stageFromNodeModules,
406
515
  selectedVariant,
407
516
  addonFilenames,
517
+ optionalPackageNativeDirs,
408
518
  addonLabel,
409
519
  candidates,
410
520
  versionSentinelExport,
411
- isWorkspaceLoad,
412
521
  };
413
522
  }
414
523
 
415
- export function loadNative() {
416
- const ctx = initLoaderContext();
417
- const require_ = createRequire(import.meta.url);
524
+ /** Embedded standalone payloads are the complete trust boundary for their matching build. */
525
+ export function embeddedAddonIsAuthoritative(ctx, addon = embeddedAddon) {
526
+ return (
527
+ ctx.isCompiledBinary && addon?.platformTag === ctx.platformTag && addon.version === ctx.packageVersion
528
+ );
529
+ }
418
530
 
419
- const errors = [];
420
- const embeddedCandidate = maybeExtractEmbeddedAddon(ctx, errors);
421
- const stagedCandidate = embeddedCandidate ? null : maybeStageNodeModulesAddon(ctx, errors);
422
- const prepended = [embeddedCandidate, stagedCandidate].filter(c => typeof c === "string");
423
- const runtimeCandidates = prepended.length > 0 ? [...prepended, ...ctx.candidates] : ctx.candidates;
531
+ export function loadNative(options = {}) {
532
+ const require_ = options.requireCandidate ? null : createRequire(import.meta.url);
533
+ const ctx = options.context ?? initLoaderContext(require_);
424
534
 
425
- for (const candidate of runtimeCandidates) {
426
- try {
427
- const bindings = require_(candidate);
428
- validateLoadedBindings(ctx, bindings, candidate);
429
- return bindings;
430
- } catch (err) {
431
- const message = err instanceof Error ? err.message : String(err);
432
- errors.push(`${candidate}: ${message}`);
433
- }
434
- }
535
+ const errors = [];
536
+ const embeddedCandidates = (options.extractEmbeddedAddons ?? maybeExtractEmbeddedAddons)(ctx, errors);
537
+ const embeddedIsAuthoritative = embeddedAddonIsAuthoritative(ctx);
538
+ const stagedCandidate =
539
+ embeddedCandidates.length > 0 || embeddedIsAuthoritative
540
+ ? null
541
+ : (options.stageNodeModulesAddon ?? maybeStageNodeModulesAddon)(ctx, errors);
542
+ const prepended = [...embeddedCandidates, stagedCandidate].filter(c => typeof c === "string");
543
+ const runtimeCandidates = embeddedIsAuthoritative ? prepended : prepended.length > 0 ? [...prepended, ...ctx.candidates] : ctx.candidates;
544
+ const loaded = loadFromCandidates({
545
+ candidates: runtimeCandidates,
546
+ requireCandidate: options.requireCandidate ?? (candidate => require_(candidate)),
547
+ validateCandidate: options.validateCandidate ?? ((bindings, candidate) => validateLoadedBindings(ctx, bindings, candidate)),
548
+ describeCandidate: candidate => candidate,
549
+ });
550
+ if (loaded.bindings) return loaded.bindings;
551
+ errors.push(...loaded.errors);
435
552
 
436
553
  if (!SUPPORTED_PLATFORMS.includes(ctx.platformTag)) {
437
554
  throw new Error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sayknow-cli/natives",
3
- "version": "0.4.6",
3
+ "version": "0.5.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://sayknow-cli.com",
@@ -61,11 +61,11 @@
61
61
  "README.md"
62
62
  ],
63
63
  "optionalDependencies": {
64
- "@sayknow-cli/natives-darwin-arm64": "0.4.6",
65
- "@sayknow-cli/natives-darwin-x64": "0.4.6",
66
- "@sayknow-cli/natives-linux-arm64": "0.4.6",
67
- "@sayknow-cli/natives-linux-x64": "0.4.6",
68
- "@sayknow-cli/natives-win32-x64": "0.4.6"
64
+ "@sayknow-cli/natives-darwin-arm64": "0.5.0",
65
+ "@sayknow-cli/natives-darwin-x64": "0.5.0",
66
+ "@sayknow-cli/natives-linux-arm64": "0.5.0",
67
+ "@sayknow-cli/natives-linux-x64": "0.5.0",
68
+ "@sayknow-cli/natives-win32-x64": "0.5.0"
69
69
  },
70
70
  "exports": {
71
71
  ".": {