@limrun/api 0.28.6 → 0.28.7

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/ios-shim.mjs ADDED
@@ -0,0 +1,346 @@
1
+ import fs from 'fs';
2
+ import http from 'http';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ const FORWARDED_SIMCTL_COMMANDS = new Set([
6
+ 'openurl',
7
+ 'launch',
8
+ 'terminate',
9
+ 'uninstall',
10
+ 'privacy',
11
+ 'location',
12
+ 'status_bar',
13
+ 'spawn',
14
+ ]);
15
+ export async function startXcrunShim(client) {
16
+ const server = await startXcrunShimServer({ client, udid: client.deviceInfo.udid });
17
+ try {
18
+ const dir = await createXcrunShim({ shimUrl: server.url, udid: client.deviceInfo.udid });
19
+ return {
20
+ dir,
21
+ close: async () => {
22
+ await server.close();
23
+ fs.rmSync(dir, { recursive: true, force: true });
24
+ },
25
+ };
26
+ }
27
+ catch (error) {
28
+ await server.close().catch(() => { });
29
+ throw error;
30
+ }
31
+ }
32
+ export async function startXcrunShimServer({ client, udid, }) {
33
+ const server = http.createServer(async (req, res) => {
34
+ if (req.method !== 'POST' || req.url !== '/xcrun') {
35
+ sendJson(res, 404, { code: 127, stdout: '', stderr: 'not found' });
36
+ return;
37
+ }
38
+ try {
39
+ const body = (await readJson(req));
40
+ const args = body.args ?? [];
41
+ const result = await handleShimmedXcrun(client, udid, args);
42
+ sendJson(res, 200, result);
43
+ }
44
+ catch {
45
+ sendJson(res, 200, {
46
+ code: 1,
47
+ stdout: '',
48
+ stderr: 'limrun xcrun shim failed to execute the requested command.',
49
+ });
50
+ }
51
+ });
52
+ await listen(server, 0);
53
+ const address = server.address();
54
+ if (!address || typeof address === 'string') {
55
+ throw new Error('Failed to start Limrun xcrun shim server.');
56
+ }
57
+ return {
58
+ url: `http://127.0.0.1:${address.port}/xcrun`,
59
+ close: () => closeServer(server),
60
+ };
61
+ }
62
+ export async function createXcrunShim(options) {
63
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'limrun-xcrun-shim-'));
64
+ const shimPath = path.join(dir, 'xcrun');
65
+ fs.writeFileSync(shimPath, xcrunShimSource(options), 'utf8');
66
+ fs.chmodSync(shimPath, 0o755);
67
+ return dir;
68
+ }
69
+ async function handleShimmedXcrun(client, udid, args) {
70
+ if (args[0] !== 'simctl') {
71
+ return { code: 127, stdout: '', stderr: `unsupported xcrun command: ${args.join(' ')}` };
72
+ }
73
+ const simctlArgs = args.slice(1);
74
+ const command = simctlArgs[0];
75
+ const target = simctlTarget(command, simctlArgs);
76
+ if (command === 'list') {
77
+ // Maestro's device picker expects the full CoreSimulator JSON shape. Limrun
78
+ // only has one remote simulator in this context, so we synthesize that entry.
79
+ return simctlList(client.deviceInfo.udid, simctlArgs);
80
+ }
81
+ if (!isLimrunTarget(target, udid)) {
82
+ return { code: 64, stdout: '', stderr: `unsupported non-Limrun simctl target '${target ?? ''}'.` };
83
+ }
84
+ if (command === 'listapps') {
85
+ const apps = await client.listApps();
86
+ return {
87
+ code: 0,
88
+ stdout: `${JSON.stringify(toSimctlListApps(apps))}\n`,
89
+ stderr: '',
90
+ };
91
+ }
92
+ if (command === 'install') {
93
+ const appPath = simctlArgs[2];
94
+ if (!appPath) {
95
+ return { code: 64, stdout: '', stderr: 'simctl install requires a local .app path.' };
96
+ }
97
+ await client.syncApp(appPath, { install: true, watch: false });
98
+ return { code: 0, stdout: '', stderr: '' };
99
+ }
100
+ const unsupported = unsupportedPathBearingCommand(command);
101
+ if (unsupported) {
102
+ return unsupported;
103
+ }
104
+ if (!command || !FORWARDED_SIMCTL_COMMANDS.has(command)) {
105
+ return { code: 64, stdout: '', stderr: `limrun xcrun shim does not support simctl ${command ?? ''}.` };
106
+ }
107
+ return await client.simctl(simctlArgs).wait();
108
+ }
109
+ function simctlList(udid, simctlArgs) {
110
+ if (!simctlArgs.includes('-j')) {
111
+ return { code: 64, stdout: '', stderr: 'limrun xcrun shim only supports `xcrun simctl list -j`.' };
112
+ }
113
+ return {
114
+ code: 0,
115
+ stdout: `${JSON.stringify(toSimctlList(udid))}\n`,
116
+ stderr: '',
117
+ };
118
+ }
119
+ function unsupportedPathBearingCommand(command) {
120
+ // These commands either return local simulator filesystem paths or consume
121
+ // host-side media files. They need explicit staging/translation before they
122
+ // are safe to claim as supported.
123
+ if (command === 'get_app_container') {
124
+ return {
125
+ code: 64,
126
+ stdout: '',
127
+ stderr: 'limrun xcrun shim does not support get_app_container because upstream tools expect a local filesystem path.',
128
+ };
129
+ }
130
+ if (command === 'keychain') {
131
+ return { code: 64, stdout: '', stderr: 'limrun xcrun shim does not support simctl keychain in v1.' };
132
+ }
133
+ if (command === 'io') {
134
+ return {
135
+ code: 64,
136
+ stdout: '',
137
+ stderr: 'limrun xcrun shim does not support simctl io recordVideo in v1.',
138
+ };
139
+ }
140
+ if (command === 'push' || command === 'addmedia') {
141
+ return {
142
+ code: 64,
143
+ stdout: '',
144
+ stderr: `limrun xcrun shim does not support path-bearing simctl ${command} in v1.`,
145
+ };
146
+ }
147
+ return undefined;
148
+ }
149
+ function isLimrunTarget(value, udid) {
150
+ return value === udid || value === 'booted';
151
+ }
152
+ function simctlTarget(command, simctlArgs) {
153
+ if (command === 'launch') {
154
+ return simctlArgs.slice(1).find((arg) => !arg.startsWith('-'));
155
+ }
156
+ return simctlArgs[1];
157
+ }
158
+ function toSimctlList(udid) {
159
+ const runtimeIdentifier = 'com.apple.CoreSimulator.SimRuntime.iOS-18-0';
160
+ const deviceTypeIdentifier = 'com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro';
161
+ return {
162
+ devices: {
163
+ [runtimeIdentifier]: [
164
+ {
165
+ availability: '(available)',
166
+ dataPath: `/tmp/limrun-sim/${udid}/data`,
167
+ logPath: `/tmp/limrun-sim/${udid}/logs`,
168
+ isAvailable: true,
169
+ name: process.env['LIMRUN_IOS_DEVICE_NAME'] || 'Limrun iPhone',
170
+ state: 'Booted',
171
+ udid,
172
+ deviceTypeIdentifier,
173
+ availabilityError: null,
174
+ },
175
+ ],
176
+ },
177
+ devicetypes: [
178
+ {
179
+ bundlePath: '',
180
+ identifier: deviceTypeIdentifier,
181
+ maxRuntimeVersion: 999999,
182
+ maxRuntimeVersionString: null,
183
+ minRuntimeVersion: 0,
184
+ minRuntimeVersionString: null,
185
+ modelIdentifier: 'iPhone17,1',
186
+ name: 'iPhone 16 Pro',
187
+ productFamily: 'iPhone',
188
+ },
189
+ ],
190
+ pairs: {},
191
+ runtimes: [
192
+ {
193
+ availability: '(available)',
194
+ bundlePath: '',
195
+ buildversion: '22A000',
196
+ identifier: runtimeIdentifier,
197
+ isInternal: false,
198
+ isAvailable: true,
199
+ name: process.env['LIMRUN_IOS_RUNTIME_NAME'] || 'iOS 18.0',
200
+ platform: 'iOS',
201
+ runtimeRoot: '',
202
+ supportedDeviceTypes: [],
203
+ version: '18.0',
204
+ },
205
+ ],
206
+ };
207
+ }
208
+ function toSimctlListApps(apps) {
209
+ return Object.fromEntries(apps.map((app) => [
210
+ app.bundleId,
211
+ {
212
+ CFBundleIdentifier: app.bundleId,
213
+ ...(app.name ? { CFBundleName: app.name } : {}),
214
+ },
215
+ ]));
216
+ }
217
+ function xcrunShimSource(options) {
218
+ // Keep the executable tiny: it decides whether this is a Limrun-targeted
219
+ // simctl call, then asks the local shim server to perform the real work.
220
+ // Non-Limrun calls still delegate to the host xcrun.
221
+ const embeddedShimUrl = options ? JSON.stringify(options.shimUrl) : 'process.env.LIMRUN_XCRUN_SHIM_URL';
222
+ const embeddedUdid = options ? JSON.stringify(options.udid) : 'process.env.LIMRUN_IOS_UDID';
223
+ return `#!/usr/bin/env node
224
+ const http = require('node:http');
225
+ const { spawnSync } = require('node:child_process');
226
+
227
+ const args = process.argv.slice(2);
228
+ const realXcrun = process.env.LIMRUN_REAL_XCRUN || '/usr/bin/xcrun';
229
+
230
+ function delegate() {
231
+ const result = spawnSync(realXcrun, args, { stdio: 'inherit' });
232
+ if (result.error) {
233
+ process.stderr.write(String(result.error.message || result.error) + '\\n');
234
+ process.exit(127);
235
+ }
236
+ process.exit(result.status ?? 1);
237
+ }
238
+
239
+ function fail(message) {
240
+ process.stderr.write('limrun xcrun shim: ' + message + '\\n');
241
+ process.exit(64);
242
+ }
243
+
244
+ if (args[0] !== 'simctl') {
245
+ delegate();
246
+ }
247
+
248
+ const shimUrl = ${embeddedShimUrl};
249
+ const udid = ${embeddedUdid};
250
+ if (!shimUrl || !udid) {
251
+ fail('LIMRUN_XCRUN_SHIM_URL and LIMRUN_IOS_UDID are required.');
252
+ }
253
+
254
+ const simctlArgs = args.slice(1);
255
+ const command = simctlArgs[0];
256
+ function simctlTarget(command, simctlArgs) {
257
+ if (command === 'launch') {
258
+ return simctlArgs.slice(1).find((arg) => !arg.startsWith('-'));
259
+ }
260
+ return simctlArgs[1];
261
+ }
262
+ const target = simctlTarget(command, simctlArgs);
263
+ function isLimrunTarget(value) {
264
+ return value === udid || value === 'booted';
265
+ }
266
+
267
+ if (command !== 'list' && !isLimrunTarget(target)) {
268
+ delegate();
269
+ }
270
+
271
+ const parsed = new URL(shimUrl);
272
+ const body = JSON.stringify({ args });
273
+ const req = http.request(parsed, {
274
+ method: 'POST',
275
+ headers: {
276
+ 'content-type': 'application/json',
277
+ 'content-length': Buffer.byteLength(body),
278
+ },
279
+ }, (res) => {
280
+ const chunks = [];
281
+ res.on('data', (chunk) => chunks.push(chunk));
282
+ res.on('end', () => {
283
+ let payload;
284
+ try {
285
+ payload = JSON.parse(Buffer.concat(chunks).toString('utf8'));
286
+ } catch (error) {
287
+ process.stderr.write('limrun xcrun shim: invalid shim response\\n');
288
+ process.exit(1);
289
+ }
290
+ if (payload.stdout) process.stdout.write(payload.stdout);
291
+ if (payload.stderr) process.stderr.write(payload.stderr);
292
+ process.exit(typeof payload.code === 'number' ? payload.code : 1);
293
+ });
294
+ });
295
+ req.on('error', (error) => {
296
+ process.stderr.write('limrun xcrun shim: ' + error.message + '\\n');
297
+ process.exit(1);
298
+ });
299
+ req.end(body);
300
+ `;
301
+ }
302
+ function listen(server, port) {
303
+ return new Promise((resolve, reject) => {
304
+ server.once('error', reject);
305
+ server.listen(port, '127.0.0.1', () => {
306
+ server.off('error', reject);
307
+ resolve();
308
+ });
309
+ });
310
+ }
311
+ function closeServer(server) {
312
+ return new Promise((resolve, reject) => {
313
+ server.close((error) => {
314
+ if (error) {
315
+ reject(error);
316
+ }
317
+ else {
318
+ resolve();
319
+ }
320
+ });
321
+ });
322
+ }
323
+ function readJson(req) {
324
+ return new Promise((resolve, reject) => {
325
+ const chunks = [];
326
+ req.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
327
+ req.on('end', () => {
328
+ try {
329
+ resolve(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}'));
330
+ }
331
+ catch (error) {
332
+ reject(error);
333
+ }
334
+ });
335
+ req.on('error', reject);
336
+ });
337
+ }
338
+ function sendJson(res, statusCode, payload) {
339
+ const body = JSON.stringify(payload);
340
+ res.writeHead(statusCode, {
341
+ 'content-type': 'application/json',
342
+ 'content-length': Buffer.byteLength(body),
343
+ });
344
+ res.end(body);
345
+ }
346
+ //# sourceMappingURL=ios-shim.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ios-shim.mjs","sourceRoot":"","sources":["src/ios-shim.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limrun/api",
3
- "version": "0.28.6",
3
+ "version": "0.28.7",
4
4
  "description": "The official TypeScript library for the Limrun API",
