@limrun/api 0.17.5 → 0.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/src/ios-client.ts CHANGED
@@ -25,16 +25,187 @@ export interface SimctlExecutionEvents {
25
25
  error: (error: Error) => void;
26
26
  }
27
27
 
28
+ // ============================================================================
29
+ // Accessibility Selector - used for element-based operations
30
+ // ============================================================================
31
+
32
+ /**
33
+ * Selector criteria for finding accessibility elements.
34
+ * All non-undefined fields must match for an element to be selected.
35
+ */
36
+ export type AccessibilitySelector = {
37
+ /** Match by AXUniqueId (accessibilityIdentifier) - exact match */
38
+ accessibilityId?: string;
39
+ /** Match by AXLabel - exact match */
40
+ label?: string;
41
+ /** Match by AXLabel - contains (case-insensitive) */
42
+ labelContains?: string;
43
+ /** Match by element type/role (e.g., "Button", "TextField") - case-insensitive */
44
+ elementType?: string;
45
+ /** Match by title - exact match */
46
+ title?: string;
47
+ /** Match by title - contains (case-insensitive) */
48
+ titleContains?: string;
49
+ /** Match by AXValue - exact match */
50
+ value?: string;
51
+ };
52
+
53
+ /**
54
+ * A point on the screen for accessibility queries
55
+ */
56
+ export type AccessibilityPoint = {
57
+ x: number;
58
+ y: number;
59
+ };
60
+
61
+ // ============================================================================
62
+ // Result Types
63
+ // ============================================================================
64
+
65
+ export type ScreenshotData = {
66
+ /** Base64-encoded JPEG image data */
67
+ base64: string;
68
+ /** Width in points (for tap coordinates) */
69
+ width: number;
70
+ /** Height in points (for tap coordinates) */
71
+ height: number;
72
+ };
73
+
74
+ export type TapElementResult = {
75
+ elementLabel?: string;
76
+ elementType?: string;
77
+ };
78
+
79
+ export type ElementResult = {
80
+ elementLabel?: string;
81
+ };
82
+
83
+ export type InstalledApp = {
84
+ bundleId: string;
85
+ name: string;
86
+ installType: string;
87
+ };
88
+
89
+ export type LsofEntry = {
90
+ kind: 'unix';
91
+ path: string;
92
+ };
93
+
94
+ export type AppInstallationResult = {
95
+ /** The URL the app was installed from */
96
+ url: string;
97
+ /** Bundle ID of the installed app */
98
+ bundleId: string;
99
+ };
100
+
101
+ export type AppInstallationOptions = {
102
+ /** MD5 hash for caching - if provided and matches cached version, skips download */
103
+ md5?: string;
104
+ /**
105
+ * Launch mode after installation:
106
+ * - 'ForegroundIfRunning': Bring to foreground if already running, otherwise launch
107
+ * - 'RelaunchIfRunning': Kill and relaunch if already running
108
+ * - 'FailIfRunning': Fail if the app is already running
109
+ * - undefined: Don't launch after installation
110
+ */
111
+ launchMode?: 'ForegroundIfRunning' | 'RelaunchIfRunning' | 'FailIfRunning';
112
+ };
113
+
28
114
  /**
29
115
  * A client for interacting with a Limrun iOS instance
30
116
  */
