@limrun/api 0.23.2 → 0.24.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.
Files changed (123) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/client.d.mts +3 -0
  3. package/client.d.mts.map +1 -1
  4. package/client.d.ts +3 -0
  5. package/client.d.ts.map +1 -1
  6. package/client.js +3 -0
  7. package/client.js.map +1 -1
  8. package/client.mjs +3 -0
  9. package/client.mjs.map +1 -1
  10. package/exec-client.d.mts.map +1 -1
  11. package/exec-client.d.ts.map +1 -1
  12. package/exec-client.js +4 -2
  13. package/exec-client.js.map +1 -1
  14. package/exec-client.mjs +4 -2
  15. package/exec-client.mjs.map +1 -1
  16. package/folder-sync.d.mts.map +1 -1
  17. package/folder-sync.d.ts.map +1 -1
  18. package/folder-sync.js +5 -2
  19. package/folder-sync.js.map +1 -1
  20. package/folder-sync.mjs +5 -2
  21. package/folder-sync.mjs.map +1 -1
  22. package/instance-client.d.mts +19 -4
  23. package/instance-client.d.mts.map +1 -1
  24. package/instance-client.d.ts +19 -4
  25. package/instance-client.d.ts.map +1 -1
  26. package/instance-client.js +42 -2
  27. package/instance-client.js.map +1 -1
  28. package/instance-client.mjs +42 -2
  29. package/instance-client.mjs.map +1 -1
  30. package/internal/download-file.d.mts +2 -0
  31. package/internal/download-file.d.mts.map +1 -0
  32. package/internal/download-file.d.ts +2 -0
  33. package/internal/download-file.d.ts.map +1 -0
  34. package/internal/download-file.js +35 -0
  35. package/internal/download-file.js.map +1 -0
  36. package/internal/download-file.mjs +31 -0
  37. package/internal/download-file.mjs.map +1 -0
  38. package/internal/proxy-transport.d.mts +14 -0
  39. package/internal/proxy-transport.d.mts.map +1 -0
  40. package/internal/proxy-transport.d.ts +14 -0
  41. package/internal/proxy-transport.d.ts.map +1 -0
  42. package/internal/proxy-transport.js +59 -0
  43. package/internal/proxy-transport.js.map +1 -0
  44. package/internal/proxy-transport.mjs +56 -0
  45. package/internal/proxy-transport.mjs.map +1 -0
  46. package/internal/shims.d.mts.map +1 -1
  47. package/internal/shims.d.ts.map +1 -1
  48. package/internal/shims.js +2 -1
  49. package/internal/shims.js.map +1 -1
  50. package/internal/shims.mjs +2 -1
  51. package/internal/shims.mjs.map +1 -1
  52. package/internal/utils/env.js +2 -2
  53. package/internal/utils/env.js.map +1 -1
  54. package/internal/utils/env.mjs +2 -2
  55. package/internal/utils/env.mjs.map +1 -1
  56. package/ios-client.d.mts.map +1 -1
  57. package/ios-client.d.ts.map +1 -1
  58. package/ios-client.js +11 -34
  59. package/ios-client.js.map +1 -1
  60. package/ios-client.mjs +10 -33
  61. package/ios-client.mjs.map +1 -1
  62. package/package.json +4 -1
  63. package/resources/android-instances.d.mts +5 -4
  64. package/resources/android-instances.d.mts.map +1 -1
  65. package/resources/android-instances.d.ts +5 -4
  66. package/resources/android-instances.d.ts.map +1 -1
  67. package/resources/assets-helpers.d.mts.map +1 -1
  68. package/resources/assets-helpers.d.ts.map +1 -1
  69. package/resources/assets-helpers.js +2 -1
  70. package/resources/assets-helpers.js.map +1 -1
  71. package/resources/assets-helpers.mjs +2 -1
  72. package/resources/assets-helpers.mjs.map +1 -1
  73. package/resources/index.d.mts +1 -0
  74. package/resources/index.d.mts.map +1 -1
  75. package/resources/index.d.ts +1 -0
  76. package/resources/index.d.ts.map +1 -1
  77. package/resources/index.js +3 -1
  78. package/resources/index.js.map +1 -1
  79. package/resources/index.mjs +1 -0
  80. package/resources/index.mjs.map +1 -1
  81. package/resources/ios-instances.d.mts +4 -4
  82. package/resources/ios-instances.d.ts +4 -4
  83. package/resources/xcode-instances.d.mts +122 -0
  84. package/resources/xcode-instances.d.mts.map +1 -0
  85. package/resources/xcode-instances.d.ts +122 -0
  86. package/resources/xcode-instances.d.ts.map +1 -0
  87. package/resources/xcode-instances.js +40 -0
  88. package/resources/xcode-instances.js.map +1 -0
  89. package/resources/xcode-instances.mjs +36 -0
  90. package/resources/xcode-instances.mjs.map +1 -0
  91. package/sandbox-client.d.mts.map +1 -1
  92. package/sandbox-client.d.ts.map +1 -1
  93. package/sandbox-client.js +2 -1
  94. package/sandbox-client.js.map +1 -1
  95. package/sandbox-client.mjs +2 -1
  96. package/sandbox-client.mjs.map +1 -1
  97. package/src/client.ts +17 -0
  98. package/src/exec-client.ts +4 -2
  99. package/src/folder-sync.ts +15 -12
  100. package/src/instance-client.ts +86 -8
  101. package/src/internal/download-file.ts +33 -0
  102. package/src/internal/proxy-transport.ts +69 -0
  103. package/src/internal/shims.ts +2 -1
  104. package/src/internal/utils/env.ts +2 -2
  105. package/src/ios-client.ts +10 -35
  106. package/src/resources/android-instances.ts +6 -4
  107. package/src/resources/assets-helpers.ts +2 -1
  108. package/src/resources/index.ts +7 -0
  109. package/src/resources/ios-instances.ts +4 -4
  110. package/src/resources/xcode-instances.ts +177 -0
  111. package/src/sandbox-client.ts +2 -1
  112. package/src/tunnel.ts +5 -0
  113. package/src/version.ts +1 -1
  114. package/tunnel.d.mts.map +1 -1
  115. package/tunnel.d.ts.map +1 -1
  116. package/tunnel.js +5 -0
  117. package/tunnel.js.map +1 -1
  118. package/tunnel.mjs +5 -0
  119. package/tunnel.mjs.map +1 -1
  120. package/version.d.mts +1 -1
  121. package/version.d.ts +1 -1
  122. package/version.js +1 -1
  123. package/version.mjs +1 -1
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { createEventSource, type EventSourceClient, type EventSourceMessage } from 'eventsource-client';
9
+ import { nodeProxyTransport } from './internal/proxy-transport';
9
10
 
