@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.
- package/CHANGELOG.md +30 -0
- package/exec-client.d.mts +6 -0
- package/exec-client.d.mts.map +1 -1
- package/exec-client.d.ts +6 -0
- package/exec-client.d.ts.map +1 -1
- package/exec-client.js.map +1 -1
- package/exec-client.mjs.map +1 -1
- package/internal/types.d.mts +6 -6
- package/internal/types.d.mts.map +1 -1
- package/internal/types.d.ts +6 -6
- package/internal/types.d.ts.map +1 -1
- package/internal/utils/log.d.mts.map +1 -1
- package/internal/utils/log.d.ts.map +1 -1
- package/internal/utils/log.js +2 -0
- package/internal/utils/log.js.map +1 -1
- package/internal/utils/log.mjs +2 -0
- package/internal/utils/log.mjs.map +1 -1
- package/ios-client.d.mts +50 -1
- package/ios-client.d.mts.map +1 -1
- package/ios-client.d.ts +50 -1
- package/ios-client.d.ts.map +1 -1
- package/ios-client.js +62 -5
- package/ios-client.js.map +1 -1
- package/ios-client.mjs +61 -5
- package/ios-client.mjs.map +1 -1
- package/package.json +1 -1
- package/resources/xcode-instances-helpers.d.mts +7 -2
- package/resources/xcode-instances-helpers.d.mts.map +1 -1
- package/resources/xcode-instances-helpers.d.ts +7 -2
- package/resources/xcode-instances-helpers.d.ts.map +1 -1
- package/resources/xcode-instances-helpers.js +2 -2
- package/resources/xcode-instances-helpers.js.map +1 -1
- package/resources/xcode-instances-helpers.mjs +2 -2
- package/resources/xcode-instances-helpers.mjs.map +1 -1
- package/src/exec-client.ts +6 -0
- package/src/internal/types.ts +6 -8
- package/src/internal/utils/log.ts +2 -0
- package/src/ios-client.ts +137 -13
- package/src/resources/xcode-instances-helpers.ts +11 -4
- 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
|
*/
|
|
@@ -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:
|
|
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
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
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>(
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
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
|
|
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
|
|
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 });
|