@limrun/api 0.19.2 → 0.20.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/CHANGELOG.md +19 -0
- package/client.d.mts +1 -0
- package/client.d.mts.map +1 -1
- package/client.d.ts +1 -0
- package/client.d.ts.map +1 -1
- package/client.js +10 -2
- package/client.js.map +1 -1
- package/client.mjs +10 -2
- package/client.mjs.map +1 -1
- package/exec-client.d.mts +101 -0
- package/exec-client.d.mts.map +1 -0
- package/exec-client.d.ts +101 -0
- package/exec-client.d.ts.map +1 -0
- package/exec-client.js +265 -0
- package/exec-client.js.map +1 -0
- package/exec-client.mjs +259 -0
- package/exec-client.mjs.map +1 -0
- package/folder-sync.d.mts +16 -2
- package/folder-sync.d.mts.map +1 -1
- package/folder-sync.d.ts +16 -2
- package/folder-sync.d.ts.map +1 -1
- package/folder-sync.js +43 -14
- package/folder-sync.js.map +1 -1
- package/folder-sync.mjs +43 -13
- package/folder-sync.mjs.map +1 -1
- package/index.d.mts +2 -0
- package/index.d.mts.map +1 -1
- package/index.d.ts +2 -0
- package/index.d.ts.map +1 -1
- package/index.js +5 -1
- package/index.js.map +1 -1
- package/index.mjs +2 -0
- package/index.mjs.map +1 -1
- package/internal/parse.d.mts.map +1 -1
- package/internal/parse.d.ts.map +1 -1
- package/internal/parse.js +5 -0
- package/internal/parse.js.map +1 -1
- package/internal/parse.mjs +5 -0
- package/internal/parse.mjs.map +1 -1
- package/ios-client.d.mts +60 -1
- package/ios-client.d.mts.map +1 -1
- package/ios-client.d.ts +60 -1
- package/ios-client.d.ts.map +1 -1
- package/ios-client.js +131 -2
- package/ios-client.js.map +1 -1
- package/ios-client.mjs +129 -1
- package/ios-client.mjs.map +1 -1
- package/package.json +23 -1
- package/sandbox-client.d.mts +124 -0
- package/sandbox-client.d.mts.map +1 -0
- package/sandbox-client.d.ts +124 -0
- package/sandbox-client.d.ts.map +1 -0
- package/sandbox-client.js +149 -0
- package/sandbox-client.js.map +1 -0
- package/sandbox-client.mjs +146 -0
- package/sandbox-client.mjs.map +1 -0
- package/src/client.ts +10 -2
- package/src/exec-client.ts +333 -0
- package/src/folder-sync.ts +66 -18
- package/src/index.ts +16 -0
- package/src/internal/parse.ts +6 -0
- package/src/ios-client.ts +207 -2
- package/src/sandbox-client.ts +267 -0
- package/src/version.ts +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
package/src/folder-sync.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { spawn } from 'child_process';
|
|
|
6
6
|
import { watchFolderTree } from './folder-sync-watcher';
|
|
7
7
|
import { Readable } from 'stream';
|
|
8
8
|
import * as zlib from 'zlib';
|
|
9
|
+
import ignore, { type Ignore } from 'ignore';
|
|
9
10
|
|
|
10
11
|
// =============================================================================
|
|
11
12
|
// Folder Sync (HTTP batch)
|
|
@@ -20,7 +21,7 @@ export type FolderSyncOptions = {
|
|
|
20
21
|
* Used to store the last-synced “basis” copies of files (and related sync metadata) so we can compute xdelta patches
|
|
21
22
|
* on subsequent syncs without re-downloading server state.
|
|
22
23
|
*
|
|
23
|
-
* Can be absolute or relative to process.cwd(). Defaults to `.
|
|
24
|
+
* Can be absolute or relative to process.cwd(). Defaults to `.limsync-cache/`.
|
|
24
25
|
*/
|
|
25
26
|
basisCacheDir?: string;
|
|
26
27
|
install?: boolean;
|
|
@@ -31,6 +32,21 @@ export type FolderSyncOptions = {
|
|
|
31
32
|
maxPatchBytes?: number;
|
|
32
33
|
/** Controls logging verbosity */
|
|
33
34
|
log?: (level: 'debug' | 'info' | 'warn' | 'error', msg: string) => void;
|
|
35
|
+
/**
|
|
36
|
+
* Optional filter function to include/exclude files and directories.
|
|
37
|
+
* Called with the relative path from localFolderPath (using forward slashes).
|
|
38
|
+
* For directories, the path ends with '/'.
|
|
39
|
+
* Return true to include, false to exclude.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* // Exclude build folder
|
|
43
|
+
* filter: (path) => !path.startsWith('build/')
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // Only include source files
|
|
47
|
+
* filter: (path) => path.startsWith('src/') || path.endsWith('.json')
|
|
48
|
+
*/
|
|
49
|
+
filter?: (relativePath: string) => boolean;
|
|
34
50
|
};
|
|
35
51
|
|
|
36
52
|
export type SyncFolderResult = {
|
|
@@ -63,6 +79,10 @@ type FolderSyncHttpMeta = {
|
|
|
63
79
|
type FolderSyncHttpResponse = {
|
|
64
80
|
ok: boolean;
|
|
65
81
|
needFull?: string[];
|
|
82
|
+
// Timing fields
|
|
83
|
+
syncDurationMs?: number;
|
|
84
|
+
installDurationMs?: number; // limulator only
|
|
85
|
+
// Install result fields (limulator only)
|
|
66
86
|
installedAppPath?: string;
|
|
67
87
|
bundleId?: string;
|
|
68
88
|
error?: string;
|
|
@@ -123,7 +143,7 @@ async function mapLimit<T, R>(items: T[], limit: number, fn: (item: T) => Promis
|
|
|
123
143
|
}
|
|
124
144
|
|
|
125
145
|
function folderSyncHttpUrl(apiUrl: string): string {
|
|
126
|
-
return `${apiUrl}/
|
|
146
|
+
return `${apiUrl}/sync`;
|
|
127
147
|
}
|
|
128
148
|
|
|
129
149
|
function u32be(n: number): Buffer {
|
|
@@ -226,23 +246,42 @@ async function sha256FileHex(filePath: string): Promise<string> {
|
|
|
226
246
|
});
|
|
227
247
|
}
|
|
228
248
|
|
|
229
|
-
async function walkFiles(root: string): Promise<FileEntry[]> {
|
|
230
|
-
const out: FileEntry[] = [];
|
|
231
|
-
const stack: string[] = [root];
|
|
249
|
+
async function walkFiles(root: string, filter?: (relativePath: string) => boolean): Promise<FileEntry[]> {
|
|
232
250
|
const rootResolved = path.resolve(root);
|
|
251
|
+
|
|
252
|
+
// Load .gitignore if it exists
|
|
253
|
+
const ig = await loadGitignore(rootResolved);
|
|
254
|
+
|
|
255
|
+
const out: FileEntry[] = [];
|
|
256
|
+
const stack: string[] = [rootResolved];
|
|
233
257
|
while (stack.length) {
|
|
234
258
|
const dir = stack.pop()!;
|
|
235
259
|
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
236
260
|
for (const ent of entries) {
|
|
237
|
-
|
|
261
|
+
// Always skip .git folder and .DS_Store
|
|
262
|
+
if (ent.name === '.DS_Store' || ent.name === '.git') continue;
|
|
263
|
+
|
|
238
264
|
const abs = path.join(dir, ent.name);
|
|
265
|
+
const rel = path.relative(rootResolved, abs).split(path.sep).join('/');
|
|
266
|
+
|
|
267
|
+
// Check if ignored by .gitignore
|
|
268
|
+
if (ig.ignores(rel)) continue;
|
|
269
|
+
|
|
239
270
|
if (ent.isDirectory()) {
|
|
271
|
+
// For directories, check with trailing slash
|
|
272
|
+
const relDir = rel + '/';
|
|
273
|
+
if (ig.ignores(relDir)) continue;
|
|
274
|
+
// Check custom filter (directories have trailing slash)
|
|
275
|
+
if (filter && !filter(relDir)) continue;
|
|
240
276
|
stack.push(abs);
|
|
241
277
|
continue;
|
|
242
278
|
}
|
|
243
279
|
if (!ent.isFile()) continue;
|
|
280
|
+
|
|
281
|
+
// Check custom filter for files
|
|
282
|
+
if (filter && !filter(rel)) continue;
|
|
283
|
+
|
|
244
284
|
const st = await fs.promises.stat(abs);
|
|
245
|
-
const rel = path.relative(rootResolved, abs).split(path.sep).join('/');
|
|
246
285
|
const sha256 = await sha256FileHex(abs);
|
|
247
286
|
// Preserve POSIX permission bits (including +x). Mask out file-type bits.
|
|
248
287
|
const mode = st.mode & 0o7777;
|
|
@@ -253,6 +292,22 @@ async function walkFiles(root: string): Promise<FileEntry[]> {
|
|
|
253
292
|
return out;
|
|
254
293
|
}
|
|
255
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Load and parse .gitignore file if it exists.
|
|
297
|
+
* Returns an Ignore instance that can be used to test paths.
|
|
298
|
+
*/
|
|
299
|
+
async function loadGitignore(rootDir: string): Promise<Ignore> {
|
|
300
|
+
const ig = ignore();
|
|
301
|
+
const gitignorePath = path.join(rootDir, '.gitignore');
|
|
302
|
+
try {
|
|
303
|
+
const content = await fs.promises.readFile(gitignorePath, 'utf-8');
|
|
304
|
+
ig.add(content);
|
|
305
|
+
} catch {
|
|
306
|
+
// No .gitignore file, return empty ignore instance
|
|
307
|
+
}
|
|
308
|
+
return ig;
|
|
309
|
+
}
|
|
310
|
+
|
|
256
311
|
let xdelta3Ready: Promise<void> | null = null;
|
|
257
312
|
async function ensureXdelta3(): Promise<void> {
|
|
258
313
|
if (!xdelta3Ready) {
|
|
@@ -291,7 +346,7 @@ function localBasisCacheRoot(opts: FolderSyncOptions, localFolderPath: string):
|
|
|
291
346
|
const rootOverride =
|
|
292
347
|
opts.basisCacheDir ?
|
|
293
348
|
path.resolve(process.cwd(), opts.basisCacheDir)
|
|
294
|
-
: path.join(process.cwd(), '.
|
|
349
|
+
: path.join(process.cwd(), '.limsync-cache');
|
|
295
350
|
// Include folder identity to avoid collisions between different roots.
|
|
296
351
|
return path.join(rootOverride, 'folder-sync', hostKey, opts.udid, `${base}-${hash}`);
|
|
297
352
|
}
|
|
@@ -310,7 +365,8 @@ export type SyncAppResult = SyncFolderResult;
|
|
|
310
365
|
|
|
311
366
|
export async function syncApp(localFolderPath: string, opts: FolderSyncOptions): Promise<SyncFolderResult> {
|
|
312
367
|
if (!opts.watch) {
|
|
313
|
-
|
|
368
|
+
const result = await syncFolderOnce(localFolderPath, opts);
|
|
369
|
+
return result;
|
|
314
370
|
}
|
|
315
371
|
// Initial sync, then watch for changes and re-run sync in the background.
|
|
316
372
|
const first = await syncFolderOnce(localFolderPath, opts, 'startup');
|
|
@@ -353,14 +409,6 @@ export async function syncApp(localFolderPath: string, opts: FolderSyncOptions):
|
|
|
353
409
|
};
|
|
354
410
|
}
|
|
355
411
|
|
|
356
|
-
// Back-compat alias (older callers)
|
|
357
|
-
export async function syncFolder(
|
|
358
|
-
localFolderPath: string,
|
|
359
|
-
opts: FolderSyncOptions,
|
|
360
|
-
): Promise<SyncFolderResult> {
|
|
361
|
-
return await syncApp(localFolderPath, opts);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
412
|
async function syncFolderOnce(
|
|
365
413
|
localFolderPath: string,
|
|
366
414
|
opts: FolderSyncOptions,
|
|
@@ -377,7 +425,7 @@ async function syncFolderOnce(
|
|
|
377
425
|
const tEnsureMs = nowMs() - tEnsureStart;
|
|
378
426
|
|
|
379
427
|
const tWalkStart = nowMs();
|
|
380
|
-
const files = await walkFiles(localFolderPath);
|
|
428
|
+
const files = await walkFiles(localFolderPath, opts.filter);
|
|
381
429
|
const tWalkMs = nowMs() - tWalkStart;
|
|
382
430
|
const fileMap = new Map(files.map((f) => [f.path, f]));
|
|
383
431
|
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,22 @@ export { Limrun, type ClientOptions } from './client';
|
|
|
8
8
|
export { PagePromise } from './core/pagination';
|
|
9
9
|
export * from './instance-client';
|
|
10
10
|
export * as Ios from './ios-client';
|
|
11
|
+
export {
|
|
12
|
+
createXCodeSandboxClient,
|
|
13
|
+
type XCodeSandboxClient,
|
|
14
|
+
type CreateXCodeSandboxClientOptions,
|
|
15
|
+
type SimulatorConfig,
|
|
16
|
+
type SyncOptions,
|
|
17
|
+
type SyncResult,
|
|
18
|
+
type XcodeBuildConfig,
|
|
19
|
+
} from './sandbox-client';
|
|
20
|
+
export {
|
|
21
|
+
exec,
|
|
22
|
+
type ExecRequest,
|
|
23
|
+
type ExecOptions,
|
|
24
|
+
type ExecResult,
|
|
25
|
+
type ExecChildProcess,
|
|
26
|
+
} from './exec-client';
|
|
11
27
|
export {
|
|
12
28
|
LimrunError,
|
|
13
29
|
APIError,
|
package/src/internal/parse.ts
CHANGED
|
@@ -29,6 +29,12 @@ export async function defaultParseResponse<T>(client: Limrun, props: APIResponse
|
|
|
29
29
|
const mediaType = contentType?.split(';')[0]?.trim();
|
|
30
30
|
const isJSON = mediaType?.includes('application/json') || mediaType?.endsWith('+json');
|
|
31
31
|
if (isJSON) {
|
|
32
|
+
const contentLength = response.headers.get('content-length');
|
|
33
|
+
if (contentLength === '0') {
|
|
34
|
+
// if there is no content we can't do anything
|
|
35
|
+
return undefined as T;
|
|
36
|
+
}
|
|
37
|
+
|
|
32
38
|
const json = await response.json();
|
|
33
39
|
return json as T;
|
|
34
40
|
}
|
package/src/ios-client.ts
CHANGED
|
@@ -153,12 +153,22 @@ export type InstanceClient = {
|
|
|
153
153
|
elementTree: (point?: AccessibilityPoint) => Promise<string>;
|
|
154
154
|
|
|
155
155
|
/**
|
|
156
|
-
* Tap at the specified coordinates
|
|
156
|
+
* Tap at the specified coordinates (uses device's native screen dimensions)
|
|
157
157
|
* @param x X coordinate in points
|
|
158
158
|
* @param y Y coordinate in points
|
|
159
159
|
*/
|
|
160
160
|
tap: (x: number, y: number) => Promise<void>;
|
|
161
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Tap at coordinates with explicit screen size.
|
|
164
|
+
* Use this when coordinates are in a different coordinate space than the device's native dimensions.
|
|
165
|
+
* @param x X coordinate in the provided screen coordinate space
|
|
166
|
+
* @param y Y coordinate in the provided screen coordinate space
|
|
167
|
+
* @param screenWidth Width of the coordinate space
|
|
168
|
+
* @param screenHeight Height of the coordinate space
|
|
169
|
+
*/
|
|
170
|
+
tapWithScreenSize: (x: number, y: number, screenWidth: number, screenHeight: number) => Promise<void>;
|
|
171
|
+
|
|
162
172
|
/**
|
|
163
173
|
* Tap an accessibility element by selector
|
|
164
174
|
* @param selector The selector criteria to find the element
|
|
@@ -209,6 +219,26 @@ export type InstanceClient = {
|
|
|
209
219
|
*/
|
|
210
220
|
launchApp: (bundleId: string) => Promise<void>;
|
|
211
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Fetch the last N lines of app logs (combined stdout/stderr)
|
|
224
|
+
* @param bundleId Bundle identifier of the app
|
|
225
|
+
* @param lines Number of lines to return (clamped to server limit)
|
|
226
|
+
*/
|
|
227
|
+
appLogTail: (bundleId: string, lines: number) => Promise<string>;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Stream app logs for a bundle ID (batched lines every ~500ms)
|
|
231
|
+
* @param bundleId Bundle identifier of the app
|
|
232
|
+
* @returns LogStream handle (use .stop() to unsubscribe)
|
|
233
|
+
*/
|
|
234
|
+
streamAppLog: (bundleId: string) => LogStream;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Stream syslog (batched lines every ~500ms)
|
|
238
|
+
* @returns LogStream handle (use .stop() to unsubscribe)
|
|
239
|
+
*/
|
|
240
|
+
streamSyslog: () => LogStream;
|
|
241
|
+
|
|
212
242
|
/**
|
|
213
243
|
* List installed apps on the simulator
|
|
214
244
|
* @returns Array of installed apps with bundleId, name, and installType
|
|
@@ -479,6 +509,10 @@ type ServerResponse = {
|
|
|
479
509
|
stdout?: string;
|
|
480
510
|
stderr?: string;
|
|
481
511
|
exitCode?: number;
|
|
512
|
+
// Log tail fields
|
|
513
|
+
logs?: string;
|
|
514
|
+
// App log streaming fields
|
|
515
|
+
lines?: string[];
|
|
482
516
|
};
|
|
483
517
|
|
|
484
518
|
/**
|
|
@@ -653,6 +687,134 @@ export class SimctlExecution extends EventEmitter {
|
|
|
653
687
|
}
|
|
654
688
|
}
|
|
655
689
|
|
|
690
|
+
/**
|
|
691
|
+
* Handle for a running log stream subscription (app logs or syslog).
|
|
692
|
+
*
|
|
693
|
+
* Uses a dedicated WebSocket connection separate from the main signaling connection.
|
|
694
|
+
* Emits batched log lines every ~500ms when new lines arrive.
|
|
695
|
+
*/
|
|
696
|
+
export interface LogStreamEvents {
|
|
697
|
+
lines: (lines: string[]) => void;
|
|
698
|
+
line: (line: string) => void;
|
|
699
|
+
error: (error: Error) => void;
|
|
700
|
+
close: () => void;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/** @internal - Message from log stream WebSocket */
|
|
704
|
+
type LogStreamMessage = {
|
|
705
|
+
type: string;
|
|
706
|
+
id: string;
|
|
707
|
+
lines?: string[];
|
|
708
|
+
error?: string;
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Log stream with dedicated WebSocket connection.
|
|
713
|
+
* Each LogStream opens its own WebSocket to isolate log traffic from signaling.
|
|
714
|
+
*/
|
|
715
|
+
export class LogStream extends EventEmitter {
|
|
716
|
+
private ws: WebSocket | null = null;
|
|
717
|
+
private subscriptionId: string;
|
|
718
|
+
private stopped = false;
|
|
719
|
+
private terminateMessageType: string;
|
|
720
|
+
|
|
721
|
+
/** @internal */
|
|
722
|
+
constructor(
|
|
723
|
+
private wsUrl: string,
|
|
724
|
+
private subscribeMessage: object,
|
|
725
|
+
terminateMessageType: string,
|
|
726
|
+
subscriptionId: string,
|
|
727
|
+
) {
|
|
728
|
+
super();
|
|
729
|
+
this.terminateMessageType = terminateMessageType;
|
|
730
|
+
this.subscriptionId = subscriptionId;
|
|
731
|
+
this._connect();
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
override on<E extends keyof LogStreamEvents>(event: E, listener: LogStreamEvents[E]): this {
|
|
735
|
+
return super.on(event, listener as any);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
override once<E extends keyof LogStreamEvents>(event: E, listener: LogStreamEvents[E]): this {
|
|
739
|
+
return super.once(event, listener as any);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
override off<E extends keyof LogStreamEvents>(event: E, listener: LogStreamEvents[E]): this {
|
|
743
|
+
return super.off(event, listener as any);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/** Stop the log stream and close the dedicated WebSocket connection */
|
|
747
|
+
stop(): void {
|
|
748
|
+
if (this.stopped) return;
|
|
749
|
+
this.stopped = true;
|
|
750
|
+
|
|
751
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
752
|
+
// Send terminate message before closing
|
|
753
|
+
const terminateMsg = { type: this.terminateMessageType, id: this.subscriptionId };
|
|
754
|
+
try {
|
|
755
|
+
this.ws.send(JSON.stringify(terminateMsg));
|
|
756
|
+
} catch {
|
|
757
|
+
// Ignore send errors during shutdown
|
|
758
|
+
}
|
|
759
|
+
this.ws.close();
|
|
760
|
+
}
|
|
761
|
+
this.ws = null;
|
|
762
|
+
this.emit('close');
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/** @internal - Establish the dedicated WebSocket connection */
|
|
766
|
+
private _connect(): void {
|
|
767
|
+
this.ws = new WebSocket(this.wsUrl);
|
|
768
|
+
|
|
769
|
+
this.ws.on('open', () => {
|
|
770
|
+
if (this.stopped) {
|
|
771
|
+
this.ws?.close();
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
// Send subscription message
|
|
775
|
+
this.ws?.send(JSON.stringify(this.subscribeMessage), (err?: Error) => {
|
|
776
|
+
if (err) {
|
|
777
|
+
this.emit('error', err);
|
|
778
|
+
this.stop();
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
this.ws.on('message', (data: Data) => {
|
|
784
|
+
if (this.stopped) return;
|
|
785
|
+
try {
|
|
786
|
+
const message: LogStreamMessage = JSON.parse(data.toString());
|
|
787
|
+
if (message.error) {
|
|
788
|
+
this.emit('error', new Error(message.error));
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (message.lines && message.lines.length > 0) {
|
|
792
|
+
this.emit('lines', message.lines);
|
|
793
|
+
for (const line of message.lines) {
|
|
794
|
+
this.emit('line', line);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
} catch (e) {
|
|
798
|
+
// Ignore parse errors for non-JSON messages
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
this.ws.on('error', (err: Error) => {
|
|
803
|
+
if (!this.stopped) {
|
|
804
|
+
this.emit('error', err);
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
this.ws.on('close', () => {
|
|
809
|
+
if (!this.stopped) {
|
|
810
|
+
this.stopped = true;
|
|
811
|
+
this.emit('close');
|
|
812
|
+
}
|
|
813
|
+
this.ws = null;
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
656
818
|
/**
|
|
657
819
|
* Creates a client for interacting with a Limrun iOS instance
|
|
658
820
|
* @param options Configuration options including webrtcUrl, token and log level
|
|
@@ -835,6 +997,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
835
997
|
typeTextResult: () => undefined,
|
|
836
998
|
pressKeyResult: () => undefined,
|
|
837
999
|
launchAppResult: () => undefined,
|
|
1000
|
+
appLogTailResult: (msg) => msg.logs ?? '',
|
|
838
1001
|
listAppsResult: (msg) => JSON.parse(msg.apps || '[]') as InstalledApp[],
|
|
839
1002
|
listOpenFilesResult: (msg) => msg.files || [],
|
|
840
1003
|
deviceInfoResult: (msg) => ({
|
|
@@ -1003,6 +1166,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
1003
1166
|
screenshot,
|
|
1004
1167
|
elementTree,
|
|
1005
1168
|
tap,
|
|
1169
|
+
tapWithScreenSize,
|
|
1006
1170
|
tapElement,
|
|
1007
1171
|
incrementElement,
|
|
1008
1172
|
decrementElement,
|
|
@@ -1010,6 +1174,9 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
1010
1174
|
typeText,
|
|
1011
1175
|
pressKey,
|
|
1012
1176
|
launchApp,
|
|
1177
|
+
appLogTail,
|
|
1178
|
+
streamAppLog,
|
|
1179
|
+
streamSyslog,
|
|
1013
1180
|
listApps,
|
|
1014
1181
|
openUrl,
|
|
1015
1182
|
installApp,
|
|
@@ -1043,7 +1210,29 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
1043
1210
|
};
|
|
1044
1211
|
|
|
1045
1212
|
const tap = (x: number, y: number): Promise<void> => {
|
|
1046
|
-
return sendRequest<void>('tap', {
|
|
1213
|
+
return sendRequest<void>('tap', {
|
|
1214
|
+
x,
|
|
1215
|
+
y,
|
|
1216
|
+
screenWidth: cachedDeviceInfo?.screenWidth,
|
|
1217
|
+
screenHeight: cachedDeviceInfo?.screenHeight,
|
|
1218
|
+
});
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Tap at coordinates with explicit screen size.
|
|
1223
|
+
* Use this when coordinates are in a different coordinate space than the device's native dimensions.
|
|
1224
|
+
* @param x - X coordinate in the provided screen coordinate space
|
|
1225
|
+
* @param y - Y coordinate in the provided screen coordinate space
|
|
1226
|
+
* @param screenWidth - Width of the coordinate space
|
|
1227
|
+
* @param screenHeight - Height of the coordinate space
|
|
1228
|
+
*/
|
|
1229
|
+
const tapWithScreenSize = (
|
|
1230
|
+
x: number,
|
|
1231
|
+
y: number,
|
|
1232
|
+
screenWidth: number,
|
|
1233
|
+
screenHeight: number,
|
|
1234
|
+
): Promise<void> => {
|
|
1235
|
+
return sendRequest<void>('tap', { x, y, screenWidth, screenHeight });
|
|
1047
1236
|
};
|
|
1048
1237
|
|
|
1049
1238
|
const tapElement = (selector: AccessibilitySelector): Promise<TapElementResult> => {
|
|
@@ -1074,6 +1263,22 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
1074
1263
|
return sendRequest<void>('launchApp', { bundleId });
|
|
1075
1264
|
};
|
|
1076
1265
|
|
|
1266
|
+
const appLogTail = (bundleId: string, lines: number): Promise<string> => {
|
|
1267
|
+
return sendRequest<string>('appLogTail', { bundleId, lines });
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1270
|
+
const streamAppLog = (bundleId: string): LogStream => {
|
|
1271
|
+
const id = generateId();
|
|
1272
|
+
const subscribeMessage = { type: 'streamAppLog', id, bundleId };
|
|
1273
|
+
return new LogStream(endpointWebSocketUrl, subscribeMessage, 'streamAppLogTerminate', id);
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1276
|
+
const streamSyslog = (): LogStream => {
|
|
1277
|
+
const id = generateId();
|
|
1278
|
+
const subscribeMessage = { type: 'streamSyslog', id };
|
|
1279
|
+
return new LogStream(endpointWebSocketUrl, subscribeMessage, 'streamSyslogTerminate', id);
|
|
1280
|
+
};
|
|
1281
|
+
|
|
1077
1282
|
const listApps = (): Promise<InstalledApp[]> => {
|
|
1078
1283
|
return sendRequest<InstalledApp[]>('listApps');
|
|
1079
1284
|
};
|