@limrun/api 0.18.2 → 0.19.0

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +1 -1
  3. package/README.md +3 -1
  4. package/folder-sync-watcher.d.mts +17 -0
  5. package/folder-sync-watcher.d.mts.map +1 -0
  6. package/folder-sync-watcher.d.ts +17 -0
  7. package/folder-sync-watcher.d.ts.map +1 -0
  8. package/folder-sync-watcher.js +91 -0
  9. package/folder-sync-watcher.js.map +1 -0
  10. package/folder-sync-watcher.mjs +87 -0
  11. package/folder-sync-watcher.mjs.map +1 -0
  12. package/folder-sync.d.mts +23 -0
  13. package/folder-sync.d.mts.map +1 -0
  14. package/folder-sync.d.ts +23 -0
  15. package/folder-sync.d.ts.map +1 -0
  16. package/folder-sync.js +447 -0
  17. package/folder-sync.js.map +1 -0
  18. package/folder-sync.mjs +442 -0
  19. package/folder-sync.mjs.map +1 -0
  20. package/ios-client.d.mts +96 -0
  21. package/ios-client.d.mts.map +1 -1
  22. package/ios-client.d.ts +96 -0
  23. package/ios-client.d.ts.map +1 -1
  24. package/ios-client.js +89 -1
  25. package/ios-client.js.map +1 -1
  26. package/ios-client.mjs +89 -1
  27. package/ios-client.mjs.map +1 -1
  28. package/package.json +21 -1
  29. package/resources/android-instances.d.mts +2 -2
  30. package/resources/android-instances.d.ts +2 -2
  31. package/resources/assets.d.mts +9 -0
  32. package/resources/assets.d.mts.map +1 -1
  33. package/resources/assets.d.ts +9 -0
  34. package/resources/assets.d.ts.map +1 -1
  35. package/resources/ios-instances.d.mts +2 -2
  36. package/resources/ios-instances.d.ts +2 -2
  37. package/src/folder-sync-watcher.ts +99 -0
  38. package/src/folder-sync.ts +557 -0
  39. package/src/ios-client.ts +221 -4
  40. package/src/resources/android-instances.ts +2 -2
  41. package/src/resources/assets.ts +11 -0
  42. package/src/resources/ios-instances.ts +2 -2
  43. package/src/version.ts +1 -1
  44. package/version.d.mts +1 -1
  45. package/version.d.ts +1 -1
  46. package/version.js +1 -1
  47. package/version.mjs +1 -1
package/src/ios-client.ts CHANGED
@@ -2,6 +2,7 @@ import { WebSocket, Data } from 'ws';
2
2
  import fs from 'fs';
3
3
  import { EventEmitter } from 'events';
4
4
  import { isNonRetryableError } from './tunnel';
5
+ import { syncApp as syncAppImpl, type SyncFolderResult, type FolderSyncOptions } from './folder-sync';
5
6
 
