agent-device-proxy 0.1.5 → 0.2.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/.env.example ADDED
@@ -0,0 +1,19 @@
1
+ AGENT_DEVICE_PROXY_HOST=0.0.0.0
2
+ AGENT_DEVICE_PROXY_PORT=9124
3
+ # Shared secret checked by host API (Authorization: Bearer ...).
4
+ # Must match client value: AGENT_DEVICE_PROXY_BEARER_TOKEN.
5
+ # Generate with: openssl rand -hex 32
6
+ AGENT_DEVICE_PROXY_BEARER_TOKEN=replace-with-strong-token
7
+ # Optional override for where agent-device state lives on the macOS host.
8
+ # The proxy resolves the live daemon port/token from
9
+ # AGENT_DEVICE_STATE_DIR/daemon.json (defaults to ~/.agent-device/daemon.json).
10
+ # AGENT_DEVICE_STATE_DIR=/Users/you/.agent-device
11
+ AGENT_DEVICE_PROXY_VALIDATE_DAEMON_STARTUP=true
12
+ AGENT_DEVICE_PROXY_DAEMON_PROBE_TIMEOUT_MS=3000
13
+
14
+ # Host-local Metro bridge used for iOS sandbox-host runs.
15
+ AGENT_DEVICE_PROXY_METRO_BRIDGE_ENABLED=true
16
+ AGENT_DEVICE_PROXY_METRO_BRIDGE_HOST=127.0.0.1
17
+ AGENT_DEVICE_PROXY_METRO_BRIDGE_PORT=8081
18
+ AGENT_DEVICE_PROXY_METRO_BRIDGE_ANDROID_HOST=10.0.2.2
19
+ AGENT_DEVICE_PROXY_METRO_BRIDGE_TIMEOUT_MS=120000
@@ -0,0 +1,263 @@
1
+ # agent-device-proxy Architecture
2
+
3
+ See [README.md](./README.md) for install and day-to-day usage. This document
4
+ covers the system model, repo integration, and endpoint behavior.
5
+
6
+ ## Architecture
7
+
8
+ ```
9
+ Vercel Sandbox (Linux) macOS host
10
+ ┌───────────────────────┐ ┌──────────────────────────────┐
11
+ │ │ │ agent-device-proxy (:9124) │
12
+ │ Metro dev server │ /api/metro/ │ ┌────────────────────────┐ │
13
+ │ (port 8081 inside │── bridge ───▶│ │ Metro bridge (:8081) │ │
14
+ │ sandbox) │ │ │ HTTP + WebSocket proxy │ │
15
+ │ │ │ └───────────┬────────────┘ │
16
+ │ agent-device CLI │ /agent-device │ │ │
17
+ │ (npm global) │── /rpc ──▶│ ┌───────────▼────────────┐ │
18
+ │ │ │ │ Daemon forwarder │ │
19
+ │ .forfiter/ scripts │ │ │ → agent-device daemon │ │
20
+ │ │ │ └────────────────────────┘ │
21
+ └───────────────────────┘ │ │
22
+ │ │
23
+ │ iOS Simulator / Android Emu │
24
+ │ → 127.0.0.1:8081 (iOS) │
25
+ │ → 10.0.2.2:8081 (Android) │
26
+ └──────────────────────────────┘
27
+ ```
28
+
29
+ ## Why This Exists
30
+
31
+ The QA worker runs in a Vercel sandbox. Metro also runs in that sandbox.
32
+
33
+ The iOS simulator and Android emulator do not run in the sandbox. They run on a
34
+ macOS host machine, and `agent-device` also runs there.
35
+
36
+ That means the app cannot reliably load JavaScript directly from the sandbox's
37
+ public URL:
38
+
39
+ - sandbox Metro is externally reachable as `https://<sandbox>.vercel.run`
40
+ - stock React Native iOS builds do not reliably consume that public HTTPS URL as
41
+ a packager origin
42
+ - Android emulator networking is also host-specific
43
+
44
+ So the supported architecture is:
45
+
46
+ 1. Metro runs in the sandbox.
47
+ 2. The sandbox helper resolves the public upstream Metro URL.
48
+ 3. `agent-device-proxy` creates a host-local HTTP bridge on macOS.
49
+ 4. Runtime hints point the app at that host-local bridge:
50
+ - iOS simulator: `127.0.0.1:<bridge-port>`
51
+ - Android emulator: `10.0.2.2:<bridge-port>`
52
+
53
+ The app talks to a normal local HTTP Metro endpoint. The proxy forwards that
54
+ traffic back to the sandbox's public Metro origin.
55
+
56
+ ## What Is Bridged
57
+
58
+ `agent-device-proxy` currently exposes:
59
+
60
+ - transparent forwarding for any `/agent-device/*` path to the upstream daemon
61
+ - common examples are `GET /agent-device/health` and `POST /agent-device/rpc`
62
+ - `POST /api/metro/resolve` to normalize Metro runtime hints
63
+ - `POST /api/metro/probe` to probe upstream Metro reachability
64
+ - `POST /api/metro/bridge` to create or reconfigure the host-local Metro bridge
65
+ - `GET /api/health` for proxy health
66
+ - `GET /healthz` for lightweight health
67
+
68
+ The proxy is not the product boundary for artifact semantics in this repo. QA
69
+ artifact selection happens in workflow code, the sandbox must resolve a
70
+ host-downloadable install source plus any required auth, and app installation
71
+ happens through the typed `agent-device` client on the host. If no
72
+ host-downloadable source can be produced, QA blocks instead of falling back to
73
+ sandbox-local installation.
74
+
75
+ ## Runtime Model
76
+
77
+ Runtime hints are still relevant, but their role changed.
78
+
79
+ They are no longer meant to point apps at the public sandbox Metro URL. Instead,
80
+ they tell the app which host-local bridge endpoint to use.
81
+
82
+ Typical runtime values returned by `/api/metro/bridge`:
83
+
84
+ - iOS:
85
+ - `metro_host=127.0.0.1`
86
+ - `metro_port=8081`
87
+ - `metro_bundle_url=http://127.0.0.1:8081/index.bundle?...`
88
+ - Android:
89
+ - `metro_host=10.0.2.2`
90
+ - `metro_port=8081`
91
+ - `metro_bundle_url=http://10.0.2.2:8081/index.bundle?...`
92
+
93
+ The upstream public sandbox URL is still tracked and probed, but the app should
94
+ not launch against it directly when the bridge is available.
95
+
96
+ ## QA Flow In This Repo
97
+
98
+ The staged sandbox Metro helper is the source of truth for Metro:
99
+
100
+ - `.forfiter/start-metro-runtime.js`
101
+ - is invoked only when the repo-side QA bootstrap determines that the artifact
102
+ needs a Metro-backed dev runtime
103
+ - starts Metro inside the sandbox
104
+ - resolves the public upstream Metro runtime
105
+ - calls `POST /api/metro/bridge`
106
+ - writes `.forfiter/metro-runtime.json`
107
+ - fails closed if a bridge is required but unavailable
108
+
109
+ App launch is no longer owned by a sandbox helper script. The workflow bootstrap
110
+ reads `.forfiter/metro-runtime.json`, installs the app through the typed
111
+ `agent-device` client, and then calls `open({ runtime })`.
112
+
113
+ For iOS React Native dev builds, that bridge-backed runtime remains the
114
+ supported launch path.
115
+
116
+ ## Host Env File
117
+
118
+ Use [.env.example](.env.example) as the host-side template.
119
+
120
+ Important defaults:
121
+
122
+ - `AGENT_DEVICE_PROXY_PORT=9124`
123
+ - `AGENT_DEVICE_PROXY_METRO_BRIDGE_ENABLED=true`
124
+ - `AGENT_DEVICE_PROXY_METRO_BRIDGE_HOST=127.0.0.1`
125
+ - `AGENT_DEVICE_PROXY_METRO_BRIDGE_PORT=8081`
126
+ - `AGENT_DEVICE_PROXY_METRO_BRIDGE_ANDROID_HOST=10.0.2.2`
127
+
128
+ The proxy validates the upstream daemon health on startup by default. If
129
+ `~/.agent-device/daemon.json` does not point at a live HTTP daemon, proxy
130
+ startup should fail early.
131
+
132
+ Practical startup modes:
133
+
134
+ - Installed package, auto-discovered daemon:
135
+ - set `AGENT_DEVICE_PROXY_BEARER_TOKEN`
136
+ - ensure `~/.agent-device/daemon.json` exists, or set `AGENT_DEVICE_STATE_DIR`
137
+ - run `agent-device-proxy serve`
138
+ - Installed package, helper-managed local daemon:
139
+ - set `AGENT_DEVICE_PROXY_BEARER_TOKEN`
140
+ - run `agent-device-proxy dev`
141
+
142
+ ## Worker Envs
143
+
144
+ The Linux/Vercel worker uses two related connections:
145
+
146
+ ### 1. Remote `agent-device` daemon bridge
147
+
148
+ This is what the workflow-side typed `agent-device` client uses:
149
+
150
+ ```bash
151
+ AGENT_DEVICE_DAEMON_BASE_URL=http://<mac-host>:9124/agent-device
152
+ AGENT_DEVICE_DAEMON_AUTH_TOKEN=<strong-token>
153
+ AGENT_DEVICE_DAEMON_TRANSPORT=http
154
+ ```
155
+
156
+ ### 2. Direct proxy API
157
+
158
+ This is what the staged Metro helper uses:
159
+
160
+ ```bash
161
+ AGENT_DEVICE_PROXY_BASE_URL=http://<mac-host>:9124
162
+ AGENT_DEVICE_PROXY_BEARER_TOKEN=<strong-token>
163
+ ```
164
+
165
+ At runtime inside the sandbox, QA writes `~/.agent-device/config.json` with the
166
+ derived daemon settings and the per-run session/platform lock. The runtime
167
+ agent then uses plain `agent-device` commands without wrapper scripts.
168
+
169
+ ## Metro Endpoints
170
+
171
+ ### `/api/metro/resolve`
172
+
173
+ Normalizes incoming runtime hints into a resolved upstream runtime.
174
+
175
+ Use this when you want to inspect how the proxy interprets:
176
+
177
+ - `metro_bundle_url`
178
+ - `metro_host + metro_port`
179
+
180
+ ### `/api/metro/probe`
181
+
182
+ Probes the upstream Metro endpoint and returns reachability details.
183
+
184
+ This is useful as a diagnostic endpoint. It is not enough by itself to prove
185
+ that the app is using the correct runtime.
186
+
187
+ ### `/api/metro/bridge`
188
+
189
+ Creates or reconfigures the host-local Metro bridge and returns the bridge-backed
190
+ runtime hints that apps should use.
191
+
192
+ This is the important endpoint for sandbox-host QA.
193
+
194
+ Example response shape:
195
+
196
+ ```json
197
+ {
198
+ "base_url": "http://127.0.0.1:8081",
199
+ "status_url": "http://127.0.0.1:8081/status",
200
+ "bundle_url": "http://127.0.0.1:8081/index.bundle?platform=ios&dev=true&minify=false",
201
+ "ios_runtime": {
202
+ "metro_host": "127.0.0.1",
203
+ "metro_port": 8081,
204
+ "metro_bundle_url": "http://127.0.0.1:8081/index.bundle?platform=ios&dev=true&minify=false"
205
+ },
206
+ "android_runtime": {
207
+ "metro_host": "10.0.2.2",
208
+ "metro_port": 8081,
209
+ "metro_bundle_url": "http://10.0.2.2:8081/index.bundle?platform=android&dev=true&minify=false"
210
+ },
211
+ "upstream": {
212
+ "bundle_url": "https://<sandbox>.vercel.run/index.bundle?...",
213
+ "host": "<sandbox>.vercel.run",
214
+ "port": 443,
215
+ "status_url": "https://<sandbox>.vercel.run/status"
216
+ }
217
+ }
218
+ ```
219
+
220
+ ## Operational Notes
221
+
222
+ - The Metro bridge is lazy. It starts listening when `/api/metro/bridge` is
223
+ called.
224
+ - Initial bundle fetch is supported. Websocket and HMR traffic are also proxied
225
+ by the bridge.
226
+ - The current repo QA flow assumes the bridge is available when
227
+ `AGENT_DEVICE_PROXY_BASE_URL` and `AGENT_DEVICE_PROXY_BEARER_TOKEN` are set.
228
+ - Public sandbox Metro URLs are still recorded as upstream metadata, but they
229
+ should be treated as debug or probe inputs, not as the primary app runtime.
230
+
231
+ ## Troubleshooting
232
+
233
+ **Proxy startup fails with daemon health check error**
234
+
235
+ The proxy validates the upstream daemon on startup. Make sure `agent-device` is
236
+ running in HTTP mode:
237
+
238
+ ```bash
239
+ export AGENT_DEVICE_DAEMON_SERVER_MODE=http
240
+ agent-device session list --json --daemon-transport http
241
+ ```
242
+
243
+ If using the helper command, the proxy resolves the live daemon from
244
+ `~/.agent-device/daemon.json` automatically. If that file is missing or stale,
245
+ restart `agent-device` in HTTP mode.
246
+
247
+ **Port 8081 already in use**
248
+
249
+ The Metro bridge binds to port 8081 by default. If another Metro instance or
250
+ process already occupies that port, either stop it or set a different bridge
251
+ port:
252
+
253
+ ```bash
254
+ export AGENT_DEVICE_PROXY_METRO_BRIDGE_PORT=8082
255
+ ```
256
+
257
+ Note: the sandbox scripts and runtime hints must also be updated to match.
258
+
259
+ **Bearer token mismatch**
260
+
261
+ The proxy rejects requests where the `Authorization: Bearer` header does not
262
+ match `AGENT_DEVICE_PROXY_BEARER_TOKEN`. Ensure the same token is exported on
263
+ both the worker side and the macOS host side.
package/README.md CHANGED
@@ -1,196 +1,88 @@
1
1
  # agent-device-proxy
