@limrun/api 0.17.4 → 0.18.0-rc.1
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/CHANGELOG.md +8 -0
- package/ios-client.d.mts +117 -9
- package/ios-client.d.mts.map +1 -1
- package/ios-client.d.ts +117 -9
- package/ios-client.d.ts.map +1 -1
- package/ios-client.js +160 -192
- package/ios-client.js.map +1 -1
- package/ios-client.mjs +160 -192
- package/ios-client.mjs.map +1 -1
- package/package.json +1 -1
- package/resources/ios-instances.d.mts +1 -0
- package/resources/ios-instances.d.mts.map +1 -1
- package/resources/ios-instances.d.ts +1 -0
- package/resources/ios-instances.d.ts.map +1 -1
- package/src/ios-client.ts +364 -285
- package/src/resources/ios-instances.ts +2 -0
- package/src/version.ts +1 -1
- package/version.d.mts +1 -1
- package/version.d.mts.map +1 -1
- package/version.d.ts +1 -1
- package/version.d.ts.map +1 -1
- package/version.js +1 -1
- package/version.js.map +1 -1
- package/version.mjs +1 -1
- package/version.mjs.map +1 -1
package/src/ios-client.ts
CHANGED
|
@@ -25,16 +25,158 @@ 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
|
+
|
|
28
94
|
/**
|
|
29
95
|
* A client for interacting with a Limrun iOS instance
|
|
30
96
|
*/
|
|
31
97
|
export type InstanceClient = {
|
|
32
98
|
/**
|
|
33
99
|
* Take a screenshot of the current screen
|
|
34
|
-
* @returns A promise that resolves to the screenshot data
|
|
100
|
+
* @returns A promise that resolves to the screenshot data with base64 image and dimensions
|
|
35
101
|
*/
|
|
36
102
|
screenshot: () => Promise<ScreenshotData>;
|
|
37
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Get the element tree (accessibility hierarchy) of the current screen
|
|
106
|
+
* @param point Optional point to get the element at that specific location
|
|
107
|
+
* @returns A promise that resolves to the JSON string of the accessibility tree
|
|
108
|
+
*/
|
|
109
|
+
elementTree: (point?: AccessibilityPoint) => Promise<string>;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Tap at the specified coordinates
|
|
113
|
+
* @param x X coordinate in points
|
|
114
|
+
* @param y Y coordinate in points
|
|
115
|
+
*/
|
|
116
|
+
tap: (x: number, y: number) => Promise<void>;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Tap an accessibility element by selector
|
|
120
|
+
* @param selector The selector criteria to find the element
|
|
121
|
+
* @returns Information about the tapped element
|
|
122
|
+
*/
|
|
123
|
+
tapElement: (selector: AccessibilitySelector) => Promise<TapElementResult>;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Increment an accessibility element (useful for sliders, steppers, etc.)
|
|
127
|
+
* @param selector The selector criteria to find the element
|
|
128
|
+
* @returns Information about the incremented element
|
|
129
|
+
*/
|
|
130
|
+
incrementElement: (selector: AccessibilitySelector) => Promise<ElementResult>;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Decrement an accessibility element (useful for sliders, steppers, etc.)
|
|
134
|
+
* @param selector The selector criteria to find the element
|
|
135
|
+
* @returns Information about the decremented element
|
|
136
|
+
*/
|
|
137
|
+
decrementElement: (selector: AccessibilitySelector) => Promise<ElementResult>;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Set the value of an accessibility element (useful for text fields, etc.)
|
|
141
|
+
* This is much faster than typing character by character.
|
|
142
|
+
* @param text The text value to set
|
|
143
|
+
* @param selector The selector criteria to find the element
|
|
144
|
+
* @returns Information about the modified element
|
|
145
|
+
*/
|
|
146
|
+
setElementValue: (text: string, selector: AccessibilitySelector) => Promise<ElementResult>;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Type text into the currently focused input field
|
|
150
|
+
* @param text The text to type
|
|
151
|
+
* @param pressEnter If true, press Enter after typing
|
|
152
|
+
*/
|
|
153
|
+
typeText: (text: string, pressEnter?: boolean) => Promise<void>;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Press a key on the keyboard, optionally with modifiers
|
|
157
|
+
* @param key The key to press (e.g., 'a', 'enter', 'backspace', 'up', 'f1')
|
|
158
|
+
* @param modifiers Optional modifier keys (e.g., ['shift'], ['command', 'shift'])
|
|
159
|
+
*/
|
|
160
|
+
pressKey: (key: string, modifiers?: string[]) => Promise<void>;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Launch an installed app by bundle identifier
|
|
164
|
+
* @param bundleId Bundle identifier of the app to launch
|
|
165
|
+
*/
|
|
166
|
+
launchApp: (bundleId: string) => Promise<void>;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* List installed apps on the simulator
|
|
170
|
+
* @returns Array of installed apps with bundleId, name, and installType
|
|
171
|
+
*/
|
|
172
|
+
listApps: () => Promise<InstalledApp[]>;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Open a URL in the simulator (web URLs open in Safari, deep links open corresponding apps)
|
|
176
|
+
* @param url The URL to open
|
|
177
|
+
*/
|
|
178
|
+
openUrl: (url: string) => Promise<void>;
|
|
179
|
+
|
|
38
180
|
/**
|
|
39
181
|
* Disconnect from the Limrun instance
|
|
40
182
|
*/
|
|
@@ -109,11 +251,6 @@ export type InstanceClient = {
|
|
|
109
251
|
lsof: () => Promise<LsofEntry[]>;
|
|
110
252
|
};
|
|
111
253
|
|
|
112
|
-
export type LsofEntry = {
|
|
113
|
-
kind: 'unix';
|
|
114
|
-
path: string;
|
|
115
|
-
};
|
|
116
|
-
|
|
117
254
|
/**
|
|
118
255
|
* Controls the verbosity of logging in the client
|
|
119
256
|
*/
|
|
@@ -153,27 +290,38 @@ export type InstanceClientOptions = {
|
|
|
153
290
|
maxReconnectDelay?: number;
|
|
154
291
|
};
|
|
155
292
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
type ScreenshotResponse = {
|
|
162
|
-
type: 'screenshot';
|
|
163
|
-
dataUri: string;
|
|
164
|
-
id: string;
|
|
165
|
-
};
|
|
293
|
+
// ============================================================================
|
|
294
|
+
// Internal Types - Message Protocol
|
|
295
|
+
// ============================================================================
|
|
166
296
|
|
|
167
|
-
|
|
168
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Generic pending request tracker
|
|
299
|
+
*/
|
|
300
|
+
type PendingRequest<T> = {
|
|
301
|
+
resolve: (value: T) => void;
|
|
302
|
+
reject: (reason: Error) => void;
|
|
303
|
+
timeout: NodeJS.Timeout;
|
|
169
304
|
};
|
|
170
305
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
306
|
+
/**
|
|
307
|
+
* Mapping from response type to the result type it produces
|
|
308
|
+
*/
|
|
309
|
+
type ResponseTypeMap = {
|
|
310
|
+
screenshotResult: ScreenshotData;
|
|
311
|
+
viewTreeResult: string;
|
|
312
|
+
tapResult: void;
|
|
313
|
+
tapElementResult: TapElementResult;
|
|
314
|
+
incrementElementResult: ElementResult;
|
|
315
|
+
decrementElementResult: ElementResult;
|
|
316
|
+
setElementValueResult: ElementResult;
|
|
317
|
+
typeTextResult: void;
|
|
318
|
+
pressKeyResult: void;
|
|
319
|
+
launchAppResult: void;
|
|
320
|
+
listAppsResult: InstalledApp[];
|
|
321
|
+
listOpenFilesResult: LsofEntry[];
|
|
175
322
|
};
|
|
176
323
|
|
|
324
|
+
// Simctl uses streaming, so it's handled separately
|
|
177
325
|
type SimctlRequest = {
|
|
178
326
|
type: 'simctl';
|
|
179
327
|
id: string;
|
|
@@ -188,33 +336,28 @@ type SimctlStreamResponse = {
|
|
|
188
336
|
exitCode?: number;
|
|
189
337
|
};
|
|
190
338
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
type ListOpenFilesRequest = {
|
|
198
|
-
type: 'listOpenFiles';
|
|
199
|
-
id: string;
|
|
200
|
-
kind: 'unix';
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
type ListOpenFilesResponse = {
|
|
204
|
-
type: 'listOpenFilesResult';
|
|
339
|
+
/**
|
|
340
|
+
* Generic server response with optional error
|
|
341
|
+
*/
|
|
342
|
+
type ServerResponse = {
|
|
343
|
+
type: string;
|
|
205
344
|
id: string;
|
|
206
|
-
files?: LsofEntry[];
|
|
207
345
|
error?: string;
|
|
346
|
+
// Response-specific fields
|
|
347
|
+
base64?: string;
|
|
348
|
+
width?: number;
|
|
349
|
+
height?: number;
|
|
350
|
+
json?: string;
|
|
351
|
+
elementLabel?: string;
|
|
352
|
+
elementType?: string;
|
|
353
|
+
apps?: string;
|
|
354
|
+
files?: LsofEntry[];
|
|
355
|
+
// Simctl streaming fields
|
|
356
|
+
stdout?: string;
|
|
357
|
+
stderr?: string;
|
|
358
|
+
exitCode?: number;
|
|
208
359
|
};
|
|
209
360
|
|
|
210
|
-
type ServerMessage =
|
|
211
|
-
| ScreenshotResponse
|
|
212
|
-
| ScreenshotErrorResponse
|
|
213
|
-
| SimctlStreamResponse
|
|
214
|
-
| SimctlErrorResponse
|
|
215
|
-
| ListOpenFilesResponse
|
|
216
|
-
| { type: string; [key: string]: unknown };
|
|
217
|
-
|
|
218
361
|
/**
|
|
219
362
|
* Handle for a running simctl command execution.
|
|
220
363
|
*
|
|
@@ -408,22 +551,10 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
408
551
|
let intentionalDisconnect = false;
|
|
409
552
|
let lastError: string | undefined;
|
|
410
553
|
|
|
411
|
-
|
|
412
|
-
|
|
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();
|
|
554
|
+
// Centralized pending requests map - handles all request/response patterns
|
|
555
|
+
const pendingRequests: Map<string, PendingRequest<any>> = new Map();
|
|
426
556
|
|
|
557
|
+
// Simctl uses streaming, so it needs separate handling
|
|
427
558
|
const simctlExecutions: Map<string, SimctlExecution> = new Map();
|
|
428
559
|
|
|
429
560
|
const stateChangeCallbacks: Set<ConnectionStateCallback> = new Set();
|
|
@@ -459,11 +590,11 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
459
590
|
};
|
|
460
591
|
|
|
461
592
|
const failPendingRequests = (reason: string): void => {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
593
|
+
pendingRequests.forEach((request) => {
|
|
594
|
+
clearTimeout(request.timeout);
|
|
595
|
+
request.reject(new Error(reason));
|
|
596
|
+
});
|
|
597
|
+
pendingRequests.clear();
|
|
467
598
|
|
|
468
599
|
simctlExecutions.forEach((execution) => execution._handleError(new Error(reason)));
|
|
469
600
|
simctlExecutions.clear();
|
|
@@ -523,6 +654,69 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
523
654
|
}, currentDelay);
|
|
524
655
|
};
|
|
525
656
|
|
|
657
|
+
// Generate unique request ID
|
|
658
|
+
const generateId = (): string => {
|
|
659
|
+
return `ts-client-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
// Generic request sender with timeout and response handling
|
|
663
|
+
const sendRequest = <T>(
|
|
664
|
+
type: string,
|
|
665
|
+
params: Record<string, unknown> = {},
|
|
666
|
+
timeoutMs: number = 30000,
|
|
667
|
+
): Promise<T> => {
|
|
668
|
+
return new Promise((resolve, reject) => {
|
|
669
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
670
|
+
reject(new Error('WebSocket is not connected or connection is not open.'));
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const id = generateId();
|
|
675
|
+
const timeout = setTimeout(() => {
|
|
676
|
+
pendingRequests.delete(id);
|
|
677
|
+
reject(new Error(`Request ${type} timed out`));
|
|
678
|
+
}, timeoutMs);
|
|
679
|
+
|
|
680
|
+
pendingRequests.set(id, { resolve, reject, timeout });
|
|
681
|
+
|
|
682
|
+
const request = { type, id, ...params };
|
|
683
|
+
logger.debug('Sending request:', request);
|
|
684
|
+
|
|
685
|
+
ws.send(JSON.stringify(request), (err?: Error) => {
|
|
686
|
+
if (err) {
|
|
687
|
+
clearTimeout(timeout);
|
|
688
|
+
pendingRequests.delete(id);
|
|
689
|
+
logger.error(`Failed to send ${type} request:`, err);
|
|
690
|
+
reject(err);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
// Response handlers - transform raw responses to typed results
|
|
697
|
+
const responseHandlers: Record<string, (msg: ServerResponse) => unknown> = {
|
|
698
|
+
screenshotResult: (msg) => ({
|
|
699
|
+
base64: msg.base64!,
|
|
700
|
+
width: msg.width!,
|
|
701
|
+
height: msg.height!,
|
|
702
|
+
}),
|
|
703
|
+
elementTreeResult: (msg) => msg.json!,
|
|
704
|
+
tapResult: () => undefined,
|
|
705
|
+
tapElementResult: (msg) => ({
|
|
706
|
+
elementLabel: msg.elementLabel,
|
|
707
|
+
elementType: msg.elementType,
|
|
708
|
+
}),
|
|
709
|
+
incrementElementResult: (msg) => ({ elementLabel: msg.elementLabel }),
|
|
710
|
+
decrementElementResult: (msg) => ({ elementLabel: msg.elementLabel }),
|
|
711
|
+
setElementValueResult: (msg) => ({ elementLabel: msg.elementLabel }),
|
|
712
|
+
typeTextResult: () => undefined,
|
|
713
|
+
pressKeyResult: () => undefined,
|
|
714
|
+
launchAppResult: () => undefined,
|
|
715
|
+
listAppsResult: (msg) => JSON.parse(msg.apps || '[]') as InstalledApp[],
|
|
716
|
+
listOpenFilesResult: (msg) => msg.files || [],
|
|
717
|
+
openUrlResult: () => undefined,
|
|
718
|
+
};
|
|
719
|
+
|
|
526
720
|
const setupWebSocket = (): void => {
|
|
527
721
|
cleanup();
|
|
528
722
|
updateConnectionState('connecting');
|
|
@@ -530,7 +724,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
530
724
|
ws = new WebSocket(endpointWebSocketUrl);
|
|
531
725
|
|
|
532
726
|
ws.on('message', (data: Data) => {
|
|
533
|
-
let message:
|
|
727
|
+
let message: ServerResponse;
|
|
534
728
|
try {
|
|
535
729
|
message = JSON.parse(data.toString());
|
|
536
730
|
} catch (e) {
|
|
@@ -538,163 +732,66 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
538
732
|
return;
|
|
539
733
|
}
|
|
540
734
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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;
|
|
735
|
+
// Handle simctl streaming separately (it uses multiple messages per request)
|
|
736
|
+
if (message.type === 'simctlStream') {
|
|
737
|
+
const execution = simctlExecutions.get(message.id);
|
|
738
|
+
if (!execution) {
|
|
739
|
+
logger.warn(`Received simctl stream for unknown execution: ${message.id}`);
|
|
740
|
+
return;
|
|
562
741
|
}
|
|
563
|
-
case 'screenshotError': {
|
|
564
|
-
if (!('message' in message) || !('id' in message)) {
|
|
565
|
-
logger.warn('Received invalid screenshot error message:', message);
|
|
566
|
-
break;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
const errorMessage = message as ScreenshotErrorResponse;
|
|
570
|
-
const request = screenshotRequests.get(errorMessage.id);
|
|
571
742
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
743
|
+
if (message.stdout) {
|
|
744
|
+
try {
|
|
745
|
+
execution._handleStdout(Buffer.from(message.stdout, 'base64'));
|
|
746
|
+
} catch (err) {
|
|
747
|
+
logger.error('Failed to decode stdout data:', err);
|
|
577
748
|
}
|
|
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
749
|
}
|
|
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
750
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
751
|
+
if (message.stderr) {
|
|
752
|
+
try {
|
|
753
|
+
execution._handleStderr(Buffer.from(message.stderr, 'base64'));
|
|
754
|
+
} catch (err) {
|
|
755
|
+
logger.error('Failed to decode stderr data:', err);
|
|
601
756
|
}
|
|
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
757
|
}
|
|
633
|
-
case 'simctlError': {
|
|
634
|
-
if (!('message' in message) || !('id' in message)) {
|
|
635
|
-
logger.warn('Received invalid simctl error message:', message);
|
|
636
|
-
break;
|
|
637
|
-
}
|
|
638
758
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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;
|
|
759
|
+
if (message.exitCode !== undefined) {
|
|
760
|
+
logger.debug(`Simctl execution ${message.id} completed with exit code ${message.exitCode}`);
|
|
761
|
+
execution._handleExit(message.exitCode);
|
|
762
|
+
simctlExecutions.delete(message.id);
|
|
656
763
|
}
|
|
657
|
-
|
|
658
|
-
|
|
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);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
665
766
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
767
|
+
// Handle all other request/response patterns generically
|
|
768
|
+
const request = pendingRequests.get(message.id);
|
|
769
|
+
if (!request) {
|
|
770
|
+
logger.debug(`Received response for unknown or already handled request: ${message.id}`);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
672
773
|
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
}
|
|
774
|
+
clearTimeout(request.timeout);
|
|
775
|
+
pendingRequests.delete(message.id);
|
|
682
776
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
777
|
+
// Check for error
|
|
778
|
+
if (message.error) {
|
|
779
|
+
logger.error(`Server error for ${message.type}: ${message.error}`);
|
|
780
|
+
request.reject(new Error(message.error));
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
689
783
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
784
|
+
// Use handler to transform response, or resolve with raw message
|
|
785
|
+
const handler = responseHandlers[message.type];
|
|
786
|
+
if (handler) {
|
|
787
|
+
try {
|
|
788
|
+
request.resolve(handler(message));
|
|
789
|
+
} catch (err) {
|
|
790
|
+
request.reject(err as Error);
|
|
694
791
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
792
|
+
} else {
|
|
793
|
+
logger.warn('Received unexpected message type:', message.type);
|
|
794
|
+
request.resolve(message);
|
|
698
795
|
}
|
|
699
796
|
});
|
|
700
797
|
|
|
@@ -749,6 +846,17 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
749
846
|
hasResolved = true;
|
|
750
847
|
resolveConnection({
|
|
751
848
|
screenshot,
|
|
849
|
+
elementTree,
|
|
850
|
+
tap,
|
|
851
|
+
tapElement,
|
|
852
|
+
incrementElement,
|
|
853
|
+
decrementElement,
|
|
854
|
+
setElementValue,
|
|
855
|
+
typeText,
|
|
856
|
+
pressKey,
|
|
857
|
+
launchApp,
|
|
858
|
+
listApps,
|
|
859
|
+
openUrl,
|
|
752
860
|
disconnect,
|
|
753
861
|
getConnectionState,
|
|
754
862
|
onConnectionStateChange,
|
|
@@ -760,93 +868,64 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
760
868
|
});
|
|
761
869
|
};
|
|
762
870
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
}
|
|
871
|
+
// ========================================================================
|
|
872
|
+
// Client Methods - using centralized sendRequest
|
|
873
|
+
// ========================================================================
|
|
767
874
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
id,
|
|
772
|
-
};
|
|
875
|
+
const screenshot = (): Promise<ScreenshotData> => {
|
|
876
|
+
return sendRequest<ScreenshotData>('screenshot');
|
|
877
|
+
};
|
|
773
878
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
if (err) {
|
|
778
|
-
logger.error('Failed to send screenshot request:', err);
|
|
779
|
-
reject(err);
|
|
780
|
-
}
|
|
781
|
-
});
|
|
879
|
+
const elementTree = (point?: AccessibilityPoint): Promise<string> => {
|
|
880
|
+
return sendRequest<string>('elementTree', { point });
|
|
881
|
+
};
|
|
782
882
|
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
});
|
|
883
|
+
const tap = (x: number, y: number): Promise<void> => {
|
|
884
|
+
return sendRequest<void>('tap', { x, y });
|
|
803
885
|
};
|
|
804
886
|
|
|
805
|
-
const
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
}
|
|
887
|
+
const tapElement = (selector: AccessibilitySelector): Promise<TapElementResult> => {
|
|
888
|
+
return sendRequest<TapElementResult>('tapElement', { selector });
|
|
889
|
+
};
|
|
809
890
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
id,
|
|
814
|
-
kind: 'unix',
|
|
815
|
-
};
|
|
891
|
+
const incrementElement = (selector: AccessibilitySelector): Promise<ElementResult> => {
|
|
892
|
+
return sendRequest<ElementResult>('incrementElement', { selector });
|
|
893
|
+
};
|
|
816
894
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
if (err) {
|
|
821
|
-
logger.error('Failed to send listOpenFiles request:', err);
|
|
822
|
-
reject(err);
|
|
823
|
-
}
|
|
824
|
-
});
|
|
895
|
+
const decrementElement = (selector: AccessibilitySelector): Promise<ElementResult> => {
|
|
896
|
+
return sendRequest<ElementResult>('decrementElement', { selector });
|
|
897
|
+
};
|
|
825
898
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
899
|
+
const setElementValue = (text: string, selector: AccessibilitySelector): Promise<ElementResult> => {
|
|
900
|
+
return sendRequest<ElementResult>('setElementValue', { text, selector });
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
const typeText = (text: string, pressEnter?: boolean): Promise<void> => {
|
|
904
|
+
return sendRequest<void>('typeText', { text, pressEnter });
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
const pressKey = (key: string, modifiers?: string[]): Promise<void> => {
|
|
908
|
+
return sendRequest<void>('pressKey', { key, modifiers });
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
const launchApp = (bundleId: string): Promise<void> => {
|
|
912
|
+
return sendRequest<void>('launchApp', { bundleId });
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
const listApps = (): Promise<InstalledApp[]> => {
|
|
916
|
+
return sendRequest<InstalledApp[]>('listApps');
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
const openUrl = (url: string): Promise<void> => {
|
|
920
|
+
return sendRequest<void>('openUrl', { url });
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
const lsof = (): Promise<LsofEntry[]> => {
|
|
924
|
+
return sendRequest<LsofEntry[]>('listOpenFiles', { kind: 'unix' });
|
|
846
925
|
};
|
|
847
926
|
|
|
848
927
|
const simctl = (args: string[], opts: { disconnectOnExit?: boolean } = {}): SimctlExecution => {
|
|
849
|
-
const id =
|
|
928
|
+
const id = generateId();
|
|
850
929
|
|
|
851
930
|
const cancelCallback = () => {
|
|
852
931
|
// Clean up execution tracking
|