@oh-my-pi/pi-natives 18.2.8 → 18.2.9

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
@@ -648,7 +648,7 @@ export declare function __ompInstallTokioRuntime(): void
648
648
  * `packages/natives/native/index.js` (which derives the name from
649
649
  * `package.json#version`).
650
650
  */
651
- export declare function __piNativesV18_2_8(): void
651
+ export declare function __piNativesV18_2_9(): void
652
652
 
653
653
  /**
654
654
  * Apply ast-grep rewrite rules to matching files; honors `dryRun` and returns
package/native/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { loadNative } from "./loader-state.js";
1
+ import { loadNative, missingNativeExport } from "./loader-state.js";
2
2
  import { adaptDesktopSession } from "./desktop-adapter.js";
3
3
 
4
4
  /**
@@ -12,6 +12,12 @@ import { adaptDesktopSession } from "./desktop-adapter.js";
12
12
  * Loader logic lives in `./loader-state.js` so the pure helpers stay
13
13
  * unit-testable without triggering AVX2 detection or filesystem probes on
14
14
  * import. Types are auto-generated by napi-rs in `./index.d.ts`.
15
+ *
16
+ * Function exports fall back to `missingNativeExport` instead of a bare
17
+ * `undefined`: a workspace tree that pulled a new release loads its previous
18
+ * addon by design (see `validateLoadedBindings`), and every symbol added since
19
+ * that build would otherwise fail as `<symbol> is not a function` inside
20
+ * whichever tool used it first.
15
21
  */
16
22
 
17
23
  const nativeBindings = loadNative();
@@ -38,95 +44,95 @@ export const VcsJjWorkspace = nativeBindings.VcsJjWorkspace;
38
44
  export const VcsRepo = nativeBindings.VcsRepo;
39
45
 
40
46
  // functions