2
2
 
3
- Reusable macOS `agent-device-proxy` sidecar for:
3
+ Reusable macOS sidecar for remote mobile QA.
4
4
 
5
- - native `agent-device` remote-daemon bridge
6
- - Metro resolve/probe
7
- - artifact upload/install for `.app` and `.apk`
5
+ It has two jobs:
8
6
 
9
- Default hardening:
7
+ - expose a host-local HTTP bridge to a macOS `agent-device` HTTP daemon
8
+ - expose a host-local HTTP Metro bridge for simulator/emulator app traffic
10
9
 
11
- - streamed artifact uploads (no in-memory full body for uploads)
12
- - ZIP entry validation before `.app` extraction
13
- - strict command status (`exit_code != 0` -> HTTP `502`)
14
- - artifact retention cleanup (TTL + max total bytes)
10
+ ## Why
15
11
 
16
- ## Hidden Transport Behavior
12
+ The QA worker and Metro run in a Linux sandbox, while the iOS simulator,
13
+ Android emulator, and `agent-device` run on a macOS host. `agent-device-proxy`
14
+ bridges those two environments so the worker can talk to the host daemon and
15
+ the app can use a normal host-local Metro endpoint.
17
16
 
18
- - `.app` directories are zipped by client API and unzipped by host API
19
- - `.apk` files are sent raw (not zipped)
20
-
21
- Consumers should pass `.app` / `.apk` paths and call the client API.
22
-
23
- ## Primary endpoints
24
-
25
- - `GET /api/health`
26
- - `GET /agent-device/health`
27
- - `POST /agent-device/rpc`
28
- - `POST /api/artifacts/upload`
29
- - `POST /api/agent-device/install`
30
- - `POST /api/agent-device/exec`
31
- - `POST /api/metro/resolve`
32
- - `POST /api/metro/probe`
33
-
34
- Compatibility aliases remain available under `/v1/*`.
35
-
36
- ## Run
17
+ ## Install
37
18
 