31
117
  export type InstanceClient = {
32
118
  /**
33
119
  * Take a screenshot of the current screen
34
- * @returns A promise that resolves to the screenshot data
120
+ * @returns A promise that resolves to the screenshot data with base64 image and dimensions
35
121
  */
36
122
  screenshot: () => Promise<ScreenshotData>;
37
123
 
124
+ /**
125
+ * Get the element tree (accessibility hierarchy) of the current screen
126
+ * @param point Optional point to get the element at that specific location
127
+ * @returns A promise that resolves to the JSON string of the accessibility tree
128
+ */
129
+ elementTree: (point?: AccessibilityPoint) => Promise<string>;
130
+
131
+ /**
132
+ * Tap at the specified coordinates
133
+ * @param x X coordinate in points
134
+ * @param y Y coordinate in points
135
+ */
136
+ tap: (x: number, y: number) => Promise<void>;
137
+
138
+ /**
139
+ * Tap an accessibility element by selector
140
+ * @param selector The selector criteria to find the element
141
+ * @returns Information about the tapped element
142
+ */
143
+ tapElement: (selector: AccessibilitySelector) => Promise<TapElementResult>;
144
+
145
+ /**
146
+ * Increment an accessibility element (useful for sliders, steppers, etc.)
147
+ * @param selector The selector criteria to find the element
148
+ * @returns Information about the incremented element
149
+ */
150
+ incrementElement: (selector: AccessibilitySelector) => Promise<ElementResult>;
151
+
152
+ /**
153
+ * Decrement an accessibility element (useful for sliders, steppers, etc.)
154
+ * @param selector The selector criteria to find the element
155
+ * @returns Information about the decremented element
156
+ */
157
+ decrementElement: (selector: AccessibilitySelector) => Promise<ElementResult>;
158
+
159
+ /**
160
+ * Set the value of an accessibility element (useful for text fields, etc.)
161
+ * This is much faster than typing character by character.
162
+ * @param text The text value to set
163
+ * @param selector The selector criteria to find the element
164
+ * @returns Information about the modified element
165
+ */
166
+ setElementValue: (text: string, selector: AccessibilitySelector) => Promise<ElementResult>;
167
+
168
+ /**
169
+ * Type text into the currently focused input field
170
+ * @param text The text to type
171
+ * @param pressEnter If true, press Enter after typing
172
+ */
173
+ typeText: (text: string, pressEnter?: boolean) => Promise<void>;
174
+
175
+ /**
176
+ * Press a key on the keyboard, optionally with modifiers
177
+ * @param key The key to press (e.g., 'a', 'enter', 'backspace', 'up', 'f1')
178
+ * @param modifiers Optional modifier keys (e.g., ['shift'], ['command', 'shift'])
179
+ */
180
+ pressKey: (key: string, modifiers?: string[]) => Promise<void>;
181
+
182
+ /**
183
+ * Launch an installed app by bundle identifier
184
+ * @param bundleId Bundle identifier of the app to launch
185
+ */
186
+ launchApp: (bundleId: string) => Promise<void>;
187
+
188
+ /**
189
+ * List installed apps on the simulator
190
+ * @returns Array of installed apps with bundleId, name, and installType
191
+ */
192
+ listApps: () => Promise<InstalledApp[]>;
193
+
194
+ /**
195
+ * Open a URL in the simulator (web URLs open in Safari, deep links open corresponding apps)
196
+ * @param url The URL to open
197
+ */
198
+ openUrl: (url: string) => Promise<void>;
199
+
200
+ /**
201
+ * Install an app from a URL (supports .ipa or .app files, optionally zipped)
202
+ * @param url The URL to download the app from
203
+ * @param options Optional installation options (md5 for caching, launchMode)
204
+ * @returns The installation result with bundle ID on success
205
+ * @throws Error if installation fails (e.g., invalid app, download failure)
206
+ */
207
+ installApp: (url: string, options?: AppInstallationOptions) => Promise<AppInstallationResult>;
208
+
38
209
  /**
39
210
  * Disconnect from the Limrun instance
40
211
  */
@@ -109,11 +280,6 @@ export type InstanceClient = {
109
280
  lsof: () => Promise<LsofEntry[]>;
110
281
  };
111
282
 
112
- export type LsofEntry = {
113
- kind: 'unix';
114
- path: string;
115
- };
116
-
117
283
  /**
118
284
  * Controls the verbosity of logging in the client
119
285
  */
@@ -153,68 +319,50 @@ export type InstanceClientOptions = {
153
319
  maxReconnectDelay?: number;
154
320
  };
155
321
 
156
- type ScreenshotRequest = {
157
- type: 'screenshot';
158
- id: string;
159
- };
160
-
161
- type ScreenshotResponse = {
162
- type: 'screenshot';
163
- dataUri: string;
164
- id: string;
165
- };
166
-
167
- type ScreenshotData = {
168
- dataUri: string;
169
- };
322
+ // ============================================================================
323
+ // Internal Types - Message Protocol
324
+ // ============================================================================
170
325
 
171
- type ScreenshotErrorResponse = {
172
- type: 'screenshotError';
173
- message: string;
174
- id: string;
326
+ /**
327
+ * Generic pending request tracker
328
+ */
329
+ type PendingRequest<T> = {
330
+ resolve: (value: T) => void;
331
+ reject: (reason: Error) => void;
332
+ timeout: NodeJS.Timeout;
175
333
  };
176
334
 
335
+ // Simctl uses streaming, so it's handled separately
177
336
  type SimctlRequest = {
178
337
  type: 'simctl';
179
338
  id: string;
180
339
  args: string[];
181
340
  };
182
341
 
183
- type SimctlStreamResponse = {
184
- type: 'simctlStream';
185
- id: string;
186
- stdout?: string; // base64 encoded
187
- stderr?: string; // base64 encoded
188
- exitCode?: number;
189
- };
190
-
191
- type SimctlErrorResponse = {
192
- type: 'simctlError';
193
- id: string;
194
- message: string;
195
- };
196
-
197
- type ListOpenFilesRequest = {
198
- type: 'listOpenFiles';
199
- id: string;
200
- kind: 'unix';
201
- };
202
-
203
- type ListOpenFilesResponse = {
204
- type: 'listOpenFilesResult';
342
+ /**
343
+ * Generic server response with optional error
344
+ */
345
+ type ServerResponse = {
346
+ type: string;
205
347
  id: string;
206
- files?: LsofEntry[];
207
348
  error?: string;
349
+ // Response-specific fields
350
+ base64?: string;
351
+ width?: number;
352
+ height?: number;
353
+ json?: string;
354
+ elementLabel?: string;
355
+ elementType?: string;
356
+ apps?: string;
357
+ url?: string;
358
+ bundleId?: string;
359
+ files?: LsofEntry[];
360
+ // Simctl streaming fields
361
+ stdout?: string;
362
+ stderr?: string;
363
+ exitCode?: number;
208
364
  };
209
365
 
210
- type ServerMessage =
211
- | ScreenshotResponse
212
- | ScreenshotErrorResponse
213
- | SimctlStreamResponse
214
- | SimctlErrorResponse
215
- | ListOpenFilesResponse
216
- | { type: string; [key: string]: unknown };
217
-
218
366
  /**
219
367
  * Handle for a running simctl command execution.
220
368
  *
@@ -408,22 +556,10 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
408
556
  let intentionalDisconnect = false;
409
557
  let lastError: string | undefined;
410
558
 
411
- const screenshotRequests: Map<
412
- string,
413
- {
414
- resolver: (value: ScreenshotData | PromiseLike<ScreenshotData>) => void;
415
- rejecter: (reason?: any) => void;
416
- }
417
- > = new Map();
418
-
419
- const lsofRequests: Map<
420
- string,
421
- {
422
- resolver: (value: LsofEntry[] | PromiseLike<LsofEntry[]>) => void;
423
- rejecter: (reason?: any) => void;
424
- }
425
- > = new Map();
559
+ // Centralized pending requests map - handles all request/response patterns
560
+ const pendingRequests: Map<string, PendingRequest<any>> = new Map();
426
561
 
562
+ // Simctl uses streaming, so it needs separate handling
427
563
  const simctlExecutions: Map<string, SimctlExecution> = new Map();
428
564
 
429
565
  const stateChangeCallbacks: Set<ConnectionStateCallback> = new Set();
@@ -459,11 +595,11 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
459
595
  };
460
596
 
461
597
  const failPendingRequests = (reason: string): void => {
462
- screenshotRequests.forEach((request) => request.rejecter(new Error(reason)));
463
- screenshotRequests.clear();
464
-
465
- lsofRequests.forEach((request) => request.rejecter(new Error(reason)));
466
- lsofRequests.clear();
598
+ pendingRequests.forEach((request) => {
599
+ clearTimeout(request.timeout);
600
+ request.reject(new Error(reason));
601
+ });
602
+ pendingRequests.clear();
467
603
 
468
604
  simctlExecutions.forEach((execution) => execution._handleError(new Error(reason)));
469
605
  simctlExecutions.clear();
@@ -523,6 +659,73 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
523
659
  }, currentDelay);
524
660
  };
525
661
 
662
+ // Generate unique request ID
663
+ const generateId = (): string => {
664
+ return `ts-client-${Date.now()}-${Math.random().toString(36).substring(7)}`;
665
+ };
666
+
667
+ // Generic request sender with timeout and response handling
668
+ const sendRequest = <T>(
669
+ type: string,
670
+ params: Record<string, unknown> = {},
671
+ timeoutMs: number = 30000,
672
+ ): Promise<T> => {
673
+ return new Promise((resolve, reject) => {
674
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
675
+ reject(new Error('WebSocket is not connected or connection is not open.'));
676
+ return;
677
+ }
678
+
679
+ const id = generateId();
680
+ const timeout = setTimeout(() => {
681
+ pendingRequests.delete(id);
682
+ reject(new Error(`Request ${type} timed out`));
683
+ }, timeoutMs);
684
+
685
+ pendingRequests.set(id, { resolve, reject, timeout });
686
+
687
+ const request = { type, id, ...params };
688
+ logger.debug('Sending request:', request);
689
+
690
+ ws.send(JSON.stringify(request), (err?: Error) => {
691
+ if (err) {
692
+ clearTimeout(timeout);
693
+ pendingRequests.delete(id);
694
+ logger.error(`Failed to send ${type} request:`, err);
695
+ reject(err);
696
+ }
697
+ });
698
+ });
699
+ };
700
+
701
+ // Response handlers - transform raw responses to typed results
702
+ const responseHandlers: Record<string, (msg: ServerResponse) => unknown> = {
703
+ screenshotResult: (msg) => ({
704
+ base64: msg.base64!,
705
+ width: msg.width!,
706
+ height: msg.height!,
707
+ }),
708
+ elementTreeResult: (msg) => msg.json!,
709
+ tapResult: () => undefined,
710
+ tapElementResult: (msg) => ({
711
+ elementLabel: msg.elementLabel,
712
+ elementType: msg.elementType,
713
+ }),
714
+ incrementElementResult: (msg) => ({ elementLabel: msg.elementLabel }),
715
+ decrementElementResult: (msg) => ({ elementLabel: msg.elementLabel }),
716
+ setElementValueResult: (msg) => ({ elementLabel: msg.elementLabel }),
717
+ typeTextResult: () => undefined,
718
+ pressKeyResult: () => undefined,
719
+ launchAppResult: () => undefined,
720
+ listAppsResult: (msg) => JSON.parse(msg.apps || '[]') as InstalledApp[],
721
+ listOpenFilesResult: (msg) => msg.files || [],
722
+ openUrlResult: () => undefined,
723
+ appInstallationResult: (msg) => ({
724
+ url: msg.url || '',
725
+ bundleId: msg.bundleId || '',
726
+ }),
727
+ };
728
+
526
729
  const setupWebSocket = (): void => {
527
730
  cleanup();
528
731
  updateConnectionState('connecting');
@@ -530,7 +733,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
530
733
  ws = new WebSocket(endpointWebSocketUrl);
531
734
 
532
735
  ws.on('message', (data: Data) => {
533
- let message: ServerMessage;
736
+ let message: ServerResponse;
534
737
  try {
535
738
  message = JSON.parse(data.toString());
536
739
  } catch (e) {
@@ -538,163 +741,66 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
538
741
  return;
539
742
  }
540
743
 
541
- switch (message.type) {
542
- case 'screenshot': {
543
- if (!('dataUri' in message) || typeof message.dataUri !== 'string' || !('id' in message)) {
544
- logger.warn('Received invalid screenshot message:', message);
545
- break;
546
- }
547
-
548
- const screenshotMessage = message as ScreenshotResponse;
549
- const request = screenshotRequests.get(screenshotMessage.id);
550
-
551
- if (!request) {
552
- logger.warn(
553
- `Received screenshot data for unknown or already handled session: ${screenshotMessage.id}`,
554
- );
555
- break;
556
- }
557
-
558
- logger.debug(`Received screenshot data URI for session ${screenshotMessage.id}.`);
559
- request.resolver({ dataUri: screenshotMessage.dataUri });
560
- screenshotRequests.delete(screenshotMessage.id);
561
- break;
744
+ // Handle simctl streaming separately (it uses multiple messages per request)
745
+ if (message.type === 'simctlStream') {
746
+ const execution = simctlExecutions.get(message.id);
747
+ if (!execution) {
748
+ logger.warn(`Received simctl stream for unknown execution: ${message.id}`);
749
+ return;
562
750
  }
563
- case 'screenshotError': {
564
- if (!('message' in message) || !('id' in message)) {
565
- logger.warn('Received invalid screenshot error message:', message);
566
- break;
567
- }
568
751
 
569
- const errorMessage = message as ScreenshotErrorResponse;
570
- const request = screenshotRequests.get(errorMessage.id);
571
-
572
- if (!request) {
573
- logger.warn(
574
- `Received screenshot error for unknown or already handled session: ${errorMessage.id}`,
575
- );
576
- break;
752
+ if (message.stdout) {
753
+ try {
754
+ execution._handleStdout(Buffer.from(message.stdout, 'base64'));
755
+ } catch (err) {
756
+ logger.error('Failed to decode stdout data:', err);
577
757
  }
578
-
579
- logger.error(
580
- `Server reported an error capturing screenshot for session ${errorMessage.id}:`,
581
- errorMessage.message,
582
- );
583
- request.rejecter(new Error(errorMessage.message));
584
- screenshotRequests.delete(errorMessage.id);
585
- break;
586
758
  }
587
- case 'simctlStream': {
588
- if (!('id' in message)) {
589
- logger.warn('Received invalid simctl stream message:', message);
590
- break;
591
- }
592
-
593
- const streamMessage = message as SimctlStreamResponse;
594
- const execution = simctlExecutions.get(streamMessage.id);
595
759
 
596
- if (!execution) {
597
- logger.warn(
598
- `Received simctl stream for unknown or already completed execution: ${streamMessage.id}`,
599
- );
600
- break;
760
+ if (message.stderr) {
761
+ try {
762
+ execution._handleStderr(Buffer.from(message.stderr, 'base64'));
763
+ } catch (err) {
764
+ logger.error('Failed to decode stderr data:', err);
601
765
  }
602
-
603
- // Handle stdout if present
604
- if (streamMessage.stdout) {
605
- try {
606
- const stdoutBuffer = Buffer.from(streamMessage.stdout, 'base64');
607
- execution._handleStdout(stdoutBuffer);
608
- } catch (err) {
609
- logger.error('Failed to decode stdout data:', err);
610
- }
611
- }
612
-
613
- // Handle stderr if present
614
- if (streamMessage.stderr) {
615
- try {
616
- const stderrBuffer = Buffer.from(streamMessage.stderr, 'base64');
617
- execution._handleStderr(stderrBuffer);
618
- } catch (err) {
619
- logger.error('Failed to decode stderr data:', err);
620
- }
621
- }
622
-
623
- // Handle exit code if present (final message)
624
- if (streamMessage.exitCode !== undefined) {
625
- logger.debug(
626
- `Simctl execution ${streamMessage.id} completed with exit code ${streamMessage.exitCode}`,
627
- );
628
- execution._handleExit(streamMessage.exitCode);
629
- simctlExecutions.delete(streamMessage.id);
630
- }
631
- break;
632
766
  }
633
- case 'simctlError': {
634
- if (!('message' in message) || !('id' in message)) {
635
- logger.warn('Received invalid simctl error message:', message);
636
- break;
637
- }
638
-
639
- const errorMessage = message as SimctlErrorResponse;
640
- const execution = simctlExecutions.get(errorMessage.id);
641
767
 
642
- if (!execution) {
643
- logger.warn(
644
- `Received simctl error for unknown or already handled execution: ${errorMessage.id}`,
645
- );
646
- break;
647
- }
648
-
649
- logger.error(
650
- `Server reported an error for simctl execution ${errorMessage.id}:`,
651
- errorMessage.message,
652
- );
653
- execution._handleError(new Error(errorMessage.message));
654
- simctlExecutions.delete(errorMessage.id);
655
- break;
768
+ if (message.exitCode !== undefined) {
769
+ logger.debug(`Simctl execution ${message.id} completed with exit code ${message.exitCode}`);
770
+ execution._handleExit(message.exitCode);
771
+ simctlExecutions.delete(message.id);
656
772
  }
657
- case 'listOpenFilesResult': {
658
- if (!('id' in message)) {
659
- logger.warn('Received invalid listOpenFilesResult message:', message);
660
- break;
661
- }
662
-
663
- const lsofMessage = message as ListOpenFilesResponse;
664
- const request = lsofRequests.get(lsofMessage.id);
773
+ return;
774
+ }
665
775
 
666
- if (!request) {
667
- logger.warn(
668
- `Received listOpenFilesResult for unknown or already handled session: ${lsofMessage.id}`,
669
- );
670
- break;
671
- }
776
+ // Handle all other request/response patterns generically
777
+ const request = pendingRequests.get(message.id);
778
+ if (!request) {
779
+ logger.debug(`Received response for unknown or already handled request: ${message.id}`);
780
+ return;
781
+ }
672
782
 
673
- if (lsofMessage.error) {
674
- logger.error(
675
- `Server reported an error listing open files for session ${lsofMessage.id}:`,
676
- lsofMessage.error,
677
- );
678
- request.rejecter(new Error(lsofMessage.error));
679
- lsofRequests.delete(lsofMessage.id);
680
- break;
681
- }
783
+ clearTimeout(request.timeout);
784
+ pendingRequests.delete(message.id);
682
785
 
683
- if (!lsofMessage.files || !Array.isArray(lsofMessage.files)) {
684
- logger.warn('Received listOpenFilesResult without files array:', message);
685
- request.rejecter(new Error('Invalid listOpenFilesResult: missing files array'));
686
- lsofRequests.delete(lsofMessage.id);
687
- break;
688
- }
786
+ // Check for error
787
+ if (message.error) {
788
+ logger.error(`Server error for ${message.type}: ${message.error}`);
789
+ request.reject(new Error(message.error));
790
+ return;
791
+ }
689
792
 
690
- logger.debug(`Received listOpenFilesResult for session ${lsofMessage.id}.`);
691
- request.resolver(lsofMessage.files);
692
- lsofRequests.delete(lsofMessage.id);
693
- break;
793
+ // Use handler to transform response, or resolve with raw message
794
+ const handler = responseHandlers[message.type];
795
+ if (handler) {
796
+ try {
797
+ request.resolve(handler(message));
798
+ } catch (err) {
799
+ request.reject(err as Error);
694
800
  }
695
- default:
696
- logger.warn('Received unexpected message type:', message);
697
- break;
801
+ } else {
802
+ logger.warn('Received unexpected message type:', message.type);
803
+ request.resolve(message);
698
804
  }
699
805
  });
700
806
 
@@ -749,6 +855,18 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
749
855
  hasResolved = true;
750
856
  resolveConnection({
751
857
  screenshot,
858
+ elementTree,
859
+ tap,
860
+ tapElement,
861
+ incrementElement,
862
+ decrementElement,
863
+ setElementValue,
864
+ typeText,
865
+ pressKey,
866
+ launchApp,
867
+ listApps,
868
+ openUrl,
869
+ installApp,
752
870
  disconnect,
753
871
  getConnectionState,
754
872
  onConnectionStateChange,
@@ -760,93 +878,72 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
760
878
  });
761
879
  };
762
880
 
763
- const screenshot = async (): Promise<ScreenshotData> => {
764
- if (!ws || ws.readyState !== WebSocket.OPEN) {
765
- return Promise.reject(new Error('WebSocket is not connected or connection is not open.'));
766
- }
881
+ // ========================================================================
882
+ // Client Methods - using centralized sendRequest
883
+ // ========================================================================
767
884
 
768
- const id = 'ts-client-' + Date.now();
769
- const screenshotRequest: ScreenshotRequest = {
770
- type: 'screenshot',
771
- id,
772
- };
885
+ const screenshot = (): Promise<ScreenshotData> => {
886
+ return sendRequest<ScreenshotData>('screenshot');
887
+ };
773
888
 
774
- return new Promise<ScreenshotData>((resolve, reject) => {
775
- logger.debug('Sending screenshot request:', screenshotRequest);
776
- ws!.send(JSON.stringify(screenshotRequest), (err?: Error) => {
777
- if (err) {
778
- logger.error('Failed to send screenshot request:', err);
779
- reject(err);
780
- }
781
- });
889
+ const elementTree = (point?: AccessibilityPoint): Promise<string> => {
890
+ return sendRequest<string>('elementTree', { point });
891
+ };
782
892
 
783
- const timeout = setTimeout(() => {
784
- if (screenshotRequests.has(id)) {
785
- logger.error(`Screenshot request timed out for session ${id}`);
786
- screenshotRequests.get(id)?.rejecter(new Error('Screenshot request timed out'));
787
- screenshotRequests.delete(id);
788
- }
789
- }, 30000);
790
- screenshotRequests.set(id, {
791
- resolver: (value: ScreenshotData | PromiseLike<ScreenshotData>) => {
792
- clearTimeout(timeout);
793
- resolve(value);
794
- screenshotRequests.delete(id);
795
- },
796
- rejecter: (reason?: any) => {
797
- clearTimeout(timeout);
798
- reject(reason);
799
- screenshotRequests.delete(id);
800
- },
801
- });
802
- });
893
+ const tap = (x: number, y: number): Promise<void> => {
894
+ return sendRequest<void>('tap', { x, y });
803
895
  };
804
896
 
805
- const lsof = async (): Promise<LsofEntry[]> => {
806
- if (!ws || ws.readyState !== WebSocket.OPEN) {
807
- return Promise.reject(new Error('WebSocket is not connected or connection is not open.'));
808
- }
897
+ const tapElement = (selector: AccessibilitySelector): Promise<TapElementResult> => {
898
+ return sendRequest<TapElementResult>('tapElement', { selector });
899
+ };
809
900
 
810
- const id = 'ts-client-' + Date.now();
811
- const lsofRequest: ListOpenFilesRequest = {
812
- type: 'listOpenFiles',
813
- id,
814
- kind: 'unix',
815
- };
901
+ const incrementElement = (selector: AccessibilitySelector): Promise<ElementResult> => {
902
+ return sendRequest<ElementResult>('incrementElement', { selector });
903
+ };
816
904
 
817
- return new Promise<LsofEntry[]>((resolve, reject) => {
818
- logger.debug('Sending listOpenFiles request:', lsofRequest);
819
- ws!.send(JSON.stringify(lsofRequest), (err?: Error) => {
820
- if (err) {
821
- logger.error('Failed to send listOpenFiles request:', err);
822
- reject(err);
823
- }
824
- });
905
+ const decrementElement = (selector: AccessibilitySelector): Promise<ElementResult> => {
906
+ return sendRequest<ElementResult>('decrementElement', { selector });
907
+ };
825
908
 
826
- const timeout = setTimeout(() => {
827
- if (lsofRequests.has(id)) {
828
- logger.error(`listOpenFiles request timed out for session ${id}`);
829
- lsofRequests.get(id)?.rejecter(new Error('listOpenFiles request timed out'));
830
- lsofRequests.delete(id);
831
- }
832
- }, 30000);
833
- lsofRequests.set(id, {
834
- resolver: (value: LsofEntry[] | PromiseLike<LsofEntry[]>) => {
835
- clearTimeout(timeout);
836
- resolve(value);
837
- lsofRequests.delete(id);
838
- },
839
- rejecter: (reason?: any) => {
840
- clearTimeout(timeout);
841
- reject(reason);
842
- lsofRequests.delete(id);
843
- },
844
- });
909
+ const setElementValue = (text: string, selector: AccessibilitySelector): Promise<ElementResult> => {
910
+ return sendRequest<ElementResult>('setElementValue', { text, selector });
911
+ };
912
+
913
+ const typeText = (text: string, pressEnter?: boolean): Promise<void> => {
914
+ return sendRequest<void>('typeText', { text, pressEnter });
915
+ };
916
+
917
+ const pressKey = (key: string, modifiers?: string[]): Promise<void> => {
918
+ return sendRequest<void>('pressKey', { key, modifiers });
919
+ };
920
+
921
+ const launchApp = (bundleId: string): Promise<void> => {
922
+ return sendRequest<void>('launchApp', { bundleId });
923
+ };
924
+
925
+ const listApps = (): Promise<InstalledApp[]> => {
926
+ return sendRequest<InstalledApp[]>('listApps');
927
+ };
928
+
929
+ const openUrl = (url: string): Promise<void> => {
930
+ return sendRequest<void>('openUrl', { url });
931
+ };
932
+
933
+ const installApp = (url: string, options?: AppInstallationOptions): Promise<AppInstallationResult> => {
934
+ return sendRequest<AppInstallationResult>('appInstallation', {
935
+ url,
936
+ md5: options?.md5,
937
+ launchMode: options?.launchMode,
845
938
  });
846
939
  };
847
940
 
941
+ const lsof = (): Promise<LsofEntry[]> => {
942
+ return sendRequest<LsofEntry[]>('listOpenFiles', { kind: 'unix' });
943
+ };
944
+
848
945
  const simctl = (args: string[], opts: { disconnectOnExit?: boolean } = {}): SimctlExecution => {
849
- const id = 'ts-simctl-' + Date.now() + '-' + Math.random().toString(36).substring(7);
946
+ const id = generateId();
850
947
 
851
948
  const cancelCallback = () => {
852
949
  // Clean up execution tracking