@limrun/api 0.6.2 → 0.7.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 +13 -0
- package/client.d.mts +2 -1
- package/client.d.mts.map +1 -1
- package/client.d.ts +2 -1
- package/client.d.ts.map +1 -1
- package/client.js +2 -2
- package/client.js.map +1 -1
- package/client.mjs +1 -1
- package/client.mjs.map +1 -1
- package/package.json +1 -1
- package/resources/android-instances-helpers.d.mts +5 -56
- package/resources/android-instances-helpers.d.mts.map +1 -1
- package/resources/android-instances-helpers.d.ts +5 -56
- package/resources/android-instances-helpers.d.ts.map +1 -1
- package/resources/android-instances-helpers.js +19 -217
- package/resources/android-instances-helpers.js.map +1 -1
- package/resources/android-instances-helpers.mjs +17 -216
- package/resources/android-instances-helpers.mjs.map +1 -1
- package/resources/index.d.mts +2 -1
- package/resources/index.d.mts.map +1 -1
- package/resources/index.d.ts +2 -1
- package/resources/index.d.ts.map +1 -1
- package/resources/index.js +2 -2
- package/resources/index.js.map +1 -1
- package/resources/index.mjs +1 -1
- package/resources/index.mjs.map +1 -1
- package/resources/instance-client.d.mts +57 -0
- package/resources/instance-client.d.mts.map +1 -0
- package/resources/instance-client.d.ts +57 -0
- package/resources/instance-client.d.ts.map +1 -0
- package/resources/instance-client.js +221 -0
- package/resources/instance-client.js.map +1 -0
- package/resources/instance-client.mjs +218 -0
- package/resources/instance-client.mjs.map +1 -0
- package/src/client.ts +1 -1
- package/src/resources/android-instances-helpers.ts +24 -347
- package/src/resources/index.ts +2 -1
- package/src/resources/instance-client.ts +350 -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
|
@@ -1,350 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export type InstanceClient = {
|
|
13
|
-
/**
|
|
14
|
-
* Take a screenshot of the current screen
|
|
15
|
-
* @returns A promise that resolves to the screenshot data
|
|
16
|
-
*/
|
|
17
|
-
screenshot: () => Promise<ScreenshotData>;
|
|
18
|
-
/**
|
|
19
|
-
* Disconnect from the Limbar instance
|
|
20
|
-
*/
|
|
21
|
-
disconnect: () => void;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Establish an ADB tunnel to the instance.
|
|
25
|
-
* Returns the local TCP port and a cleanup function.
|
|
26
|
-
*/
|
|
27
|
-
startAdbTunnel: () => Promise<Tunnel>;
|
|
28
|
-
/**
|
|
29
|
-
* Send an asset URL to the instance. The instance will download the asset
|
|
30
|
-
* and process it (currently APK install is supported). Resolves on success,
|
|
31
|
-
* rejects with an Error on failure.
|
|
32
|
-
*/
|
|
33
|
-
sendAsset: (url: string) => Promise<void>;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Controls the verbosity of logging in the client
|
|
38
|
-
*/
|
|
39
|
-
export type LogLevel = 'none' | 'error' | 'warn' | 'info' | 'debug';
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Configuration options for creating an Instance API client
|
|
43
|
-
*/
|
|
44
|
-
export type InstanceClientOptions = {
|
|
45
|
-
/**
|
|
46
|
-
* Path to the ADB executable.
|
|
47
|
-
* @default 'adb'
|
|
48
|
-
*/
|
|
49
|
-
adbPath?: string;
|
|
50
|
-
/**
|
|
51
|
-
* Controls logging verbosity
|
|
52
|
-
* @default 'info'
|
|
53
|
-
*/
|
|
54
|
-
logLevel?: LogLevel;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
type ScreenshotRequest = {
|
|
58
|
-
type: 'screenshot';
|
|
59
|
-
id: string;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
type ScreenshotResponse = {
|
|
63
|
-
type: 'screenshot';
|
|
64
|
-
dataUri: string;
|
|
65
|
-
id: string;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
type ScreenshotData = {
|
|
69
|
-
dataUri: string;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
type ScreenshotErrorResponse = {
|
|
73
|
-
type: 'screenshotError';
|
|
74
|
-
message: string;
|
|
75
|
-
id: string;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
type AssetRequest = {
|
|
79
|
-
type: 'asset';
|
|
80
|
-
url: string;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
type AssetResultResponse = {
|
|
84
|
-
type: 'assetResult';
|
|
85
|
-
result: 'success' | 'failure' | string;
|
|
86
|
-
url: string;
|
|
87
|
-
message?: string;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
type ServerMessage =
|
|
91
|
-
| ScreenshotResponse
|
|
92
|
-
| ScreenshotErrorResponse
|
|
93
|
-
| AssetResultResponse
|
|
94
|
-
| { type: string; [key: string]: unknown };
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Creates a client for interacting with a Limbar instance
|
|
98
|
-
* @param options Configuration options including webrtcUrl, token and log level
|
|
99
|
-
* @returns An InstanceClient for controlling the instance
|
|
100
|
-
*/
|
|
101
|
-
export async function createInstanceClient(
|
|
102
|
-
androidInstance: AndroidInstance,
|
|
103
|
-
options: InstanceClientOptions = {
|
|
104
|
-
adbPath: 'adb',
|
|
105
|
-
logLevel: 'info',
|
|
106
|
-
},
|
|
107
|
-
): Promise<InstanceClient> {
|
|
108
|
-
const token = androidInstance.status.token;
|
|
109
|
-
const serverAddress = `${androidInstance.status.endpointWebSocketUrl}?token=${token}`;
|
|
110
|
-
const logLevel = options.logLevel ?? 'info';
|
|
111
|
-
let ws: WebSocket | undefined = undefined;
|
|
112
|
-
|
|
113
|
-
const screenshotRequests: Map<
|
|
114
|
-
string,
|
|
115
|
-
{
|
|
116
|
-
resolver: (value: ScreenshotData | PromiseLike<ScreenshotData>) => void;
|
|
117
|
-
rejecter: (reason?: any) => void;
|
|
1
|
+
import { RequestOptions } from '../internal/request-options';
|
|
2
|
+
import {
|
|
3
|
+
AndroidInstance,
|
|
4
|
+
AndroidInstanceCreateParams,
|
|
5
|
+
AndroidInstances as GeneratedAndroidInstances,
|
|
6
|
+
} from './android-instances';
|
|
7
|
+
|
|
8
|
+
export class AndroidInstances extends GeneratedAndroidInstances {
|
|
9
|
+
async getOrCreate(params: AndroidInstanceCreateParams, options?: RequestOptions): Promise<AndroidInstance> {
|
|
10
|
+
if (!params.metadata || !params.metadata.labels || Object.keys(params.metadata.labels).length === 0) {
|
|
11
|
+
return Promise.reject(new Error('At least one label is required for getOrCreate operation'));
|
|
118
12
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
13
|
+
const instances = await super.list(
|
|
14
|
+
{
|
|
15
|
+
labelSelector: Object.entries(params.metadata.labels)
|
|
16
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
17
|
+
.join(','),
|
|
18
|
+
state: 'ready',
|
|
19
|
+
},
|
|
20
|
+
options,
|
|
21
|
+
);
|
|
22
|
+
if (instances && instances.length > 0) {
|
|
23
|
+
return instances[0]!;
|
|
126
24
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// Logger functions
|
|
130
|
-
const logger = {
|
|
131
|
-
debug: (...args: any[]) => {
|
|
132
|
-
if (logLevel === 'debug') console.log(...args);
|
|
133
|
-
},
|
|
134
|
-
info: (...args: any[]) => {
|
|
135
|
-
if (logLevel === 'info' || logLevel === 'debug') console.log(...args);
|
|
136
|
-
},
|
|
137
|
-
warn: (...args: any[]) => {
|
|
138
|
-
if (logLevel === 'warn' || logLevel === 'info' || logLevel === 'debug') console.warn(...args);
|
|
139
|
-
},
|
|
140
|
-
error: (...args: any[]) => {
|
|
141
|
-
if (logLevel !== 'none') console.error(...args);
|
|
142
|
-
},
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
return new Promise<InstanceClient>((resolveConnection, rejectConnection) => {
|
|
146
|
-
logger.debug(`Attempting to connect to WebSocket server at ${serverAddress}...`);
|
|
147
|
-
ws = new WebSocket(serverAddress);
|
|
148
|
-
ws.on('message', (data: Data) => {
|
|
149
|
-
let message: ServerMessage;
|
|
150
|
-
try {
|
|
151
|
-
message = JSON.parse(data.toString());
|
|
152
|
-
} catch (e) {
|
|
153
|
-
logger.error({ data, error: e }, 'Failed to parse JSON message');
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
switch (message.type) {
|
|
158
|
-
case 'screenshot': {
|
|
159
|
-
if (!('dataUri' in message) || typeof message.dataUri !== 'string' || !('id' in message)) {
|
|
160
|
-
logger.warn('Received invalid screenshot message:', message);
|
|
161
|
-
break;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const screenshotMessage = message as ScreenshotResponse;
|
|
165
|
-
const request = screenshotRequests.get(screenshotMessage.id);
|
|
166
|
-
|
|
167
|
-
if (!request) {
|
|
168
|
-
logger.warn(
|
|
169
|
-
`Received screenshot data for unknown or already handled session: ${screenshotMessage.id}`,
|
|
170
|
-
);
|
|
171
|
-
break;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
logger.debug(`Received screenshot data URI for session ${screenshotMessage.id}.`);
|
|
175
|
-
request.resolver({ dataUri: screenshotMessage.dataUri });
|
|
176
|
-
screenshotRequests.delete(screenshotMessage.id);
|
|
177
|
-
break;
|
|
178
|
-
}
|
|
179
|
-
case 'screenshotError': {
|
|
180
|
-
if (!('message' in message) || !('id' in message)) {
|
|
181
|
-
logger.warn('Received invalid screenshot error message:', message);
|
|
182
|
-
break;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const errorMessage = message as ScreenshotErrorResponse;
|
|
186
|
-
const request = screenshotRequests.get(errorMessage.id);
|
|
187
|
-
|
|
188
|
-
if (!request) {
|
|
189
|
-
logger.warn(
|
|
190
|
-
`Received screenshot error for unknown or already handled session: ${errorMessage.id}`,
|
|
191
|
-
);
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
logger.error(
|
|
196
|
-
`Server reported an error capturing screenshot for session ${errorMessage.id}:`,
|
|
197
|
-
errorMessage.message,
|
|
198
|
-
);
|
|
199
|
-
request.rejecter(new Error(errorMessage.message));
|
|
200
|
-
screenshotRequests.delete(errorMessage.id);
|
|
201
|
-
break;
|
|
202
|
-
}
|
|
203
|
-
case 'assetResult': {
|
|
204
|
-
logger.debug('Received assetResult:', message);
|
|
205
|
-
const request = assetRequests.get(message.url as string);
|
|
206
|
-
if (!request) {
|
|
207
|
-
logger.warn(`Received assetResult for unknown or already handled url: ${message.url}`);
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
210
|
-
if (message.result === 'success') {
|
|
211
|
-
logger.debug('Asset result is success');
|
|
212
|
-
request.resolver();
|
|
213
|
-
assetRequests.delete(message.url as string);
|
|
214
|
-
break;
|
|
215
|
-
}
|
|
216
|
-
const errorMessage =
|
|
217
|
-
typeof message.message === 'string' && message.message ?
|
|
218
|
-
message.message
|
|
219
|
-
: `Asset processing failed: ${JSON.stringify(message)}`;
|
|
220
|
-
logger.debug('Asset result is failure', errorMessage);
|
|
221
|
-
request.rejecter(new Error(errorMessage));
|
|
222
|
-
assetRequests.delete(message.url as string);
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
225
|
-
default:
|
|
226
|
-
logger.warn(`Received unexpected message type: ${message.type}`);
|
|
227
|
-
break;
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
ws.on('error', (err: Error) => {
|
|
232
|
-
logger.error('WebSocket error:', err.message);
|
|
233
|
-
if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
|
|
234
|
-
rejectConnection(err);
|
|
235
|
-
}
|
|
236
|
-
screenshotRequests.forEach((request) => request.rejecter(err));
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
ws.on('close', () => {
|
|
240
|
-
logger.debug('Disconnected from server.');
|
|
241
|
-
screenshotRequests.forEach((request) => request.rejecter('Disconnected from server'));
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
const screenshot = async (): Promise<ScreenshotData> => {
|
|
245
|
-
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
246
|
-
return Promise.reject(new Error('WebSocket is not connected or connection is not open.'));
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const id = 'ts-client-' + Date.now();
|
|
250
|
-
const screenshotRequest: ScreenshotRequest = {
|
|
251
|
-
type: 'screenshot',
|
|
252
|
-
id,
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
return new Promise<ScreenshotData>((resolve, reject) => {
|
|
256
|
-
logger.debug('Sending screenshot request:', screenshotRequest);
|
|
257
|
-
ws!.send(JSON.stringify(screenshotRequest), (err?: Error) => {
|
|
258
|
-
if (err) {
|
|
259
|
-
logger.error('Failed to send screenshot request:', err);
|
|
260
|
-
reject(err);
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
const timeout = setTimeout(() => {
|
|
265
|
-
if (screenshotRequests.has(id)) {
|
|
266
|
-
logger.error(`Screenshot request timed out for session ${id}`);
|
|
267
|
-
screenshotRequests.get(id)?.rejecter(new Error('Screenshot request timed out'));
|
|
268
|
-
screenshotRequests.delete(id);
|
|
269
|
-
}
|
|
270
|
-
}, 30000);
|
|
271
|
-
screenshotRequests.set(id, {
|
|
272
|
-
resolver: (value: ScreenshotData | PromiseLike<ScreenshotData>) => {
|
|
273
|
-
clearTimeout(timeout);
|
|
274
|
-
resolve(value);
|
|
275
|
-
screenshotRequests.delete(id);
|
|
276
|
-
},
|
|
277
|
-
rejecter: (reason?: any) => {
|
|
278
|
-
clearTimeout(timeout);
|
|
279
|
-
reject(reason);
|
|
280
|
-
screenshotRequests.delete(id);
|
|
281
|
-
},
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
const disconnect = (): void => {
|
|
287
|
-
if (ws) {
|
|
288
|
-
logger.debug('Closing WebSocket connection.');
|
|
289
|
-
ws.close();
|
|
290
|
-
}
|
|
291
|
-
screenshotRequests.forEach((request) => request.rejecter('Websocket connection closed'));
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Opens a WebSocket TCP proxy for the ADB port and connects the local adb
|
|
296
|
-
* client to it.
|
|
297
|
-
*/
|
|
298
|
-
const startAdbTunnel = async (): Promise<Tunnel> => {
|
|
299
|
-
if (!androidInstance.status.adbWebSocketUrl) {
|
|
300
|
-
return Promise.reject(new Error('ADB WebSocket URL is not set'));
|
|
301
|
-
}
|
|
302
|
-
const { address, close } = await startTcpTunnel(
|
|
303
|
-
androidInstance.status.adbWebSocketUrl,
|
|
304
|
-
token,
|
|
305
|
-
'127.0.0.1',
|
|
306
|
-
0,
|
|
307
|
-
);
|
|
308
|
-
try {
|
|
309
|
-
await new Promise<void>((resolve, reject) => {
|
|
310
|
-
exec(`${options.adbPath ?? 'adb'} connect ${address.address}:${address.port}`, (err) => {
|
|
311
|
-
if (err) return reject(err);
|
|
312
|
-
resolve();
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
logger.debug(`ADB connected on ${address.address}`);
|
|
316
|
-
} catch (err) {
|
|
317
|
-
close();
|
|
318
|
-
throw err;
|
|
319
|
-
}
|
|
320
|
-
return { address, close };
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
const sendAsset = async (url: string): Promise<void> => {
|
|
324
|
-
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
325
|
-
return Promise.reject(new Error('WebSocket is not connected or connection is not open.'));
|
|
326
|
-
}
|
|
327
|
-
const assetRequest: AssetRequest = {
|
|
328
|
-
type: 'asset',
|
|
329
|
-
url,
|
|
330
|
-
};
|
|
331
|
-
ws.send(JSON.stringify(assetRequest), (err?: Error) => {
|
|
332
|
-
if (err) {
|
|
333
|
-
logger.error('Failed to send asset request:', err);
|
|
334
|
-
}
|
|
335
|
-
});
|
|
336
|
-
return new Promise<void>((resolve, reject) => {
|
|
337
|
-
assetRequests.set(url, { resolver: resolve, rejecter: reject });
|
|
338
|
-
});
|
|
339
|
-
};
|
|
340
|
-
ws.on('open', () => {
|
|
341
|
-
logger.debug(`Connected to ${serverAddress}`);
|
|
342
|
-
resolveConnection({
|
|
343
|
-
screenshot,
|
|
344
|
-
disconnect,
|
|
345
|
-
startAdbTunnel,
|
|
346
|
-
sendAsset,
|
|
347
|
-
});
|
|
348
|
-
});
|
|
349
|
-
});
|
|
25
|
+
return super.create(params, options);
|
|
26
|
+
}
|
|
350
27
|
}
|
package/src/resources/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
|
2
2
|
|
|
3
3
|
export {
|
|
4
|
-
AndroidInstances,
|
|
5
4
|
type AndroidInstance,
|
|
6
5
|
type AndroidInstanceListResponse,
|
|
7
6
|
type AndroidInstanceCreateParams,
|
|
@@ -16,4 +15,6 @@ export {
|
|
|
16
15
|
type AssetGetOrCreateParams,
|
|
17
16
|
} from './assets';
|
|
18
17
|
|
|
18
|
+
export { AndroidInstances } from './android-instances-helpers';
|
|
19
|
+
|
|
19
20
|
export { Assets, AssetGetOrUploadParams, AssetGetOrUploadResponse } from './assets-helpers';
|