@oh-my-pi/pi-natives 18.0.0 → 18.0.3

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.
@@ -0,0 +1,28 @@
1
+ interface AdaptedDesktopCapabilities {
2
+ readonly [key: string]: unknown;
3
+ readonly ax: boolean;
4
+ readonly backgroundWindowInput: boolean;
5
+ readonly deliveryModes: readonly string[];
6
+ readonly axPermission: string;
7
+ }
8
+
9
+ interface AdaptedDesktopSession {
10
+ readonly capabilities: AdaptedDesktopCapabilities;
11
+ listWindows(): Promise<Array<Record<string, unknown>>>;
12
+ capture(target: string, caps?: unknown): Promise<Record<string, unknown>>;
13
+ click(
14
+ target: string,
15
+ x: number,
16
+ y: number,
17
+ options?: { button?: string; count?: number; modifiers?: string[]; deliveryMode?: string },
18
+ ): Promise<void>;
19
+ typeText(target: string, text: string, options?: { deliveryMode?: string }): Promise<void>;
20
+ keyChord(target: string, keys: string[], options?: { deliveryMode?: string }): Promise<void>;
21
+ close(): Promise<void>;
22
+ }
23
+
24
+ interface AdaptedDesktopSessionConstructor {
25
+ new (options: Record<string, unknown>): AdaptedDesktopSession;
26
+ }
27
+
28
+ export function adaptDesktopSession(NativeDesktopSession: unknown): AdaptedDesktopSessionConstructor;
@@ -0,0 +1,341 @@
1
+ const LEGACY_ERROR_CODES = {
2
+ DESKTOP_INVALID_OPTIONS: "InvalidTarget",
3
+ DESKTOP_INVALID_ACTION: "InvalidTarget",
4
+ DESKTOP_BACKEND_UNAVAILABLE: null,
5
+ DESKTOP_PERMISSION_DENIED: "PermissionDenied",
6
+ DESKTOP_CAPTURE_FAILED: "CaptureFailed",
7
+ DESKTOP_INPUT_FAILED: "InputFailed",
8
+ DESKTOP_DEADLINE_EXCEEDED: "Timeout",
9
+ DESKTOP_LAYOUT_CHANGED: "InvalidCoordinateFrame",
10
+ DESKTOP_COORDINATE_OUT_OF_BOUNDS: "InvalidCoordinateFrame",
11
+ DESKTOP_SESSION_CLOSED: "Closed",
12
+ DESKTOP_WORKER_FAILED: "Internal",
13
+ };
14
+
15
+ const ADAPTED_SESSION_CLASSES = new WeakMap();
16
+
17
+ function desktopError(code, message) {
18
+ return new Error(`${code}: ${message}`);
19
+ }
20
+
21
+ function normalizeError(error, fallbackCode) {
22
+ if (!(error instanceof Error)) return desktopError(fallbackCode, String(error));
23
+ if (/^[A-Z][A-Za-z]+: /.test(error.message)) return error;
24
+
25
+ const match = /^(DESKTOP_[A-Z_]+):\s*(.*)$/.exec(error.message);
26
+ if (match === null) return desktopError(fallbackCode, error.message);
27
+ const code = LEGACY_ERROR_CODES[match[1]] ?? fallbackCode;
28
+ return desktopError(code, match[2]);
29
+ }
30
+
31
+ function normalizeCapabilities(capabilities) {
32
+ return {
33
+ ...capabilities,
34
+ ax: false,
35
+ backgroundWindowInput: false,
36
+ deliveryModes: ["foreground"],
37
+ axPermission: "unavailable",
38
+ };
39
+ }
40
+
41
+ function legacyPoint(point) {
42
+ return { x: Math.round(point.x), y: Math.round(point.y) };
43
+ }
44
+
45
+ function captureCapsKey(caps) {
46
+ return `${caps?.maxWidth ?? ""}:${caps?.maxHeight ?? ""}`;
47
+ }
48
+
49
+ function legacyButton(button) {
50
+ return button === "middle" ? "wheel" : button;
51
+ }
52
+ function sourceDimensions(capture) {
53
+ if (capture.sourceWidth !== undefined && capture.sourceHeight !== undefined) {
54
+ return { sourceWidth: capture.sourceWidth, sourceHeight: capture.sourceHeight };
55
+ }
56
+ const displays = Array.isArray(capture.displays) ? capture.displays : [];
57
+ if (displays.length === 0) {
58
+ return { sourceWidth: capture.width, sourceHeight: capture.height };
59
+ }
60
+ const minX = Math.min(...displays.map(display => display.x));
61
+ const minY = Math.min(...displays.map(display => display.y));
62
+ const maxX = Math.max(...displays.map(display => display.x + display.width));
63
+ const maxY = Math.max(...displays.map(display => display.y + display.height));
64
+ const nativeScale = Math.max(1, ...displays.map(display => display.scale ?? 1));
65
+ return {
66
+ sourceWidth: Math.max(1, Math.round((maxX - minX) * nativeScale)),
67
+ sourceHeight: Math.max(1, Math.round((maxY - minY) * nativeScale)),
68
+ };
69
+ }
70
+
71
+ function frameSignature(capture) {
72
+ return JSON.stringify({
73
+ target: capture.target,
74
+ width: capture.width,
75
+ height: capture.height,
76
+ displays: capture.displays?.map(display => ({
77
+ id: display.id,
78
+ x: display.x,
79
+ y: display.y,
80
+ width: display.width,
81
+ height: display.height,
82
+ scale: display.scale,
83
+ pixelX: display.pixelX,
84
+ pixelY: display.pixelY,
85
+ pixelWidth: display.pixelWidth,
86
+ pixelHeight: display.pixelHeight,
87
+ })),
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Adapt the pre-parity desktop addon ABI used by pull-request CI artifacts to
93
+ * the current session contract. Released addons exposed capture/execute/close;
94
+ * current addons already expose the complete API and pass through unchanged.
95
+ */
96
+ export function adaptDesktopSession(NativeDesktopSession) {
97
+ if (typeof NativeDesktopSession?.prototype?.click === "function") return NativeDesktopSession;
98
+ const cached = ADAPTED_SESSION_CLASSES.get(NativeDesktopSession);
99
+ if (cached) return cached;
100
+
101
+ class DesktopSession {
102
+ #native;
103
+ #nativeDesktopSession;
104
+ #options;
105
+ #sessions;
106
+ #closed = false;
107
+ #capturedTargets = new Map();
108
+
109
+ constructor(options) {
110
+ try {
111
+ this.#nativeDesktopSession = NativeDesktopSession;
112
+ this.#options = options;
113
+ this.#native = new NativeDesktopSession(options);
114
+ this.#sessions = new Map([[captureCapsKey(), this.#native]]);
115
+ } catch (error) {
116
+ throw normalizeError(error, "Internal");
117
+ }
118
+ }
119
+
120
+ get capabilities() {
121
+ return normalizeCapabilities(this.#native.capabilities);
122
+ }
123
+
124
+ #ensureOpen() {
125
+ if (this.#closed) throw desktopError("Closed", "desktop session is closed");
126
+ }
127
+
128
+ #nativeForCapturedTarget(target) {
129
+ const capture = this.#capturedTargets.get(target);
130
+ if (capture) return capture.native;
131
+ throw desktopError(
132
+ "InvalidCoordinateFrame",
133
+ `no capture of '${target}' yet — take a screenshot of this target first`,
134
+ );
135
+ }
136
+
137
+ #sessionForCapture(caps) {
138
+ const key = captureCapsKey(caps);
139
+ const existing = this.#sessions.get(key);
140
+ if (existing) return existing;
141
+ try {
142
+ const native = new this.#nativeDesktopSession({
143
+ ...this.#options,
144
+ maxWidth: caps?.maxWidth,
145
+ maxHeight: caps?.maxHeight,
146
+ });
147
+ this.#sessions.set(key, native);
148
+ return native;
149
+ } catch (error) {
150
+ throw normalizeError(error, "CaptureFailed");
151
+ }
152
+ }
153
+
154
+ #ensureForeground(target, options) {
155
+ if (options?.deliveryMode !== "foreground" && (target !== "desktop" || options?.deliveryMode !== undefined)) {
156
+ throw desktopError(
157
+ "BackgroundUnavailable",
158
+ "the installed native addon supports foreground input only",
159
+ );
160
+ }
161
+ }
162
+
163
+ async #execute(actions, target, native = this.#native, fallbackCode = "InputFailed") {
164
+ try {
165
+ const capture = await native.execute(Array.isArray(actions) ? actions : [actions], target);
166
+ const previous = this.#capturedTargets.get(target);
167
+ if (capture && previous?.native === native && frameSignature(capture) !== previous.signature) {
168
+ this.#capturedTargets.delete(target);
169
+ }
170
+ } catch (error) {
171
+ throw normalizeError(error, fallbackCode);
172
+ }
173
+ }
174
+
175
+ async listDisplays() {
176
+ this.#ensureOpen();
177
+ throw desktopError("CaptureFailed", "the installed native addon does not expose display enumeration");
178
+ }
179
+
180
+ async listWindows() {
181
+ this.#ensureOpen();
182
+ if (typeof this.#native.listWindows !== "function") {
183
+ throw desktopError("CaptureFailed", "the installed native addon does not expose window enumeration");
184
+ }
185
+ try {
186
+ return await this.#native.listWindows();
187
+ } catch (error) {
188
+ throw normalizeError(error, "CaptureFailed");
189
+ }
190
+ }
191
+
192
+ async capture(target, caps) {
193
+ this.#ensureOpen();
194
+ if (target !== "desktop" && !/^\d+$/.test(target)) {
195
+ throw desktopError("InvalidTarget", `invalid window target '${target}'`);
196
+ }
197
+ if (target !== "desktop" && typeof this.#native.listWindows !== "function") {
198
+ throw desktopError("CaptureFailed", "the installed native addon does not support window capture");
199
+ }
200
+ try {
201
+ const native = this.#sessionForCapture(caps);
202
+ const capture = await native.capture(target);
203
+ const adapted = { ...capture, ...sourceDimensions(capture), target };
204
+ this.#capturedTargets.set(target, { native, signature: frameSignature(adapted) });
205
+ return adapted;
206
+ } catch (error) {
207
+ throw normalizeError(error, "CaptureFailed");
208
+ }
209
+ }
210
+
211
+ async click(target, x, y, options) {
212
+ this.#ensureOpen();
213
+ this.#ensureForeground(target, options);
214
+ const native = this.#nativeForCapturedTarget(target);
215
+ const count = Math.max(1, options?.count ?? 1);
216
+ const button = legacyButton(options?.button ?? "left");
217
+ const point = { x: Math.round(x), y: Math.round(y), keys: options?.modifiers ?? [] };
218
+ const actions =
219
+ count === 2 && button === "left"
220
+ ? [{ type: "double_click", ...point }]
221
+ : Array.from({ length: count }, () => ({ type: "click", ...point, button }));
222
+ await this.#execute(actions, target, native);
223
+ }
224
+
225
+ async moveMouse(target, x, y, options) {
226
+ this.#ensureOpen();
227
+ this.#ensureForeground(target, options);
228
+ await this.#execute(
229
+ { type: "move", x: Math.round(x), y: Math.round(y), keys: options?.modifiers ?? [] },
230
+ target,
231
+ this.#nativeForCapturedTarget(target),
232
+ );
233
+ }
234
+
235
+ async drag(target, path, options) {
236
+ this.#ensureOpen();
237
+ this.#ensureForeground(target, options);
238
+ await this.#execute(
239
+ { type: "drag", path: path.map(legacyPoint), keys: options?.modifiers ?? [] },
240
+ target,
241
+ this.#nativeForCapturedTarget(target),
242
+ );
243
+ }
244
+
245
+ async scroll(target, x, y, dx, dy, options) {
246
+ this.#ensureOpen();
247
+ this.#ensureForeground(target, options);
248
+ await this.#execute(
249
+ {
250
+ type: "scroll",
251
+ x: Math.round(x),
252
+ y: Math.round(y),
253
+ scroll_x: Math.round(dx),
254
+ scroll_y: Math.round(dy),
255
+ keys: options?.modifiers ?? [],
256
+ },
257
+ target,
258
+ this.#nativeForCapturedTarget(target),
259
+ );
260
+ }
261
+
262
+ async typeText(target, text, options) {
263
+ this.#ensureOpen();
264
+ this.#ensureForeground(target, options);
265
+ await this.#execute({ type: "type", text }, target, this.#capturedTargets.get(target)?.native ?? this.#native);
266
+ }
267
+
268
+ async keyChord(target, keys, options) {
269
+ this.#ensureOpen();
270
+ this.#ensureForeground(target, options);
271
+ await this.#execute({ type: "keypress", keys }, target, this.#capturedTargets.get(target)?.native ?? this.#native);
272
+ }
273
+
274
+ async raiseWindow() {
275
+ this.#ensureOpen();
276
+ throw desktopError("BackgroundUnavailable", "the installed native addon does not support window control");
277
+ }
278
+
279
+ async axSnapshot() {
280
+ this.#ensureOpen();
281
+ throw desktopError("AxUnsupported", "accessibility is unavailable in the installed native addon");
282
+ }
283
+
284
+ async axQuery() {
285
+ return this.axSnapshot();
286
+ }
287
+
288
+ async axElementAt() {
289
+ return this.axSnapshot();
290
+ }
291
+
292
+ async axFocused() {
293
+ return this.axSnapshot();
294
+ }
295
+
296
+ async axNode() {
297
+ return this.axSnapshot();
298
+ }
299
+
300
+ async axAttributes() {
301
+ return this.axSnapshot();
302
+ }
303
+
304
+ async axChildren() {
305
+ return this.axSnapshot();
306
+ }
307
+
308
+ async axParent() {
309
+ return this.axSnapshot();
310
+ }
311
+
312
+ async axPerform() {
313
+ return this.axSnapshot();
314
+ }
315
+
316
+ async axSetValue() {
317
+ return this.axSnapshot();
318
+ }
319
+
320
+ async axFocus() {
321
+ return this.axSnapshot();
322
+ }
323
+
324
+ async axClick() {
325
+ return this.axSnapshot();
326
+ }
327
+
328
+ async close() {
329
+ if (this.#closed) return;
330
+ this.#closed = true;
331
+ try {
332
+ await Promise.all([...this.#sessions.values()].map(native => native.close()));
333
+ } catch (error) {
334
+ throw normalizeError(error, "Internal");
335
+ }
336
+ }
337
+ }
338
+
339
+ ADAPTED_SESSION_CLASSES.set(NativeDesktopSession, DesktopSession);
340
+ return DesktopSession;
341
+ }
package/native/desktop.js CHANGED
@@ -1,10 +1,13 @@
1
+ import { adaptDesktopSession } from "./desktop-adapter.js";
1
2
  import { loadNative } from "./loader-state.js";
2
3
 
4
+ let DesktopSession;
5
+
3
6
  /**
4
7
  * Construct a desktop session without loading the native addon until the
5
8
  * computer worker receives its initialization message.
6
9
  */
7
10
  export function createDesktopSession(options) {
8
- const { DesktopSession } = loadNative();
11
+ DesktopSession ??= adaptDesktopSession(loadNative().DesktopSession);
9
12
  return new DesktopSession(options);
10
13
  }
package/native/index.d.ts CHANGED
@@ -339,7 +339,7 @@ export declare function __ompInstallTokioRuntime(): void
339
339
  * `packages/natives/native/index.js` (which derives the name from
340
340
  * `package.json#version`).
341
341
  */
342
- export declare function __piNativesV18_0_0(): void
342
+ export declare function __piNativesV18_0_3(): void
343
343
 
344
344
  /**
345
345
  * Apply ast-grep rewrite rules to matching files; honors `dryRun` and returns
package/native/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { loadNative } from "./loader-state.js";
2
+ import { adaptDesktopSession } from "./desktop-adapter.js";
2
3
 
3
4
  /**
4
5
  * Native addon entrypoint.
@@ -18,7 +19,7 @@ const nativeBindings = loadNative();
18
19
  // classes
19
20
  export const AudioCapture = nativeBindings.AudioCapture;
20
21
  export const AudioPlayback = nativeBindings.AudioPlayback;
21
- export const DesktopSession = nativeBindings.DesktopSession;
22
+ export const DesktopSession = adaptDesktopSession(nativeBindings.DesktopSession);
22
23
  export const FileLock = nativeBindings.FileLock;
23
24
  export const HighlightStream = nativeBindings.HighlightStream;
24
25
  export const LiveWebRtcPeer = nativeBindings.LiveWebRtcPeer;
@@ -31,7 +32,7 @@ export const TtyWriter = nativeBindings.TtyWriter;
31
32
 
32
33
  // functions
33
34
  export const __ompInstallTokioRuntime = nativeBindings.__ompInstallTokioRuntime;
34
- export const __piNativesV18_0_0 = nativeBindings.__piNativesV18_0_0;
35
+ export const __piNativesV18_0_3 = nativeBindings.__piNativesV18_0_3;
35
36
  export const astEdit = nativeBindings.astEdit;
36
37
  export const astGrep = nativeBindings.astGrep;
37
38
  export const astMatch = nativeBindings.astMatch;
@@ -648,6 +648,27 @@ function maybeStageNodeModulesAddon(ctx, errors) {
648
648
  return stagedPath;
649
649
  }
650
650
 
651
+
652
+ /**
653
+ * Before version sentinels were exported, published native addons still shared
654
+ * this stable core ABI. Let those on-disk addons bridge a package-version bump
655
+ * when they expose the signature; keep every versioned addon and a current
656
+ * on-disk file paired with resident old exports on the strict path below.
657
+ */
658
+ function isCompatiblePreSentinelNativeAddon(bindings, diskHasExpectedSentinel) {
659
+ if (diskHasExpectedSentinel) return false;
660
+ if (Object.keys(bindings).some(key => /^__piNativesV[A-Za-z0-9_]+$/.test(key))) return false;
661
+ return (
662
+ typeof bindings.countTokens === "function" &&
663
+ typeof bindings.executeShell === "function" &&
664
+ typeof bindings.visibleWidth === "function" &&
665
+ typeof bindings.DesktopSession === "function" &&
666
+ typeof bindings.DesktopSession.prototype?.capture === "function" &&
667
+ typeof bindings.DesktopSession.prototype?.execute === "function" &&
668
+ typeof bindings.DesktopSession.prototype?.close === "function"
669
+ );
670
+ }
671
+
651
672
  export function validateLoadedBindings(ctx, bindings, candidate) {
652
673
  // In workspace dev (running out of `packages/natives/native/` rather than a
653
674
  // `node_modules` install or a compiled bundle) the local `.node` only gains
@@ -680,6 +701,7 @@ export function validateLoadedBindings(ctx, bindings, candidate) {
680
701
  // The successful require above normally guarantees readability. If the
681
702
  // file disappears concurrently, retain the safe reinstall diagnosis.
682
703
  }
704
+ if (isCompatiblePreSentinelNativeAddon(bindings, diskHasExpectedSentinel)) return;
683
705
  if (residentSentinel && diskHasExpectedSentinel) {
684
706
  const residentVersion = residentSentinel.slice("__piNativesV".length).replace(/_/g, ".");
685
707
  throw new Error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-natives",
3
- "version": "18.0.0",
3
+ "version": "18.0.3",
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",
@@ -68,6 +68,8 @@
68
68
  "native/clipboard.d.ts",
69
69
  "native/desktop.js",
70
70
  "native/desktop.d.ts",
71
+ "native/desktop-adapter.js",
72
+ "native/desktop-adapter.d.ts",
71
73
  "native/loader-state.js",
72
74
  "native/loader-state.d.ts",
73
75
  "native/embedded-addon.js",
@@ -90,10 +92,10 @@
90
92
  }
91
93
  },
92
94
  "optionalDependencies": {
93
- "@oh-my-pi/pi-natives-linux-x64": "18.0.0",
94
- "@oh-my-pi/pi-natives-linux-arm64": "18.0.0",
95
- "@oh-my-pi/pi-natives-darwin-x64": "18.0.0",
96
- "@oh-my-pi/pi-natives-darwin-arm64": "18.0.0",
97
- "@oh-my-pi/pi-natives-win32-x64": "18.0.0"
95
+ "@oh-my-pi/pi-natives-linux-x64": "18.0.3",
96
+ "@oh-my-pi/pi-natives-linux-arm64": "18.0.3",
97
+ "@oh-my-pi/pi-natives-darwin-x64": "18.0.3",
98
+ "@oh-my-pi/pi-natives-darwin-arm64": "18.0.3",
99
+ "@oh-my-pi/pi-natives-win32-x64": "18.0.3"
98
100
  }
99
101
  }