38
19
  ```bash
39
- pnpm --filter agent-device-proxy start
20
+ npm install -g agent-device-proxy
40
21
  ```
41
22
 
42
- From repo root:
23
+ ## Run
24
+
25
+ Start the proxy against an already-running `agent-device` HTTP daemon:
43
26
 
44
27
  ```bash
45
- pnpm dev:agent-device-proxy
28
+ export AGENT_DEVICE_PROXY_BEARER_TOKEN='<strong-token>' # openssl rand -hex 32
29
+ agent-device-proxy serve
46
30
  ```
47
31
 
48
- ## Remote macOS + Linux/Vercel setup
49
-
50
- ### 1) Remote macOS machine (runs `agent-device-proxy` + `agent-device`)
32
+ For `serve`, set:
51
33
 
52
- Use `agent-device-proxy/.env.example` as the host-side env template.
53
- Use one shared bearer token value on both sides:
34
+ - `AGENT_DEVICE_PROXY_BEARER_TOKEN` (e.g. generated with `openssl rand -hex 32`)
35
+ - a readable `~/.agent-device/daemon.json` (set by `agent-device`) or `AGENT_DEVICE_STATE_DIR`
54
36
 
55
- - host: `AGENT_DEVICE_PROXY_BEARER_TOKEN`
56
- - client: `AGENT_DEVICE_PROXY_BEARER_TOKEN`
57
- - generate once: `openssl rand -hex 32`
37
+ If you want the package to prepare the local `agent-device` HTTP daemon first,
38
+ use the helper command instead:
58
39
 
