@limrun/api 0.28.4 → 0.28.6

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 (62) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/client.d.mts +2 -2
  3. package/client.d.mts.map +1 -1
  4. package/client.d.ts +2 -2
  5. package/client.d.ts.map +1 -1
  6. package/client.js.map +1 -1
  7. package/client.mjs.map +1 -1
  8. package/exec-client.d.mts +5 -0
  9. package/exec-client.d.mts.map +1 -1
  10. package/exec-client.d.ts +5 -0
  11. package/exec-client.d.ts.map +1 -1
  12. package/exec-client.js.map +1 -1
  13. package/exec-client.mjs.map +1 -1
  14. package/index.d.mts +1 -1
  15. package/index.d.mts.map +1 -1
  16. package/index.d.ts +1 -1
  17. package/index.d.ts.map +1 -1
  18. package/index.js.map +1 -1
  19. package/index.mjs.map +1 -1
  20. package/ios-client.d.mts +48 -1
  21. package/ios-client.d.mts.map +1 -1
  22. package/ios-client.d.ts +48 -1
  23. package/ios-client.d.ts.map +1 -1
  24. package/ios-client.js +61 -4
  25. package/ios-client.js.map +1 -1
  26. package/ios-client.mjs +60 -4
  27. package/ios-client.mjs.map +1 -1
  28. package/package.json +1 -1
  29. package/resources/index.d.mts +1 -1
  30. package/resources/index.d.mts.map +1 -1
  31. package/resources/index.d.ts +1 -1
  32. package/resources/index.d.ts.map +1 -1
  33. package/resources/index.js.map +1 -1
  34. package/resources/index.mjs.map +1 -1
  35. package/resources/xcode-instances-helpers.d.mts +20 -0
  36. package/resources/xcode-instances-helpers.d.mts.map +1 -1
  37. package/resources/xcode-instances-helpers.d.ts +20 -0
  38. package/resources/xcode-instances-helpers.d.ts.map +1 -1
  39. package/resources/xcode-instances-helpers.js +4 -0
  40. package/resources/xcode-instances-helpers.js.map +1 -1
  41. package/resources/xcode-instances-helpers.mjs +4 -0
  42. package/resources/xcode-instances-helpers.mjs.map +1 -1
  43. package/src/client.ts +2 -0
  44. package/src/exec-client.ts +5 -0
  45. package/src/index.ts +1 -0
  46. package/src/ios-client.ts +125 -8
  47. package/src/resources/index.ts +1 -0
  48. package/src/resources/xcode-instances-helpers.ts +25 -0
  49. package/src/tunnel.ts +445 -0
  50. package/src/version.ts +1 -1
  51. package/tunnel.d.mts +50 -0
  52. package/tunnel.d.mts.map +1 -1
  53. package/tunnel.d.ts +50 -0
  54. package/tunnel.d.ts.map +1 -1
  55. package/tunnel.js +362 -0
  56. package/tunnel.js.map +1 -1
  57. package/tunnel.mjs +360 -0
  58. package/tunnel.mjs.map +1 -1
  59. package/version.d.mts +1 -1
  60. package/version.d.ts +1 -1
  61. package/version.js +1 -1
  62. 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
  */
@@ -431,8 +491,12 @@ export type InstanceClient = {
431
491
  * @param mode Optional launch mode:
432
492
  * - 'ForegroundIfRunning' (default): bring to foreground if already running
433
493
  * - 'RelaunchIfRunning': terminate and relaunch if already running
494
+ * Or a launch options object with optional mode and launch runtime.
434
495
  */
435
- 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
+ };
436
500
 
437
501
  /**
438
502
  * Terminate a running app by bundle identifier.
@@ -625,6 +689,12 @@ export type InstanceClient = {
625
689
  */
626
690
  softReset: (bundleId: string, options?: SoftResetOptions) => Promise<SoftResetResult>;
627
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
+
628
698
  /**
629
699
  * Disconnect from the Limrun instance
630
700
  */
@@ -1291,6 +1361,21 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1291
1361
  return `ts-client-${Date.now()}-${Math.random().toString(36).substring(7)}`;
1292
1362
  };
1293
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
+
1294
1379
  // Generic request sender with timeout and response handling