6
7
  /**
7
8
  * Connection state of the instance client
@@ -91,6 +92,17 @@ export type LsofEntry = {
91
92
  path: string;
92
93
  };
93
94
 
95
+ export type DeviceInfo = {
96
+ /** Device UDID */
97
+ udid: string;
98
+ /** Screen width in points (Swift Double) */
99
+ screenWidth: number;
100
+ /** Screen height in points (Swift Double) */
101
+ screenHeight: number;
102
+ /** Device model name */
103
+ model: string;
104
+ };
105
+
94
106
  export type AppInstallationResult = {
95
107
  /** The URL the app was installed from */
96
108
  url: string;
@@ -98,6 +110,18 @@ export type AppInstallationResult = {
98
110
  bundleId: string;
99
111
  };
100
112
 
113
+ /**
114
+ * Result from a command execution (xcrun, xcodebuild, etc.)
115
+ */
116
+ export type CommandResult = {
117
+ /** Standard output from the command */
118
+ stdout: string;
119
+ /** Standard error from the command */
120
+ stderr: string;
121
+ /** Exit code of the command */
122
+ exitCode: number;
123
+ };
124
+
101
125
  export type AppInstallationOptions = {
102
126
  /** MD5 hash for caching - if provided and matches cached version, skips download */
103
127
  md5?: string;
@@ -212,6 +236,33 @@ export type InstanceClient = {
212
236
  */
213
237
  setOrientation: (orientation: 'Portrait' | 'Landscape') => Promise<void>;
214
238
 
239
+ /**
240
+ * Scroll in a direction by a specified number of pixels
241
+ * @param direction Direction content moves: "up", "down", "left", "right"
242
+ * @param pixels Total pixels to scroll (finger movement distance)
243
+ * @param options Optional scroll options
244
+ * @param options.coordinate Starting coordinate [x, y]. Defaults to screen center.
245
+ * @param options.momentum 0.0-1.0 controlling scroll speed and inertia. 0 (default) = slow scroll, no momentum. 1 = fastest with max inertia.
246
+ */
247
+ scroll: (
248
+ direction: 'up' | 'down' | 'left' | 'right',
249
+ pixels: number,
250
+ options?: { coordinate?: [number, number]; momentum?: number },
251
+ ) => Promise<void>;
252
+
253
+ /**
254
+ * Sync an iOS app bundle folder to the server and (optionally) install/launch it.
255
+ */
256
+ syncApp: (
257
+ localAppBundlePath: string,
258
+ opts?: {
259
+ install?: boolean;
260
+ maxPatchBytes?: number;
261
+ launchMode?: 'ForegroundIfRunning' | 'RelaunchIfRunning' | 'FailIfRunning';
262
+ watch?: boolean;
263
+ },
264
+ ) => Promise<SyncFolderResult>;
265
+
215
266
  /**
216
267
  * Disconnect from the Limrun instance
217
268
  */
@@ -278,12 +329,68 @@ export type InstanceClient = {
278
329
  */
279
330
  cp: (name: string, path: string) => Promise<string>;
280
331
 
332
+ /**
333
+ * Run `xcrun` command with the given arguments.
334
+ * Unlike simctl, this returns the complete output once the command finishes (non-streaming).
335
+ *
336
+ * Only the following flags are allowed:
337
+ * - `--sdk <value>`: Specify the SDK (e.g., 'iphonesimulator', 'iphoneos')
338
+ * - `--show-sdk-version`: Show the SDK version
339
+ * - `--show-sdk-build-version`: Show the SDK build version
340
+ * - `--show-sdk-platform-version`: Show the SDK platform version
341
+ *
342
+ * @param args Arguments to pass to xcrun
343
+ * @returns A promise that resolves to the command result with stdout, stderr, and exit code
344
+ *
345
+ * @example
346
+ * ```typescript
347
+ * // Get the SDK version for iphonesimulator
348
+ * const result = await client.xcrun(['--sdk', 'iphonesimulator', '--show-sdk-version']);
349
+ * console.log('SDK version:', result.stdout.trim());
350
+ *
351
+ * // Get the SDK build version (default SDK)
352
+ * const buildResult = await client.xcrun(['--show-sdk-build-version']);
353
+ * console.log('Build version:', buildResult.stdout.trim());
354
+ *
355
+ * // Get the SDK platform version for iphoneos
356
+ * const platformResult = await client.xcrun(['--sdk', 'iphoneos', '--show-sdk-platform-version']);
357
+ * console.log('Platform version:', platformResult.stdout.trim());
358
+ * ```
359
+ */
360
+ xcrun: (args: string[]) => Promise<CommandResult>;
361
+
362
+ /**
363
+ * Run `xcodebuild` command with the given arguments.
364
+ * Returns the complete output once the command finishes (non-streaming).
365
+ *
366
+ * Only `-version` is allowed (validated server-side).
367
+ *
368
+ * @param args Arguments to pass to xcodebuild
369
+ * @returns A promise that resolves to the command result with stdout, stderr, and exit code
370
+ *
371
+ * @example
372
+ * ```typescript
373
+ * // Get the Xcode version
374
+ * const result = await client.xcodebuild(['-version']);
375
+ * console.log('Xcode version:', result.stdout);
376
+ * // Output: Xcode 16.0
377
+ * // Build version 16A242d
378
+ * ```
379
+ */
380
+ xcodebuild: (args: string[]) => Promise<CommandResult>;
381
+
281
382
  /**
282
383
  * List all open files on the instance. Useful to start tunnel to the
283
384
  * UNIX sockets listed here.
284
385
  * @returns A promise that resolves to a list of open files.
285
386
  */
286
387
  lsof: () => Promise<LsofEntry[]>;
388
+
389
+ /**
390
+ * Device information fetched during client initialization.
391
+ * Contains id, udid, screen dimensions, and model.
392
+ */
393
+ deviceInfo: DeviceInfo;
287
394
  };
288
395
 
289
396
  /**
@@ -363,6 +470,11 @@ type ServerResponse = {
363
470
  url?: string;
364
471
  bundleId?: string;
365
472
  files?: LsofEntry[];
473
+ // Device info fields
474
+ udid?: string;
475
+ screenWidth?: number;
476
+ screenHeight?: number;
477
+ model?: string;
366
478
  // Simctl streaming fields
367
479
  stdout?: string;
368
480
  stderr?: string;
@@ -725,12 +837,29 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
725
837
  launchAppResult: () => undefined,
726
838
  listAppsResult: (msg) => JSON.parse(msg.apps || '[]') as InstalledApp[],
727
839
  listOpenFilesResult: (msg) => msg.files || [],
840
+ deviceInfoResult: (msg) => ({
841
+ udid: msg.udid!,
842
+ screenWidth: msg.screenWidth!,
843
+ screenHeight: msg.screenHeight!,
844
+ model: msg.model!,
845
+ }),
728
846
  openUrlResult: () => undefined,
729
847
  appInstallationResult: (msg) => ({
730
848
  url: msg.url || '',
731
849
  bundleId: msg.bundleId || '',
732
850
  }),
733
851
  setOrientationResult: () => undefined,
852
+ scrollResult: () => undefined,
853
+ xcrunResult: (msg): CommandResult => ({
854
+ stdout: msg.stdout ? Buffer.from(msg.stdout, 'base64').toString('utf-8') : '',
855
+ stderr: msg.stderr ? Buffer.from(msg.stderr, 'base64').toString('utf-8') : '',
856
+ exitCode: msg.exitCode ?? -1,
857
+ }),
858
+ xcodebuildResult: (msg): CommandResult => ({
859
+ stdout: msg.stdout ? Buffer.from(msg.stdout, 'base64').toString('utf-8') : '',
860
+ stderr: msg.stderr ? Buffer.from(msg.stderr, 'base64').toString('utf-8') : '',
861
+ exitCode: msg.exitCode ?? -1,
862
+ }),
734
863
  };
735
864
 
736
865
  const setupWebSocket = (): void => {
@@ -846,7 +975,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
846
975
  }
847
976
  });
848
977
 
849
- ws.on('open', () => {
978
+ ws.on('open', async () => {
850
979
  logger.debug(`Connected to ${endpointWebSocketUrl}`);
851
980
  reconnectAttempts = 0;
852
981
  lastError = undefined;
@@ -859,6 +988,16 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
859
988
  }, 30_000);
860
989
 
861
990
  if (!hasResolved) {
991
+ try {
992
+ // Fetch device info before resolving connection
993
+ cachedDeviceInfo = await fetchDeviceInfo();
994
+ logger.debug('Device info fetched:', cachedDeviceInfo);
995
+ } catch (err) {
996
+ logger.error('Failed to fetch device info:', err);
997
+ rejectConnection(err as Error);
998
+ return;
999
+ }
1000
+
862
1001
  hasResolved = true;
863
1002
  resolveConnection({
864
1003
  screenshot,
@@ -875,12 +1014,17 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
875
1014
  openUrl,
876
1015
  installApp,
877
1016
  setOrientation,
1017
+ scroll,
1018
+ syncApp,
878
1019
  disconnect,
879
1020
  getConnectionState,
880
1021
  onConnectionStateChange,
881
1022
  simctl,
1023
+ xcrun,
1024
+ xcodebuild,
882
1025
  cp,
883
1026
  lsof,
1027
+ deviceInfo: cachedDeviceInfo,
884
1028
  });
885
1029
  }
886
1030
  });
@@ -950,10 +1094,81 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
950
1094
  return sendRequest<void>('setOrientation', { orientation });
951
1095
  };
952
1096
 
1097
+ const scroll = (
1098
+ direction: 'up' | 'down' | 'left' | 'right',
1099
+ pixels: number,
1100
+ options?: { coordinate?: [number, number]; momentum?: number },
1101
+ ): Promise<void> => {
1102
+ return sendRequest<void>('scroll', {
1103
+ direction,
1104
+ pixels,
1105
+ coordinate: options?.coordinate,
1106
+ momentum: options?.momentum,
1107
+ });
1108
+ };
1109
+
1110
+ const syncApp = async (
1111
+ localAppBundlePath: string,
1112
+ opts?: {
1113
+ install?: boolean;
1114
+ maxPatchBytes?: number;
1115
+ launchMode?: 'ForegroundIfRunning' | 'RelaunchIfRunning' | 'FailIfRunning';
1116
+ watch?: boolean;
1117
+ },
1118
+ ): Promise<SyncFolderResult> => {
1119
+ if (!cachedDeviceInfo) {
1120
+ throw new Error('Device info not available yet; wait for client connection to be established.');
1121
+ }
1122
+ const appSyncOpts: FolderSyncOptions = {
1123
+ apiUrl: options.apiUrl,
1124
+ token: options.token,
1125
+ udid: cachedDeviceInfo.udid,
1126
+ log: (level, msg) => {
1127
+ switch (level) {
1128
+ case 'debug':
1129
+ logger.debug(msg);
1130
+ break;
1131
+ case 'info':
1132
+ logger.info(msg);
1133
+ break;
1134
+ case 'warn':
1135
+ logger.warn(msg);
1136
+ break;
1137
+ case 'error':
1138
+ logger.error(msg);
1139
+ break;
1140
+ default:
1141
+ logger.info(msg);
1142
+ break;
1143
+ }
1144
+ },
1145
+ ...(opts?.install !== undefined ? { install: opts.install } : {}),
1146
+ ...(opts?.maxPatchBytes !== undefined ? { maxPatchBytes: opts.maxPatchBytes } : {}),
1147
+ ...(opts?.launchMode !== undefined ? { launchMode: opts.launchMode } : {}),
1148
+ ...(opts?.watch !== undefined ? { watch: opts.watch } : {}),
1149
+ };
1150
+ return await syncAppImpl(localAppBundlePath, appSyncOpts);
1151
+ };
1152
+
953
1153
  const lsof = (): Promise<LsofEntry[]> => {
954
1154
  return sendRequest<LsofEntry[]>('listOpenFiles', { kind: 'unix' });
955
1155
  };
956
1156
 
1157
+ const xcrun = (args: string[]): Promise<CommandResult> => {
1158
+ return sendRequest<CommandResult>('xcrun', { args });
1159
+ };
1160
+
1161
+ const xcodebuild = (args: string[]): Promise<CommandResult> => {
1162
+ return sendRequest<CommandResult>('xcodebuild', { args });
1163
+ };
1164
+
1165
+ const fetchDeviceInfo = (): Promise<DeviceInfo> => {
1166
+ return sendRequest<DeviceInfo>('deviceInfo');
1167
+ };
1168
+
1169
+ // Cached device info, populated during connection
1170
+ let cachedDeviceInfo: DeviceInfo;
1171
+
957
1172
  const simctl = (args: string[], opts: { disconnectOnExit?: boolean } = {}): SimctlExecution => {
958
1173
  const id = generateId();
959
1174
 
@@ -1014,6 +1229,8 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1014
1229
  const fileStream = fs.createReadStream(filePath);
1015
1230
  const uploadUrl = `${options.apiUrl}/files?name=${encodeURIComponent(name)}`;
1016
1231
  try {
1232
+ // Node's fetch (undici) supports streaming request bodies but TS DOM types may not include
1233
+ // `duplex` and may not accept Node ReadStreams as BodyInit in some configs.
1017
1234
  const response = await fetch(uploadUrl, {
1018
1235
  method: 'PUT',
1019
1236
  headers: {
@@ -1021,9 +1238,9 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1021
1238
  'Content-Length': fs.statSync(filePath).size.toString(),
1022
1239
  Authorization: `Bearer ${options.token}`,
1023
1240
  },
1024
- body: fileStream,
1025
- duplex: 'half',
1026
- });
1241
+ body: fileStream as any,
1242
+ duplex: 'half' as any,
1243
+ } as any);
1027
1244
  if (!response.ok) {
1028
1245
  const errorBody = await response.text();
1029
1246
  logger.debug(`Upload failed: ${response.status} ${errorBody}`);
@@ -132,12 +132,12 @@ export interface AndroidInstanceCreateParams {
132
132
  wait?: boolean;
133
133
 
134
134
  /**
135
- * Body param:
135
+ * Body param
136
136
  */
137
137
  metadata?: AndroidInstanceCreateParams.Metadata;
138
138
 
139
139
  /**
140
- * Body param:
140
+ * Body param
141
141
  */
142
142
  spec?: AndroidInstanceCreateParams.Spec;
143
143
  }
@@ -56,11 +56,22 @@ export interface Asset {
56
56
 
57
57
  name: string;
58
58
 
59
+ /**
60
+ * Human-readable display name for the asset. If not set, the name should be used.
61
+ */
62
+ displayName?: string;
63
+
59
64
  /**
60
65
  * Returned only if there is a corresponding file uploaded already.
61
66
  */
62
67
  md5?: string;
63
68
 
69
+ /**
70
+ * The operating system this asset is for. If not set, the asset is available for
71
+ * all platforms.
72
+ */
73
+ os?: 'ios' | 'android';
74
+
64
75
  signedDownloadUrl?: string;
65
76
 
66
77
  signedUploadUrl?: string;
@@ -120,12 +120,12 @@ export interface IosInstanceCreateParams {
120
120
  wait?: boolean;
121
121
 
122
122
  /**
123
- * Body param:
123
+ * Body param
124
124
  */
125
125
  metadata?: IosInstanceCreateParams.Metadata;
126
126
 
127
127
  /**
128
- * Body param:
128
+ * Body param
129
129
  */
130
130
  spec?: IosInstanceCreateParams.Spec;
131
131
  }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.18.2'; // x-release-please-version
1
+ export const VERSION = '0.19.0'; // x-release-please-version
package/version.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.18.2";
1
+ export declare const VERSION = "0.19.0";
2
2
  //# sourceMappingURL=version.d.mts.map
package/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.18.2";
1
+ export declare const VERSION = "0.19.0";
2
2
  //# sourceMappingURL=version.d.ts.map
package/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
- exports.VERSION = '0.18.2'; // x-release-please-version
4
+ exports.VERSION = '0.19.0'; // x-release-please-version
5
5
  //# sourceMappingURL=version.js.map
package/version.mjs CHANGED
@@ -1,2 +1,2 @@
1
- export const VERSION = '0.18.2'; // x-release-please-version
1
+ export const VERSION = '0.19.0'; // x-release-please-version
2
2
  //# sourceMappingURL=version.mjs.map