@limrun/api 0.28.3 → 0.28.5

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 (53) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/exec-client.d.mts +6 -0
  3. package/exec-client.d.mts.map +1 -1
  4. package/exec-client.d.ts +6 -0
  5. package/exec-client.d.ts.map +1 -1
  6. package/exec-client.js.map +1 -1
  7. package/exec-client.mjs.map +1 -1
  8. package/internal/types.d.mts +6 -6
  9. package/internal/types.d.mts.map +1 -1
  10. package/internal/types.d.ts +6 -6
  11. package/internal/types.d.ts.map +1 -1
  12. package/internal/utils/log.d.mts.map +1 -1
  13. package/internal/utils/log.d.ts.map +1 -1
  14. package/internal/utils/log.js +2 -0
  15. package/internal/utils/log.js.map +1 -1
  16. package/internal/utils/log.mjs +2 -0
  17. package/internal/utils/log.mjs.map +1 -1
  18. package/ios-client.d.mts +50 -1
  19. package/ios-client.d.mts.map +1 -1
  20. package/ios-client.d.ts +50 -1
  21. package/ios-client.d.ts.map +1 -1
  22. package/ios-client.js +62 -5
  23. package/ios-client.js.map +1 -1
  24. package/ios-client.mjs +61 -5
  25. package/ios-client.mjs.map +1 -1
  26. package/package.json +1 -1
  27. package/resources/xcode-instances-helpers.d.mts +7 -2
  28. package/resources/xcode-instances-helpers.d.mts.map +1 -1
  29. package/resources/xcode-instances-helpers.d.ts +7 -2
  30. package/resources/xcode-instances-helpers.d.ts.map +1 -1
  31. package/resources/xcode-instances-helpers.js +2 -2
  32. package/resources/xcode-instances-helpers.js.map +1 -1
  33. package/resources/xcode-instances-helpers.mjs +2 -2
  34. package/resources/xcode-instances-helpers.mjs.map +1 -1
  35. package/src/exec-client.ts +6 -0
  36. package/src/internal/types.ts +6 -8
  37. package/src/internal/utils/log.ts +2 -0
  38. package/src/ios-client.ts +137 -13
  39. package/src/resources/xcode-instances-helpers.ts +11 -4
  40. package/src/tunnel.ts +445 -0
  41. package/src/version.ts +1 -1
  42. package/tunnel.d.mts +50 -0
  43. package/tunnel.d.mts.map +1 -1
  44. package/tunnel.d.ts +50 -0
  45. package/tunnel.d.ts.map +1 -1
  46. package/tunnel.js +362 -0
  47. package/tunnel.js.map +1 -1
  48. package/tunnel.mjs +360 -0
  49. package/tunnel.mjs.map +1 -1
  50. package/version.d.mts +1 -1
  51. package/version.d.ts +1 -1
  52. package/version.js +1 -1
  53. package/version.mjs +1 -1
package/src/ios-client.ts CHANGED
@@ -4,7 +4,7 @@ import path from 'path';
4
4
  import fs from 'fs';
5
5
  import { WebSocket, Data } from 'ws';
6
6
  import { EventEmitter } from 'events';
7
- import { isNonRetryableError } from './tunnel';
7
+ import { assertPort, isNonRetryableError, startReverseTcpTunnel, type ReverseTunnel } from './tunnel';
8
8
  import { type SyncFolderResult, type FolderSyncOptions, syncFolder } from './folder-sync';
9
9
  import { createIgnoreFn } from './folder-sync-ignore';
10
10
  import { downloadFileToLocalPath } from './internal/download-file';
@@ -21,11 +21,31 @@ export type ConnectionState = 'connecting' | 'connected' | 'disconnected' | 'rec
21
21
  export type ConnectionStateCallback = (state: ConnectionState) => void;
22
22
 
23
23
  const ACTIVE_RECORDING_FILENAME = 'recording.mp4';
24
+ export const REVERSE_TUNNEL_REMOTE_PORT_MIN = 57090;
25
+ export const REVERSE_TUNNEL_REMOTE_PORT_MAX = 57099;
24
26
 
25
27
  function buildDownloadUrl(apiUrl: string): string {
26
28
  return `${apiUrl}/files?name=${encodeURIComponent(ACTIVE_RECORDING_FILENAME)}`;
27
29
  }