41
- export const __ompInstallTokioRuntime = nativeBindings.__ompInstallTokioRuntime;
42
- export const __piNativesV18_2_8 = nativeBindings.__piNativesV18_2_8;
43
- export const astEdit = nativeBindings.astEdit;
44
- export const astGrep = nativeBindings.astGrep;
45
- export const astMatch = nativeBindings.astMatch;
46
- export const blockRangeAt = nativeBindings.blockRangeAt;
47
- export const copyToClipboard = nativeBindings.copyToClipboard;
48
- export const cosineSimilarityPairs = nativeBindings.cosineSimilarityPairs;
49
- export const countTokens = nativeBindings.countTokens;
50
- export const decodeSixelToPng = nativeBindings.decodeSixelToPng;
51
- export const detectMacOSAppearance = nativeBindings.detectMacOSAppearance;
52
- export const deviceCheckGenerateToken = nativeBindings.deviceCheckGenerateToken;
53
- export const diffLineRuns = nativeBindings.diffLineRuns;
54
- export const diffLines = nativeBindings.diffLines;
55
- export const diffWords = nativeBindings.diffWords;
56
- export const editAutoGeneratedMessage = nativeBindings.editAutoGeneratedMessage;
57
- export const editDescription = nativeBindings.editDescription;
58
- export const editDiffString = nativeBindings.editDiffString;
59
- export const editGrammar = nativeBindings.editGrammar;
60
- export const editInspect = nativeBindings.editInspect;
61
- export const enclosingBlockBoundaries = nativeBindings.enclosingBlockBoundaries;
62
- export const encodeSixel = nativeBindings.encodeSixel;
63
- export const execReplace = nativeBindings.execReplace;
64
- export const executeShell = nativeBindings.executeShell;
65
- export const extractInlineSloppyRegions = nativeBindings.extractInlineSloppyRegions;
66
- export const extractSegments = nativeBindings.extractSegments;
67
- export const fuzzyFind = nativeBindings.fuzzyFind;
68
- export const getSupportedLanguages = nativeBindings.getSupportedLanguages;
69
- export const getWorkProfile = nativeBindings.getWorkProfile;
70
- export const glob = nativeBindings.glob;
71
- export const grep = nativeBindings.grep;
72
- export const hashlineCountOps = nativeBindings.hashlineCountOps;
73
- export const hashlineFileHash = nativeBindings.hashlineFileHash;
74
- export const hashlineFormatHeader = nativeBindings.hashlineFormatHeader;
75
- export const hashlineFormatNumberedLines = nativeBindings.hashlineFormatNumberedLines;
76
- export const hashlineIsReadTruncationNotice = nativeBindings.hashlineIsReadTruncationNotice;
77
- export const hashlineStripPrefixes = nativeBindings.hashlineStripPrefixes;
78
- export const hasMatch = nativeBindings.hasMatch;
79
- export const highlightCode = nativeBindings.highlightCode;
80
- export const htmlToMarkdown = nativeBindings.htmlToMarkdown;
81
- export const invalidateFsScanCache = nativeBindings.invalidateFsScanCache;
82
- export const isoBackend = nativeBindings.isoBackend;
83
- export const isoDiff = nativeBindings.isoDiff;
84
- export const isoIsUnavailableError = nativeBindings.isoIsUnavailableError;
85
- export const isoProbe = nativeBindings.isoProbe;
86
- export const isoResolve = nativeBindings.isoResolve;
87
- export const isoStart = nativeBindings.isoStart;
88
- export const isoStop = nativeBindings.isoStop;
89
- export const listWorkspace = nativeBindings.listWorkspace;
90
- export const macOSAutocorrectWord = nativeBindings.macOSAutocorrectWord;
91
- export const macOSCheckSpelling = nativeBindings.macOSCheckSpelling;
92
- export const macOSCompleteWord = nativeBindings.macOSCompleteWord;
93
- export const macOSSpellCheckerAvailable = nativeBindings.macOSSpellCheckerAvailable;
94
- export const macOSSpellingGuesses = nativeBindings.macOSSpellingGuesses;
95
- export const matchesKey = nativeBindings.matchesKey;
96
- export const matchesKittySequence = nativeBindings.matchesKittySequence;
97
- export const matchesLegacySequence = nativeBindings.matchesLegacySequence;
98
- export const mmrRerankIndices = nativeBindings.mmrRerankIndices;
99
- export const nodeChainAt = nativeBindings.nodeChainAt;
100
- export const notebookToEditableText = nativeBindings.notebookToEditableText;
101
- export const parseKey = nativeBindings.parseKey;
102
- export const parseKittySequence = nativeBindings.parseKittySequence;
103
- export const pdfToMarkdown = nativeBindings.pdfToMarkdown;
104
- export const rasterizeSvg = nativeBindings.rasterizeSvg;
105
- export const readImageFromClipboard = nativeBindings.readImageFromClipboard;
106
- export const renderMermaidAscii = nativeBindings.renderMermaidAscii;
107
- export const renderSnapcompactPng = nativeBindings.renderSnapcompactPng;
108
- export const search = nativeBindings.search;
109
- export const setHangulCompatJamoWidthOverride = nativeBindings.setHangulCompatJamoWidthOverride;
110
- export const sliceWithWidth = nativeBindings.sliceWithWidth;
111
- export const snapcompactSupportedChars = nativeBindings.snapcompactSupportedChars;
112
- export const structuredPatchHunks = nativeBindings.structuredPatchHunks;
113
- export const summarizeCode = nativeBindings.summarizeCode;
114
- export const supportsLanguage = nativeBindings.supportsLanguage;
115
- export const truncateToWidth = nativeBindings.truncateToWidth;
116
- export const vcsDetachGitDir = nativeBindings.vcsDetachGitDir;
117
- export const vcsDiscover = nativeBindings.vcsDiscover;
118
- export const vcsDiscoverForDisplay = nativeBindings.vcsDiscoverForDisplay;
119
- export const vcsGitClone = nativeBindings.vcsGitClone;
120
- export const vcsGitDiscover = nativeBindings.vcsGitDiscover;
121
- export const vcsGitRepoInfo = nativeBindings.vcsGitRepoInfo;
122
- export const vcsIsPureJj = nativeBindings.vcsIsPureJj;
123
- export const vcsJjDiscover = nativeBindings.vcsJjDiscover;
124
- export const vcsJoinPatches = nativeBindings.vcsJoinPatches;
125
- export const vcsValidateHunkSelections = nativeBindings.vcsValidateHunkSelections;
126
- export const vectorIndexTopK = nativeBindings.vectorIndexTopK;
127
- export const visibleWidth = nativeBindings.visibleWidth;
128
- export const warmHighlighter = nativeBindings.warmHighlighter;
129
- export const wrapTextWithAnsi = nativeBindings.wrapTextWithAnsi;
47
+ export const __ompInstallTokioRuntime = nativeBindings.__ompInstallTokioRuntime ?? missingNativeExport("__ompInstallTokioRuntime");
48
+ export const __piNativesV18_2_9 = nativeBindings.__piNativesV18_2_9;
49
+ export const astEdit = nativeBindings.astEdit ?? missingNativeExport("astEdit");
50
+ export const astGrep = nativeBindings.astGrep ?? missingNativeExport("astGrep");
51
+ export const astMatch = nativeBindings.astMatch ?? missingNativeExport("astMatch");
52
+ export const blockRangeAt = nativeBindings.blockRangeAt ?? missingNativeExport("blockRangeAt");
53
+ export const copyToClipboard = nativeBindings.copyToClipboard ?? missingNativeExport("copyToClipboard");
54
+ export const cosineSimilarityPairs = nativeBindings.cosineSimilarityPairs ?? missingNativeExport("cosineSimilarityPairs");
55
+ export const countTokens = nativeBindings.countTokens ?? missingNativeExport("countTokens");
56
+ export const decodeSixelToPng = nativeBindings.decodeSixelToPng ?? missingNativeExport("decodeSixelToPng");
57
+ export const detectMacOSAppearance = nativeBindings.detectMacOSAppearance ?? missingNativeExport("detectMacOSAppearance");
58
+ export const deviceCheckGenerateToken = nativeBindings.deviceCheckGenerateToken ?? missingNativeExport("deviceCheckGenerateToken");
59
+ export const diffLineRuns = nativeBindings.diffLineRuns ?? missingNativeExport("diffLineRuns");
60
+ export const diffLines = nativeBindings.diffLines ?? missingNativeExport("diffLines");
61
+ export const diffWords = nativeBindings.diffWords ?? missingNativeExport("diffWords");
62
+ export const editAutoGeneratedMessage = nativeBindings.editAutoGeneratedMessage ?? missingNativeExport("editAutoGeneratedMessage");
63
+ export const editDescription = nativeBindings.editDescription ?? missingNativeExport("editDescription");
64
+ export const editDiffString = nativeBindings.editDiffString ?? missingNativeExport("editDiffString");
65
+ export const editGrammar = nativeBindings.editGrammar ?? missingNativeExport("editGrammar");
66
+ export const editInspect = nativeBindings.editInspect ?? missingNativeExport("editInspect");
67
+ export const enclosingBlockBoundaries = nativeBindings.enclosingBlockBoundaries ?? missingNativeExport("enclosingBlockBoundaries");
68
+ export const encodeSixel = nativeBindings.encodeSixel ?? missingNativeExport("encodeSixel");
69
+ export const execReplace = nativeBindings.execReplace ?? missingNativeExport("execReplace");
70
+ export const executeShell = nativeBindings.executeShell ?? missingNativeExport("executeShell");
71
+ export const extractInlineSloppyRegions = nativeBindings.extractInlineSloppyRegions ?? missingNativeExport("extractInlineSloppyRegions");
72
+ export const extractSegments = nativeBindings.extractSegments ?? missingNativeExport("extractSegments");
73
+ export const fuzzyFind = nativeBindings.fuzzyFind ?? missingNativeExport("fuzzyFind");
74
+ export const getSupportedLanguages = nativeBindings.getSupportedLanguages ?? missingNativeExport("getSupportedLanguages");
75
+ export const getWorkProfile = nativeBindings.getWorkProfile ?? missingNativeExport("getWorkProfile");
76
+ export const glob = nativeBindings.glob ?? missingNativeExport("glob");
77
+ export const grep = nativeBindings.grep ?? missingNativeExport("grep");
78
+ export const hashlineCountOps = nativeBindings.hashlineCountOps ?? missingNativeExport("hashlineCountOps");
79
+ export const hashlineFileHash = nativeBindings.hashlineFileHash ?? missingNativeExport("hashlineFileHash");
80
+ export const hashlineFormatHeader = nativeBindings.hashlineFormatHeader ?? missingNativeExport("hashlineFormatHeader");
81
+ export const hashlineFormatNumberedLines = nativeBindings.hashlineFormatNumberedLines ?? missingNativeExport("hashlineFormatNumberedLines");
82
+ export const hashlineIsReadTruncationNotice = nativeBindings.hashlineIsReadTruncationNotice ?? missingNativeExport("hashlineIsReadTruncationNotice");
83
+ export const hashlineStripPrefixes = nativeBindings.hashlineStripPrefixes ?? missingNativeExport("hashlineStripPrefixes");
84
+ export const hasMatch = nativeBindings.hasMatch ?? missingNativeExport("hasMatch");
85
+ export const highlightCode = nativeBindings.highlightCode ?? missingNativeExport("highlightCode");
86
+ export const htmlToMarkdown = nativeBindings.htmlToMarkdown ?? missingNativeExport("htmlToMarkdown");
87
+ export const invalidateFsScanCache = nativeBindings.invalidateFsScanCache ?? missingNativeExport("invalidateFsScanCache");
88
+ export const isoBackend = nativeBindings.isoBackend ?? missingNativeExport("isoBackend");
89
+ export const isoDiff = nativeBindings.isoDiff ?? missingNativeExport("isoDiff");
90
+ export const isoIsUnavailableError = nativeBindings.isoIsUnavailableError ?? missingNativeExport("isoIsUnavailableError");
91
+ export const isoProbe = nativeBindings.isoProbe ?? missingNativeExport("isoProbe");
92
+ export const isoResolve = nativeBindings.isoResolve ?? missingNativeExport("isoResolve");
93
+ export const isoStart = nativeBindings.isoStart ?? missingNativeExport("isoStart");
94
+ export const isoStop = nativeBindings.isoStop ?? missingNativeExport("isoStop");
95
+ export const listWorkspace = nativeBindings.listWorkspace ?? missingNativeExport("listWorkspace");
96
+ export const macOSAutocorrectWord = nativeBindings.macOSAutocorrectWord ?? missingNativeExport("macOSAutocorrectWord");
97
+ export const macOSCheckSpelling = nativeBindings.macOSCheckSpelling ?? missingNativeExport("macOSCheckSpelling");
98
+ export const macOSCompleteWord = nativeBindings.macOSCompleteWord ?? missingNativeExport("macOSCompleteWord");
99
+ export const macOSSpellCheckerAvailable = nativeBindings.macOSSpellCheckerAvailable ?? missingNativeExport("macOSSpellCheckerAvailable");
100
+ export const macOSSpellingGuesses = nativeBindings.macOSSpellingGuesses ?? missingNativeExport("macOSSpellingGuesses");
101
+ export const matchesKey = nativeBindings.matchesKey ?? missingNativeExport("matchesKey");
102
+ export const matchesKittySequence = nativeBindings.matchesKittySequence ?? missingNativeExport("matchesKittySequence");
103
+ export const matchesLegacySequence = nativeBindings.matchesLegacySequence ?? missingNativeExport("matchesLegacySequence");
104
+ export const mmrRerankIndices = nativeBindings.mmrRerankIndices ?? missingNativeExport("mmrRerankIndices");
105
+ export const nodeChainAt = nativeBindings.nodeChainAt ?? missingNativeExport("nodeChainAt");
106
+ export const notebookToEditableText = nativeBindings.notebookToEditableText ?? missingNativeExport("notebookToEditableText");
107
+ export const parseKey = nativeBindings.parseKey ?? missingNativeExport("parseKey");
108
+ export const parseKittySequence = nativeBindings.parseKittySequence ?? missingNativeExport("parseKittySequence");
109
+ export const pdfToMarkdown = nativeBindings.pdfToMarkdown ?? missingNativeExport("pdfToMarkdown");
110
+ export const rasterizeSvg = nativeBindings.rasterizeSvg ?? missingNativeExport("rasterizeSvg");
111
+ export const readImageFromClipboard = nativeBindings.readImageFromClipboard ?? missingNativeExport("readImageFromClipboard");
112
+ export const renderMermaidAscii = nativeBindings.renderMermaidAscii ?? missingNativeExport("renderMermaidAscii");
113
+ export const renderSnapcompactPng = nativeBindings.renderSnapcompactPng ?? missingNativeExport("renderSnapcompactPng");
114
+ export const search = nativeBindings.search ?? missingNativeExport("search");
115
+ export const setHangulCompatJamoWidthOverride = nativeBindings.setHangulCompatJamoWidthOverride ?? missingNativeExport("setHangulCompatJamoWidthOverride");
116
+ export const sliceWithWidth = nativeBindings.sliceWithWidth ?? missingNativeExport("sliceWithWidth");
117
+ export const snapcompactSupportedChars = nativeBindings.snapcompactSupportedChars ?? missingNativeExport("snapcompactSupportedChars");
118
+ export const structuredPatchHunks = nativeBindings.structuredPatchHunks ?? missingNativeExport("structuredPatchHunks");
119
+ export const summarizeCode = nativeBindings.summarizeCode ?? missingNativeExport("summarizeCode");
120
+ export const supportsLanguage = nativeBindings.supportsLanguage ?? missingNativeExport("supportsLanguage");
121
+ export const truncateToWidth = nativeBindings.truncateToWidth ?? missingNativeExport("truncateToWidth");
122
+ export const vcsDetachGitDir = nativeBindings.vcsDetachGitDir ?? missingNativeExport("vcsDetachGitDir");
123
+ export const vcsDiscover = nativeBindings.vcsDiscover ?? missingNativeExport("vcsDiscover");
124
+ export const vcsDiscoverForDisplay = nativeBindings.vcsDiscoverForDisplay ?? missingNativeExport("vcsDiscoverForDisplay");
125
+ export const vcsGitClone = nativeBindings.vcsGitClone ?? missingNativeExport("vcsGitClone");
126
+ export const vcsGitDiscover = nativeBindings.vcsGitDiscover ?? missingNativeExport("vcsGitDiscover");
127
+ export const vcsGitRepoInfo = nativeBindings.vcsGitRepoInfo ?? missingNativeExport("vcsGitRepoInfo");
128
+ export const vcsIsPureJj = nativeBindings.vcsIsPureJj ?? missingNativeExport("vcsIsPureJj");
129
+ export const vcsJjDiscover = nativeBindings.vcsJjDiscover ?? missingNativeExport("vcsJjDiscover");
130
+ export const vcsJoinPatches = nativeBindings.vcsJoinPatches ?? missingNativeExport("vcsJoinPatches");
131
+ export const vcsValidateHunkSelections = nativeBindings.vcsValidateHunkSelections ?? missingNativeExport("vcsValidateHunkSelections");
132
+ export const vectorIndexTopK = nativeBindings.vectorIndexTopK ?? missingNativeExport("vectorIndexTopK");
133
+ export const visibleWidth = nativeBindings.visibleWidth ?? missingNativeExport("visibleWidth");
134
+ export const warmHighlighter = nativeBindings.warmHighlighter ?? missingNativeExport("warmHighlighter");
135
+ export const wrapTextWithAnsi = nativeBindings.wrapTextWithAnsi ?? missingNativeExport("wrapTextWithAnsi");
130
136
 
