@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/CHANGELOG.md +4 -0
- 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 +1 -0
- package/index.d.mts.map +1 -1
- package/index.d.ts +1 -0
- 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/src/http-proxy.ts +85 -0
- package/src/index.ts +1 -0
- package/src/ios-client.ts +71 -2
- package/src/ios-shim.ts +401 -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/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
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.28.
|
|
1
|
+
export const VERSION = '0.28.7'; // x-release-please-version
|
package/version.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.28.
|
|
1
|
+
export declare const VERSION = "0.28.7";
|
|
2
2
|
//# sourceMappingURL=version.d.mts.map
|
package/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.28.
|
|
1
|
+
export declare const VERSION = "0.28.7";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/version.js
CHANGED
package/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = '0.28.
|
|
1
|
+
export const VERSION = '0.28.7'; // x-release-please-version
|
|
2
2
|
//# sourceMappingURL=version.mjs.map
|