1295
1380
  const sendRequest = <T>(
1296
1381
  type: string,
@@ -1316,7 +1401,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1316
1401
  );
1317
1402
 
1318
1403
  const request = { type, id, ...params };
1319
- logger.debug('Sending request:', request);
1404
+ logger.debug('Sending request:', redactRequestForDebug(request));
1320
1405
 
1321
1406
  ws.send(JSON.stringify(request), (err?: Error) => {
1322
1407
  if (err) {
@@ -1552,6 +1637,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1552
1637
  clearStoreKitConfig,
1553
1638
  discoverStoreKitConfig,
1554
1639
  softReset,
1640
+ startReverseTunnel,
1555
1641
  disconnect,
1556
1642
  getConnectionState,
1557
1643
  onConnectionStateChange,
@@ -1636,11 +1722,24 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1636
1722
  return sendRequest<void>('toggleKeyboard');
1637
1723
  };
1638
1724
 
1639
- const launchApp = (
1640
- bundleId: string,
1641
- mode?: 'ForegroundIfRunning' | 'RelaunchIfRunning',
1642
- ): Promise<void> => {
1643
- 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
+ });
1644
1743
  };
1645
1744
 
1646
1745
  const terminateApp = (bundleId: string): Promise<void> => {
@@ -2018,6 +2117,24 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
2018
2117
  return out;
2019
2118
  };
2020
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
+
2021
2138
  const disconnect = (): void => {
2022
2139
  intentionalDisconnect = true;
2023
2140
  cleanup();
@@ -36,4 +36,5 @@ export {
36
36
  type XcodeClient,
37
37
  type XcodeProjectConfig,
38
38
  type XcodeBuildOptions,
39
+ type ReactNativeBuildConfig,
39
40
  } from './xcode-instances-helpers';
@@ -52,6 +52,11 @@ export type XcodeProjectConfig = {
52
52
  project?: string;
53
53
  scheme?: string;
54
54
  sdk?: 'iphonesimulator' | 'iphoneos' | 'watchsimulator' | 'watchos';
55
+ /**
56
+ * xcodebuild configuration. Omit to use limbuild's project-type default:
57
+ * Debug for native Xcode builds and Release for React Native / Expo builds.
58
+ */
59
+ configuration?: 'Debug' | 'Release';
55
60
  };
56
61
 
57
62
  export type XcodeSigningConfig = {
@@ -60,9 +65,25 @@ export type XcodeSigningConfig = {
60
65
  provisioningProfileBase64?: string;
61
66
  };
62
67
 
68
+ export type ReactNativeBuildConfig = {
69
+ /**
70
+ * Relative path from the synced workspace root to the Expo app directory.
71
+ * Omit to let limbuild auto-detect the app.
72
+ */
73
+ expoAppDir?: string;
74
+ /**
75
+ * Direct Metro / Expo development server URL for RN/Expo Debug builds, such
76
+ * as the https://...exp.direct URL printed by `expo start --tunnel`.
77
+ * Requires `configuration: 'Debug'`, Expo SDK 52+, React Native 0.76+, and
78
+ * the default Swift AppDelegate generated by Expo prebuild.
79
+ */
80
+ devServerURL?: string;
81
+ };
82
+
63
83
  export type XcodeBuildOptions = {
64
84
  upload?: { assetName: string } | { signedUploadUrl: string };
65
85
  signing?: XcodeSigningConfig;
86
+ reactNative?: ReactNativeBuildConfig;
66
87
  };
67
88
 
68
89
  export type XcodeClient = {
@@ -232,9 +253,13 @@ export class XcodeInstances extends GeneratedXcodeInstances {
232
253
  },
233
254
 
234
255
  xcodebuild(settings?: XcodeProjectConfig, options?: XcodeBuildOptions): ExecChildProcess {
256
+ if (options?.reactNative?.devServerURL && settings?.configuration !== 'Debug') {
257
+ throw new Error("reactNative.devServerURL requires xcodebuild configuration 'Debug'");
258
+ }
235
259
  const request: ExecRequest = {
236
260
  command: 'xcodebuild',
237
261
  ...(settings && { xcodebuild: settings }),
262
+ ...(options?.reactNative && { reactNative: options.reactNative }),
238
263
  ...(options?.signing && { signing: options.signing }),
239
264
  };
240
265