10
11
  // =============================================================================
11
12
  // Types
@@ -153,7 +154,7 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
153
154
  return;
154
155
  }
155
156
  try {
156
- await fetch(`${this.options.apiUrl}/exec/${this.execId}/cancel`, {
157
+ await nodeProxyTransport.fetch(`${this.options.apiUrl}/exec/${this.execId}/cancel`, {
157
158
  method: 'POST',
158
159
  headers: {
159
160
  Authorization: `Bearer ${this.options.token}`,
@@ -172,7 +173,7 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
172
173
  // 1. Trigger the build via POST /exec
173
174
  let execRes: Response;
174
175
  try {
175
- execRes = await fetch(`${apiUrl}/exec`, {
176
+ execRes = await nodeProxyTransport.fetch(`${apiUrl}/exec`, {
176
177
  method: 'POST',
177
178
  headers: {
178
179
  'Content-Type': 'application/json',
@@ -273,6 +274,7 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
273
274
  try {
274
275
  const eventSource = createEventSource({
275
276
  url: eventsUrl,
277
+ fetch: nodeProxyTransport.fetch,
276
278
  headers: { Authorization: `Bearer ${this.options.token}` },
277
279
  onMessage: (message: EventSourceMessage) => {
278
280
  const data = typeof message.data === 'string' ? message.data : String(message.data ?? '');
@@ -7,6 +7,7 @@ import { watchFolderTree } from './folder-sync-watcher';
7
7
  import { type IgnoreFn } from './folder-sync-ignore';
8
8
  import { Readable } from 'stream';
9
9
  import * as zlib from 'zlib';
10
+ import { nodeProxyTransport } from './internal/proxy-transport';
10
11
 
11
12
  // =============================================================================
12
13
  // Folder Sync (HTTP batch)
@@ -218,18 +219,20 @@ async function httpFolderSyncBatch(
218
219
  };
219
220
  sourceStream.on('error', onStreamError);
220
221
  bodyStream.on('error', onStreamError);
221
- const res = await fetch(url, {
222
- method: 'POST',
223
- headers,
224
- body: bodyStream as any,
225
- duplex: 'half' as any,
226
- signal: controller.signal,
227
- } as any).catch((err) => {
228
- if (streamError) {
229
- throw streamError;
230
- }
231
- throw err;
232
- });
222
+ const res = await nodeProxyTransport
223
+ .fetch(url, {
224
+ method: 'POST',
225
+ headers,
226
+ body: bodyStream as any,
227
+ duplex: 'half' as any,
228
+ signal: controller.signal,
229
+ } as any)
230
+ .catch((err) => {
231
+ if (streamError) {
232
+ throw streamError;
233
+ }
234
+ throw err;
235
+ });
233
236
  const text = await res.text();
234
237
  if (!res.ok) {
235
238
  throw new Error(`folder-sync http failed: ${res.status} ${text}`);
@@ -1,9 +1,14 @@
1
1
  import { WebSocket, Data } from 'ws';
2
2
  import { exec } from 'node:child_process';
3
3
 
4
+ import { downloadFileToLocalPath } from './internal/download-file';
5
+ import { nodeProxyTransport } from './internal/proxy-transport';
4
6
  import { startTcpTunnel, isNonRetryableError } from './tunnel';
5
7
  import type { Tunnel } from './tunnel';
6
8
 
9
+ const ANDROID_RECORDING_PATH = '/data/local/tmp/recordings/video_recording.mp4';
10
+ const ANDROID_SIGNALING_PATH = '/ws';
11
+
7
12
  /**
8
13
  * Connection state of the instance client
9
14
  */
@@ -14,6 +19,19 @@ export type ConnectionState = 'connecting' | 'connected' | 'disconnected' | 'rec
14
19
  */
15
20
  export type ConnectionStateCallback = (state: ConnectionState) => void;
16
21
 
22
+ function deriveEndpointWebSocketUrl(apiUrl: string): string {
23
+ const parsed = new URL(apiUrl);
24
+ parsed.protocol = parsed.protocol === 'https:' ? 'wss:' : 'ws:';
25
+ parsed.search = '';
26
+ parsed.hash = '';
27
+ parsed.pathname = `${parsed.pathname.replace(/\/$/, '')}${ANDROID_SIGNALING_PATH}`;
28
+ return parsed.toString().replace(/\/$/, '');
29
+ }
30
+
31
+ function buildDownloadUrl(apiUrl: string): string {
32
+ return `${apiUrl}/files?path=${encodeURIComponent(ANDROID_RECORDING_PATH)}`;
33
+ }
34
+
17
35
  /**
18
36
  * A client for interacting with a Limbar instance
19
37
  */
@@ -64,6 +82,17 @@ export type InstanceClient = {
64
82
  * Open a URL/deeplink on Android.
65
83
  */
66
84
  openUrl: (url: string) => Promise<OpenUrlResult>;
85
+ /**
86
+ * Start recording device video. Use stopRecording() to finish the recording.
87
+ */
88
+ startRecording: () => Promise<void>;
89
+ /**
90
+ * Stop the active server-side recording.
91
+ * If `saveTo.presignedUrl` is provided, the server uploads the completed file there before resolving.
92
+ * If `saveTo.localPath` is provided, the client downloads the completed file to that path.
93
+ * Returns a download URL for the completed recording.
94
+ */
95
+ stopRecording: (saveTo: { presignedUrl?: string; localPath?: string }) => Promise<string>;
67
96
  /**
68
97
  * Disconnect from the Limbar instance
69
98
  */
@@ -151,13 +180,14 @@ export type AndroidElementNode = {
151
180
  */
152
181
  export type InstanceClientOptions = {
153
182
  /**
154
- * The URL of the ADB WebSocket endpoint.
183
+ * HTTP base URL for the Android daemon. WebSocket control is derived from it
184
+ * using the `/ws` path, and recording downloads use the same base URL.
155
185
  */
156
- adbUrl: string;
186
+ apiUrl: string;
157
187
  /**
158
- * The URL of the main endpoint WebSocket.
188
+ * The URL of the ADB WebSocket endpoint.
159
189
  */
160
- endpointUrl: string;
190
+ adbUrl?: string;
161
191
  /**
162
192
  * The token to use for the WebSocket connections.
163
193
  */
@@ -234,6 +264,8 @@ export type OpenUrlResult = {
234
264
  url: string;
235
265
  };
236
266
 
267
+ type EmptyCommandResult = Record<string, never>;
268
+
237
269
  type ScreenshotErrorResponse = {
238
270
  type: 'screenshotError';
239
271
  message: string;
@@ -321,6 +353,20 @@ type OpenUrlResultMessage = {
321
353
  error?: CommandError;
322
354
  };
323
355
 
356
+ type StartVideoRecordingResultMessage = {
357
+ type: 'startRecordingResult';
358
+ id: string;
359
+ payload?: EmptyCommandResult;
360
+ error?: CommandError;
361
+ };
362
+
363
+ type StopVideoRecordingResultMessage = {
364
+ type: 'stopRecordingResult';
365
+ id: string;
366
+ payload?: EmptyCommandResult;
367
+ error?: CommandError;
368
+ };
369
+
324
370
  type KnownCommandResultMessage =
325
371
  | ScreenshotResultMessage
326
372
  | GetElementTreeResultMessage
@@ -330,7 +376,9 @@ type KnownCommandResultMessage =
330
376
  | PressKeyResultMessage
331
377
  | ScrollScreenResultMessage
332
378
  | ScrollElementResultMessage
333
- | OpenUrlResultMessage;
379
+ | OpenUrlResultMessage
380
+ | StartVideoRecordingResultMessage
381
+ | StopVideoRecordingResultMessage;
334
382
 
335
383
  type ServerMessage =
336
384
  | ScreenshotResponse
@@ -349,6 +397,8 @@ type CommandRequestMap = {
349
397
  scrollScreen: { direction: ScrollDirection; amount?: number };
350
398
  scrollElement: AndroidElementTarget & { direction: ScrollDirection; amount?: number };
351
399
  openUrl: { url: string };
400
+ startRecording: Record<string, never>;
401
+ stopRecording: { upload?: { presignedUrl: string } };
352
402
  };
353
403
 
354
404
  type CommandResultMap = {
@@ -361,6 +411,8 @@ type CommandResultMap = {
361
411
  scrollScreen: ScrollResult;
362
412
  scrollElement: ScrollResult;
363
413
  openUrl: OpenUrlResult;
414
+ startRecording: EmptyCommandResult;
415
+ stopRecording: EmptyCommandResult;
364
416
  };
365
417
 
366
418
  type PendingRequest<T> = {
@@ -375,7 +427,9 @@ type PendingRequest<T> = {
375
427
  * @returns An InstanceClient for controlling the instance
376
428
  */
377
429
  export async function createInstanceClient(options: InstanceClientOptions): Promise<InstanceClient> {
378
- const serverAddress = `${options.endpointUrl}?token=${options.token}`;
430
+ const endpointWebSocketUrl = deriveEndpointWebSocketUrl(options.apiUrl);
431
+ const serverAddress = `${endpointWebSocketUrl}?token=${options.token}`;
432
+ const recordingApiUrl = options.apiUrl;
379
433
  const logLevel = options.logLevel ?? 'info';
380
434
  const maxReconnectAttempts = options.maxReconnectAttempts ?? 6;
381
435
  const reconnectDelay = options.reconnectDelay ?? 1000;
@@ -387,7 +441,6 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
387
441
  let reconnectTimeout: NodeJS.Timeout | undefined;
388
442
  let intentionalDisconnect = false;
389
443
  let lastError: string | undefined;
390
-
391
444
  const pendingRequests: Map<string, PendingRequest<unknown>> = new Map();
392
445
  const pendingAssetRequestsByUrl: Map<string, Array<PendingRequest<void>>> = new Map();
393
446
 
@@ -521,6 +574,8 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
521
574
  case 'scrollScreenResult':
522
575
  case 'scrollElementResult':
523
576
  case 'openUrlResult':
577
+ case 'startRecordingResult':
578
+ case 'stopRecordingResult':
524
579
  return 'id' in message && typeof message.id === 'string';
525
580
  default:
526
581
  return false;
@@ -597,7 +652,8 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
597
652
  cleanup();
598
653
  updateConnectionState('connecting');
599
654
 
600
- ws = new WebSocket(serverAddress);
655
+ const proxyAgent = nodeProxyTransport.getWebSocketAgent(serverAddress);
656
+ ws = new WebSocket(serverAddress, proxyAgent ? { agent: proxyAgent } : {});
601
657
 
602
658
  ws.on('message', (data: Data) => {
603
659
  let message: ServerMessage;
@@ -757,6 +813,8 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
757
813
  scrollScreen,
758
814
  scrollElement,
759
815
  openUrl,
816
+ startRecording,
817
+ stopRecording,
760
818
  disconnect,
761
819
  startAdbTunnel,
762
820
  sendAsset,
@@ -846,6 +904,23 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
846
904
  };
847
905
  };
848
906
 
907
+ const startRecording = async (): Promise<void> => {
908
+ await sendRequest('startRecording', {});
909
+ };
910
+
911
+ const stopRecording = async (saveTo: { presignedUrl?: string; localPath?: string }): Promise<string> => {
912
+ const request: CommandRequestMap['stopRecording'] = {};
913
+ if (saveTo.presignedUrl) {
914
+ request.upload = { presignedUrl: saveTo.presignedUrl };
915
+ }
916
+ await sendRequest('stopRecording', request);
917
+ const downloadUrl = buildDownloadUrl(recordingApiUrl);
918
+ if (saveTo.localPath) {
919
+ await downloadFileToLocalPath(downloadUrl, options.token, saveTo.localPath);
920
+ }
921
+ return downloadUrl;
922
+ };
923
+
849
924
  const disconnect = (): void => {
850
925
  intentionalDisconnect = true;
851
926
  cleanup();
@@ -870,6 +945,9 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
870
945
  * client to it.
871
946
  */
872
947
  const startAdbTunnel = async (): Promise<Tunnel> => {
948
+ if (!options.adbUrl) {
949
+ throw new Error('adbUrl is required to start an ADB tunnel.');
950
+ }
873
951
  const tunnel = await startTcpTunnel(options.adbUrl, options.token, '127.0.0.1', 0, {
874
952
  maxReconnectAttempts,
875
953
  reconnectDelay,
@@ -0,0 +1,33 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { Readable } from 'stream';
4
+ import { pipeline } from 'stream/promises';
5
+
6
+ import { nodeProxyTransport } from './proxy-transport';
7
+
8
+ export async function downloadFileToLocalPath(url: string, token: string, localPath: string): Promise<void> {
9
+ const maxRetries = 3;
10
+
11
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
12
+ const response = await nodeProxyTransport.fetch(url, {
13
+ method: 'GET',
14
+ headers: {
15
+ Authorization: `Bearer ${token}`,
16
+ },
17
+ });
18
+ if (!response.ok) {
19
+ const errorBody = await response.text();
20
+ const isRetriable = response.status >= 500 && response.status < 600;
21
+ if (isRetriable && attempt < maxRetries) {
22
+ continue;
23
+ }
24
+ throw new Error(`Download failed: ${response.status} ${errorBody}`);
25
+ }
26
+ if (!response.body) {
27
+ throw new Error('Download failed: response body is missing');
28
+ }
29
+ await fs.promises.mkdir(path.dirname(localPath), { recursive: true });
30
+ await pipeline(Readable.fromWeb(response.body as any), fs.createWriteStream(localPath));
31
+ return;
32
+ }
33
+ }
@@ -0,0 +1,69 @@
1
+ import type { Agent as HttpAgent } from 'http';
2
+
3
+ import { HttpsProxyAgent } from 'https-proxy-agent';
4
+ import { getProxyForUrl } from 'proxy-from-env';
5
+ import { EnvHttpProxyAgent } from 'undici';
6
+
7
+ import type { Fetch } from './builtin-types';
8
+
9
+ class NodeProxyTransport {
10
+ private envHttpProxyAgent: EnvHttpProxyAgent | undefined;
11
+ private websocketAgents = new Map<string, HttpAgent>();
12
+
13
+ fetch: Fetch = async (input, init) => {
14
+ if (!this.hasProxyEnv()) {
15
+ return fetch(input, init);
16
+ }
17
+
18
+ return (fetch as any)(input, {
19
+ ...(init ?? {}),
20
+ dispatcher: this.getEnvHttpProxyAgent(),
21
+ });
22
+ };
23
+
24
+ getWebSocketAgent(url: string): HttpAgent | undefined {
25
+ if (!this.hasProxyEnv()) {
26
+ return undefined;
27
+ }
28
+
29
+ const proxyUrl = getProxyForUrl(this.getWebSocketProxyLookupUrl(url));
30
+ if (!proxyUrl) {
31
+ return undefined;
32
+ }
33
+
34
+ let agent = this.websocketAgents.get(proxyUrl);
35
+ if (!agent) {
36
+ const createdAgent = new HttpsProxyAgent(proxyUrl);
37
+ this.websocketAgents.set(proxyUrl, createdAgent);
38
+ agent = createdAgent;
39
+ }
40
+
41
+ return agent;
42
+ }
43
+
44
+ private getEnvHttpProxyAgent(): EnvHttpProxyAgent {
45
+ this.envHttpProxyAgent ??= new EnvHttpProxyAgent();
46
+ return this.envHttpProxyAgent;
47
+ }
48
+
49
+ private hasProxyEnv(): boolean {
50
+ if (typeof process === 'undefined' || !process.versions?.node) {
51
+ return false;
52
+ }
53
+
54
+ const env = process.env;
55
+ return !!(env['http_proxy'] || env['HTTP_PROXY'] || env['https_proxy'] || env['HTTPS_PROXY']);
56
+ }
57
+
58
+ private getWebSocketProxyLookupUrl(url: string): string {
59
+ const lookupUrl = new URL(url);
60
+ if (lookupUrl.protocol === 'ws:') {
61
+ lookupUrl.protocol = 'http:';
62
+ } else if (lookupUrl.protocol === 'wss:') {
63
+ lookupUrl.protocol = 'https:';
64
+ }
65
+ return lookupUrl.toString();
66
+ }
67
+ }
68
+
69
+ export const nodeProxyTransport = new NodeProxyTransport();
@@ -8,11 +8,12 @@
8
8
  */
9
9
 
10
10
  import type { Fetch } from './builtin-types';
11
+ import { nodeProxyTransport } from './proxy-transport';
11
12
  import type { ReadableStream } from './shim-types';
12
13
 
13
14
  export function getDefaultFetch(): Fetch {
14
15
  if (typeof fetch !== 'undefined') {
15
- return fetch as any;
16
+ return nodeProxyTransport.fetch;
16
17
  }
17
18
 
18
19
  throw new Error(
@@ -9,10 +9,10 @@
9
9
  */
10
10
  export const readEnv = (env: string): string | undefined => {
11
11
  if (typeof (globalThis as any).process !== 'undefined') {
12
- return (globalThis as any).process.env?.[env]?.trim() ?? undefined;
12
+ return (globalThis as any).process.env?.[env]?.trim() || undefined;
13
13
  }
14
14
  if (typeof (globalThis as any).Deno !== 'undefined') {
15
- return (globalThis as any).Deno.env?.get?.(env)?.trim();
15
+ return (globalThis as any).Deno.env?.get?.(env)?.trim() || undefined;
16
16
  }
17
17
  return undefined;
18
18
  };
package/src/ios-client.ts CHANGED
@@ -1,14 +1,14 @@
1
- import { WebSocket, Data } from 'ws';
2
- import fs from 'fs';
3
1
  import os from 'os';
4
- import path from 'path';
5
2
  import crypto from 'crypto';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import { WebSocket, Data } from 'ws';
6
6
  import { EventEmitter } from 'events';
7
- import { Readable } from 'stream';
8
- import { pipeline } from 'stream/promises';
9
7
  import { isNonRetryableError } from './tunnel';
10
8
  import { type SyncFolderResult, type FolderSyncOptions, syncFolder } from './folder-sync';
11
9
  import { createIgnoreFn } from './folder-sync-ignore';
10
+ import { downloadFileToLocalPath } from './internal/download-file';
11
+ import { nodeProxyTransport } from './internal/proxy-transport';
12
12
 
13
13
  /**
14
14
  * Connection state of the instance client
@@ -29,33 +29,6 @@ function generateRecordingFilename(): string {
29
29
  return `ios_video_${formattedDate}_${rand}.mp4`;
30
30
  }
31
31
 
32
- async function downloadFileToLocalPath(url: string, token: string, localPath: string): Promise<void> {
33
- const maxRetries = 3;
34
-
35
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
36
- const response = await fetch(url, {
37
- method: 'GET',
38
- headers: {
39
- Authorization: `Bearer ${token}`,
40
- },
41
- });
42
- if (!response.ok) {
43
- const errorBody = await response.text();
44
- const isRetriable = response.status >= 500 && response.status < 600;
45
- if (isRetriable && attempt < maxRetries) {
46
- continue;
47
- }
48
- throw new Error(`Download failed: ${response.status} ${errorBody}`);
49
- }
50
- if (!response.body) {
51
- throw new Error('Download failed: response body is missing');
52
- }
53
- await fs.promises.mkdir(path.dirname(localPath), { recursive: true });
54
- await pipeline(Readable.fromWeb(response.body as any), fs.createWriteStream(localPath));
55
- return;
56
- }
57
- }
58
-
59
32
  function buildDownloadUrl(apiUrl: string, filename: string): string {
60
33
  return `${apiUrl}/files?name=${encodeURIComponent(filename)}`;
61
34
  }
@@ -849,7 +822,8 @@ export class LogStream extends EventEmitter {
849
822
 
850
823
  /** @internal - Establish the dedicated WebSocket connection */
851
824
  private _connect(): void {
852
- this.ws = new WebSocket(this.wsUrl);
825
+ const proxyAgent = nodeProxyTransport.getWebSocketAgent(this.wsUrl);
826
+ this.ws = new WebSocket(this.wsUrl, proxyAgent ? { agent: proxyAgent } : {});
853
827
 
854
828
  this.ws.on('open', () => {
855
829
  if (this.stopped) {
@@ -1121,7 +1095,8 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1121
1095
  cleanup();
1122
1096
  updateConnectionState('connecting');
1123
1097
 
1124
- ws = new WebSocket(endpointWebSocketUrl);
1098
+ const proxyAgent = nodeProxyTransport.getWebSocketAgent(endpointWebSocketUrl);
1099
+ ws = new WebSocket(endpointWebSocketUrl, proxyAgent ? { agent: proxyAgent } : {});
1125
1100
 
1126
1101
  ws.on('message', (data: Data) => {
1127
1102
  let message: ServerResponse;
@@ -1594,7 +1569,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1594
1569
  try {
1595
1570
  // Node's fetch (undici) supports streaming request bodies but TS DOM types may not include
1596
1571
  // `duplex` and may not accept Node ReadStreams as BodyInit in some configs.
1597
- const response = await fetch(uploadUrl, {
1572
+ const response = await nodeProxyTransport.fetch(uploadUrl, {
1598
1573
  method: 'PUT',
1599
1574
  headers: {
1600
1575
  'Content-Type': 'application/octet-stream',
@@ -72,8 +72,8 @@ export namespace AndroidInstance {
72
72
  export interface Spec {
73
73
  /**
74
74
  * After how many minutes of inactivity should the instance be terminated. Example
75
- * values 1m, 10m, 3h. Default is 3m. Providing "0" disables inactivity checks
76
- * altogether.
75
+ * values 1m, 10m, 3h. Default is 3m. Providing "0" uses the organization's default
76
+ * inactivity timeout.
77
77
  */
78
78
  inactivityTimeout: string;
79
79
 
@@ -97,6 +97,8 @@ export namespace AndroidInstance {
97
97
 
98
98
  adbWebSocketUrl?: string;
99
99
 
100
+ apiUrl?: string;
101
+
100
102
  endpointWebSocketUrl?: string;
101
103
 
102
104
  errorMessage?: string;
@@ -162,8 +164,8 @@ export namespace AndroidInstanceCreateParams {
162
164
 
163
165
  /**
164
166
  * After how many minutes of inactivity should the instance be terminated. Example
165
- * values 1m, 10m, 3h. Default is 3m. Providing "0" disables inactivity checks
166
- * altogether.
167
+ * values 1m, 10m, 3h. Default is 3m. Providing "0" uses the organization's default
168
+ * inactivity timeout.
167
169
  */
168
170
  inactivityTimeout?: string;
169
171
 
@@ -3,6 +3,7 @@ import { createHash } from 'crypto';
3
3
  import { promises as fs } from 'fs';
4
4
 
5
5
  import { RequestOptions } from '../internal/request-options';
6
+ import { nodeProxyTransport } from '../internal/proxy-transport';
6
7
  import { Assets as GeneratedAssets } from './assets';
7
8
 
8
9
  export interface AssetGetOrUploadParams {
@@ -45,7 +46,7 @@ export class Assets extends GeneratedAssets {
45
46
  md5: creationResponse.md5,
46
47
  };
47
48
  }
48
- const uploadResponse = await fetch(creationResponse.signedUploadUrl, {
49
+ const uploadResponse = await nodeProxyTransport.fetch(creationResponse.signedUploadUrl, {
49
50
  headers: {
50
51
  'Content-Length': data.length.toString(),
51
52
  'Content-Type': 'application/octet-stream',
@@ -22,5 +22,12 @@ export {
22
22
  type IosInstanceListParams,
23
23
  type IosInstancesItems,
24
24
  } from './ios-instances';
25
+ export {
26
+ XcodeInstances,
27
+ type XcodeInstance,
28
+ type XcodeInstanceCreateParams,
29
+ type XcodeInstanceListParams,
30
+ type XcodeInstancesItems,
31
+ } from './xcode-instances';
25
32
 
26
33
  export { Assets, AssetGetOrUploadParams, AssetGetOrUploadResponse } from './assets-helpers';
@@ -72,8 +72,8 @@ export namespace IosInstance {
72
72
  export interface Spec {
73
73
  /**
74
74
  * After how many minutes of inactivity should the instance be terminated. Example
75
- * values 1m, 10m, 3h. Default is 3m. Providing "0" disables inactivity checks
76
- * altogether.
75
+ * values 1m, 10m, 3h. Default is 3m. Providing "0" uses the organization's default
76
+ * inactivity timeout.
77
77
  */
78
78
  inactivityTimeout: string;
79
79
 
@@ -162,8 +162,8 @@ export namespace IosInstanceCreateParams {
162
162
 
163
163
  /**
164
164
  * After how many minutes of inactivity should the instance be terminated. Example
165
- * values 1m, 10m, 3h. Default is 3m. Providing "0" disables inactivity checks
166
- * altogether.
165
+ * values 1m, 10m, 3h. Default is 3m. Providing "0" uses the organization's default
166
+ * inactivity timeout.
167
167
  */
168
168
  inactivityTimeout?: string;
169
169