@limrun/api 0.28.5 → 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/CHANGELOG.md +8 -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/http-proxy.d.mts +11 -0
- package/http-proxy.d.mts.map +1 -0
- package/http-proxy.d.ts +11 -0
- package/http-proxy.d.ts.map +1 -0
- package/http-proxy.js +66 -0
- package/http-proxy.js.map +1 -0
- package/http-proxy.mjs +62 -0
- package/http-proxy.mjs.map +1 -0
- package/index.d.mts +2 -1
- package/index.d.mts.map +1 -1
- package/index.d.ts +2 -1
- package/index.d.ts.map +1 -1
- package/index.js +3 -1
- package/index.js.map +1 -1
- package/index.mjs +1 -0
- package/index.mjs.map +1 -1
- package/ios-client.d.mts +18 -0
- package/ios-client.d.mts.map +1 -1
- package/ios-client.d.ts +18 -0
- package/ios-client.d.ts.map +1 -1
- package/ios-client.js +44 -2
- package/ios-client.js.map +1 -1
- package/ios-client.mjs +44 -2
- package/ios-client.mjs.map +1 -1
- package/ios-shim.d.mts +25 -0
- package/ios-shim.d.mts.map +1 -0
- package/ios-shim.d.ts +25 -0
- package/ios-shim.d.ts.map +1 -0
- package/ios-shim.js +352 -0
- package/ios-shim.js.map +1 -0
- package/ios-shim.mjs +346 -0
- package/ios-shim.mjs.map +1 -0
- package/package.json +21 -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/http-proxy.ts +85 -0
- package/src/index.ts +2 -0
- package/src/ios-client.ts +71 -2
- package/src/ios-shim.ts +401 -0
- package/src/resources/index.ts +1 -0
- package/src/resources/xcode-instances-helpers.ts +25 -0
- package/src/version.ts +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/client.ts
CHANGED
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
XcodeClient,
|
|
54
54
|
XcodeProjectConfig,
|
|
55
55
|
XcodeBuildOptions,
|
|
56
|
+
ReactNativeBuildConfig,
|
|
56
57
|
} from './resources/xcode-instances-helpers';
|
|
57
58
|
import { type Fetch } from './internal/builtin-types';
|
|
58
59
|
import { HeadersLike, NullableHeaders, buildHeaders } from './internal/headers';
|
|
@@ -850,5 +851,6 @@ export declare namespace Limrun {
|
|
|
850
851
|
type XcodeClient as XcodeClient,
|
|
851
852
|
type XcodeProjectConfig as XcodeProjectConfig,
|
|
852
853
|
type XcodeBuildOptions as XcodeBuildOptions,
|
|
854
|
+
type ReactNativeBuildConfig as ReactNativeBuildConfig,
|
|
853
855
|
};
|
|
854
856
|
}
|
package/src/exec-client.ts
CHANGED
|
@@ -19,6 +19,11 @@ export type ExecRequest = {
|
|
|
19
19
|
project?: string;
|
|
20
20
|
scheme?: string;
|
|
21
21
|
sdk?: 'iphonesimulator' | 'iphoneos' | 'watchsimulator' | 'watchos';
|
|
22
|
+
configuration?: 'Debug' | 'Release';
|
|
23
|
+
};
|
|
24
|
+
reactNative?: {
|
|
25
|
+
expoAppDir?: string;
|
|
26
|
+
devServerURL?: string;
|
|
22
27
|
};
|
|
23
28
|
signing?: {
|
|
24
29
|
certificateP12Base64?: string;
|
|
@@ -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,
|
|
@@ -20,6 +21,7 @@ export {
|
|
|
20
21
|
type XcodeClient,
|
|
21
22
|
type XcodeProjectConfig,
|
|
22
23
|
type XcodeBuildOptions,
|
|
24
|
+
type ReactNativeBuildConfig,
|
|
23
25
|
} from './resources/xcode-instances-helpers';
|
|
24
26
|
export {
|
|
25
27
|
LimrunError,
|
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
|
|
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
|
-
|
|
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();
|
package/src/ios-shim.ts
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import http, { type IncomingMessage, type ServerResponse } from 'http';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
import type { InstalledApp, InstanceClient } from './ios-client';
|
|
7
|
+
|
|
8
|
+
const FORWARDED_SIMCTL_COMMANDS = new Set([
|
|
9
|
+
'openurl',
|
|
10
|
+
'launch',
|
|
11
|
+
'terminate',
|
|
12
|
+
'uninstall',
|
|
13
|
+
'privacy',
|
|
14
|
+
'location',
|
|
15
|
+
'status_bar',
|
|
16
|
+
'spawn',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export type IosXcrunShimServer = {
|
|
20
|
+
url: string;
|
|
21
|
+
close: () => Promise<void>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type IosXcrunShim = {
|
|
25
|
+
dir: string;
|
|
26
|
+
close: () => Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type IosXcrunShimClient = Pick<InstanceClient, 'deviceInfo' | 'listApps' | 'simctl' | 'syncApp'>;
|
|
30
|
+
|
|
31
|
+
export type IosShimSimctlResult = {
|
|
32
|
+
code: number;
|
|
33
|
+
stdout: string;
|
|
34
|
+
stderr: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export async function startXcrunShim(client: IosXcrunShimClient): Promise<IosXcrunShim> {
|
|
38
|
+
const server = await startXcrunShimServer({ client, udid: client.deviceInfo.udid });
|
|
39
|
+
try {
|
|
40
|
+
const dir = await createXcrunShim({ shimUrl: server.url, udid: client.deviceInfo.udid });
|
|
41
|
+
return {
|
|
42
|
+
dir,
|
|
43
|
+
close: async () => {
|
|
44
|
+
await server.close();
|
|
45
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
} catch (error) {
|
|
49
|
+
await server.close().catch(() => {});
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function startXcrunShimServer({
|
|
55
|
+
client,
|
|
56
|
+
udid,
|
|
57
|
+
}: {
|
|
58
|
+
client: IosXcrunShimClient;
|
|
59
|
+
udid: string;
|
|
60
|
+
}): Promise<IosXcrunShimServer> {
|
|
61
|
+
const server = http.createServer(async (req, res) => {
|
|
62
|
+
if (req.method !== 'POST' || req.url !== '/xcrun') {
|
|
63
|
+
sendJson(res, 404, { code: 127, stdout: '', stderr: 'not found' });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const body = (await readJson(req)) as { args?: string[] };
|
|
68
|
+
const args = body.args ?? [];
|
|
69
|
+
const result = await handleShimmedXcrun(client, udid, args);
|
|
70
|
+
sendJson(res, 200, result);
|
|
71
|
+
} catch {
|
|
72
|
+
sendJson(res, 200, {
|
|
73
|
+
code: 1,
|
|
74
|
+
stdout: '',
|
|
75
|
+
stderr: 'limrun xcrun shim failed to execute the requested command.',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await listen(server, 0);
|
|
81
|
+
const address = server.address();
|
|
82
|
+
if (!address || typeof address === 'string') {
|
|
83
|
+
throw new Error('Failed to start Limrun xcrun shim server.');
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
url: `http://127.0.0.1:${address.port}/xcrun`,
|
|
87
|
+
close: () => closeServer(server),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function createXcrunShim(options?: { shimUrl: string; udid: string }): Promise<string> {
|
|
92
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'limrun-xcrun-shim-'));
|
|
93
|
+
const shimPath = path.join(dir, 'xcrun');
|
|
94
|
+
fs.writeFileSync(shimPath, xcrunShimSource(options), 'utf8');
|
|
95
|
+
fs.chmodSync(shimPath, 0o755);
|
|
96
|
+
return dir;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function handleShimmedXcrun(
|
|
100
|
+
client: IosXcrunShimClient,
|
|
101
|
+
udid: string,
|
|
102
|
+
args: string[],
|
|
103
|
+
): Promise<IosShimSimctlResult> {
|
|
104
|
+
if (args[0] !== 'simctl') {
|
|
105
|
+
return { code: 127, stdout: '', stderr: `unsupported xcrun command: ${args.join(' ')}` };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const simctlArgs = args.slice(1);
|
|
109
|
+
const command = simctlArgs[0];
|
|
110
|
+
const target = simctlTarget(command, simctlArgs);
|
|
111
|
+
|
|
112
|
+
if (command === 'list') {
|
|
113
|
+
// Maestro's device picker expects the full CoreSimulator JSON shape. Limrun
|
|
114
|
+
// only has one remote simulator in this context, so we synthesize that entry.
|
|
115
|
+
return simctlList(client.deviceInfo.udid, simctlArgs);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!isLimrunTarget(target, udid)) {
|
|
119
|
+
return { code: 64, stdout: '', stderr: `unsupported non-Limrun simctl target '${target ?? ''}'.` };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (command === 'listapps') {
|
|
123
|
+
const apps = await client.listApps();
|
|
124
|
+
return {
|
|
125
|
+
code: 0,
|
|
126
|
+
stdout: `${JSON.stringify(toSimctlListApps(apps))}\n`,
|
|
127
|
+
stderr: '',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (command === 'install') {
|
|
132
|
+
const appPath = simctlArgs[2];
|
|
133
|
+
if (!appPath) {
|
|
134
|
+
return { code: 64, stdout: '', stderr: 'simctl install requires a local .app path.' };
|
|
135
|
+
}
|
|
136
|
+
await client.syncApp(appPath, { install: true, watch: false });
|
|
137
|
+
return { code: 0, stdout: '', stderr: '' };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const unsupported = unsupportedPathBearingCommand(command);
|
|
141
|
+
if (unsupported) {
|
|
142
|
+
return unsupported;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!command || !FORWARDED_SIMCTL_COMMANDS.has(command)) {
|
|
146
|
+
return { code: 64, stdout: '', stderr: `limrun xcrun shim does not support simctl ${command ?? ''}.` };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return await client.simctl(simctlArgs).wait();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function simctlList(udid: string, simctlArgs: string[]): IosShimSimctlResult {
|
|
153
|
+
if (!simctlArgs.includes('-j')) {
|
|
154
|
+
return { code: 64, stdout: '', stderr: 'limrun xcrun shim only supports `xcrun simctl list -j`.' };
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
code: 0,
|
|
158
|
+
stdout: `${JSON.stringify(toSimctlList(udid))}\n`,
|
|
159
|
+
stderr: '',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function unsupportedPathBearingCommand(command: string | undefined): IosShimSimctlResult | undefined {
|
|
164
|
+
// These commands either return local simulator filesystem paths or consume
|
|
165
|
+
// host-side media files. They need explicit staging/translation before they
|
|
166
|
+
// are safe to claim as supported.
|
|
167
|
+
if (command === 'get_app_container') {
|
|
168
|
+
return {
|
|
169
|
+
code: 64,
|
|
170
|
+
stdout: '',
|
|
171
|
+
stderr:
|
|
172
|
+
'limrun xcrun shim does not support get_app_container because upstream tools expect a local filesystem path.',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (command === 'keychain') {
|
|
176
|
+
return { code: 64, stdout: '', stderr: 'limrun xcrun shim does not support simctl keychain in v1.' };
|
|
177
|
+
}
|
|
178
|
+
if (command === 'io') {
|
|
179
|
+
return {
|
|
180
|
+
code: 64,
|
|
181
|
+
stdout: '',
|
|
182
|
+
stderr: 'limrun xcrun shim does not support simctl io recordVideo in v1.',
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (command === 'push' || command === 'addmedia') {
|
|
186
|
+
return {
|
|
187
|
+
code: 64,
|
|
188
|
+
stdout: '',
|
|
189
|
+
stderr: `limrun xcrun shim does not support path-bearing simctl ${command} in v1.`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function isLimrunTarget(value: string | undefined, udid: string): boolean {
|
|
196
|
+
return value === udid || value === 'booted';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function simctlTarget(command: string | undefined, simctlArgs: string[]): string | undefined {
|
|
200
|
+
if (command === 'launch') {
|
|
201
|
+
return simctlArgs.slice(1).find((arg) => !arg.startsWith('-'));
|
|
202
|
+
}
|
|
203
|
+
return simctlArgs[1];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function toSimctlList(udid: string): Record<string, unknown> {
|
|
207
|
+
const runtimeIdentifier = 'com.apple.CoreSimulator.SimRuntime.iOS-18-0';
|
|
208
|
+
const deviceTypeIdentifier = 'com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro';
|
|
209
|
+
return {
|
|
210
|
+
devices: {
|
|
211
|
+
[runtimeIdentifier]: [
|
|
212
|
+
{
|
|
213
|
+
availability: '(available)',
|
|
214
|
+
dataPath: `/tmp/limrun-sim/${udid}/data`,
|
|
215
|
+
logPath: `/tmp/limrun-sim/${udid}/logs`,
|
|
216
|
+
isAvailable: true,
|
|
217
|
+
name: process.env['LIMRUN_IOS_DEVICE_NAME'] || 'Limrun iPhone',
|
|
218
|
+
state: 'Booted',
|
|
219
|
+
udid,
|
|
220
|
+
deviceTypeIdentifier,
|
|
221
|
+
availabilityError: null,
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
devicetypes: [
|
|
226
|
+
{
|
|
227
|
+
bundlePath: '',
|
|
228
|
+
identifier: deviceTypeIdentifier,
|
|
229
|
+
maxRuntimeVersion: 999999,
|
|
230
|
+
maxRuntimeVersionString: null,
|
|
231
|
+
minRuntimeVersion: 0,
|
|
232
|
+
minRuntimeVersionString: null,
|
|
233
|
+
modelIdentifier: 'iPhone17,1',
|
|
234
|
+
name: 'iPhone 16 Pro',
|
|
235
|
+
productFamily: 'iPhone',
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
pairs: {},
|
|
239
|
+
runtimes: [
|
|
240
|
+
{
|
|
241
|
+
availability: '(available)',
|
|
242
|
+
bundlePath: '',
|
|
243
|
+
buildversion: '22A000',
|
|
244
|
+
identifier: runtimeIdentifier,
|
|
245
|
+
isInternal: false,
|
|
246
|
+
isAvailable: true,
|
|
247
|
+
name: process.env['LIMRUN_IOS_RUNTIME_NAME'] || 'iOS 18.0',
|
|
248
|
+
platform: 'iOS',
|
|
249
|
+
runtimeRoot: '',
|
|
250
|
+
supportedDeviceTypes: [],
|
|
251
|
+
version: '18.0',
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function toSimctlListApps(
|
|
258
|
+
apps: InstalledApp[],
|
|
259
|
+
): Record<string, { CFBundleIdentifier: string; CFBundleName?: string }> {
|
|
260
|
+
return Object.fromEntries(
|
|
261
|
+
apps.map((app) => [
|
|
262
|
+
app.bundleId,
|
|
263
|
+
{
|
|
264
|
+
CFBundleIdentifier: app.bundleId,
|
|
265
|
+
...(app.name ? { CFBundleName: app.name } : {}),
|
|
266
|
+
},
|
|
267
|
+
]),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function xcrunShimSource(options?: { shimUrl: string; udid: string }): string {
|
|
272
|
+
// Keep the executable tiny: it decides whether this is a Limrun-targeted
|
|
273
|
+
// simctl call, then asks the local shim server to perform the real work.
|
|
274
|
+
// Non-Limrun calls still delegate to the host xcrun.
|
|
275
|
+
const embeddedShimUrl = options ? JSON.stringify(options.shimUrl) : 'process.env.LIMRUN_XCRUN_SHIM_URL';
|
|
276
|
+
const embeddedUdid = options ? JSON.stringify(options.udid) : 'process.env.LIMRUN_IOS_UDID';
|
|
277
|
+
return `#!/usr/bin/env node
|
|
278
|
+
const http = require('node:http');
|
|
279
|
+
const { spawnSync } = require('node:child_process');
|
|
280
|
+
|
|
281
|
+
const args = process.argv.slice(2);
|
|
282
|
+
const realXcrun = process.env.LIMRUN_REAL_XCRUN || '/usr/bin/xcrun';
|
|
283
|
+
|
|
284
|
+
function delegate() {
|
|
285
|
+
const result = spawnSync(realXcrun, args, { stdio: 'inherit' });
|
|
286
|
+
if (result.error) {
|
|
287
|
+
process.stderr.write(String(result.error.message || result.error) + '\\n');
|
|
288
|
+
process.exit(127);
|
|
289
|
+
}
|
|
290
|
+
process.exit(result.status ?? 1);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function fail(message) {
|
|
294
|
+
process.stderr.write('limrun xcrun shim: ' + message + '\\n');
|
|
295
|
+
process.exit(64);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (args[0] !== 'simctl') {
|
|
299
|
+
delegate();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const shimUrl = ${embeddedShimUrl};
|
|
303
|
+
const udid = ${embeddedUdid};
|
|
304
|
+
if (!shimUrl || !udid) {
|
|
305
|
+
fail('LIMRUN_XCRUN_SHIM_URL and LIMRUN_IOS_UDID are required.');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const simctlArgs = args.slice(1);
|
|
309
|
+
const command = simctlArgs[0];
|
|
310
|
+
function simctlTarget(command, simctlArgs) {
|
|
311
|
+
if (command === 'launch') {
|
|
312
|
+
return simctlArgs.slice(1).find((arg) => !arg.startsWith('-'));
|
|
313
|
+
}
|
|
314
|
+
return simctlArgs[1];
|
|
315
|
+
}
|
|
316
|
+
const target = simctlTarget(command, simctlArgs);
|
|
317
|
+
function isLimrunTarget(value) {
|
|
318
|
+
return value === udid || value === 'booted';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (command !== 'list' && !isLimrunTarget(target)) {
|
|
322
|
+
delegate();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const parsed = new URL(shimUrl);
|
|
326
|
+
const body = JSON.stringify({ args });
|
|
327
|
+
const req = http.request(parsed, {
|
|
328
|
+
method: 'POST',
|
|
329
|
+
headers: {
|
|
330
|
+
'content-type': 'application/json',
|
|
331
|
+
'content-length': Buffer.byteLength(body),
|
|
332
|
+
},
|
|
333
|
+
}, (res) => {
|
|
334
|
+
const chunks = [];
|
|
335
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
336
|
+
res.on('end', () => {
|
|
337
|
+
let payload;
|
|
338
|
+
try {
|
|
339
|
+
payload = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
340
|
+
} catch (error) {
|
|
341
|
+
process.stderr.write('limrun xcrun shim: invalid shim response\\n');
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
if (payload.stdout) process.stdout.write(payload.stdout);
|
|
345
|
+
if (payload.stderr) process.stderr.write(payload.stderr);
|
|
346
|
+
process.exit(typeof payload.code === 'number' ? payload.code : 1);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
req.on('error', (error) => {
|
|
350
|
+
process.stderr.write('limrun xcrun shim: ' + error.message + '\\n');
|
|
351
|
+
process.exit(1);
|
|
352
|
+
});
|
|
353
|
+
req.end(body);
|
|
354
|
+
`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function listen(server: http.Server, port: number): Promise<void> {
|
|
358
|
+
return new Promise((resolve, reject) => {
|
|
359
|
+
server.once('error', reject);
|
|
360
|
+
server.listen(port, '127.0.0.1', () => {
|
|
361
|
+
server.off('error', reject);
|
|
362
|
+
resolve();
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function closeServer(server: http.Server): Promise<void> {
|
|
368
|
+
return new Promise((resolve, reject) => {
|
|
369
|
+
server.close((error) => {
|
|
370
|
+
if (error) {
|
|
371
|
+
reject(error);
|
|
372
|
+
} else {
|
|
373
|
+
resolve();
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function readJson(req: IncomingMessage): Promise<unknown> {
|
|
380
|
+
return new Promise((resolve, reject) => {
|
|
381
|
+
const chunks: Buffer[] = [];
|
|
382
|
+
req.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
383
|
+
req.on('end', () => {
|
|
384
|
+
try {
|
|
385
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}'));
|
|
386
|
+
} catch (error) {
|
|
387
|
+
reject(error);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
req.on('error', reject);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function sendJson(res: ServerResponse, statusCode: number, payload: unknown): void {
|
|
395
|
+
const body = JSON.stringify(payload);
|
|
396
|
+
res.writeHead(statusCode, {
|
|
397
|
+
'content-type': 'application/json',
|
|
398
|
+
'content-length': Buffer.byteLength(body),
|
|
399
|
+
});
|
|
400
|
+
res.end(body);
|
|
401
|
+
}
|