28
30
 
31
+ export function deriveReverseTunnelUrl(apiUrl: string, remotePort: number): string {
32
+ const url = new URL(apiUrl);
33
+ if (url.protocol === 'https:') {
34
+ url.protocol = 'wss:';
35
+ } else if (url.protocol === 'http:') {
36
+ url.protocol = 'ws:';
37
+ } else {
38
+ throw new Error(`Unsupported apiUrl protocol for reverse tunnel: ${url.protocol}`);
39
+ }
40
+ url.pathname = `${url.pathname.replace(/\/+$/, '')}/reverse-tunnel`;
41
+ url.search = '';
42
+ url.hash = '';
43
+ url.searchParams.set('remotePort', String(remotePort));
44
+ return url.toString();
45
+ }
46
+
47
+ export type { ReverseTunnel } from './tunnel';
48
+
29
49
  /**
30
50
  * Events emitted by a simctl execution
31
51
  */
@@ -227,6 +247,35 @@ export type SoftResetOptions = {
227
247
  strategy?: SoftResetStrategy;
228
248
  };
229
249
 
250
+ export type LaunchAppMode = 'ForegroundIfRunning' | 'RelaunchIfRunning';
251
+
252
+ export type DetoxLaunchRuntime = {
253
+ kind: 'detox';
254
+ serverUrl: string;
255
+ sessionId: string;
256
+ version?: string;
257
+ };
258
+
259
+ export type LaunchAppRuntime = DetoxLaunchRuntime;
260
+
261
+ type StandardLaunchAppOptions = {
262
+ /**
263
+ * Launch behavior when the app may already be running.
264
+ * Defaults to `ForegroundIfRunning` server-side.
265
+ */
266
+ mode?: LaunchAppMode;
267
+ runtime?: undefined;
268
+ };
269
+
270
+ type RuntimeLaunchAppOptions = {
271
+ /** Runtime launches must relaunch so runtime injection is applied. */
272
+ mode?: Extract<LaunchAppMode, 'RelaunchIfRunning'>;
273
+ /** Optional app runtime to attach during launch. */
274
+ runtime: LaunchAppRuntime;
275
+ };
276
+
277
+ export type LaunchAppOptions = StandardLaunchAppOptions | RuntimeLaunchAppOptions;
278
+
230
279
  /**
231
280
  * Result of a {@link InstanceClient.softReset} call.
232
281
  */
@@ -244,6 +293,17 @@ export type SoftResetResult = {
244
293
  durationMs: number;
245
294
  };
246
295
 
296
+ export type ReverseTunnelOptions = {
297
+ /** Port to listen on near the simulator. Must be in 57090-57099. */
298
+ remotePort: number;
299
+ /** Local port for a client-first service on the user's machine. Defaults to remotePort. */
300
+ localPort?: number;
301
+ /** Local host on the user's machine. Defaults to 127.0.0.1. */
302
+ localHost?: string;
303
+ /** Controls tunnel logging verbosity. Defaults to the instance client's log level. */
304
+ logLevel?: LogLevel;
305
+ };
306
+
247
307
  /**
248
308
  * Result from a command execution (xcrun, xcodebuild, etc.)
249
309
  */