59
40
  ```bash
60
- # on remote macOS
61
- git clone https://github.com/callstackincubator/forfiter.git
62
-
63
- cd forfiter/agent-device-proxy && pnpm install
64
-
65
- # run agent-device HTTP daemon
66
- AGENT_DEVICE_DAEMON_SERVER_MODE=http \
67
- agent-device session list --json
68
-
69
- # run agent-device-proxy API
70
- AGENT_DEVICE_PROXY_BEARER_TOKEN='<strong-token>' \
71
- AGENT_DEVICE_PROXY_DAEMON_BASE_URL='http://127.0.0.1:4310/agent-device' \
72
- pnpm --filter agent-device-proxy start
41
+ export AGENT_DEVICE_PROXY_BEARER_TOKEN='<strong-token>'
42
+ agent-device-proxy dev
73
43
  ```
74
44
 
75
- Optional hardening envs:
45
+ `dev` is macOS-host oriented. It expects:
76
46
 
77
- ```bash
78
- AGENT_DEVICE_PROXY_STRICT_COMMAND_STATUS=true
79
- AGENT_DEVICE_PROXY_ARTIFACT_TTL_HOURS=24
80
- AGENT_DEVICE_PROXY_ARTIFACT_MAX_TOTAL_BYTES=5368709120
81
- ```
47
+ - `agent-device` to be installed locally and runnable
48
+ - standard host utilities such as `ps` and `lsof`
49
+ - `AGENT_DEVICE_PROXY_BEARER_TOKEN` to already be set
82
50
 
