@oh-my-pi/pi-natives 18.17.0 → 18.18.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.js CHANGED
@@ -1,4 +1,4 @@
1
- import { loadNative, missingNativeExport } from "./loader-state.js";
1
+ import { loadNative, missingNativeClass, missingNativeExport } from "./loader-state.js";
2
2
  import { adaptDesktopSession } from "./desktop-adapter.js";
3
3
 
4
4
  /**
@@ -23,33 +23,33 @@ import { adaptDesktopSession } from "./desktop-adapter.js";
23
23
  const nativeBindings = loadNative();
24
24
  // --- generated native exports (do not edit) ---
25
25
  // classes
26
- export const AudioCapture = nativeBindings.AudioCapture;
27
- export const AudioPlayback = nativeBindings.AudioPlayback;
26
+ export const AudioCapture = nativeBindings.AudioCapture ?? missingNativeClass("AudioCapture");
27
+ export const AudioPlayback = nativeBindings.AudioPlayback ?? missingNativeClass("AudioPlayback");
28
28
  export const DesktopSession = adaptDesktopSession(nativeBindings.DesktopSession);
29
- export const DiffStream = nativeBindings.DiffStream;
30
- export const EditSession = nativeBindings.EditSession;
31
- export const EditStore = nativeBindings.EditStore;
32
- export const FileLock = nativeBindings.FileLock;
33
- export const HighlightStream = nativeBindings.HighlightStream;
34
- export const IpcConnection = nativeBindings.IpcConnection;
35
- export const IpcServer = nativeBindings.IpcServer;
36
- export const LineMatcher = nativeBindings.LineMatcher;
37
- export const LiveWebRtcPeer = nativeBindings.LiveWebRtcPeer;
38
- export const MacAppearanceObserver = nativeBindings.MacAppearanceObserver;
39
- export const MacOSFrontApp = nativeBindings.MacOSFrontApp;
40
- export const NativeOAuthCallback = nativeBindings.NativeOAuthCallback;
41
- export const PipeStreams = nativeBindings.PipeStreams;
42
- export const PowerAssertion = nativeBindings.PowerAssertion;
43
- export const Process = nativeBindings.Process;
44
- export const PtySession = nativeBindings.PtySession;
45
- export const RunTrace = nativeBindings.RunTrace;
46
- export const Shell = nativeBindings.Shell;
47
- export const ShellVisualization = nativeBindings.ShellVisualization;
48
- export const TextPredictor = nativeBindings.TextPredictor;
49
- export const TtyWriter = nativeBindings.TtyWriter;
50
- export const VcsGitRepo = nativeBindings.VcsGitRepo;
51
- export const VcsJjWorkspace = nativeBindings.VcsJjWorkspace;
52
- export const VcsRepo = nativeBindings.VcsRepo;
29
+ export const DiffStream = nativeBindings.DiffStream ?? missingNativeClass("DiffStream");
30
+ export const EditSession = nativeBindings.EditSession ?? missingNativeClass("EditSession");
31
+ export const EditStore = nativeBindings.EditStore ?? missingNativeClass("EditStore");
32
+ export const FileLock = nativeBindings.FileLock ?? missingNativeClass("FileLock");
33
+ export const HighlightStream = nativeBindings.HighlightStream ?? missingNativeClass("HighlightStream");
34
+ export const IpcConnection = nativeBindings.IpcConnection ?? missingNativeClass("IpcConnection");
35
+ export const IpcServer = nativeBindings.IpcServer ?? missingNativeClass("IpcServer");
36
+ export const LineMatcher = nativeBindings.LineMatcher ?? missingNativeClass("LineMatcher");
37
+ export const LiveWebRtcPeer = nativeBindings.LiveWebRtcPeer ?? missingNativeClass("LiveWebRtcPeer");
38
+ export const MacAppearanceObserver = nativeBindings.MacAppearanceObserver ?? missingNativeClass("MacAppearanceObserver");
39
+ export const MacOSFrontApp = nativeBindings.MacOSFrontApp ?? missingNativeClass("MacOSFrontApp");
40
+ export const NativeOAuthCallback = nativeBindings.NativeOAuthCallback ?? missingNativeClass("NativeOAuthCallback");
41
+ export const PipeStreams = nativeBindings.PipeStreams ?? missingNativeClass("PipeStreams");
42
+ export const PowerAssertion = nativeBindings.PowerAssertion ?? missingNativeClass("PowerAssertion");
43
+ export const Process = nativeBindings.Process ?? missingNativeClass("Process");
44
+ export const PtySession = nativeBindings.PtySession ?? missingNativeClass("PtySession");
45
+ export const RunTrace = nativeBindings.RunTrace ?? missingNativeClass("RunTrace");
46
+ export const Shell = nativeBindings.Shell ?? missingNativeClass("Shell");
47
+ export const ShellVisualization = nativeBindings.ShellVisualization ?? missingNativeClass("ShellVisualization");
48
+ export const TextPredictor = nativeBindings.TextPredictor ?? missingNativeClass("TextPredictor");
49
+ export const TtyWriter = nativeBindings.TtyWriter ?? missingNativeClass("TtyWriter");
50
+ export const VcsGitRepo = nativeBindings.VcsGitRepo ?? missingNativeClass("VcsGitRepo");
51
+ export const VcsJjWorkspace = nativeBindings.VcsJjWorkspace ?? missingNativeClass("VcsJjWorkspace");
52
+ export const VcsRepo = nativeBindings.VcsRepo ?? missingNativeClass("VcsRepo");
53
53
 
54
54
  // functions
55
55
  export const __ompInstallTokioRuntime = nativeBindings.__ompInstallTokioRuntime ?? missingNativeExport("__ompInstallTokioRuntime");
@@ -145,6 +145,29 @@ export function missingNativeExport(
145
145
  addon?: NativeAddonStatus | null,
146
146
  ): (() => never) | undefined;
147
147
 
148
+ /**
149
+ * Stub for a class the addon does not provide: `undefined` on a current addon,
150
+ * a class whose constructor throws on a stale one.
151
+ */
152
+ export function missingNativeClass(
153
+ symbolName: string,
154
+ addon?: NativeAddonStatus | null,
155
+ ): (new (...args: unknown[]) => never) | undefined;
156
+
157
+ /** Notice a workspace tree prints when its addon is from another release. */
158
+ export function staleWorkspaceAddonNotice(addon: NativeAddonStatus): string;
159
+
160
+ /**
161
+ * Write {@link staleWorkspaceAddonNotice} once, from the main thread, and only
162
+ * on a workspace load. The latch is per loaded copy of this module. Returns
163
+ * whether this call wrote it.
164
+ */
165
+ export function reportStaleWorkspaceAddon(
166
+ ctx: { isWorkspaceLoad: boolean },
167
+ addon: NativeAddonStatus,
168
+ deps?: { write?: (text: string) => void; state?: { reported: boolean } },
169
+ ): boolean;
170
+
148
171
  /** Actionable text for {@link missingNativeExport}. */
