@limrun/api 0.33.1 → 0.33.3
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 +12 -0
- package/app-archive.d.mts +19 -0
- package/app-archive.d.mts.map +1 -0
- package/app-archive.d.ts +19 -0
- package/app-archive.d.ts.map +1 -0
- package/app-archive.js +296 -0
- package/app-archive.js.map +1 -0
- package/app-archive.mjs +290 -0
- package/app-archive.mjs.map +1 -0
- package/instance-client.d.mts +15 -0
- package/instance-client.d.mts.map +1 -1
- package/instance-client.d.ts +15 -0
- package/instance-client.d.ts.map +1 -1
- package/instance-client.js +11 -0
- package/instance-client.js.map +1 -1
- package/instance-client.mjs +11 -0
- package/instance-client.mjs.map +1 -1
- package/ios-client.d.mts.map +1 -1
- package/ios-client.d.ts.map +1 -1
- package/ios-client.js +25 -5
- package/ios-client.js.map +1 -1
- package/ios-client.mjs +25 -5
- package/ios-client.mjs.map +1 -1
- package/package.json +13 -2
- package/src/app-archive.ts +345 -0
- package/src/instance-client.ts +42 -0
- package/src/ios-client.ts +25 -5
- 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
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { pipeline } from 'stream/promises';
|
|
6
|
+
import * as yauzl from 'yauzl';
|
|
7
|
+
|
|
8
|
+
type LogFn = (level: 'debug' | 'info' | 'warn' | 'error', msg: string) => void;
|
|
9
|
+
|
|
10
|
+
export type PreparedAppBundle = {
|
|
11
|
+
appPath: string;
|
|
12
|
+
cacheIdentityPath: string;
|
|
13
|
+
isArchive: boolean;
|
|
14
|
+
archivePath?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ArchiveWatcher = {
|
|
18
|
+
close: () => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const noopLogger: LogFn = () => {};
|
|
22
|
+
|
|
23
|
+
function sleep(ms: number): Promise<void> {
|
|
24
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function stableArchiveRoot(archivePath: string): string {
|
|
28
|
+
const resolved = path.resolve(archivePath);
|
|
29
|
+
const hash = crypto.createHash('sha1').update(resolved).digest('hex').slice(0, 12);
|
|
30
|
+
const base = path
|
|
31
|
+
.basename(resolved, path.extname(resolved))
|
|
32
|
+
.replace(/[^A-Za-z0-9._-]+/g, '-')
|
|
33
|
+
.replace(/^-+|-+$/g, '');
|
|
34
|
+
const safeBase = base === '' ? 'app' : base;
|
|
35
|
+
return path.join(os.tmpdir(), `limrun-sync-app-${safeBase}-${hash}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function stableAppPath(archivePath: string): string {
|
|
39
|
+
return path.join(stableArchiveRoot(archivePath), 'Extracted.app');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toZipPath(name: string): string {
|
|
43
|
+
return name.replace(/\\/g, '/');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isUnsafeZipPath(name: string): boolean {
|
|
47
|
+
if (name.includes('\0')) return true;
|
|
48
|
+
if (path.posix.isAbsolute(name)) return true;
|
|
49
|
+
return name.split('/').some((part) => part === '..');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function entryMode(entry: yauzl.Entry): number {
|
|
53
|
+
return (entry.externalFileAttributes >>> 16) & 0xffff;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isSymlinkEntry(entry: yauzl.Entry): boolean {
|
|
57
|
+
return (entryMode(entry) & 0o170000) === 0o120000;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isDirectoryEntry(entry: yauzl.Entry): boolean {
|
|
61
|
+
return entry.fileName.endsWith('/');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function entryPerm(entry: yauzl.Entry, fallback: number): number {
|
|
65
|
+
return entryMode(entry) & 0o7777 || fallback;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function openZip(archivePath: string): Promise<yauzl.ZipFile> {
|
|
69
|
+
try {
|
|
70
|
+
return await yauzl.openPromise(archivePath, {
|
|
71
|
+
autoClose: false,
|
|
72
|
+
lazyEntries: true,
|
|
73
|
+
strictFileNames: true,
|
|
74
|
+
validateEntrySizes: true,
|
|
75
|
+
});
|
|
76
|
+
} catch (err) {
|
|
77
|
+
const message = err instanceof Error ? `: ${err.message}` : '';
|
|
78
|
+
throw new Error(
|
|
79
|
+
`The path is not a valid app bundle directory or ZIP/IPA archive: ${archivePath}${message}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function readEntries(zip: yauzl.ZipFile): Promise<yauzl.Entry[]> {
|
|
85
|
+
const entries: yauzl.Entry[] = [];
|
|
86
|
+
for await (const entry of zip.eachEntry()) {
|
|
87
|
+
entries.push(entry);
|
|
88
|
+
}
|
|
89
|
+
return entries;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function ensureDirectoryNotSymlink(dir: string): Promise<void> {
|
|
93
|
+
const stat = await fs.promises.lstat(dir).catch((err: NodeJS.ErrnoException) => {
|
|
94
|
+
if (err.code === 'ENOENT') return null;
|
|
95
|
+
throw err;
|
|
96
|
+
});
|
|
97
|
+
if (stat && (!stat.isDirectory() || stat.isSymbolicLink())) {
|
|
98
|
+
await fs.promises.rm(dir, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function discoverPayloadApp(entries: yauzl.Entry[], archivePath: string): string {
|
|
104
|
+
const appNames = new Set<string>();
|
|
105
|
+
const appsWithInfoPlist = new Set<string>();
|
|
106
|
+
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
const name = toZipPath(entry.fileName);
|
|
109
|
+
if (isUnsafeZipPath(name)) {
|
|
110
|
+
throw new Error(`ZIP entry has an unsafe path: ${entry.fileName}`);
|
|
111
|
+
}
|
|
112
|
+
if (isDirectoryEntry(entry)) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const rest = name.startsWith('Payload/') ? name.slice('Payload/'.length) : '';
|
|
116
|
+
if (rest === '') {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const [appName, ...parts] = rest.split('/');
|
|
120
|
+
if (!appName?.endsWith('.app')) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
appNames.add(appName);
|
|
124
|
+
if (parts.join('/') === 'Info.plist') {
|
|
125
|
+
appsWithInfoPlist.add(appName);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (appNames.size === 0) {
|
|
130
|
+
throw new Error(`ZIP/IPA archive contains no Payload/*.app bundle: ${archivePath}`);
|
|
131
|
+
}
|
|
132
|
+
if (appNames.size > 1) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`ZIP/IPA archive contains more than one Payload/*.app bundle (${[...appNames].join(
|
|
135
|
+
', ',
|
|
136
|
+
)}); expected exactly one`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const appName = [...appNames][0]!;
|
|
141
|
+
if (!appsWithInfoPlist.has(appName)) {
|
|
142
|
+
throw new Error(`ZIP/IPA archive app bundle is missing Info.plist: Payload/${appName}/Info.plist`);
|
|
143
|
+
}
|
|
144
|
+
return appName;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function extractEntry(
|
|
148
|
+
zip: yauzl.ZipFile,
|
|
149
|
+
entry: yauzl.Entry,
|
|
150
|
+
target: string,
|
|
151
|
+
appRoot: string,
|
|
152
|
+
): Promise<void> {
|
|
153
|
+
if (isDirectoryEntry(entry)) {
|
|
154
|
+
await fs.promises.mkdir(target, { recursive: true, mode: entryPerm(entry, 0o755) });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
await fs.promises.mkdir(path.dirname(target), { recursive: true });
|
|
159
|
+
if (isSymlinkEntry(entry)) {
|
|
160
|
+
const stream = await zip.openReadStreamPromise(entry);
|
|
161
|
+
const chunks: Buffer[] = [];
|
|
162
|
+
for await (const chunk of stream) {
|
|
163
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
164
|
+
}
|
|
165
|
+
const linkTarget = Buffer.concat(chunks).toString('utf8');
|
|
166
|
+
const resolved = path.resolve(path.dirname(target), linkTarget);
|
|
167
|
+
const appRootResolved = path.resolve(appRoot);
|
|
168
|
+
if (resolved !== appRootResolved && !resolved.startsWith(appRootResolved + path.sep)) {
|
|
169
|
+
throw new Error(`ZIP/IPA symlink escapes app bundle: ${entry.fileName} -> ${linkTarget}`);
|
|
170
|
+
}
|
|
171
|
+
await fs.promises.rm(target, { force: true });
|
|
172
|
+
await fs.promises.symlink(linkTarget, target);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const stream = await zip.openReadStreamPromise(entry);
|
|
177
|
+
await pipeline(stream, fs.createWriteStream(target, { mode: entryPerm(entry, 0o644) }));
|
|
178
|
+
await fs.promises.chmod(target, entryPerm(entry, 0o644));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function extractAppArchiveToStablePath(archivePath: string): Promise<PreparedAppBundle> {
|
|
182
|
+
const resolvedArchivePath = path.resolve(archivePath);
|
|
183
|
+
const appPath = stableAppPath(resolvedArchivePath);
|
|
184
|
+
const stableRoot = path.dirname(appPath);
|
|
185
|
+
|
|
186
|
+
await ensureDirectoryNotSymlink(stableRoot);
|
|
187
|
+
const zip = await openZip(resolvedArchivePath);
|
|
188
|
+
try {
|
|
189
|
+
const entries = await readEntries(zip);
|
|
190
|
+
const appName = discoverPayloadApp(entries, resolvedArchivePath);
|
|
191
|
+
const payloadPrefix = `Payload/${appName}/`;
|
|
192
|
+
await fs.promises.rm(appPath, { recursive: true, force: true });
|
|
193
|
+
await ensureDirectoryNotSymlink(appPath);
|
|
194
|
+
|
|
195
|
+
for (const entry of entries) {
|
|
196
|
+
const name = toZipPath(entry.fileName);
|
|
197
|
+
if (name !== `Payload/${appName}` && !name.startsWith(payloadPrefix)) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const rel = name === `Payload/${appName}` ? '' : name.slice(payloadPrefix.length);
|
|
201
|
+
if (rel === '') {
|
|
202
|
+
await fs.promises.mkdir(appPath, { recursive: true });
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (isUnsafeZipPath(rel)) {
|
|
206
|
+
throw new Error(`ZIP entry has an unsafe app-relative path: ${entry.fileName}`);
|
|
207
|
+
}
|
|
208
|
+
const target = path.join(appPath, rel.split('/').join(path.sep));
|
|
209
|
+
const stagingAppResolved = path.resolve(appPath);
|
|
210
|
+
const targetResolved = path.resolve(target);
|
|
211
|
+
if (
|
|
212
|
+
targetResolved !== stagingAppResolved &&
|
|
213
|
+
!targetResolved.startsWith(stagingAppResolved + path.sep)
|
|
214
|
+
) {
|
|
215
|
+
throw new Error(`ZIP entry escapes app bundle: ${entry.fileName}`);
|
|
216
|
+
}
|
|
217
|
+
await extractEntry(zip, entry, target, appPath);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
appPath,
|
|
222
|
+
cacheIdentityPath: resolvedArchivePath,
|
|
223
|
+
isArchive: true,
|
|
224
|
+
archivePath: resolvedArchivePath,
|
|
225
|
+
};
|
|
226
|
+
} finally {
|
|
227
|
+
zip.close();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export async function prepareAppBundlePath(inputPath: string): Promise<PreparedAppBundle> {
|
|
232
|
+
const resolvedPath = path.resolve(inputPath);
|
|
233
|
+
const stat = await fs.promises.stat(resolvedPath);
|
|
234
|
+
if (stat.isDirectory()) {
|
|
235
|
+
return {
|
|
236
|
+
appPath: resolvedPath,
|
|
237
|
+
cacheIdentityPath: resolvedPath,
|
|
238
|
+
isArchive: false,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (!stat.isFile()) {
|
|
242
|
+
throw new Error(`The path is neither an app bundle directory nor a ZIP/IPA archive: ${inputPath}`);
|
|
243
|
+
}
|
|
244
|
+
return await extractAppArchiveToStablePath(resolvedPath);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function waitForStableArchiveFile(archivePath: string, shouldStop: () => boolean): Promise<boolean> {
|
|
248
|
+
const deadline = Date.now() + 10_000;
|
|
249
|
+
let previous: { size: number; mtimeMs: number } | undefined;
|
|
250
|
+
|
|
251
|
+
while (!shouldStop() && Date.now() < deadline) {
|
|
252
|
+
const stat = await fs.promises.stat(archivePath).catch((err: NodeJS.ErrnoException) => {
|
|
253
|
+
if (err.code === 'ENOENT') return null;
|
|
254
|
+
throw err;
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
if (stat?.isFile()) {
|
|
258
|
+
const current = { size: stat.size, mtimeMs: stat.mtimeMs };
|
|
259
|
+
if (previous && previous.size === current.size && previous.mtimeMs === current.mtimeMs) {
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
previous = current;
|
|
263
|
+
} else {
|
|
264
|
+
previous = undefined;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
await sleep(100);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function watchAppArchive(opts: {
|
|
274
|
+
archivePath: string;
|
|
275
|
+
log?: LogFn;
|
|
276
|
+
onExtracted?: () => void | Promise<void>;
|
|
277
|
+
}): ArchiveWatcher {
|
|
278
|
+
const archivePath = path.resolve(opts.archivePath);
|
|
279
|
+
const log = opts.log ?? noopLogger;
|
|
280
|
+
const parentDir = path.dirname(archivePath);
|
|
281
|
+
const archiveBase = path.basename(archivePath);
|
|
282
|
+
let timer: NodeJS.Timeout | undefined;
|
|
283
|
+
let inFlight = false;
|
|
284
|
+
let queued = false;
|
|
285
|
+
let closed = false;
|
|
286
|
+
|
|
287
|
+
const run = async () => {
|
|
288
|
+
if (closed) return;
|
|
289
|
+
if (inFlight) {
|
|
290
|
+
queued = true;
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
inFlight = true;
|
|
294
|
+
try {
|
|
295
|
+
const ready = await waitForStableArchiveFile(archivePath, () => closed);
|
|
296
|
+
if (!ready) {
|
|
297
|
+
if (!closed) {
|
|
298
|
+
log('warn', `ZIP/IPA archive did not become readable after change: ${archivePath}`);
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
await extractAppArchiveToStablePath(archivePath);
|
|
303
|
+
log('debug', `re-extracted ZIP/IPA archive: ${archivePath}`);
|
|
304
|
+
await opts.onExtracted?.();
|
|
305
|
+
} catch (err) {
|
|
306
|
+
log(
|
|
307
|
+
'error',
|
|
308
|
+
`failed to re-extract ZIP/IPA archive ${archivePath}: ${err instanceof Error ? err.message : err}`,
|
|
309
|
+
);
|
|
310
|
+
} finally {
|
|
311
|
+
inFlight = false;
|
|
312
|
+
if (queued) {
|
|
313
|
+
queued = false;
|
|
314
|
+
void run();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const schedule = () => {
|
|
320
|
+
if (closed) return;
|
|
321
|
+
if (timer) clearTimeout(timer);
|
|
322
|
+
timer = setTimeout(() => {
|
|
323
|
+
timer = undefined;
|
|
324
|
+
void run();
|
|
325
|
+
}, 500);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const watcher = fs.watch(parentDir, (_eventType, filename) => {
|
|
329
|
+
if (filename?.toString() === archiveBase) {
|
|
330
|
+
schedule();
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
log('debug', `watchAppArchive: ${archivePath}`);
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
close: () => {
|
|
337
|
+
closed = true;
|
|
338
|
+
if (timer) {
|
|
339
|
+
clearTimeout(timer);
|
|
340
|
+
timer = undefined;
|
|
341
|
+
}
|
|
342
|
+
watcher.close();
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
package/src/instance-client.ts
CHANGED
|
@@ -88,6 +88,12 @@ export type InstanceClient = {
|
|
|
88
88
|
* Open a URL/deeplink on Android.
|
|
89
89
|
*/
|
|
90
90
|
openUrl: (url: string) => Promise<OpenUrlResult>;
|
|
91
|
+
/**
|
|
92
|
+
* Play an on-device WAV/MP3 file as microphone input.
|
|
93
|
+
*
|
|
94
|
+
* The file must already exist on the Android instance, for example after pushing it with ADB.
|
|
95
|
+
*/
|
|
96
|
+
playOnMicrophone: (path: string, options?: PlayOnMicrophoneOptions) => Promise<PlayOnMicrophoneResult>;
|
|
91
97
|
/**
|
|
92
98
|
* Set Android Wi-Fi bandwidth limits in Kbps. Omit a direction to leave it unchanged;
|
|
93
99
|
* pass `0` to clear that direction's limit.
|
|
@@ -287,6 +293,17 @@ export type OpenUrlResult = {
|
|
|
287
293
|
url: string;
|
|
288
294
|
};
|
|
289
295
|
|
|
296
|
+
export type PlayOnMicrophoneOptions = {
|
|
297
|
+
once?: boolean;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
export type PlayOnMicrophoneResult = {
|
|
301
|
+
/** Duration of the decoded audio in microseconds. */
|
|
302
|
+
duration: number;
|
|
303
|
+
once: boolean;
|
|
304
|
+
generation: number;
|
|
305
|
+
};
|
|
306
|
+
|
|
290
307
|
export type WifiBandwidthOptions = {
|
|
291
308
|
downKbps?: number;
|
|
292
309
|
upKbps?: number;
|
|
@@ -381,6 +398,13 @@ type OpenUrlResultMessage = {
|
|
|
381
398
|
error?: CommandError;
|
|
382
399
|
};
|
|
383
400
|
|
|
401
|
+
type PlayOnMicrophoneResultMessage = {
|
|
402
|
+
type: 'playOnMicrophoneResult';
|
|
403
|
+
id: string;
|
|
404
|
+
payload?: PlayOnMicrophoneResult;
|
|
405
|
+
error?: CommandError;
|
|
406
|
+
};
|
|
407
|
+
|
|
384
408
|
type SetWifiBandwidthResultMessage = {
|
|
385
409
|
type: 'setWifiBandwidthResult';
|
|
386
410
|
id: string;
|
|
@@ -412,6 +436,7 @@ type KnownCommandResultMessage =
|
|
|
412
436
|
| ScrollScreenResultMessage
|
|
413
437
|
| ScrollElementResultMessage
|
|
414
438
|
| OpenUrlResultMessage
|
|
439
|
+
| PlayOnMicrophoneResultMessage
|
|
415
440
|
| SetWifiBandwidthResultMessage
|
|
416
441
|
| StartVideoRecordingResultMessage
|
|
417
442
|
| StopVideoRecordingResultMessage;
|
|
@@ -433,6 +458,7 @@ type CommandRequestMap = {
|
|
|
433
458
|
scrollScreen: { direction: ScrollDirection; amount?: number };
|
|
434
459
|
scrollElement: AndroidElementTarget & { direction: ScrollDirection; amount?: number };
|
|
435
460
|
openUrl: { url: string };
|
|
461
|
+
playOnMicrophone: { path: string; once?: boolean };
|
|
436
462
|
setWifiBandwidth: WifiBandwidthOptions;
|
|
437
463
|
startRecording: { quality?: RecordingQuality };
|
|
438
464
|
stopRecording: { upload?: { presignedUrl: string } };
|
|
@@ -448,6 +474,7 @@ type CommandResultMap = {
|
|
|
448
474
|
scrollScreen: ScrollResult;
|
|
449
475
|
scrollElement: ScrollResult;
|
|
450
476
|
openUrl: OpenUrlResult;
|
|
477
|
+
playOnMicrophone: PlayOnMicrophoneResult;
|
|
451
478
|
setWifiBandwidth: EmptyCommandResult;
|
|
452
479
|
startRecording: EmptyCommandResult;
|
|
453
480
|
stopRecording: EmptyCommandResult;
|
|
@@ -614,6 +641,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
614
641
|
case 'scrollScreenResult':
|
|
615
642
|
case 'scrollElementResult':
|
|
616
643
|
case 'openUrlResult':
|
|
644
|
+
case 'playOnMicrophoneResult':
|
|
617
645
|
case 'setWifiBandwidthResult':
|
|
618
646
|
case 'startRecordingResult':
|
|
619
647
|
case 'stopRecordingResult':
|
|
@@ -854,6 +882,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
854
882
|
scrollScreen,
|
|
855
883
|
scrollElement,
|
|
856
884
|
openUrl,
|
|
885
|
+
playOnMicrophone,
|
|
857
886
|
setWifiBandwidth,
|
|
858
887
|
startRecording,
|
|
859
888
|
stopRecording,
|
|
@@ -947,6 +976,19 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
947
976
|
};
|
|
948
977
|
};
|
|
949
978
|
|
|
979
|
+
const playOnMicrophone = async (
|
|
980
|
+
inputPath: string,
|
|
981
|
+
microphoneOptions?: PlayOnMicrophoneOptions,
|
|
982
|
+
): Promise<PlayOnMicrophoneResult> => {
|
|
983
|
+
if (!inputPath) {
|
|
984
|
+
throw new Error('path must be a non-empty string');
|
|
985
|
+
}
|
|
986
|
+
return sendRequest('playOnMicrophone', {
|
|
987
|
+
path: inputPath,
|
|
988
|
+
...(microphoneOptions?.once === undefined ? {} : { once: microphoneOptions.once }),
|
|
989
|
+
});
|
|
990
|
+
};
|
|
991
|
+
|
|
950
992
|
const setWifiBandwidth = async (bandwidthOptions: WifiBandwidthOptions): Promise<void> => {
|
|
951
993
|
const request: CommandRequestMap['setWifiBandwidth'] = {};
|
|
952
994
|
if (bandwidthOptions.downKbps !== undefined) {
|
package/src/ios-client.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { EventEmitter } from 'events';
|
|
|
7
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
|
+
import { prepareAppBundlePath, watchAppArchive } from './app-archive';
|
|
10
11
|
import { downloadFileToLocalPath } from './internal/download-file';
|
|
11
12
|
import { nodeProxyTransport } from './internal/proxy-transport';
|
|
12
13
|
import { startHttpProxy as startLocalHttpProxy } from './http-proxy';
|
|
@@ -1900,7 +1901,9 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
1900
1901
|
watch?: boolean;
|
|
1901
1902
|
},
|
|
1902
1903
|
): Promise<SyncFolderResult> => {
|
|
1903
|
-
const
|
|
1904
|
+
const preparedApp = await prepareAppBundlePath(localAppBundlePath);
|
|
1905
|
+
const appBundlePath = preparedApp.appPath;
|
|
1906
|
+
const infoPlistPath = path.join(appBundlePath, 'Info.plist');
|
|
1904
1907
|
const infoPlistStat = await fs.promises.stat(infoPlistPath).catch(() => null);
|
|
1905
1908
|
if (!infoPlistStat?.isFile()) {
|
|
1906
1909
|
throw new Error(`The folder is not a valid app bundle: missing Info.plist at ${infoPlistPath}`);
|
|
@@ -1908,11 +1911,12 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
1908
1911
|
if (!cachedDeviceInfo) {
|
|
1909
1912
|
throw new Error('Device info not available yet; wait for client connection to be established.');
|
|
1910
1913
|
}
|
|
1911
|
-
const resolvedPath = path.resolve(
|
|
1914
|
+
const resolvedPath = path.resolve(preparedApp.cacheIdentityPath);
|
|
1912
1915
|
const folderName = path.basename(resolvedPath);
|
|
1913
1916
|
const hash = crypto.createHash('sha1').update(resolvedPath).digest('hex').slice(0, 8);
|
|
1914
1917
|
const cacheKey = `limsync-cache-${folderName}-${hash}`;
|
|
1915
1918
|
const basisCacheDir = opts?.basisCacheDir ?? path.join(os.tmpdir(), cacheKey);
|
|
1919
|
+
const shouldWatch = opts?.watch ?? true;
|
|
1916
1920
|
const syncLog: FolderSyncOptions['log'] = (level, msg) => {
|
|
1917
1921
|
switch (level) {
|
|
1918
1922
|
case 'debug':
|
|
@@ -1936,15 +1940,31 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
1936
1940
|
apiUrl: options.apiUrl,
|
|
1937
1941
|
token: options.token,
|
|
1938
1942
|
udid: cacheKey,
|
|
1939
|
-
ignoreFn: await createIgnoreFn(
|
|
1943
|
+
ignoreFn: await createIgnoreFn(appBundlePath, { basisCacheDir, log: syncLog }),
|
|
1940
1944
|
basisCacheDir,
|
|
1941
1945
|
log: syncLog,
|
|
1942
1946
|
install: opts?.install ?? true,
|
|
1943
1947
|
maxPatchBytes: opts?.maxPatchBytes ?? 4 * 1024 * 1024,
|
|
1944
1948
|
launchMode: opts?.launchMode ?? 'ForegroundIfRunning',
|
|
1945
|
-
watch:
|
|
1949
|
+
watch: preparedApp.isArchive ? false : shouldWatch,
|
|
1950
|
+
};
|
|
1951
|
+
const result = await syncFolder(appBundlePath, folderSyncOpts);
|
|
1952
|
+
if (!preparedApp.isArchive || !preparedApp.archivePath || !shouldWatch) {
|
|
1953
|
+
return result;
|
|
1954
|
+
}
|
|
1955
|
+
const archiveWatcher = watchAppArchive({
|
|
1956
|
+
archivePath: preparedApp.archivePath,
|
|
1957
|
+
log: syncLog,
|
|
1958
|
+
onExtracted: async () => {
|
|
1959
|
+
await syncFolder(appBundlePath, folderSyncOpts);
|
|
1960
|
+
},
|
|
1961
|
+
});
|
|
1962
|
+
return {
|
|
1963
|
+
...result,
|
|
1964
|
+
stopWatching: () => {
|
|
1965
|
+
archiveWatcher.close();
|
|
1966
|
+
},
|
|
1946
1967
|
};
|
|
1947
|
-
return await syncFolder(localAppBundlePath, folderSyncOpts);
|
|
1948
1968
|
};
|
|
1949
1969
|
|
|
1950
1970
|
const lsof = (): Promise<LsofEntry[]> => {
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.33.
|
|
1
|
+
export const VERSION = '0.33.3'; // x-release-please-version
|
package/version.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.33.
|
|
1
|
+
export declare const VERSION = "0.33.3";
|
|
2
2
|
//# sourceMappingURL=version.d.mts.map
|
package/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.33.
|
|
1
|
+
export declare const VERSION = "0.33.3";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/version.js
CHANGED
package/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = '0.33.
|
|
1
|
+
export const VERSION = '0.33.3'; // x-release-please-version
|
|
2
2
|
//# sourceMappingURL=version.mjs.map
|