83
- ### 2) Client Linux/Vercel side
51
+ If you want a host-side env template, use [.env.example](.env.example).
84
52
 
85
- For native `agent-device` remote-daemon mode in sandboxes, point the client at the
86
- proxy bridge:
53
+ The package also exposes the macOS bootstrap helper as:
87
54
 
88
55
  ```bash
89
- export AGENT_DEVICE_DAEMON_BASE_URL="http://<remote-mac-ip>:9123/agent-device"
90
- export AGENT_DEVICE_DAEMON_AUTH_TOKEN="<strong-token>"
56
+ agent-device-proxy-macos-remote-setup --help
91
57
  ```
92
58
 
93
- The proxy bridge exposes `GET /agent-device/health` and `POST /agent-device/rpc`,
94
- and forwards them to the configured host daemon base URL.
59
+ ## Programmatic Usage
95
60
 
96
- For artifact upload/install and Metro helper flows, the same worker can call the
97
- `agent-device-proxy` API directly.
98
-
99
- In your workflow/agent worker:
61
+ Node client:
100
62
 
101
63
  ```ts
102
64
  import { createAgentDeviceProxyClient } from "agent-device-proxy"
103
- import { ensureMetroRuntime } from "agent-device-proxy/metro-runtime"
104
65
 
105
- const agentDeviceProxy = createAgentDeviceProxyClient({
106
- baseUrl: "http://<remote-mac-ip>:9123",
66
+ const client = createAgentDeviceProxyClient({
67
+ baseUrl: "http://<mac-host>:9124",
107
68
  bearerToken: process.env.AGENT_DEVICE_PROXY_BEARER_TOKEN,
108
69
  })
109
-
110
- const metro = await ensureMetroRuntime({
111
- projectRoot: "/workspace/RNCLI83",
112
- port: 8081,
113
- publicHost: process.env.AGENT_DEVICE_PROXY_METRO_PUBLIC_HOST, // must be reachable from remote mac
114
- })
115
-
116
- try {
117
- // iOS: pass ios_runtime so remote mac can connect to Metro
118
- await agentDeviceProxy.installApp({
119
- app: "RNCLI83",
120
- filePath: "/workspace/ios-build/RNCLI83.app",
121
- platform: "ios",
122
- device: "iPhone 17 Pro",
123
- })
124
-
125
- await agentDeviceProxy.agentDeviceExec({
126
- argv: ["open", "RNCLI83", "--platform", "ios", "--device", "iPhone 17 Pro"],
127
- ios_runtime: metro.ios_runtime,
128
- })
129
-
130
- // Android: no Metro hint needed
131
- await agentDeviceProxy.installApp({
132
- app: "com.rncli83",
133
- filePath: "/workspace/android/app/build/outputs/apk/debug/app-debug.apk",
134
- platform: "android",
135
- serial: "emulator-5554",
136
- })
137
- } finally {
138
- await metro.stop()
139
- }
140
70
  ```
