@phone-use/sdk 0.1.1 → 0.3.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/dist/index.d.mts +133 -2
- package/dist/index.mjs +278 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/backends/cloud-sandbox.ts +202 -0
- package/src/backends/device-runner.ts +205 -0
- package/src/index.ts +10 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phone-use/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Typed mobile-device SDK
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Typed mobile-device SDK \u2014 Device, backends, errors, actions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { BaseDeviceBackend } from '../backend.ts';
|
|
3
|
+
import {
|
|
4
|
+
ALL_CAPABILITIES,
|
|
5
|
+
type AlertAction,
|
|
6
|
+
type BackendAlertResult,
|
|
7
|
+
type Capability,
|
|
8
|
+
type OpenAppResult,
|
|
9
|
+
type PressTarget,
|
|
10
|
+
type ScrollDirection,
|
|
11
|
+
type Snapshot,
|
|
12
|
+
} from '../device.ts';
|
|
13
|
+
import { PhoneUseError } from '../errors.ts';
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// CloudSandboxBackend: drives a phone-use cloud sandbox (a simulator on a
|
|
17
|
+
// remote worker) over the worker's sandbox-scoped RPC endpoint. This is the
|
|
18
|
+
// client half of packages/cloud's worker protocol; it lives in the SDK so the
|
|
19
|
+
// CLI/MCP/agent can attach to a sandbox without depending on @phone-use/cloud
|
|
20
|
+
// (which also contains the server side and is not published).
|
|
21
|
+
//
|
|
22
|
+
// Wire protocol (shared with packages/cloud — keep in sync):
|
|
23
|
+
// POST <endpoint>/rpc {method, args} -> {ok:true,result} | {ok:false,error}
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
export type SandboxRpcMethod =
|
|
27
|
+
| 'snapshot'
|
|
28
|
+
| 'screenshot'
|
|
29
|
+
| 'press'
|
|
30
|
+
| 'longPress'
|
|
31
|
+
| 'fill'
|
|
32
|
+
| 'typeText'
|
|
33
|
+
| 'pressKey'
|
|
34
|
+
| 'scroll'
|
|
35
|
+
| 'pan'
|
|
36
|
+
| 'installApp'
|
|
37
|
+
| 'waitForText'
|
|
38
|
+
| 'systemAlert'
|
|
39
|
+
| 'home'
|
|
40
|
+
| 'back'
|
|
41
|
+
| 'openApp'
|
|
42
|
+
| 'listApps'
|
|
43
|
+
| 'closeSession';
|
|
44
|
+
|
|
45
|
+
export type SandboxRpcRequest = { method: SandboxRpcMethod; args: unknown[] };
|
|
46
|
+
export type SandboxRpcSuccess = { ok: true; result: unknown };
|
|
47
|
+
export type SandboxRpcFailure = {
|
|
48
|
+
ok: false;
|
|
49
|
+
error: { message: string; code?: string | undefined; retryable?: boolean | undefined };
|
|
50
|
+
};
|
|
51
|
+
export type SandboxRpcResponse = SandboxRpcSuccess | SandboxRpcFailure;
|
|
52
|
+
|
|
53
|
+
export type CloudSandboxBackendOptions = {
|
|
54
|
+
endpoint: string;
|
|
55
|
+
token: string;
|
|
56
|
+
capabilities?: Capability[] | undefined;
|
|
57
|
+
fetch?: typeof globalThis.fetch | undefined;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export class CloudSandboxBackend extends BaseDeviceBackend {
|
|
61
|
+
private readonly endpoint: string;
|
|
62
|
+
private readonly token: string;
|
|
63
|
+
private readonly fetchImpl: typeof globalThis.fetch;
|
|
64
|
+
|
|
65
|
+
constructor(opts: CloudSandboxBackendOptions) {
|
|
66
|
+
super('phone-use-cloud', opts.capabilities ?? ALL_CAPABILITIES);
|
|
67
|
+
this.endpoint = opts.endpoint.replace(/\/+$/, '');
|
|
68
|
+
this.token = opts.token;
|
|
69
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async rpc<T>(method: SandboxRpcMethod, ...args: unknown[]): Promise<T> {
|
|
73
|
+
const response = await this.fetchImpl(`${this.endpoint}/rpc`, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: {
|
|
76
|
+
authorization: `Bearer ${this.token}`,
|
|
77
|
+
'content-type': 'application/json',
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify({ method, args } satisfies SandboxRpcRequest),
|
|
80
|
+
});
|
|
81
|
+
const body = (await response.json().catch(() => ({}))) as
|
|
82
|
+
| SandboxRpcResponse
|
|
83
|
+
| { error?: string; message?: string };
|
|
84
|
+
if (!response.ok || !('ok' in body)) {
|
|
85
|
+
const message =
|
|
86
|
+
('error' in body && typeof body.error === 'string' ? body.error : undefined) ??
|
|
87
|
+
('message' in body ? body.message : undefined) ??
|
|
88
|
+
`HTTP ${response.status}`;
|
|
89
|
+
throw new PhoneUseError(message, { code: 'UNKNOWN', retryable: false });
|
|
90
|
+
}
|
|
91
|
+
if (!body.ok) {
|
|
92
|
+
throw new PhoneUseError(body.error.message, {
|
|
93
|
+
code: normalizeErrorCode(body.error.code),
|
|
94
|
+
retryable: body.error.retryable ?? false,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return body.result as T;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
override snapshot(opts?: { interactiveOnly?: boolean; depth?: number }): Promise<Snapshot> {
|
|
101
|
+
return this.rpc('snapshot', opts);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override async screenshot(opts: { path: string; overlayRefs?: boolean }): Promise<{ path: string }> {
|
|
105
|
+
const result = await this.rpc<{ base64: string }>('screenshot', {
|
|
106
|
+
overlayRefs: opts.overlayRefs,
|
|
107
|
+
});
|
|
108
|
+
await writeFile(opts.path, Buffer.from(result.base64, 'base64'));
|
|
109
|
+
return { path: opts.path };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
override press(target: PressTarget): Promise<void> {
|
|
113
|
+
return this.rpc('press', target);
|
|
114
|
+
}
|
|
115
|
+
override longPress(ref: string, durationMs?: number): Promise<void> {
|
|
116
|
+
return this.rpc('longPress', ref, durationMs);
|
|
117
|
+
}
|
|
118
|
+
override fill(ref: string, text: string): Promise<void> {
|
|
119
|
+
return this.rpc('fill', ref, text);
|
|
120
|
+
}
|
|
121
|
+
override typeText(text: string): Promise<void> {
|
|
122
|
+
return this.rpc('typeText', text);
|
|
123
|
+
}
|
|
124
|
+
override pressKey(key: 'return'): Promise<void> {
|
|
125
|
+
return this.rpc('pressKey', key);
|
|
126
|
+
}
|
|
127
|
+
override scroll(direction: ScrollDirection): Promise<void> {
|
|
128
|
+
return this.rpc('scroll', direction);
|
|
129
|
+
}
|
|
130
|
+
override pan(x: number, y: number, dx: number, dy: number, durationMs?: number): Promise<void> {
|
|
131
|
+
return this.rpc('pan', x, y, dx, dy, durationMs);
|
|
132
|
+
}
|
|
133
|
+
override waitForText(text: string, timeoutMs?: number): Promise<void> {
|
|
134
|
+
return this.rpc('waitForText', text, timeoutMs);
|
|
135
|
+
}
|
|
136
|
+
override systemAlert(action: AlertAction): Promise<BackendAlertResult> {
|
|
137
|
+
return this.rpc('systemAlert', action);
|
|
138
|
+
}
|
|
139
|
+
override home(): Promise<void> {
|
|
140
|
+
return this.rpc('home');
|
|
141
|
+
}
|
|
142
|
+
override back(): Promise<void> {
|
|
143
|
+
return this.rpc('back');
|
|
144
|
+
}
|
|
145
|
+
override openApp(opts: { app?: string; url?: string; relaunch?: boolean }): Promise<OpenAppResult> {
|
|
146
|
+
return this.rpc('openApp', opts);
|
|
147
|
+
}
|
|
148
|
+
override listApps(): Promise<string[]> {
|
|
149
|
+
return this.rpc('listApps');
|
|
150
|
+
}
|
|
151
|
+
override closeSession(): Promise<void> {
|
|
152
|
+
return this.rpc('closeSession');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Upload a zipped .app bundle (base64) and install it on the sandbox device. */
|
|
156
|
+
installApp(base64Zip: string): Promise<{ installed?: string }> {
|
|
157
|
+
return this.rpc('installApp', base64Zip);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Build a CloudSandboxBackend from explicit options or the environment:
|
|
163
|
+
* PHONE_USE_SANDBOX_URL + PHONE_USE_SANDBOX_TOKEN (printed by
|
|
164
|
+
* `phone-use sandbox env <id>`).
|
|
165
|
+
*/
|
|
166
|
+
export function createCloudSandboxBackend(config?: Partial<CloudSandboxBackendOptions>): CloudSandboxBackend {
|
|
167
|
+
const endpoint = config?.endpoint ?? process.env.PHONE_USE_SANDBOX_URL;
|
|
168
|
+
const token = config?.token ?? process.env.PHONE_USE_SANDBOX_TOKEN;
|
|
169
|
+
if (!endpoint || !token) {
|
|
170
|
+
throw new PhoneUseError(
|
|
171
|
+
'cloud sandbox backend needs PHONE_USE_SANDBOX_URL and PHONE_USE_SANDBOX_TOKEN (see `phone-use sandbox env <id>`)',
|
|
172
|
+
{ code: 'BACKEND_NOT_FOUND', retryable: false },
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return new CloudSandboxBackend({ ...config, endpoint, token });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function normalizeErrorCode(
|
|
179
|
+
code: string | undefined,
|
|
180
|
+
):
|
|
181
|
+
| 'DEVICE_NOT_FOUND'
|
|
182
|
+
| 'DEVICE_IN_USE'
|
|
183
|
+
| 'SESSION_NOT_FOUND'
|
|
184
|
+
| 'TIMEOUT'
|
|
185
|
+
| 'ACTION_FAILED'
|
|
186
|
+
| 'UNSUPPORTED_CAPABILITY'
|
|
187
|
+
| 'BACKEND_NOT_FOUND'
|
|
188
|
+
| 'ABORTED'
|
|
189
|
+
| 'UNKNOWN' {
|
|
190
|
+
const codes = new Set([
|
|
191
|
+
'DEVICE_NOT_FOUND',
|
|
192
|
+
'DEVICE_IN_USE',
|
|
193
|
+
'SESSION_NOT_FOUND',
|
|
194
|
+
'TIMEOUT',
|
|
195
|
+
'ACTION_FAILED',
|
|
196
|
+
'UNSUPPORTED_CAPABILITY',
|
|
197
|
+
'BACKEND_NOT_FOUND',
|
|
198
|
+
'ABORTED',
|
|
199
|
+
'UNKNOWN',
|
|
200
|
+
]);
|
|
201
|
+
return codes.has(code ?? '') ? (code as ReturnType<typeof normalizeErrorCode>) : 'UNKNOWN';
|
|
202
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { BaseDeviceBackend } from '../backend.ts';
|
|
2
|
+
import type {
|
|
3
|
+
AlertAction,
|
|
4
|
+
BackendAlertResult,
|
|
5
|
+
Capability,
|
|
6
|
+
OpenAppResult,
|
|
7
|
+
PressTarget,
|
|
8
|
+
ScrollDirection,
|
|
9
|
+
Snapshot,
|
|
10
|
+
} from '../device.ts';
|
|
11
|
+
import { ActionFailedError, DeviceNotFoundError, TimeoutError } from '../errors.ts';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A device-runner is addressed purely by URL, so it takes no DeviceConfig
|
|
15
|
+
* platform/udid selection — only where to reach the runner and how to auth.
|
|
16
|
+
*/
|
|
17
|
+
export type DeviceRunnerConfig = {
|
|
18
|
+
endpoint?: string | undefined;
|
|
19
|
+
token?: string | undefined;
|
|
20
|
+
timeoutMs?: number | undefined;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const RUNNER_CAPABILITIES: readonly Capability[] = [
|
|
24
|
+
'snapshot',
|
|
25
|
+
'screenshot',
|
|
26
|
+
'press',
|
|
27
|
+
'fill',
|
|
28
|
+
'type',
|
|
29
|
+
'scroll',
|
|
30
|
+
'pan',
|
|
31
|
+
'openApp',
|
|
32
|
+
'home',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Backend that speaks to an on-device runner: an XCTest-hosted JSON-RPC server
|
|
37
|
+
* running ON the iPhone itself, which holds the automation privileges iOS
|
|
38
|
+
* denies to ordinary apps.
|
|
39
|
+
*
|
|
40
|
+
* The endpoint is just a URL, so the same backend serves every topology:
|
|
41
|
+
* - `http://127.0.0.1:45678` — port-forwarded from a paired host
|
|
42
|
+
* - `http://<phone-ip>:45678` — straight over the LAN / tailnet
|
|
43
|
+
* - `https://relay.example/d/<id>` — the runner dials out to a cloud relay,
|
|
44
|
+
* which is what lets an agent anywhere drive the phone with no inbound
|
|
45
|
+
* ports and no Mac in the loop.
|
|
46
|
+
*
|
|
47
|
+
* The wire format matches the shape proven by rounak/PhoneAgent: newline-free
|
|
48
|
+
* JSON request/response over HTTP POST, one method per call.
|
|
49
|
+
*/
|
|
50
|
+
export class DeviceRunnerBackend extends BaseDeviceBackend {
|
|
51
|
+
readonly #endpoint: string;
|
|
52
|
+
readonly #token: string | undefined;
|
|
53
|
+
readonly #timeoutMs: number;
|
|
54
|
+
|
|
55
|
+
constructor(config?: DeviceRunnerConfig) {
|
|
56
|
+
// BaseDeviceBackend owns backendName/capabilities — set them via super()
|
|
57
|
+
// rather than redeclaring the fields.
|
|
58
|
+
super('device-runner', RUNNER_CAPABILITIES);
|
|
59
|
+
const endpoint = config?.endpoint ?? process.env.PHONE_USE_RUNNER_URL ?? 'http://127.0.0.1:45678';
|
|
60
|
+
this.#endpoint = endpoint.replace(/\/+$/, '');
|
|
61
|
+
this.#token = config?.token ?? process.env.PHONE_USE_RUNNER_TOKEN;
|
|
62
|
+
this.#timeoutMs = config?.timeoutMs ?? 30_000;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async #rpc<T>(method: string, params: Record<string, unknown> = {}): Promise<T> {
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const timer = setTimeout(() => controller.abort(), this.#timeoutMs);
|
|
68
|
+
let res: Response;
|
|
69
|
+
try {
|
|
70
|
+
res = await fetch(this.#endpoint, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: {
|
|
73
|
+
'content-type': 'application/json',
|
|
74
|
+
...(this.#token ? { authorization: `Bearer ${this.#token}` } : {}),
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: Date.now(), method, params }),
|
|
77
|
+
signal: controller.signal,
|
|
78
|
+
});
|
|
79
|
+
} catch (cause) {
|
|
80
|
+
if (controller.signal.aborted) {
|
|
81
|
+
throw new TimeoutError(`runner did not answer ${method} within ${this.#timeoutMs}ms`);
|
|
82
|
+
}
|
|
83
|
+
throw new DeviceNotFoundError(
|
|
84
|
+
`cannot reach the on-device runner at ${this.#endpoint} — is it activated on the phone?`,
|
|
85
|
+
{ cause },
|
|
86
|
+
);
|
|
87
|
+
} finally {
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
throw new ActionFailedError(`runner returned HTTP ${res.status} for ${method}`);
|
|
93
|
+
}
|
|
94
|
+
const body = (await res.json()) as { result?: T; error?: { message?: string } };
|
|
95
|
+
if (body.error) {
|
|
96
|
+
throw new ActionFailedError(body.error.message ?? `runner rejected ${method}`);
|
|
97
|
+
}
|
|
98
|
+
return body.result as T;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
override async snapshot(opts?: {
|
|
102
|
+
interactiveOnly?: boolean | undefined;
|
|
103
|
+
depth?: number | undefined;
|
|
104
|
+
}): Promise<Snapshot> {
|
|
105
|
+
// The runner speaks its own compact wire shape; map it onto the SDK's
|
|
106
|
+
// Snapshot contract so every consumer (CLI, MCP, agent, bench) is unaware
|
|
107
|
+
// it is talking to a phone rather than a simulator.
|
|
108
|
+
const wire = await this.#rpc<{
|
|
109
|
+
app?: string;
|
|
110
|
+
elements: Array<{
|
|
111
|
+
ref: string;
|
|
112
|
+
role?: string;
|
|
113
|
+
label?: string;
|
|
114
|
+
value?: string;
|
|
115
|
+
enabled?: boolean;
|
|
116
|
+
rect?: { x: number; y: number; w: number; h: number };
|
|
117
|
+
}>;
|
|
118
|
+
}>('get_tree', {
|
|
119
|
+
interactiveOnly: opts?.interactiveOnly ?? false,
|
|
120
|
+
depth: opts?.depth,
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
appBundleId: wire.app,
|
|
124
|
+
appName: wire.app,
|
|
125
|
+
nodes: (wire.elements ?? []).map((e) => ({
|
|
126
|
+
ref: e.ref,
|
|
127
|
+
role: e.role,
|
|
128
|
+
type: e.role,
|
|
129
|
+
label: e.label,
|
|
130
|
+
value: e.value,
|
|
131
|
+
enabled: e.enabled,
|
|
132
|
+
rect: e.rect ? { x: e.rect.x, y: e.rect.y, width: e.rect.w, height: e.rect.h } : undefined,
|
|
133
|
+
})),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
override async screenshot(opts: {
|
|
138
|
+
path: string;
|
|
139
|
+
overlayRefs?: boolean | undefined;
|
|
140
|
+
}): Promise<{ path: string }> {
|
|
141
|
+
const { base64 } = await this.#rpc<{ base64: string }>('get_screen_image', {
|
|
142
|
+
overlayRefs: opts.overlayRefs ?? false,
|
|
143
|
+
});
|
|
144
|
+
const { writeFile } = await import('node:fs/promises');
|
|
145
|
+
await writeFile(opts.path, Buffer.from(base64, 'base64'));
|
|
146
|
+
return { path: opts.path };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
override async press(target: PressTarget): Promise<void> {
|
|
150
|
+
// PressTarget is {ref} | {x,y} — never a bare string, so narrow on the key.
|
|
151
|
+
if ('ref' in target) {
|
|
152
|
+
await this.#rpc('tap_element', { ref: target.ref });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
await this.#rpc('tap', { x: target.x, y: target.y });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
override async fill(ref: string, text: string): Promise<void> {
|
|
159
|
+
await this.#rpc('enter_text', { ref, text, replace: true });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
override async typeText(text: string): Promise<void> {
|
|
163
|
+
await this.#rpc('enter_text', { text, replace: false });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
override async scroll(direction: ScrollDirection): Promise<void> {
|
|
167
|
+
await this.#rpc('scroll', { direction });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
override async pan(x: number, y: number, dx: number, dy: number, durationMs = 300): Promise<void> {
|
|
171
|
+
await this.#rpc('swipe', { x, y, dx, dy, durationMs });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
override async home(): Promise<void> {
|
|
175
|
+
await this.#rpc('home');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
override async openApp(opts: {
|
|
179
|
+
app?: string | undefined;
|
|
180
|
+
url?: string | undefined;
|
|
181
|
+
relaunch?: boolean | undefined;
|
|
182
|
+
}): Promise<OpenAppResult> {
|
|
183
|
+
return this.#rpc<OpenAppResult>('open_app', {
|
|
184
|
+
app: opts.app,
|
|
185
|
+
url: opts.url,
|
|
186
|
+
relaunch: opts.relaunch ?? false,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
override async systemAlert(action: AlertAction): Promise<BackendAlertResult> {
|
|
191
|
+
return this.#rpc<BackendAlertResult>('alert', { action });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
override async closeSession(): Promise<void> {
|
|
195
|
+
// The runner outlives any single client; nothing to tear down.
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Liveness probe used by `phone-use doctor` and the relay health check. */
|
|
199
|
+
async ping(): Promise<{ ok: boolean; ios?: string; device?: string }> {
|
|
200
|
+
return this.#rpc('get_context');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export const createDeviceRunnerBackend = (config?: DeviceRunnerConfig): DeviceRunnerBackend =>
|
|
205
|
+
new DeviceRunnerBackend(config);
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* deliberately not re-exported here.
|
|
8
8
|
*/
|
|
9
9
|
/** The published package version (kept in sync with package.json by the release flow). */
|
|
10
|
-
export const VERSION = '0.
|
|
10
|
+
export const VERSION = '0.3.0';
|
|
11
11
|
|
|
12
12
|
export {
|
|
13
13
|
type Action,
|
|
@@ -31,6 +31,15 @@ export {
|
|
|
31
31
|
registerBackend,
|
|
32
32
|
} from './backend.ts';
|
|
33
33
|
export { createAgentDeviceBackend } from './backends/agent-device.ts';
|
|
34
|
+
export {
|
|
35
|
+
CloudSandboxBackend,
|
|
36
|
+
type CloudSandboxBackendOptions,
|
|
37
|
+
createCloudSandboxBackend,
|
|
38
|
+
type SandboxRpcMethod,
|
|
39
|
+
type SandboxRpcRequest,
|
|
40
|
+
type SandboxRpcResponse,
|
|
41
|
+
} from './backends/cloud-sandbox.ts';
|
|
42
|
+
export { createDeviceRunnerBackend, DeviceRunnerBackend } from './backends/device-runner.ts';
|
|
34
43
|
export { type IosConnectOptions, type IosLaunchOptions, ios } from './backends/ios.ts';
|
|
35
44
|
export type { AndroidDeviceConfig, CommonDeviceConfig, DeviceConfig, IosDeviceConfig } from './config.ts';
|
|
36
45
|
export type {
|