@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.
- package/CHANGELOG.md +25 -0
- package/client.d.mts +2 -2
- package/client.d.mts.map +1 -1
- package/client.d.ts +2 -2
- package/client.d.ts.map +1 -1
- package/client.js.map +1 -1
- package/client.mjs.map +1 -1
- package/exec-client.d.mts +5 -0
- package/exec-client.d.mts.map +1 -1
- package/exec-client.d.ts +5 -0
- package/exec-client.d.ts.map +1 -1
- package/exec-client.js.map +1 -1
- package/exec-client.mjs.map +1 -1
- package/index.d.mts +1 -1
- package/index.d.mts.map +1 -1
- package/index.d.ts +1 -1
- package/index.d.ts.map +1 -1
- package/index.js.map +1 -1
- package/index.mjs.map +1 -1
- package/ios-client.d.mts +48 -1
- package/ios-client.d.mts.map +1 -1
- package/ios-client.d.ts +48 -1
- package/ios-client.d.ts.map +1 -1
- package/ios-client.js +61 -4
- package/ios-client.js.map +1 -1
- package/ios-client.mjs +60 -4
- package/ios-client.mjs.map +1 -1
- package/package.json +1 -1
- package/resources/index.d.mts +1 -1
- package/resources/index.d.mts.map +1 -1
- package/resources/index.d.ts +1 -1
- package/resources/index.d.ts.map +1 -1
- package/resources/index.js.map +1 -1
- package/resources/index.mjs.map +1 -1
- package/resources/xcode-instances-helpers.d.mts +20 -0
- package/resources/xcode-instances-helpers.d.mts.map +1 -1
- package/resources/xcode-instances-helpers.d.ts +20 -0
- package/resources/xcode-instances-helpers.d.ts.map +1 -1
- package/resources/xcode-instances-helpers.js +4 -0
- package/resources/xcode-instances-helpers.js.map +1 -1
- package/resources/xcode-instances-helpers.mjs +4 -0
- package/resources/xcode-instances-helpers.mjs.map +1 -1
- package/src/client.ts +2 -0
- package/src/exec-client.ts +5 -0
- package/src/index.ts +1 -0
- package/src/ios-client.ts +125 -8
- package/src/resources/index.ts +1 -0
- package/src/resources/xcode-instances-helpers.ts +25 -0
- package/src/tunnel.ts +445 -0
- package/src/version.ts +1 -1
- package/tunnel.d.mts +50 -0
- package/tunnel.d.mts.map +1 -1
- package/tunnel.d.ts +50 -0
- package/tunnel.d.ts.map +1 -1
- package/tunnel.js +362 -0
- package/tunnel.js.map +1 -1
- package/tunnel.mjs +360 -0
- package/tunnel.mjs.map +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/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:
|
|
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
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
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();
|
package/src/resources/index.ts
CHANGED
|
@@ -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
|
|