131
137
  // string/numeric enums (napi-rs string_enum produces TS-only const enum)
132
138
  export const AstMatchStrictness = {
@@ -127,4 +127,33 @@ export function validateLoadedBindings(
127
127
  candidate: string,
128
128
  ): void;
129
129
 
130
+ /** Identity of the addon `loadNative()` returned, for missing-export diagnostics. */
131
+ export interface NativeAddonStatus {
132
+ /** Absolute path of the loaded `.node`. */
133
+ path: string;
134
+ /** Sentinel the loaded addon carries, or `null` before sentinels existed. */
135
+ sentinel: string | null;
136
+ /** Sentinel this loader's package version expects. */
137
+ expectedSentinel: string;
138
+ /** `package.json#version` of the loader that loaded it. */
139
+ packageVersion: string;
140
+ /** True when the addon carries a different release than this package. */
141
+ stale: boolean;
142
+ }
143
+
144
+ /** The addon behind this process's exports; `null` before a successful load. */
145
+ export function nativeAddonStatus(): NativeAddonStatus | null;
146
+
147
+ /**
148
+ * Stub for an export the addon does not provide: `undefined` on a current
149
+ * addon, a throwing function on a stale one.
150
+ */
151
+ export function missingNativeExport(
152
+ symbolName: string,
153
+ addon?: NativeAddonStatus | null,
154
+ ): (() => never) | undefined;
155
+
156
+ /** Actionable text for {@link missingNativeExport}. */
157
+ export function missingNativeExportMessage(symbolName: string, addon?: NativeAddonStatus | null): string;
158
+
130
159
  export function loadNative(): Record<string, unknown>;
@@ -657,6 +657,18 @@ function maybeStageNodeModulesAddon(ctx, errors) {
657
657
  }
658
658
 
659
659
 
660
+ /** Any release sentinel a `.node` may carry (`__piNativesV{major}_{minor}_{patch}`). */
661
+ const VERSION_SENTINEL_ANY_RE = /^__piNativesV[A-Za-z0-9_]+$/;
662
+
663
+ /**
664
+ * Release version encoded in a sentinel export name.
665
+ * @param {string} sentinel
666
+ * @returns {string}
667
+ */
668
+ function sentinelVersion(sentinel) {
669
+ return sentinel.slice("__piNativesV".length).replace(/_/g, ".");
670
+ }
671
+
660
672
  /**
661
673
  * Before version sentinels were exported, published native addons still shared
662
674
  * this stable core ABI. Let those on-disk addons bridge a package-version bump
@@ -682,7 +694,10 @@ export function validateLoadedBindings(ctx, bindings, candidate) {
682
694
  // `node_modules` install or a compiled bundle) the local `.node` only gains
683
695
  // the renamed sentinel after `bun --cwd=packages/natives run build`. Skip
684
696
  // validation there so a stale post-pull dev tree boots while the rebuild
685
- // completes; install and compiled-binary paths still validate.
697
+ // completes; install and compiled-binary paths still validate. The mismatch
698
+ // is not swallowed silently: `native/index.js` exports `missingNativeExport`
699
+ // for every symbol the stale addon predates, so the first call through one
700
+ // reports the addon, both releases, and the rebuild command.
686
701
  if (ctx.isWorkspaceLoad) return;
687
702
  if (typeof bindings[ctx.versionSentinelExport] === "function") return;
688
703
 
@@ -696,7 +711,7 @@ export function validateLoadedBindings(ctx, bindings, candidate) {
696
711
  // exports, which carry the PRIOR sentinel — disk is already consistent,
697
712
  // so reinstall is a no-op and only restarting the process re-syncs.
698
713
  const residentSentinel = Object.keys(bindings).find(
699
- key => key !== ctx.versionSentinelExport && /^__piNativesV[A-Za-z0-9_]+$/.test(key),
714
+ key => key !== ctx.versionSentinelExport && VERSION_SENTINEL_ANY_RE.test(key),
700
715
  );
701
716
  // A prior sentinel alone cannot distinguish a resident old module from an
702
717
  // actually stale file: `require` returns the same exports in both cases.
@@ -711,7 +726,7 @@ export function validateLoadedBindings(ctx, bindings, candidate) {
711
726
  }
712
727
  if (isCompatiblePreSentinelNativeAddon(bindings, diskHasExpectedSentinel)) return;
713
728
  if (residentSentinel && diskHasExpectedSentinel) {
714
- const residentVersion = residentSentinel.slice("__piNativesV".length).replace(/_/g, ".");
729
+ const residentVersion = sentinelVersion(residentSentinel);
715
730
  throw new Error(
716
731
  `Loaded ${candidate}, which exposes the @oh-my-pi/pi-natives@${residentVersion} version ` +
717
732
  `sentinel \`${residentSentinel}\` but not the @${ctx.packageVersion} sentinel ` +
@@ -728,6 +743,87 @@ export function validateLoadedBindings(ctx, bindings, candidate) {
728
743
  );
729
744
  }
730
745
 
746
+ /**
747
+ * Identity of the addon `loadNative()` returned, in the shape the
748
+ * missing-export diagnostic reports. Null until a load succeeds.
749
+ * @type {{ path: string; sentinel: string | null; expectedSentinel: string; packageVersion: string; stale: boolean } | null}
750
+ */
751
+ let loadedAddon = null;
752
+
753
+ /**
754
+ * Describe a loaded addon so a symbol it predates can name the file, the
755
+ * release the file came from, and the release this tree expects.
756
+ * @param {Record<string, unknown>} bindings
757
+ * @param {string} candidate
758
+ * @param {{ packageVersion: string; versionSentinelExport: string }} ctx
759
+ */
760
+ function describeLoadedAddon(bindings, candidate, ctx) {
761
+ const sentinel = Object.keys(bindings).find(key => VERSION_SENTINEL_ANY_RE.test(key)) ?? null;
762
+ return {
763
+ path: candidate,
764
+ sentinel,
765
+ expectedSentinel: ctx.versionSentinelExport,
766
+ packageVersion: ctx.packageVersion,
767
+ stale: sentinel !== ctx.versionSentinelExport,
768
+ };
769
+ }
770
+
771
+ /**
772
+ * The addon behind this process's `@oh-my-pi/pi-natives` exports.
773
+ * @returns {{ path: string; sentinel: string | null; expectedSentinel: string; packageVersion: string; stale: boolean } | null}
774
+ */
775
+ export function nativeAddonStatus() {
776
+ return loadedAddon;
777
+ }
778
+
779
+ /**
780
+ * Stand-in for an export the loaded addon does not provide.
781
+ *
782
+ * A workspace tree tolerates a sentinel mismatch on purpose: a checkout that
783
+ * pulled a new release keeps running until `bun run build:native` finishes
784
+ * (see `validateLoadedBindings`), and PR CI loads release addons under a newer
785
+ * checkout the same way. Such an addon has no value for any symbol added after
786
+ * its build, so a bare `undefined` export surfaced as `<symbol> is not a
787
+ * function` — every `write` call in a tree that pulled the read-projection
788
+ * guard, for one — with nothing naming the stale addon.
789
+ *
790
+ * Only a stale addon gets the stub. On a current addon an absent export is not
791
+ * version drift but a symbol this build does not implement, and callers probe
792
+ * for exactly that (`typeof native.x === "function"`); they must keep seeing
793
+ * `undefined`.
794
+ * @param {string} symbolName
795
+ * @param {ReturnType<typeof nativeAddonStatus>} [addon]
796
+ * @returns {((...args: unknown[]) => never) | undefined}
797
+ */
798
+ export function missingNativeExport(symbolName, addon = loadedAddon) {
799
+ if (!addon?.stale) return undefined;
800
+ return () => {
801
+ throw new Error(missingNativeExportMessage(symbolName, addon));
802
+ };
803
+ }
804
+
805
+ /**
806
+ * Actionable text for {@link missingNativeExport}. `addon` is injectable so the
807
+ * wording can be pinned without a stale `.node` on disk.
808
+ * @param {string} symbolName
809
+ * @param {ReturnType<typeof nativeAddonStatus>} [addon]
810
+ * @returns {string}
811
+ */
812
+ export function missingNativeExportMessage(symbolName, addon = loadedAddon) {
813
+ const rebuild = "rebuild it with `bun run build:native`";
814
+ if (!addon) return `@oh-my-pi/pi-natives does not export \`${symbolName}\`; ${rebuild}.`;
815
+ if (!addon.stale) {
816
+ return `@oh-my-pi/pi-natives export \`${symbolName}\` is missing from ${addon.path}; ${rebuild}.`;
817
+ }
818
+ const loaded = addon.sentinel
819
+ ? `the @oh-my-pi/pi-natives@${sentinelVersion(addon.sentinel)} addon`
820
+ : "an addon built before version sentinels existed";
821
+ return (
822
+ `@oh-my-pi/pi-natives export \`${symbolName}\` is missing: ${addon.path} is ${loaded}, not ` +
823
+ `@${addon.packageVersion} (\`${addon.expectedSentinel}\`) — ${rebuild}.`
824
+ );
825
+ }
826
+
731
827
  /**
732
828
  * Install the addon's bounded Tokio runtime now that `dlopen` has returned and
733
829
  * the dynamic-loader lock is released. The Rust `#[module_init]` deliberately
@@ -875,6 +971,7 @@ export function loadNative() {
875
971
  const bindings = require_(candidate);
876
972
  validateLoadedBindings(ctx, bindings, candidate);
877
973
  installNativeTokioRuntime(bindings);
974
+ loadedAddon = describeLoadedAddon(bindings, candidate, ctx);
878
975
  cleanupStaleNativeVersions({ nativesDir: ctx.nativesDir, currentVersion: ctx.packageVersion });
879
976
  startupMarker("native:loadNative:done");
880
977
  return bindings;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-natives",
3
- "version": "18.2.8",
3
+ "version": "18.2.9",
4
4
  "description": "Native Rust bindings for PDF conversion, audio, WebRTC, grep, clipboard, image processing, syntax highlighting, PTY, and shell operations via N-API",
5
5
  "type": "module",
6
6
  "homepage": "https://omp.sh",
@@ -104,11 +104,11 @@
104
104
  }
105
105
  },
106
106
  "optionalDependencies": {
107
- "@oh-my-pi/pi-natives-linux-x64": "18.2.8",
108
- "@oh-my-pi/pi-natives-linux-arm64": "18.2.8",
109
- "@oh-my-pi/pi-natives-darwin-x64": "18.2.8",
110
- "@oh-my-pi/pi-natives-darwin-arm64": "18.2.8",
111
- "@oh-my-pi/pi-natives-win32-x64": "18.2.8",
112
- "@oh-my-pi/pi-natives-win32-arm64": "18.2.8"
107
+ "@oh-my-pi/pi-natives-linux-x64": "18.2.9",
108
+ "@oh-my-pi/pi-natives-linux-arm64": "18.2.9",
109
+ "@oh-my-pi/pi-natives-darwin-x64": "18.2.9",
110
+ "@oh-my-pi/pi-natives-darwin-arm64": "18.2.9",
111
+ "@oh-my-pi/pi-natives-win32-x64": "18.2.9",
112
+ "@oh-my-pi/pi-natives-win32-arm64": "18.2.9"
113
113
  }
114
114
  }