141
71
 
142
- ### Networking rule
72
+ Embedded server:
143
73
 
144
- `metro.publicHost:metro.port` must be reachable from remote macOS.
145
- If not reachable directly, use a tunnel and pass that tunnel endpoint in `ios_runtime`.
146
-
147
- ### Migration note
148
-
149
- Older docs may reference `host-agent`, `@platform/host-agent`, or `HOST_AGENT_*` env names.
150
- Use `agent-device-proxy`, `agent-device-proxy/*`, and `AGENT_DEVICE_PROXY_*` env names instead.
151
-
152
- ## Client
153
-
154
- ```js
155
- import { createAgentDeviceProxyClient } from "agent-device-proxy"
156
- ```
157
-
158
- ## Auxiliary helpers
159
-
160
- For Node consumers, use:
161
-
162
- ```js
163
- import {
164
- createAgentDeviceProxyClient,
165
- ensureMetroRuntime,
166
- } from "agent-device-proxy"
167
- ```
168
-
169
- Use `installApp()` for artifact handoff and `agentDeviceExec()` only when you
170
- explicitly need the proxy API from Node. This package does not install an
171
- `agent-device` bin shim; use the real `agent-device` package for CLI execution
172
- and native remote-daemon support.
173
-
174
- ## Embedded server
175
-
176
- ```js
74
+ ```ts
177
75
  import { startAgentDeviceProxyServer } from "agent-device-proxy/server"
178
76
 
179
77
  startAgentDeviceProxyServer()
180
78
  ```
181
79
 
182
- ## Metro runtime helper (Linux/Vercel)
80
+ ## More Details
183
81
 
184
- ```js
185
- import { ensureMetroRuntime } from "agent-device-proxy/metro-runtime"
82
+ See [ARCHITECTURE.md](./ARCHITECTURE.md) for:
186
83
 