5
5
  "author": "Limrun <contact@limrun.com>",
6
6
  "types": "./index.d.ts",
@@ -133,6 +133,16 @@
133
133
  "./folder-sync.mjs": {
134
134
  "default": "./folder-sync.mjs"
135
135
  },
136
+ "./http-proxy": {
137
+ "import": "./http-proxy.mjs",
138
+ "require": "./http-proxy.js"
139
+ },
140
+ "./http-proxy.js": {
141
+ "default": "./http-proxy.js"
142
+ },
143
+ "./http-proxy.mjs": {
144
+ "default": "./http-proxy.mjs"
145
+ },
136
146
  "./index": {
137
147
  "import": "./index.mjs",
138
148
  "require": "./index.js"
@@ -163,6 +173,16 @@
163
173
  "./ios-client.mjs": {
164
174
  "default": "./ios-client.mjs"
165
175
  },
176
+ "./ios-shim": {
177
+ "import": "./ios-shim.mjs",
178
+ "require": "./ios-shim.js"
179
+ },
180
+ "./ios-shim.js": {
181
+ "default": "./ios-shim.js"
182
+ },
183
+ "./ios-shim.mjs": {
184
+ "default": "./ios-shim.mjs"
185
+ },
166
186
  "./pagination": {
167
187
  "import": "./pagination.mjs",
168
188
  "require": "./pagination.js"
@@ -0,0 +1,85 @@
1
+ import http from 'http';
2
+ import https from 'https';
3
+
4
+ export type HttpProxy = {
5
+ port: number;
6
+ close: () => Promise<void>;
7
+ };
8
+
9
+ export type StartHttpProxyOptions = {
10
+ localPort?: number;
11
+ remoteBaseUrl: string;
12
+ headers?: Record<string, string>;
13
+ };
14
+
15
+ export async function startHttpProxy({
16
+ localPort = 0,
17
+ remoteBaseUrl,
18
+ headers = {},
19
+ }: StartHttpProxyOptions): Promise<HttpProxy> {
20
+ const base = trimTrailingSlashes(remoteBaseUrl);
21
+ const server = http.createServer((req, res) => {
22
+ const pathAndQuery = req.url || '/';
23
+ const upstreamUrl = new URL(`${base}${pathAndQuery.startsWith('/') ? pathAndQuery : `/${pathAndQuery}`}`);
24
+ const transport = upstreamUrl.protocol === 'https:' ? https : http;
25
+ const upstream = transport.request(
26
+ upstreamUrl,
27
+ {
28
+ method: req.method,
29
+ headers: {
30
+ ...req.headers,
31
+ host: upstreamUrl.host,
32
+ ...headers,
33
+ },
34
+ },
35
+ (upstreamResponse) => {
36
+ res.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);
37
+ upstreamResponse.pipe(res);
38
+ },
39
+ );
40
+
41
+ upstream.on('error', (error) => {
42
+ if (!res.headersSent) {
43
+ res.writeHead(502, { 'content-type': 'text/plain' });
44
+ }
45
+ res.end(error.message);
46
+ });
47
+ req.pipe(upstream);
48
+ });
49
+
50
+ await listen(server, localPort);
51
+ const address = server.address();
52
+ if (!address || typeof address === 'string') {
53
+ throw new Error('Failed to start HTTP proxy.');
54
+ }
55
+ return {
56
+ port: address.port,
57
+ close: () => closeServer(server),
58
+ };
59
+ }
60
+
61
+ function trimTrailingSlashes(value: string): string {
62
+ return value.replace(/\/+$/, '');
63
+ }
64
+
65
+ function listen(server: http.Server, port: number): Promise<void> {
66
+ return new Promise((resolve, reject) => {
67
+ server.once('error', reject);
68
+ server.listen(port, '127.0.0.1', () => {
69
+ server.off('error', reject);
70
+ resolve();
71
+ });
72
+ });
73
+ }
74
+
75
+ function closeServer(server: http.Server): Promise<void> {
76
+ return new Promise((resolve, reject) => {
77
+ server.close((error) => {
78
+ if (error) {
79
+ reject(error);
80
+ } else {
81
+ resolve();
82
+ }
83
+ });
84
+ });
85
+ }
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ export { Limrun, type ClientOptions } from './client';
8
8
  export { PagePromise } from './core/pagination';
9
9
  export * from './instance-client';
10
10
  export * as Ios from './ios-client';
11
+ export { startHttpProxy, type HttpProxy, type StartHttpProxyOptions } from './http-proxy';
11
12
  export {
12
13
  exec,
13
14
  type ExecRequest,
package/src/ios-client.ts CHANGED
@@ -9,6 +9,8 @@ import { type SyncFolderResult, type FolderSyncOptions, syncFolder } from './fol
9
9
  import { createIgnoreFn } from './folder-sync-ignore';
10
10
  import { downloadFileToLocalPath } from './internal/download-file';
11
11
  import { nodeProxyTransport } from './internal/proxy-transport';
12
+ import { startHttpProxy as startLocalHttpProxy } from './http-proxy';
13
+ import { startXcrunShim as startClientXcrunShim } from './ios-shim';
12
14
 
13
15
  /**
14
16
  * Connection state of the instance client
@@ -198,6 +200,11 @@ export type AppInstallationResult = {
198
200
  bundleId: string;
199
201
  };
200
202
 
203
+ export type HttpProxyOptions = {
204
+ remoteBaseUrl: string;
205
+ localPort: number;
206
+ };
207
+
201
208
  /**
202
209
  * Result of auto-discovering a StoreKit configuration from the
203
210
  * simulator's cached sandbox response.
@@ -695,6 +702,14 @@ export type InstanceClient = {
695
702
  */
696
703
  startReverseTunnel: (options: ReverseTunnelOptions) => Promise<ReverseTunnel>;
697
704
 
705
+ /**
706
+ * Start a local HTTP proxy to a port exposed by the iOS instance.
707
+ *
708
+ * Returns the local port the proxy is listening on. The proxy is closed when
709
+ * the client disconnects.
710
+ */
711
+ startHttpProxy: (options: HttpProxyOptions) => Promise<number>;
712
+
698
713
  /**
699
714
  * Disconnect from the Limrun instance
700
715
  */
@@ -791,6 +806,14 @@ export type InstanceClient = {
791
806
  */
792
807
  xcrun: (args: string[]) => Promise<CommandResult>;
793
808
 
809
+ /**
810
+ * Start a local `xcrun` shim for this iOS instance.
811
+ *
812
+ * The returned directory contains an `xcrun` executable that can be prepended
813
+ * to PATH for tools that shell out to `xcrun simctl`.
814
+ */
815
+ startXcrunShim: () => Promise<string>;
816
+
794
817
  /**
795
818
  * Run `xcodebuild` command with the given arguments.
796
819
  * Returns the complete output once the command finishes (non-streaming).
@@ -1257,6 +1280,9 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1257
1280
  // Simctl uses streaming, so it needs separate handling
1258
1281
  const simctlExecutions: Map<string, SimctlExecution> = new Map();
1259
1282
 
1283
+ const xcrunShimCleanups: Array<() => Promise<void>> = [];
1284
+ const httpProxyCleanups: Array<() => Promise<void>> = [];
1285
+
1260
1286
  const stateChangeCallbacks: Set<ConnectionStateCallback> = new Set();
1261
1287
 
1262
1288
  // Logger functions
@@ -1300,7 +1326,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1300
1326
  simctlExecutions.clear();
1301
1327
  };
1302
1328
 
1303
- const cleanup = (): void => {
1329
+ const cleanupConnection = (): void => {
1304
1330
  if (reconnectTimeout) {
1305
1331
  clearTimeout(reconnectTimeout);
1306
1332
  reconnectTimeout = undefined;
@@ -1318,6 +1344,22 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1318
1344
  }
1319
1345
  };
1320
1346
 
1347
+ const cleanupClientResources = (): void => {
1348
+ const shimCleanups = xcrunShimCleanups.splice(0);
1349
+ for (const closeShim of shimCleanups) {
1350
+ closeShim().catch((error) => logger.warn('Failed to close xcrun shim:', error));
1351
+ }
1352
+ const proxyCleanups = httpProxyCleanups.splice(0);
1353
+ for (const closeProxy of proxyCleanups) {
1354
+ closeProxy().catch((error) => logger.warn('Failed to close HTTP proxy:', error));
1355
+ }
1356
+ };
1357
+
1358
+ const cleanup = (): void => {
1359
+ cleanupConnection();
1360
+ cleanupClientResources();
1361
+ };
1362
+
1321
1363
  let pingInterval: NodeJS.Timeout | undefined;
1322
1364
  const keepAliveSessionId =
1323
1365
  Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
@@ -1469,7 +1511,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1469
1511
  };
1470
1512
 
1471
1513
  const setupWebSocket = (): void => {
1472
- cleanup();
1514
+ cleanupConnection();
1473
1515
  updateConnectionState('connecting');
1474
1516
 
1475
1517
  const proxyAgent = nodeProxyTransport.getWebSocketAgent(endpointWebSocketUrl);
@@ -1638,11 +1680,13 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1638
1680
  discoverStoreKitConfig,
1639
1681
  softReset,
1640
1682
  startReverseTunnel,
1683
+ startHttpProxy,
1641
1684
  disconnect,
1642
1685
  getConnectionState,
1643
1686
  onConnectionStateChange,
1644
1687
  simctl,
1645
1688
  xcrun,
1689
+ startXcrunShim,
1646
1690
  xcodebuild,
1647
1691
  cp,
1648
1692
  lsof,
@@ -1911,6 +1955,17 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1911
1955
  return sendRequest<CommandResult>('xcrun', { args });
1912
1956
  };
1913
1957
 
1958
+ const startXcrunShim = async (): Promise<string> => {
1959
+ const shim = await startClientXcrunShim({
1960
+ deviceInfo: cachedDeviceInfo,
1961
+ listApps,
1962
+ simctl,
1963
+ syncApp,
1964
+ });
1965
+ xcrunShimCleanups.push(shim.close);
1966
+ return shim.dir;
1967
+ };
1968
+
1914
1969
  const xcodebuild = (args: string[]): Promise<CommandResult> => {
1915
1970
  return sendRequest<CommandResult>('xcodebuild', { args });
1916
1971
  };
@@ -2135,6 +2190,20 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
2135
2190
  });
2136
2191
  };
2137
2192
 
2193
+ const startHttpProxy = async (proxyOptions: HttpProxyOptions): Promise<number> => {
2194
+ assertPort(proxyOptions.localPort, 'localPort', 1, 65535);
2195
+
2196
+ const proxy = await startLocalHttpProxy({
2197
+ localPort: proxyOptions.localPort,
2198
+ remoteBaseUrl: proxyOptions.remoteBaseUrl,
2199
+ headers: {
2200
+ authorization: `Bearer ${options.token}`,
2201
+ },
2202
+ });
2203
+ httpProxyCleanups.push(proxy.close);
2204
+ return proxy.port;
2205
+ };
2206
+
2138
2207
  const disconnect = (): void => {
2139
2208
  intentionalDisconnect = true;
2140
2209
  cleanup();