@limrun/api 0.38.0 → 0.39.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.
- package/CHANGELOG.md +4 -0
- package/app-archive.d.mts +1 -1
- package/app-archive.d.mts.map +1 -1
- package/app-archive.d.ts +1 -1
- package/app-archive.d.ts.map +1 -1
- package/app-archive.js +22 -10
- package/app-archive.js.map +1 -1
- package/app-archive.mjs +22 -10
- package/app-archive.mjs.map +1 -1
- package/folder-sync-watcher.d.mts +1 -1
- package/folder-sync-watcher.d.mts.map +1 -1
- package/folder-sync-watcher.d.ts +1 -1
- package/folder-sync-watcher.d.ts.map +1 -1
- package/folder-sync-watcher.js +2 -9
- package/folder-sync-watcher.js.map +1 -1
- package/folder-sync-watcher.mjs +2 -9
- package/folder-sync-watcher.mjs.map +1 -1
- package/folder-sync.d.mts +4 -4
- package/folder-sync.d.mts.map +1 -1
- package/folder-sync.d.ts +4 -4
- package/folder-sync.d.ts.map +1 -1
- package/folder-sync.js +149 -45
- package/folder-sync.js.map +1 -1
- package/folder-sync.mjs +149 -45
- package/folder-sync.mjs.map +1 -1
- package/instance-client.d.mts +10 -0
- package/instance-client.d.mts.map +1 -1
- package/instance-client.d.ts +10 -0
- package/instance-client.d.ts.map +1 -1
- package/instance-client.js +124 -0
- package/instance-client.js.map +1 -1
- package/instance-client.mjs +123 -0
- package/instance-client.mjs.map +1 -1
- package/ios-client.d.mts +0 -2
- package/ios-client.d.mts.map +1 -1
- package/ios-client.d.ts +0 -2
- package/ios-client.d.ts.map +1 -1
- package/ios-client.js +2 -3
- package/ios-client.js.map +1 -1
- package/ios-client.mjs +2 -3
- package/ios-client.mjs.map +1 -1
- package/package.json +2 -2
- package/resources/xcode-instances-helpers.d.mts +1 -3
- package/resources/xcode-instances-helpers.d.mts.map +1 -1
- package/resources/xcode-instances-helpers.d.ts +1 -3
- package/resources/xcode-instances-helpers.d.ts.map +1 -1
- package/resources/xcode-instances-helpers.js +0 -1
- package/resources/xcode-instances-helpers.js.map +1 -1
- package/resources/xcode-instances-helpers.mjs +0 -1
- package/resources/xcode-instances-helpers.mjs.map +1 -1
- package/src/app-archive.ts +22 -10
- package/src/folder-sync-watcher.ts +2 -8
- package/src/folder-sync.ts +168 -61
- package/src/instance-client.ts +159 -0
- package/src/ios-client.ts +2 -6
- package/src/resources/xcode-instances-helpers.ts +1 -4
- 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/instance-client.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { WebSocket, Data } from 'ws';
|
|
2
2
|
import { exec } from 'node:child_process';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import crypto from 'crypto';
|
|
3
7
|
|
|
4
8
|
import { downloadFileToLocalPath } from './internal/download-file';
|
|
5
9
|
import { nodeProxyTransport } from './internal/proxy-transport';
|
|
6
10
|
import { startTcpTunnel, isNonRetryableError } from './tunnel';
|
|
7
11
|
import type { Tunnel } from './tunnel';
|
|
12
|
+
import { syncFolder, type FolderSyncOptions, type SyncFolderResult } from './folder-sync';
|
|
8
13
|
|
|
9
14
|
const ANDROID_RECORDING_PATH = '/data/local/tmp/recordings/video_recording.mp4';
|
|
10
15
|
const ANDROID_SIGNALING_PATH = '/ws';
|
|
@@ -32,12 +37,103 @@ function buildDownloadUrl(apiUrl: string): string {
|
|
|
32
37
|
return `${apiUrl}/files?path=${encodeURIComponent(ANDROID_RECORDING_PATH)}`;
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
function buildSyncStateUrl(apiUrl: string): string {
|
|
41
|
+
return `${apiUrl}/sync/state`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildSyncSeedUrl(apiUrl: string, sha256: string): string {
|
|
45
|
+
return `${apiUrl}/sync/seeds/${encodeURIComponent(sha256)}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
function assertBandwidthKbps(field: keyof WifiBandwidthOptions, value: number): void {
|
|
36
49
|
if (!Number.isInteger(value) || value < 0) {
|
|
37
50
|
throw new Error(`${field} must be a non-negative integer Kbps value`);
|
|
38
51
|
}
|
|
39
52
|
}
|
|
40
53
|
|
|
54
|
+
async function sha256FileHex(filePath: string): Promise<string> {
|
|
55
|
+
return await new Promise((resolve, reject) => {
|
|
56
|
+
const hash = crypto.createHash('sha256');
|
|
57
|
+
const stream = fs.createReadStream(filePath);
|
|
58
|
+
stream.on('data', (chunk) => hash.update(chunk));
|
|
59
|
+
stream.on('error', reject);
|
|
60
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function assertApkFile(filePath: string): Promise<void> {
|
|
65
|
+
const st = await fs.promises.stat(filePath).catch(() => null);
|
|
66
|
+
if (!st?.isFile()) {
|
|
67
|
+
throw new Error(`APK file not found: ${filePath}`);
|
|
68
|
+
}
|
|
69
|
+
const fd = await fs.promises.open(filePath, 'r');
|
|
70
|
+
try {
|
|
71
|
+
const buf = Buffer.alloc(4);
|
|
72
|
+
const { bytesRead } = await fd.read(buf, 0, 4, 0);
|
|
73
|
+
if (bytesRead < 4 || buf[0] !== 0x50 || buf[1] !== 0x4b) {
|
|
74
|
+
throw new Error(`File is not an APK/ZIP archive: ${filePath}`);
|
|
75
|
+
}
|
|
76
|
+
} finally {
|
|
77
|
+
await fd.close();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function fetchAndroidSyncState(apiUrl: string, token: string): Promise<AndroidSyncState> {
|
|
82
|
+
const response = await nodeProxyTransport.fetch(buildSyncStateUrl(apiUrl), {
|
|
83
|
+
method: 'GET',
|
|
84
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
85
|
+
});
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
const text = await response.text();
|
|
88
|
+
throw new Error(`Failed to fetch Android sync state: ${response.status} ${text}`);
|
|
89
|
+
}
|
|
90
|
+
return (await response.json()) as AndroidSyncState;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function bootstrapAndroidBasisCache(
|
|
94
|
+
apkPath: string,
|
|
95
|
+
basisCacheDir: string,
|
|
96
|
+
apiUrl: string,
|
|
97
|
+
token: string,
|
|
98
|
+
log: FolderSyncOptions['log'],
|
|
99
|
+
): Promise<void> {
|
|
100
|
+
const remotePath = path.basename(apkPath);
|
|
101
|
+
const basisPath = path.join(basisCacheDir, remotePath);
|
|
102
|
+
if (fs.existsSync(basisPath)) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const state = await fetchAndroidSyncState(apiUrl, token).catch((err) => {
|
|
106
|
+
log('debug', `android sync state unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
107
|
+
return undefined;
|
|
108
|
+
});
|
|
109
|
+
if (!state) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const localSha = await sha256FileHex(apkPath);
|
|
113
|
+
for (const root of state.roots ?? []) {
|
|
114
|
+
for (const file of root.files ?? []) {
|
|
115
|
+
if (file.path === remotePath && file.sha256?.toLowerCase() === localSha) {
|
|
116
|
+
await fs.promises.mkdir(path.dirname(basisPath), { recursive: true });
|
|
117
|
+
await fs.promises.copyFile(apkPath, basisPath);
|
|
118
|
+
log('debug', `seeded Android basis cache from matching server root: ${remotePath}`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const seeds = [...(state.seeds ?? [])]
|
|
124
|
+
.filter(
|
|
125
|
+
(seed): seed is { sha256: string; size?: number; name?: string; mtime?: number } =>
|
|
126
|
+
typeof seed.sha256 === 'string' && /^[0-9a-f]{64}$/i.test(seed.sha256),
|
|
127
|
+
)
|
|
128
|
+
.sort((a, b) => (b.mtime ?? 0) - (a.mtime ?? 0));
|
|
129
|
+
const seed = seeds[0];
|
|
130
|
+
if (!seed) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
await downloadFileToLocalPath(buildSyncSeedUrl(apiUrl, seed.sha256), token, basisPath);
|
|
134
|
+
log('debug', `seeded Android basis cache from instance seed: ${seed.sha256}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
41
137
|
/**
|
|
42
138
|
* A client for interacting with a Limbar instance
|
|
43
139
|
*/
|
|
@@ -131,6 +227,19 @@ export type InstanceClient = {
|
|
|
131
227
|
*/
|
|
132
228
|
sendAsset: (url: string, timeoutMs?: number) => Promise<void>;
|
|
133
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Delta-sync a local APK to the instance and install it.
|
|
232
|
+
*/
|
|
233
|
+
syncApp: (
|
|
234
|
+
apkPath: string,
|
|
235
|
+
opts?: {
|
|
236
|
+
install?: boolean;
|
|
237
|
+
watch?: boolean;
|
|
238
|
+
launchMode?: 'ForegroundIfRunning' | 'RelaunchIfRunning';
|
|
239
|
+
basisCacheDir?: string;
|
|
240
|
+
},
|
|
241
|
+
) => Promise<SyncFolderResult>;
|
|
242
|
+
|
|
134
243
|
/**
|
|
135
244
|
* Get current connection state
|
|
136
245
|
*/
|
|
@@ -258,6 +367,14 @@ type ScreenshotData = {
|
|
|
258
367
|
dataUri: string;
|
|
259
368
|
};
|
|
260
369
|
|
|
370
|
+
type AndroidSyncState = {
|
|
371
|
+
roots?: Array<{
|
|
372
|
+
rootName?: string;
|
|
373
|
+
files?: Array<{ path?: string; sha256?: string; size?: number; mode?: number }>;
|
|
374
|
+
}>;
|
|
375
|
+
seeds?: Array<{ sha256?: string; size?: number; name?: string; mtime?: number }>;
|
|
376
|
+
};
|
|
377
|
+
|
|
261
378
|
export type ElementTreeData = {
|
|
262
379
|
xml: string;
|
|
263
380
|
nodes: AndroidElementNode[];
|
|
@@ -890,6 +1007,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
890
1007
|
disconnect,
|
|
891
1008
|
startAdbTunnel,
|
|
892
1009
|
sendAsset,
|
|
1010
|
+
syncApp,
|
|
893
1011
|
getConnectionState,
|
|
894
1012
|
onConnectionStateChange,
|
|
895
1013
|
});
|
|
@@ -1150,6 +1268,47 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
1150
1268
|
});
|
|
1151
1269
|
};
|
|
1152
1270
|
|
|
1271
|
+
const syncApp: InstanceClient['syncApp'] = async (apkPath, syncOpts) => {
|
|
1272
|
+
const resolvedPath = path.resolve(apkPath);
|
|
1273
|
+
await assertApkFile(resolvedPath);
|
|
1274
|
+
const fileName = path.basename(resolvedPath);
|
|
1275
|
+
const hash = crypto.createHash('sha1').update(resolvedPath).digest('hex').slice(0, 8);
|
|
1276
|
+
const cacheKey = `limsync-cache-android-${fileName}-${hash}`;
|
|
1277
|
+
const basisCacheDir = syncOpts?.basisCacheDir ?? path.join(os.tmpdir(), cacheKey);
|
|
1278
|
+
const syncLog: FolderSyncOptions['log'] = (level, msg) => {
|
|
1279
|
+
switch (level) {
|
|
1280
|
+
case 'debug':
|
|
1281
|
+
logger.debug(msg);
|
|
1282
|
+
break;
|
|
1283
|
+
case 'info':
|
|
1284
|
+
logger.info(msg);
|
|
1285
|
+
break;
|
|
1286
|
+
case 'warn':
|
|
1287
|
+
logger.warn(msg);
|
|
1288
|
+
break;
|
|
1289
|
+
case 'error':
|
|
1290
|
+
logger.error(msg);
|
|
1291
|
+
break;
|
|
1292
|
+
default:
|
|
1293
|
+
logger.info(msg);
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
await fs.promises.mkdir(basisCacheDir, { recursive: true });
|
|
1297
|
+
await bootstrapAndroidBasisCache(resolvedPath, basisCacheDir, options.apiUrl, options.token, syncLog);
|
|
1298
|
+
return await syncFolder(resolvedPath, {
|
|
1299
|
+
apiUrl: options.apiUrl,
|
|
1300
|
+
token: options.token,
|
|
1301
|
+
udid: cacheKey,
|
|
1302
|
+
basisCacheDir,
|
|
1303
|
+
install: syncOpts?.install ?? true,
|
|
1304
|
+
launchMode: syncOpts?.launchMode ?? 'ForegroundIfRunning',
|
|
1305
|
+
watch: syncOpts?.watch ?? false,
|
|
1306
|
+
ignoreFn: () => false,
|
|
1307
|
+
log: syncLog,
|
|
1308
|
+
compression: 'identity',
|
|
1309
|
+
});
|
|
1310
|
+
};
|
|
1311
|
+
|
|
1153
1312
|
// Start the initial connection
|
|
1154
1313
|
setupWebSocket();
|
|
1155
1314
|
});
|
package/src/ios-client.ts
CHANGED
|
@@ -641,7 +641,6 @@ export type InstanceClient = {
|
|
|
641
641
|
* @param opts Optional sync options
|
|
642
642
|
* @param opts.install If true, install the app after syncing. Defaults to true.
|
|
643
643
|
* @param opts.basisCacheDir Directory for the client-side folder-sync cache.
|
|
644
|
-
* @param opts.maxPatchBytes Max patch size (bytes) to send as delta before falling back to full upload. Defaults to 4MB.
|
|
645
644
|
* @param opts.launchMode Launch mode after installation: "ForegroundIfRunning" (default): bring to foreground if already running, otherwise launch, "RelaunchIfRunning": kill and relaunch if already running
|
|
646
645
|
* @param opts.watch If true, watch the folder and re-sync on any changes (debounced, single-flight).
|
|
647
646
|
*/
|
|
@@ -650,7 +649,6 @@ export type InstanceClient = {
|
|
|
650
649
|
opts?: {
|
|
651
650
|
install?: boolean;
|
|
652
651
|
basisCacheDir?: string;
|
|
653
|
-
maxPatchBytes?: number;
|
|
654
652
|
launchMode?: 'ForegroundIfRunning' | 'RelaunchIfRunning';
|
|
655
653
|
watch?: boolean;
|
|
656
654
|
},
|
|
@@ -1939,7 +1937,6 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
1939
1937
|
opts?: {
|
|
1940
1938
|
install?: boolean;
|
|
1941
1939
|
basisCacheDir?: string;
|
|
1942
|
-
maxPatchBytes?: number;
|
|
1943
1940
|
launchMode?: 'ForegroundIfRunning' | 'RelaunchIfRunning';
|
|
1944
1941
|
watch?: boolean;
|
|
1945
1942
|
},
|
|
@@ -1987,7 +1984,6 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
1987
1984
|
basisCacheDir,
|
|
1988
1985
|
log: syncLog,
|
|
1989
1986
|
install: opts?.install ?? true,
|
|
1990
|
-
maxPatchBytes: opts?.maxPatchBytes ?? 4 * 1024 * 1024,
|
|
1991
1987
|
launchMode: opts?.launchMode ?? 'ForegroundIfRunning',
|
|
1992
1988
|
watch: preparedApp.isArchive ? false : shouldWatch,
|
|
1993
1989
|
};
|
|
@@ -2004,8 +2000,8 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
2004
2000
|
});
|
|
2005
2001
|
return {
|
|
2006
2002
|
...result,
|
|
2007
|
-
stopWatching: () => {
|
|
2008
|
-
archiveWatcher.close();
|
|
2003
|
+
stopWatching: async () => {
|
|
2004
|
+
await archiveWatcher.close();
|
|
2009
2005
|
},
|
|
2010
2006
|
};
|
|
2011
2007
|
};
|
|
@@ -34,8 +34,6 @@ export type SyncOptions = {
|
|
|
34
34
|
* Defaults to a temporary directory under the OS temp directory.
|
|
35
35
|
*/
|
|
36
36
|
basisCacheDir?: string;
|
|
37
|
-
/** Max patch size (bytes) to send as delta before falling back to full upload. */
|
|
38
|
-
maxPatchBytes?: number;
|
|
39
37
|
/** If true, install the app after syncing. Defaults to true. */
|
|
40
38
|
install?: boolean;
|
|
41
39
|
/**
|
|
@@ -53,7 +51,7 @@ export type SyncOptions = {
|
|
|
53
51
|
|
|
54
52
|
export type SyncResult = {
|
|
55
53
|
/** Present only when watch=true; call to stop watching */
|
|
56
|
-
stopWatching?: () => void
|
|
54
|
+
stopWatching?: () => Promise<void>;
|
|
57
55
|
};
|
|
58
56
|
|
|
59
57
|
export type XcodeProjectConfig = {
|
|
@@ -615,7 +613,6 @@ export class XcodeInstances extends GeneratedXcodeInstances {
|
|
|
615
613
|
}),
|
|
616
614
|
basisCacheDir,
|
|
617
615
|
watch: opts?.watch ?? true,
|
|
618
|
-
maxPatchBytes: opts?.maxPatchBytes ?? 4 * 1024 * 1024,
|
|
619
616
|
launchMode: 'ForegroundIfRunning',
|
|
620
617
|
log,
|
|
621
618
|
...(additionalFiles ? { additionalFiles } : {}),
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.39.0'; // x-release-please-version
|
package/version.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.39.0";
|
|
2
2
|
//# sourceMappingURL=version.d.mts.map
|
package/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.39.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/version.js
CHANGED
package/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.39.0'; // x-release-please-version
|
|
2
2
|
//# sourceMappingURL=version.mjs.map
|