149
172
  export function missingNativeExportMessage(symbolName: string, addon?: NativeAddonStatus | null): string;
150
173
 
@@ -693,6 +693,48 @@ export function missingNativeExport(symbolName, addon = loadedAddon) {
693
693
  };
694
694
  }
695
695
 
696
+ /**
697
+ * Static reads a {@link missingNativeClass} stand-in answers normally rather
698
+ * than throwing: the function-object intrinsics every caller and inspector
699
+ * touches, and `then`, so an accidental `await` on the export resolves to the
700
+ * class instead of exploding somewhere unrelated.
701
+ */
702
+ const CLASS_STUB_PLAIN_STATICS = new Set(["prototype", "name", "length", "constructor", "then"]);
703
+
704
+ /**
705
+ * Class counterpart of {@link missingNativeExport}: a stand-in that fails with
706
+ * the addon's identity attached instead of as a bare `undefined` export, which
707
+ * surfaces as `undefined is not a constructor` at the callsite and names
708
+ * nothing.
709
+ *
710
+ * Same staleness rule as the function stub: only a stale addon gets a
711
+ * stand-in, so `typeof native.Thing === "function"` capability probes against
712
+ * a current addon keep their meaning.
713
+ *
714
+ * Several of these classes are entered through a static rather than a
715
+ * constructor — `FileLock.tryAcquire`, `IpcServer.listen`, `Process.fromPid`,
716
+ * `PowerAssertion.start` — so the stand-in answers any other static read with
717
+ * the same diagnostic. Only a stale addon reaches it, and only for a class
718
+ * that addon does not carry, so nothing on a healthy tree meets a proxy.
719
+ * @param {string} symbolName
720
+ * @param {ReturnType<typeof nativeAddonStatus>} [addon]
721
+ * @returns {(new (...args: unknown[]) => never) | undefined}
722
+ */
723
+ export function missingNativeClass(symbolName, addon = loadedAddon) {
724
+ if (!addon?.stale) return undefined;
725
+ const stub = class MissingNativeClass {
726
+ constructor() {
727
+ throw new Error(missingNativeExportMessage(symbolName, addon));
728
+ }
729
+ };
730
+ return new Proxy(stub, {
731
+ get(target, key, receiver) {
732
+ if (typeof key === "symbol" || CLASS_STUB_PLAIN_STATICS.has(key)) return Reflect.get(target, key, receiver);
733
+ throw new Error(missingNativeExportMessage(`${symbolName}.${key}`, addon));
734
+ },
735
+ });
736
+ }
737
+
696
738
  /**
697
739
  * Actionable text for {@link missingNativeExport}. `addon` is injectable so the
698
740
  * wording can be pinned without a stale `.node` on disk.
@@ -715,6 +757,106 @@ export function missingNativeExportMessage(symbolName, addon = loadedAddon) {
715
757
  );
716
758
  }
717
759
 
760
+ /**
761
+ * One-line notice for a workspace tree running an addon from another release.
762
+ *
763
+ * The missing-symbol stubs only speak when a caller touches a symbol the addon
764
+ * does not carry. A symbol it does carry runs that release's behaviour — an
765
+ * older shell, an older grep — with no marker at all, so this notice is the
766
+ * only provenance those failures get.
767
+ *
768
+ * `stale` is any release difference, so the wording must not assume the addon
769
+ * is the older side.
770
+ * @param {NonNullable<ReturnType<typeof nativeAddonStatus>>} addon
771
+ * @returns {string}
772
+ */
773
+ export function staleWorkspaceAddonNotice(addon) {
774
+ const loaded = addon.version ? `@oh-my-pi/pi-natives@${addon.version}` : "an addon without a release stamp";
775
+ return (
776
+ `[pi-natives] ${addon.path} is ${loaded}, but this checkout is @${addon.packageVersion}. ` +
777
+ "Symbols this checkout expects may be missing and shared ones may behave differently; " +
778
+ "run `bun run build:native` before trusting results from this tree.\n"
779
+ );
780
+ }
781
+
782
+ /**
783
+ * Whether the copy of this module running on the current thread has already
784
+ * written the mismatched-addon notice. One per loaded copy, which is one per
785
+ * main thread in every shape this package ships; the write itself is confined
786
+ * to the main thread by {@link onMainThread}.
787
+ */
788
+ const staleNoticeState = { reported: false };
789
+
790
+ /** Memoized answer from {@link onMainThread}; `undefined` until first asked. */
791
+ let mainThreadAnswer;
792
+
793
+ /**
794
+ * True on a process's main thread, false inside a worker thread.
795
+ *
796
+ * Resolved through `createRequire` on first use rather than imported at module
797
+ * scope: `packages/coding-agent/src/cli.ts` keeps `node:worker_threads` out of
798
+ * ordinary startup on purpose, and this module is reached statically from
799
+ * `sqlite-locks` on launches that never load an addon. Asking only when a
800
+ * mismatch has already been found keeps that boundary. A runtime without the
801
+ * builtin answers "main thread", which is the safe side: the notice prints.
802
+ * @returns {boolean}
803
+ */
804
+ function onMainThread() {
805
+ if (mainThreadAnswer === undefined) {
806
+ try {
807
+ mainThreadAnswer = createRequire(import.meta.url)("node:worker_threads").isMainThread !== false;
808
+ } catch {
809
+ mainThreadAnswer = true;
810
+ }
811
+ }
812
+ return mainThreadAnswer;
813
+ }
814
+
815
+ /**
816
+ * Write {@link staleWorkspaceAddonNotice} once, from the main thread, when a
817
+ * workspace tree loads an addon of another release.
818
+ *
819
+ * Three guards, each load-bearing.
820
+ *
821
+ * Main thread only, because fd 2 is shared and a worker reaches `loadNative()`
822
+ * long after interactive mode owns the terminal — the subagent, JS-eval and
823
+ * tab workers all start then, so a per-worker write paints into a rendered
824
+ * frame. The main thread has already said it at startup; a worker repeating it
825
+ * adds nothing and costs the display. A worker *process* keeps the notice: it
826
+ * has its own main thread, and `subprocess/worker-client.ts` captures its
827
+ * stderr to a file rather than the terminal.
828
+ *
829
+ * Latched, because `loadNative()` is not a startup-only call: the lazy
830
+ * entrypoints in `clipboard.js` and friends call it on every invocation.
831
+ *
832
+ * Workspace loads only, because {@link validateLoadedBindings} has a second
833
+ * success path — it accepts a pre-sentinel addon on an installed load
834
+ * (`isCompatiblePreSentinelNativeAddon`), which reports no release and so
835
+ * counts as `stale`. That user has no repository to build.
836
+ *
837
+ * The write is a synchronous `fs.writeSync` to fd 2 for the same reason as
838
+ * {@link startupMarker}: it must survive a crash that follows it. `write` and
839
+ * `state` are injectable so a test can exercise the guards without consuming
840
+ * this copy's latch.
841
+ * @param {{ isWorkspaceLoad: boolean }} ctx
842
+ * @param {NonNullable<ReturnType<typeof nativeAddonStatus>>} addon
843
+ * @param {{ write?: (text: string) => void; state?: { reported: boolean } }} [deps]
844
+ * @returns {boolean} whether this call wrote the notice
845
+ */
846
+ export function reportStaleWorkspaceAddon(ctx, addon, deps = {}) {
847
+ const state = deps.state ?? staleNoticeState;
848
+ // Thread identity last: the three cheap gates reject every ordinary launch
849
+ // before `node:worker_threads` is resolved at all.
850
+ if (!ctx.isWorkspaceLoad || !addon.stale || state.reported || !onMainThread()) return false;
851
+ state.reported = true;
852
+ try {
853
+ (deps.write ?? (text => fs.writeSync(2, text)))(staleWorkspaceAddonNotice(addon));
854
+ } catch {
855
+ // stderr unavailable; the notice is best-effort, the stubs still speak
856
+ }
857
+ return true;
858
+ }
859
+
718
860
  /**
719
861
  * Install the addon's bounded Tokio runtime now that `dlopen` has returned and
720
862
  * the dynamic-loader lock is released. The Rust `#[module_init]` deliberately
@@ -881,6 +1023,7 @@ export function loadNative() {
881
1023
  installNativeTokioRuntime(bindings);
882
1024
  configureGrammarDir(bindings, ctx.nativesDir);
883
1025
  loadedAddon = describeLoadedAddon(bindings, candidate, ctx);
1026
+ reportStaleWorkspaceAddon(ctx, loadedAddon);
884
1027
  cleanupStaleNativeVersions({ nativesDir: ctx.nativesDir, currentVersion: ctx.packageVersion });
885
1028
  startupMarker("native:loadNative:done");
886
1029
  return bindings;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-natives",
3
- "version": "18.17.0",
3
+ "version": "18.18.0",
4
4
  "description": "Native Rust bindings for PDF conversion, audio, WebRTC, grep, clipboard, image processing, syntax highlighting, PTY, and shell operations via N-API",
5
5
  "type": "module",
6
6
  "homepage": "https://omp.sh",
@@ -120,11 +120,11 @@
120
120
  }
121
121
  },
122
122
  "optionalDependencies": {
123
- "@oh-my-pi/pi-natives-linux-x64": "18.17.0",
124
- "@oh-my-pi/pi-natives-linux-arm64": "18.17.0",
125
- "@oh-my-pi/pi-natives-darwin-x64": "18.17.0",
126
- "@oh-my-pi/pi-natives-darwin-arm64": "18.17.0",
127
- "@oh-my-pi/pi-natives-win32-x64": "18.17.0",
128
- "@oh-my-pi/pi-natives-win32-arm64": "18.17.0"
123
+ "@oh-my-pi/pi-natives-linux-x64": "18.18.0",
124
+ "@oh-my-pi/pi-natives-linux-arm64": "18.18.0",
125
+ "@oh-my-pi/pi-natives-darwin-x64": "18.18.0",
126
+ "@oh-my-pi/pi-natives-darwin-arm64": "18.18.0",
127
+ "@oh-my-pi/pi-natives-win32-x64": "18.18.0",
128
+ "@oh-my-pi/pi-natives-win32-arm64": "18.18.0"
129
129
  }
130
130
  }