@@ -259,6 +319,8 @@ export type CommandResult = {
259
319
  export type AppInstallationOptions = {
260
320
  /** MD5 hash for caching - if provided and matches cached version, skips download */
261
321
  md5?: string;
322
+ /** Client-side timeout for app installation. Defaults to 120 seconds. */
323
+ timeoutMs?: number;
262
324
  /**
263
325
  * Launch mode after installation:
264
326
  * - 'ForegroundIfRunning': Bring to foreground if already running, otherwise launch
@@ -429,8 +491,12 @@ export type InstanceClient = {
429
491
  * @param mode Optional launch mode:
430
492
  * - 'ForegroundIfRunning' (default): bring to foreground if already running
431
493
  * - 'RelaunchIfRunning': terminate and relaunch if already running
494
+ * Or a launch options object with optional mode and launch runtime.
432
495
  */
433
- launchApp: (bundleId: string, mode?: 'ForegroundIfRunning' | 'RelaunchIfRunning') => Promise<void>;
496
+ launchApp: {
497
+ (bundleId: string, mode?: LaunchAppMode): Promise<void>;
498
+ (bundleId: string, options: LaunchAppOptions): Promise<void>;
499
+ };
434
500
 
435
501
  /**
436
502
  * Terminate a running app by bundle identifier.
@@ -623,6 +689,12 @@ export type InstanceClient = {
623
689
  */
624
690
  softReset: (bundleId: string, options?: SoftResetOptions) => Promise<SoftResetResult>;
625
691
 
692
+ /**
693
+ * Start a reverse tunnel from the simulator-facing LISTEN_IP:remotePort
694
+ * to a user-local client-first TCP service, such as HTTP or WebSocket.
695
+ */
696
+ startReverseTunnel: (options: ReverseTunnelOptions) => Promise<ReverseTunnel>;
697
+
626
698
  /**
627
699
  * Disconnect from the Limrun instance
628
700
  */
@@ -1289,6 +1361,21 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1289
1361
  return `ts-client-${Date.now()}-${Math.random().toString(36).substring(7)}`;
1290
1362
  };
1291
1363
 
1364
+ const redactRequestForDebug = (request: Record<string, unknown>): Record<string, unknown> => {
1365
+ if (request['type'] !== 'launchApp') {
1366
+ return request;
1367
+ }
1368
+
1369
+ const runtime = request['runtime'] as LaunchAppRuntime | undefined;
1370
+ return {
1371
+ type: request['type'],
1372
+ id: request['id'],
1373
+ bundleId: request['bundleId'],
1374
+ mode: request['mode'],
1375
+ runtime: runtime ? { kind: runtime.kind, version: runtime.version } : undefined,
1376
+ };
1377
+ };
1378
+
1292
1379
  // Generic request sender with timeout and response handling
1293
1380
  const sendRequest = <T>(
1294
1381
  type: string,
@@ -1314,7 +1401,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1314
1401
  );
1315
1402
 
1316
1403
  const request = { type, id, ...params };
1317
- logger.debug('Sending request:', request);
1404
+ logger.debug('Sending request:', redactRequestForDebug(request));
1318
1405
 
1319
1406
  ws.send(JSON.stringify(request), (err?: Error) => {
1320
1407
  if (err) {
@@ -1550,6 +1637,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1550
1637
  clearStoreKitConfig,
1551
1638
  discoverStoreKitConfig,
1552
1639
  softReset,
1640
+ startReverseTunnel,
1553
1641
  disconnect,
1554
1642
  getConnectionState,
1555
1643
  onConnectionStateChange,
@@ -1634,11 +1722,24 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1634
1722
  return sendRequest<void>('toggleKeyboard');
1635
1723
  };
1636
1724
 
1637
- const launchApp = (
1638
- bundleId: string,
1639
- mode?: 'ForegroundIfRunning' | 'RelaunchIfRunning',
1640
- ): Promise<void> => {
1641
- return sendRequest<void>('launchApp', { bundleId, mode });
1725
+ const launchApp = (bundleId: string, modeOrOptions?: LaunchAppMode | LaunchAppOptions): Promise<void> => {
1726
+ const launchOptions: LaunchAppOptions =
1727
+ typeof modeOrOptions === 'string' ? { mode: modeOrOptions } : modeOrOptions ?? {};
1728
+ const requestedMode =
1729
+ typeof modeOrOptions === 'object' ?
1730
+ (modeOrOptions as { mode?: LaunchAppMode }).mode
1731
+ : launchOptions.mode;
1732
+ if (launchOptions.runtime && requestedMode === 'ForegroundIfRunning') {
1733
+ return Promise.reject(
1734
+ new Error('launchApp runtime launches require RelaunchIfRunning so runtime injection is applied.'),
1735
+ );
1736
+ }
1737
+ const mode = launchOptions.runtime ? 'RelaunchIfRunning' : launchOptions.mode;
1738
+ return sendRequest<void>('launchApp', {
1739
+ bundleId,
1740
+ mode,
1741
+ runtime: launchOptions.runtime,
1742
+ });
1642
1743
  };
1643
1744
 
1644
1745
  const terminateApp = (bundleId: string): Promise<void> => {
@@ -1670,11 +1771,16 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1670
1771
  };
1671
1772
 
1672
1773
  const installApp = (url: string, options?: AppInstallationOptions): Promise<AppInstallationResult> => {
1673
- return sendRequest<AppInstallationResult>('appInstallation', {
1674
- url,
1675
- md5: options?.md5,
1676
- launchMode: options?.launchMode,
1677
- });
1774
+ return sendRequest<AppInstallationResult>(
1775
+ 'appInstallation',
1776
+ {
1777
+ url,
1778
+ md5: options?.md5,
1779
+ launchMode: options?.launchMode,
1780
+ },
1781
+ undefined,
1782
+ options?.timeoutMs ?? 120_000,
1783
+ );
1678
1784
  };
1679
1785
 
1680
1786
  const setOrientation = (orientation: 'Portrait' | 'Landscape'): Promise<void> => {
@@ -2011,6 +2117,24 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
2011
2117
  return out;
2012
2118
  };
2013
2119
 
2120
+ const startReverseTunnel = async (tunnelOptions: ReverseTunnelOptions): Promise<ReverseTunnel> => {
2121
+ assertPort(
2122
+ tunnelOptions.remotePort,
2123
+ 'remotePort',
2124
+ REVERSE_TUNNEL_REMOTE_PORT_MIN,
2125
+ REVERSE_TUNNEL_REMOTE_PORT_MAX,
2126
+ );
2127
+ const localPort = tunnelOptions.localPort ?? tunnelOptions.remotePort;
2128
+ assertPort(localPort, 'localPort', 1, 65535);
2129
+
2130
+ const remoteURL = deriveReverseTunnelUrl(options.apiUrl, tunnelOptions.remotePort);
2131
+ return startReverseTcpTunnel(remoteURL, options.token, {
2132
+ localHost: tunnelOptions.localHost ?? '127.0.0.1',
2133
+ localPort,
2134
+ logLevel: tunnelOptions.logLevel ?? logLevel,
2135
+ });
2136
+ };
2137
+
2014
2138
  const disconnect = (): void => {
2015
2139
  intentionalDisconnect = true;
2016
2140
  cleanup();
@@ -51,11 +51,18 @@ export type XcodeProjectConfig = {
51
51
  workspace?: string;
52
52
  project?: string;
53
53
  scheme?: string;
54
- sdk?: 'iphonesimulator' | 'iphoneos';
54
+ sdk?: 'iphonesimulator' | 'iphoneos' | 'watchsimulator' | 'watchos';
55
+ };
56
+
57
+ export type XcodeSigningConfig = {
58
+ certificateP12Base64?: string;
59
+ certificatePassword?: string;
60
+ provisioningProfileBase64?: string;
55
61
  };
56
62
 
57
63
  export type XcodeBuildOptions = {
58
- upload?: { assetName: string } | { signedUploadUrl: string; signedDownloadUrl: string };
64
+ upload?: { assetName: string } | { signedUploadUrl: string };
65
+ signing?: XcodeSigningConfig;
59
66
  };
60
67
 
61
68
  export type XcodeClient = {
@@ -228,6 +235,7 @@ export class XcodeInstances extends GeneratedXcodeInstances {
228
235
  const request: ExecRequest = {
229
236
  command: 'xcodebuild',
230
237
  ...(settings && { xcodebuild: settings }),
238
+ ...(options?.signing && { signing: options.signing }),
231
239
  };
232
240
 
233
241
  if (options?.upload && 'assetName' in options.upload) {
@@ -249,9 +257,8 @@ export class XcodeInstances extends GeneratedXcodeInstances {
249
257
  return exec(requestPromise, { apiUrl, token, log });
250
258
  }
251
259
 
252
- if (options?.upload && 'signedUploadUrl' in options.upload && 'signedDownloadUrl' in options.upload) {
260
+ if (options?.upload && 'signedUploadUrl' in options.upload) {
253
261
  request.signedUploadUrl = options.upload.signedUploadUrl;
254
- request.additionalMetadata = { signedDownloadUrl: options.upload.signedDownloadUrl };
255
262
  }
256
263
 
257
264
  return exec(request, { apiUrl, token, log });