187
- const metro = await ensureMetroRuntime({
188
- projectRoot: "/workspace/RNCLI83",
189
- publicHost: "10.0.0.10",
190
- })
191
-
192
- // use metro.ios_runtime when opening iOS app through agent-device-proxy
193
- // await agentDeviceProxy.agentDeviceExec({ argv: [...], ios_runtime: metro.ios_runtime })
194
-
195
- await metro.stop()
196
- ```
84
+ - system architecture and runtime model
85
+ - QA flow in this repo
86
+ - worker-side env wiring
87
+ - Metro endpoint behavior
88
+ - operational notes and troubleshooting
package/dist/src/224.js CHANGED
@@ -1 +1 @@
1
- import{randomUUID as t,node_path as e,promises as i,createReadStream as a}from"./402.js";import{spawn as r}from"./493.js";import{node_os as o}from"./47.js";function n(t){let r=function(t){if("string"!=typeof t||!t.trim())throw Error("baseUrl is required");return t.trim().replace(/\/+$/,"")}(t.baseUrl),o="string"==typeof t.bearerToken?t.bearerToken.trim():"",n=f(t.timeoutMs,12e4);return{health:async()=>await s({baseUrl:r,method:"GET",endpoint:"/api/health",bearerToken:o,timeoutMs:n}),metroResolve:async t=>await s({baseUrl:r,method:"POST",endpoint:"/api/metro/resolve",bodyJson:{ios_runtime:d(t)},bearerToken:o,timeoutMs:f(u(t),n)}),async metroProbe(t){let e=f(u(t),n);return await s({baseUrl:r,method:"POST",endpoint:"/api/metro/probe",bodyJson:{ios_runtime:d(t),...u(t)?{timeout_ms:u(t)}:{}},bearerToken:o,timeoutMs:e})},async agentDeviceExec(t){let e={argv:Array.isArray(t?.argv)?t.argv:[],...t?.cwd?{cwd:t.cwd}:{},...t?.run_id?{run_id:t.run_id}:{},...t?.ios_session_id?{ios_session_id:t.ios_session_id}:{},...t?.tenant_id?{tenant_id:t.tenant_id}:{},...t?.ios_runtime?{ios_runtime:t.ios_runtime}:{}};return await s({baseUrl:r,method:"POST",endpoint:"/api/agent-device/exec",bodyJson:e,bearerToken:o,timeoutMs:f(t?.timeout_ms,n)})},async uploadArtifact(t){let e=await p(t.filePath);try{let p=await i.stat(e.uploadPath);return await s({baseUrl:r,method:"POST",endpoint:"/api/artifacts/upload",rawBody:a(e.uploadPath),rawBodyLength:p.size,contentType:"application/octet-stream",extraHeaders:{"x-artifact-type":e.artifactType,"x-artifact-archive":e.archive,"x-artifact-filename":e.fileName,...t.sha256?{"x-artifact-sha256":t.sha256}:{}},bearerToken:o,timeoutMs:f(t.timeout_ms,n)})}finally{e.cleanupPath&&await h(e.cleanupPath)}},async installArtifact(t){let e={app:m(t.app,"app"),artifact_id:m(t.artifact_id,"artifact_id"),platform:m(t.platform,"platform"),...t.device?{device:t.device}:{},...t.session?{session:t.session}:{},...t.udid?{udid:t.udid}:{},...t.serial?{serial:t.serial}:{},...!0===t.reinstall?{reinstall:!0}:{},...!1===t.json?{json:!1}:{}};return await s({baseUrl:r,method:"POST",endpoint:"/api/agent-device/install",bodyJson:e,bearerToken:o,timeoutMs:f(t.timeout_ms,n)})},async installApp(t){var i;let a,r=m(t.filePath,"filePath"),o=(i=r,(a=e.resolve(i)).endsWith(".app")?e.basename(a,".app"):""),n=t.app||o;if(!n)throw Error("app is required when filePath does not end with .app");let s=await this.uploadArtifact({filePath:r,...t.sha256?{sha256:t.sha256}:{},...t.timeout_ms?{timeout_ms:t.timeout_ms}:{}}),p=await this.installArtifact({app:n,artifact_id:s.artifact_id,platform:m(t.platform,"platform"),...t.device?{device:t.device}:{},...t.session?{session:t.session}:{},...t.udid?{udid:t.udid}:{},...t.serial?{serial:t.serial}:{},...!0===t.reinstall?{reinstall:!0}:{},...!1===t.json?{json:!1}:{},...t.timeout_ms?{timeout_ms:t.timeout_ms}:{}});return{artifact:s,install:p}}}}async function s({baseUrl:t,method:e,endpoint:i,bodyJson:a,rawBody:r,rawBodyLength:o,contentType:n,extraHeaders:p,bearerToken:l,timeoutMs:d}){let u,c,m={...p||{}};void 0!==r?(u=r,m["content-type"]=n||"application/octet-stream","number"==typeof o&&Number.isFinite(o)&&o>=0&&(m["content-length"]=String(o))):void 0!==a&&(u=JSON.stringify(a),m["content-type"]="application/json"),l&&(m.authorization=`Bearer ${l}`);let f=await fetch(`${t}${i}`,{method:e,headers:m,...void 0!==u?{body:u}:{},...void 0!==r?{duplex:"half"}:{},signal:AbortSignal.timeout(d)}),h=await f.text();try{c=h?JSON.parse(h):{}}catch{c={raw:h}}if(!f.ok){var _;let t=Error(((_=c)&&"object"==typeof _?"string"==typeof _.error?_.error:_.error&&"object"==typeof _.error&&"string"==typeof _.error.message?_.error.message:null:null)??`agent-device-proxy request failed (${f.status})`);throw t.statusCode=f.status,t.payload=c,t}return c&&"object"==typeof c&&"ok"in c&&"data"in c?c.data:c}async function p(a){let r=e.resolve(m(a,"filePath")),n=await i.stat(r);if(n.isDirectory()){if(!r.endsWith(".app"))throw Error("directory uploads must be .app bundles");let i=e.join(o.tmpdir(),`agent-device-proxy-${t()}.zip`);return await l("zip",["-qry",i,e.basename(r)],e.dirname(r)),{artifactType:"app",archive:"zip",uploadPath:i,fileName:`${e.basename(r)}.zip`,cleanupPath:i}}if(!n.isFile())throw Error("filePath must point to a file or .app directory");if(!r.toLowerCase().endsWith(".apk"))throw Error("file uploads must be .apk (for iOS, pass a .app directory)");return{artifactType:"apk",archive:"raw",uploadPath:r,fileName:e.basename(r),cleanupPath:""}}async function l(t,e,i){await new Promise((a,o)=>{let n=r(t,e,{cwd:i,stdio:["ignore","pipe","pipe"]}),s="";n.stderr.on("data",t=>{s+=t.toString("utf8")}),n.on("error",e=>{o(Error(`${t} failed: ${e.message}`))}),n.on("close",e=>{0===e?a():o(Error(`${t} exited with code ${e}: ${s||"unknown error"}`))})})}function d(t){return c(t)?t.ios_runtime:t}function u(t){if(c(t))return t.timeout_ms}function c(t){return"ios_runtime"in t}function m(t,e){if("string"!=typeof t||!t.trim())throw Error(`${e} is required`);return t.trim()}function f(t,e){if(null==t||""===t)return e;let i=Number.parseInt(String(t),10);return!Number.isInteger(i)||i<100||i>36e5?e:i}async function h(t){try{await i.unlink(t)}catch{}}export{n as createAgentDeviceProxyClient};
1
+ function t(t){let i=function(t){if("string"!=typeof t||!t.trim())throw Error("baseUrl is required");return t.trim().replace(/\/+$/,"")}(t.baseUrl),a="string"==typeof t.bearerToken?t.bearerToken.trim():"",s=n(t.timeoutMs,12e4);return{health:async()=>await e({baseUrl:i,method:"GET",endpoint:"/api/health",bearerToken:a,timeoutMs:s}),metroResolve:async t=>await e({baseUrl:i,method:"POST",endpoint:"/api/metro/resolve",bodyJson:{ios_runtime:r(t)},bearerToken:a,timeoutMs:n(o(t),s)}),async metroProbe(t){let u=n(o(t),s);return await e({baseUrl:i,method:"POST",endpoint:"/api/metro/probe",bodyJson:{ios_runtime:r(t),...o(t)?{timeout_ms:o(t)}:{}},bearerToken:a,timeoutMs:u})},async metroBridge(t){let u=n(o(t),s);return await e({baseUrl:i,method:"POST",endpoint:"/api/metro/bridge",bodyJson:{ios_runtime:r(t),...o(t)?{timeout_ms:o(t)}:{}},bearerToken:a,timeoutMs:u})}}}async function e({baseUrl:t,method:r,endpoint:o,bodyJson:i,bearerToken:n,timeoutMs:a}){let s,u,m={};void 0!==i&&(s=JSON.stringify(i),m["content-type"]="application/json"),n&&(m.authorization=`Bearer ${n}`);let l=await fetch(`${t}${o}`,{method:r,headers:m,...void 0!==s?{body:s}:{},signal:AbortSignal.timeout(a)}),p=await l.text();try{u=p?JSON.parse(p):{}}catch{u={raw:p}}if(!l.ok){var c;let t=Error(((c=u)&&"object"==typeof c?"string"==typeof c.error?c.error:c.error&&"object"==typeof c.error&&"string"==typeof c.error.message?c.error.message:null:null)??`agent-device-proxy request failed (${l.status})`);throw t.statusCode=l.status,t.payload=u,t}return u&&"object"==typeof u&&"ok"in u&&"data"in u?u.data:u}function r(t){return i(t)?t.ios_runtime:t}function o(t){if(i(t))return t.timeout_ms}function i(t){return"ios_runtime"in t}function n(t,e){if(null==t||""===t)return e;let r=Number.parseInt(String(t),10);return!Number.isInteger(r)||r<100||r>36e5?e:r}export{t as createAgentDeviceProxyClient};