agent-device-proxy 0.1.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/README.md +202 -0
- package/dist/src/111.js +1 -0
- package/dist/src/224.js +1 -0
- package/dist/src/36.js +18 -0
- package/dist/src/402.js +1 -0
- package/dist/src/403.js +2 -0
- package/dist/src/47.js +1 -0
- package/dist/src/493.js +1 -0
- package/dist/src/agent-device.js +3 -0
- package/dist/src/bin.js +2 -0
- package/dist/src/client.js +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/metro-runtime.js +1 -0
- package/dist/src/server.js +1 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# agent-device-proxy
|
|
2
|
+
|
|
3
|
+
Reusable macOS agent-device-proxy (simple mode) for:
|
|
4
|
+
- `agent-device` command execution
|
|
5
|
+
- Metro resolve/probe
|
|
6
|
+
- artifact upload/install for `.app` and `.apk`
|
|
7
|
+
|
|
8
|
+
Default hardening:
|
|
9
|
+
- streamed artifact uploads (no in-memory full body for uploads)
|
|
10
|
+
- ZIP entry validation before `.app` extraction
|
|
11
|
+
- strict command status (`exit_code != 0` -> HTTP `502`)
|
|
12
|
+
- artifact retention cleanup (TTL + max total bytes)
|
|
13
|
+
|
|
14
|
+
## Hidden Transport Behavior
|
|
15
|
+
|
|
16
|
+
- `.app` directories are zipped by client API and unzipped by host API
|
|
17
|
+
- `.apk` files are sent raw (not zipped)
|
|
18
|
+
|
|
19
|
+
Consumers should pass `.app` / `.apk` paths and call the client API.
|
|
20
|
+
|
|
21
|
+
## Primary endpoints
|
|
22
|
+
|
|
23
|
+
- `GET /api/health`
|
|
24
|
+
- `GET /agent-device/health`
|
|
25
|
+
- `POST /agent-device/rpc`
|
|
26
|
+
- `POST /api/artifacts/upload`
|
|
27
|
+
- `POST /api/agent-device/install`
|
|
28
|
+
- `POST /api/agent-device/exec`
|
|
29
|
+
- `POST /api/metro/resolve`
|
|
30
|
+
- `POST /api/metro/probe`
|
|
31
|
+
|
|
32
|
+
Compatibility aliases remain available under `/v1/*`.
|
|
33
|
+
|
|
34
|
+
## Run
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pnpm --filter agent-device-proxy start
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
From repo root:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pnpm dev:agent-device-proxy
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Remote macOS + Linux/Vercel setup
|
|
47
|
+
|
|
48
|
+
### 1) Remote macOS machine (runs `agent-device-proxy` + `agent-device`)
|
|
49
|
+
|
|
50
|
+
Use `agent-device-proxy/.env.example` as the host-side env template.
|
|
51
|
+
Use one shared bearer token value on both sides:
|
|
52
|
+
- host: `AGENT_DEVICE_PROXY_BEARER_TOKEN`
|
|
53
|
+
- client: `AGENT_PROXY_BEARER_TOKEN`
|
|
54
|
+
- generate once: `openssl rand -hex 32`
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# on remote macOS
|
|
58
|
+
git clone https://github.com/callstackincubator/forfiter.git
|
|
59
|
+
|
|
60
|
+
cd forfiter/agent-device-proxy && pnpm install
|
|
61
|
+
|
|
62
|
+
# run agent-device HTTP daemon
|
|
63
|
+
AGENT_DEVICE_DAEMON_SERVER_MODE=http \
|
|
64
|
+
agent-device session list --json
|
|
65
|
+
|
|
66
|
+
# run agent-device-proxy API
|
|
67
|
+
AGENT_DEVICE_PROXY_BEARER_TOKEN='<strong-token>' \
|
|
68
|
+
AGENT_DEVICE_PROXY_DAEMON_BASE_URL='http://127.0.0.1:4310/agent-device' \
|
|
69
|
+
pnpm --filter agent-device-proxy start
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Optional hardening envs:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
AGENT_PROXY_STRICT_COMMAND_STATUS=true
|
|
76
|
+
AGENT_PROXY_ARTIFACT_TTL_HOURS=24
|
|
77
|
+
AGENT_PROXY_ARTIFACT_MAX_TOTAL_BYTES=5368709120
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 2) Client Linux/Vercel side
|
|
81
|
+
|
|
82
|
+
For native `agent-device` remote-daemon mode in sandboxes, point the client at the
|
|
83
|
+
proxy bridge:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
export AGENT_DEVICE_DAEMON_BASE_URL="http://<remote-mac-ip>:9123/agent-device"
|
|
87
|
+
export AGENT_DEVICE_DAEMON_AUTH_TOKEN="<strong-token>"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The proxy bridge exposes `GET /agent-device/health` and `POST /agent-device/rpc`,
|
|
91
|
+
and forwards them to the configured host daemon base URL.
|
|
92
|
+
|
|
93
|
+
For artifact upload/install and Metro helper flows, the same worker can also call
|
|
94
|
+
the `agent-device-proxy` API directly.
|
|
95
|
+
|
|
96
|
+
In your workflow/agent worker:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { createAgentDeviceProxyClient } from "agent-device-proxy";
|
|
100
|
+
import { ensureMetroRuntime } from "agent-device-proxy/metro-runtime";
|
|
101
|
+
|
|
102
|
+
const agentDeviceProxy = createAgentDeviceProxyClient({
|
|
103
|
+
baseUrl: "http://<remote-mac-ip>:9123",
|
|
104
|
+
bearerToken: process.env.AGENT_PROXY_BEARER_TOKEN,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const metro = await ensureMetroRuntime({
|
|
108
|
+
projectRoot: "/workspace/RNCLI83",
|
|
109
|
+
port: 8081,
|
|
110
|
+
publicHost: process.env.METRO_PUBLIC_HOST, // must be reachable from remote mac
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
// iOS: pass ios_runtime so remote mac can connect to Metro
|
|
115
|
+
await agentDeviceProxy.installApp({
|
|
116
|
+
app: "RNCLI83",
|
|
117
|
+
filePath: "/workspace/ios-build/RNCLI83.app",
|
|
118
|
+
platform: "ios",
|
|
119
|
+
device: "iPhone 17 Pro",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await agentDeviceProxy.agentDeviceExec({
|
|
123
|
+
argv: ["open", "RNCLI83", "--platform", "ios", "--device", "iPhone 17 Pro"],
|
|
124
|
+
ios_runtime: metro.ios_runtime,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Android: no Metro hint needed
|
|
128
|
+
await agentDeviceProxy.installApp({
|
|
129
|
+
app: "com.rncli83",
|
|
130
|
+
filePath: "/workspace/android/app/build/outputs/apk/debug/app-debug.apk",
|
|
131
|
+
platform: "android",
|
|
132
|
+
serial: "emulator-5554",
|
|
133
|
+
});
|
|
134
|
+
} finally {
|
|
135
|
+
await metro.stop();
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Networking rule
|
|
140
|
+
|
|
141
|
+
`metro.publicHost:metro.port` must be reachable from remote macOS.
|
|
142
|
+
If not reachable directly, use a tunnel and pass that tunnel endpoint in `ios_runtime`.
|
|
143
|
+
|
|
144
|
+
### Migration note
|
|
145
|
+
|
|
146
|
+
Older docs may reference `host-agent`, `@platform/host-agent`, or `HOST_AGENT_*` env names.
|
|
147
|
+
Use `agent-device-proxy`, `agent-device-proxy/*`, and `AGENT_PROXY_*` env names instead.
|
|
148
|
+
|
|
149
|
+
## Client
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
import { createAgentDeviceProxyClient } from "agent-device-proxy";
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Proxy-backed `agent-device` contract
|
|
156
|
+
|
|
157
|
+
The package also exposes a proxy-backed `agent-device` contract for Linux sandboxes:
|
|
158
|
+
|
|
159
|
+
- install the package in the sandbox
|
|
160
|
+
- run `npx agent-device ...`
|
|
161
|
+
- the command is forwarded to the remote macOS `agent-device-proxy` host
|
|
162
|
+
|
|
163
|
+
For Node consumers, use:
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
import {
|
|
167
|
+
createAgentDeviceProxyClient,
|
|
168
|
+
ensureMetroRuntime,
|
|
169
|
+
runAgentDeviceCommand,
|
|
170
|
+
} from "agent-device-proxy";
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`runAgentDeviceCommand()` translates `agent-device install` / `reinstall` into
|
|
174
|
+
artifact upload + remote install, and forwards the other commands through
|
|
175
|
+
`agentDeviceExec()`. If `AGENT_PROXY_METRO_PROJECT_ROOT` is set, the `agent-device`
|
|
176
|
+
bin will automatically attach Metro hints for iOS commands that require them.
|
|
177
|
+
|
|
178
|
+
## Embedded server
|
|
179
|
+
|
|
180
|
+
```js
|
|
181
|
+
import { startAgentDeviceProxyServer } from "agent-device-proxy/server";
|
|
182
|
+
|
|
183
|
+
startAgentDeviceProxyServer();
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Metro runtime helper (Linux/Vercel)
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
import { ensureMetroRuntime } from "agent-device-proxy/metro-runtime";
|
|
190
|
+
|
|
191
|
+
const metro = await ensureMetroRuntime({
|
|
192
|
+
projectRoot: "/workspace/RNCLI83",
|
|
193
|
+
publicHost: "10.0.0.10"
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// use metro.ios_runtime when opening iOS app through agent-device-proxy
|
|
197
|
+
// await agentDeviceProxy.agentDeviceExec({ argv: [...], ios_runtime: metro.ios_runtime })
|
|
198
|
+
|
|
199
|
+
await metro.stop();
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Public API reference: [docs/AGENT_PROXY_API.md](../../docs/AGENT_PROXY_API.md)
|
package/dist/src/111.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ensureMetroRuntime as t}from"./403.js";let e=new Set(["open","reopen","reload","push","trigger-app-event"]);async function r(r){if(!Array.isArray(r.argv)||0===r.argv.length)throw Error("argv must contain at least one agent-device command");let n=r.argv[0];if("install"===n||"reinstall"===n)return await i(r,"reinstall"===n);let l=null;try{var u;return function(t,r){if(!r)return!1;let i=function(t,e){let r=t.indexOf(e);if(-1===r)return;let i=t[r+1];if(!(!i||i.startsWith("--")))return i}(t,"--platform");return("ios"===i||"apple"===i)&&e.has(t[0]||"")}(r.argv,r.metro)&&(l=await t(r.metro)),u=await r.client.agentDeviceExec({argv:r.argv,...r.cwd?{cwd:r.cwd}:{},...l?{ios_runtime:l.ios_runtime}:{}}),o(u)?{exitCode:a(u.exit_code)??0,stdout:s(u.stdout)??JSON.stringify(u),stderr:s(u.stderr)??"",output:u}:{exitCode:0,stdout:JSON.stringify(u),stderr:"",output:u}}finally{await l?.stop()}}async function i(t,e){let r=function(t){let e=t.slice(1),r=[],i=new Map;for(let t=0;t<e.length;t+=1){let n=e[t];if(!n.startsWith("--")){r.push(n);continue}let a=n.slice(2),s=e[t+1];if(s&&!s.startsWith("--")){i.set(a,s),t+=1;continue}i.set(a,!0)}if(r.length<2)throw Error("agent-device install requires <app> and <path-to-binary>");let[a,s]=r,o={app:a,filePath:s,platform:function(t,e){let r=t.get(e);if("string"!=typeof r||!r.trim())throw Error(`agent-device install requires --${e}`);return r}(i,"platform")},l=n(i,"device"),u=n(i,"session"),f=n(i,"udid"),c=n(i,"serial");return l&&(o.device=l),u&&(o.session=u),f&&(o.udid=f),c&&(o.serial=c),!0===i.get("json")&&(o.json=!0),o}(t.argv),i=await t.client.installApp({...r,...e?{reinstall:!0}:{}}),l=o(i)&&o(i.install)?i.install:{},u=a(l.exit_code)??0;return{exitCode:u,stdout:s(l.stdout)??JSON.stringify(i),stderr:s(l.stderr)??"",output:i}}function n(t,e){let r=t.get(e);return"string"==typeof r&&r.trim()?r:void 0}function a(t){if("number"==typeof t&&Number.isInteger(t))return t}function s(t){return"string"==typeof t?t:void 0}function o(t){return!!t&&"object"==typeof t&&!Array.isArray(t)}export{r as runAgentDeviceCommand};
|
package/dist/src/224.js
ADDED
|
@@ -0,0 +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};
|
package/dist/src/36.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import t from"node:http";import{node_path as e,mkdirSync as r,promises as i,randomUUID as a,createHash as n,existsSync as o,createWriteStream as s}from"./402.js";import{spawnSync as u,spawn as d}from"./493.js";function l(t,e,r){let i=process.env[t];if(void 0===i||""===i)return e;let a=Number.parseInt(i,10);if(!Number.isInteger(a)||a<r.min||a>r.max)throw Error(`${t} must be an integer in range ${r.min}-${r.max}`);return a}class c extends Error{statusCode;code;details;constructor(t,e,r,i=null){super(r),this.statusCode=t,this.code=e,this.details=i}}async function f(t,e,r){let i=n("sha256"),a=s(e,{flags:"wx"}),o=0;try{for await(let e of t){let t=Buffer.isBuffer(e)?e:Buffer.from(e);if((o+=t.length)>r)throw new c(413,"invalid_request","request body too large");i.update(t),a.write(t)||await v(a)}await y(a)}catch(t){if(a.destroy(),await E(e),t instanceof c)throw t;throw new c(400,"invalid_request","invalid request body")}if(o<=0)throw await E(e),new c(400,"invalid_request","artifact body must not be empty");return{sizeBytes:o,sha256:i.digest("hex")}}async function _({archivePath:t,timeoutMs:r,runCommand:i,env:a}){let n=await i({command:"unzip",argv:["-Z1",t],cwd:e.dirname(t),env:a,timeoutMs:r,maxOutputBytes:2097152});if(0!==n.exitCode)throw new c(400,"invalid_request","invalid zip archive",{exit_code:n.exitCode,stdout:n.stdout,stderr:n.stderr});if(n.stdoutTruncated)throw new c(400,"invalid_request","zip entry listing is too large to validate safely");let o=n.stdout.split(/\r?\n/).map(t=>t.trim()).filter(Boolean);if(0===o.length)throw new c(400,"invalid_request","zip archive is empty");let s=!1;for(let t of o){let r=t.replaceAll("\\","/"),i=e.posix.normalize(r);if(r.startsWith("/")||/^[A-Za-z]:\//.test(r))throw new c(400,"invalid_request","zip archive contains absolute paths");if(".."===i||i.startsWith("../")||i.includes("/../")||i.includes("\0"))throw new c(400,"invalid_request","zip archive contains unsafe paths");i.split("/").some(t=>t.toLowerCase().endsWith(".app"))&&(s=!0)}if(!s)throw new c(400,"invalid_request","zip archive does not contain a .app bundle")}async function p({archivePath:t,timeoutMs:r,runCommand:i,env:a,maxOutputBytes:n}){let o=await i({command:"unzip",argv:["-Z","-t",t],cwd:e.dirname(t),env:a,timeoutMs:r,maxOutputBytes:n});if(0!==o.exitCode)throw new c(400,"invalid_request","invalid zip archive",{exit_code:o.exitCode,stdout:o.stdout,stderr:o.stderr});if(o.stdoutTruncated)throw new c(400,"invalid_request","zip archive summary is too large to validate safely");let s=o.stdout.match(/([\d,]+)\s+bytes\s+uncompressed/i);if(!s)throw new c(400,"invalid_request","zip archive summary is invalid");let u=Number.parseInt(s[1].replaceAll(",",""),10);if(!Number.isFinite(u)||u<0)throw new c(400,"invalid_request","zip archive summary is invalid");return u}async function m(t){let r=[t];for(;r.length>0;){let t=r.shift();if(!t)break;for(let a of(await i.readdir(t,{withFileTypes:!0}))){if(!a.isDirectory())continue;let i=e.join(t,a.name);if(a.name.endsWith(".app"))return i;r.push(i)}}return""}async function h(t){let r=[t],a=0;for(;r.length>0;){let t=r.shift();if(!t)break;let n=await i.lstat(t);if(n.isDirectory()){for(let a of(await i.readdir(t,{withFileTypes:!0})))r.push(e.join(t,a.name));continue}n.isFile()&&(a+=n.size)}return a}async function w(t){await i.mkdir(t.artifactsDir,{recursive:!0});let r=Date.now()-36e5*t.artifactTtlHours,a=await i.readdir(t.artifactsDir,{withFileTypes:!0}),n=[];for(let s of a){if(!s.isDirectory())continue;let a=e.join(t.artifactsDir,s.name),u=e.join(a,"metadata.json");if(!o(u)){await g(a);continue}let d=null;try{d=JSON.parse(await i.readFile(u,"utf8"))}catch{await g(a);continue}let l=Date.parse(String(d?.created_at||""));if(!Number.isFinite(l)||l<r){await g(a);continue}let c=Number(d?.size_bytes)||0;n.push({artifactDir:a,createdAtMs:l,sizeBytes:c})}n.sort((t,e)=>t.createdAtMs-e.createdAtMs);let s=n.reduce((t,e)=>t+e.sizeBytes,0);for(let e of n){if(s<=t.artifactMaxTotalBytes)break;await g(e.artifactDir),s-=e.sizeBytes}}async function v(t){await new Promise((e,r)=>{let i=()=>{n(),e()},a=t=>{n(),r(t)},n=()=>{t.off("drain",i),t.off("error",a)};t.on("drain",i),t.on("error",a)})}async function y(t){await new Promise((e,r)=>{let i=t=>{a(),r(t)},a=()=>{t.off("error",i)};t.end(t=>{if(t){a(),r(t);return}a(),e()}),t.on("error",i)})}async function E(t){try{await i.unlink(t)}catch{}}async function g(t){try{await i.rm(t,{recursive:!0,force:!0})}catch{}}async function b({command:t,argv:e,cwd:r,env:i,timeoutMs:a,maxOutputBytes:n}){return await new Promise(o=>{let s=d(t,e,{cwd:r,env:i,stdio:["ignore","pipe","pipe"]}),u="",l="",c=0,f=0,_=!1,p=!1,m=!1,h=!1,w=setTimeout(()=>{m=!0,s.kill("SIGKILL")},a);function v(t,e){let r=e.toString("utf8"),i=Buffer.byteLength(r);if("stdout"===t){if(c>=n){_=!0;return}let t=n-c,e=i>t?T(r,t):r;u+=e,c+=Buffer.byteLength(e),i>t&&(_=!0);return}if(f>=n){p=!0;return}let a=n-f,o=i>a?T(r,a):r;l+=o,f+=Buffer.byteLength(o),i>a&&(p=!0)}function y(t){h||(h=!0,clearTimeout(w),_&&(u=`${u}
|
|
2
|
+
[output truncated]`.trim()),p&&(l=`${l}
|
|
3
|
+
[output truncated]`.trim()),o(t))}s.stdout.on("data",t=>{v("stdout",t)}),s.stderr.on("data",t=>{v("stderr",t)}),s.on("close",(t,e)=>{y({exitCode:m?124:"number"==typeof t?t:1,stdout:u,stderr:m?`${l}
|
|
4
|
+
proxy timeout after ${a}ms`.trim():l,signal:e??"",timedOut:m,stdoutTruncated:_,stderrTruncated:p})}),s.on("error",t=>{y({exitCode:1,stdout:u,stderr:`${l}
|
|
5
|
+
${t.message}`.trim(),signal:"",timedOut:m,stdoutTruncated:_,stderrTruncated:p})})})}function A({result:t,action:e,strictCommandStatus:r}){if(r&&0!==t.exitCode)throw new c(502,"command_failed",`${e} failed`,{exit_code:t.exitCode,timed_out:t.timedOut,signal:t.signal,stdout:t.stdout,stderr:t.stderr})}function T(t,e){return e<=0?"":Buffer.from(t,"utf8").subarray(0,e).toString("utf8")}let O=new Map([["/v1/agent-device/exec","/api/agent-device/exec"],["/v1/agent-device/install","/api/agent-device/install"],["/v1/artifacts/upload","/api/artifacts/upload"],["/v1/metro/resolve","/api/metro/resolve"],["/v1/metro/probe","/api/metro/probe"]]);function x(t,e,r,i,a){a?S(t,e,r,i):S(t,e,{ok:!0,data:r,request_id:i},i)}function R(t,e,r,i,a,n,o=null){n?S(t,e,{error:i},a):S(t,e,{ok:!1,error:{code:r,message:i,...o?{details:o}:{}},request_id:a},a)}function S(t,e,r,i){t.statusCode=e,t.setHeader("content-type","application/json; charset=utf-8"),t.setHeader("x-request-id",i),t.end(JSON.stringify(r))}async function N(t,e){let r=(await D(t,e)).toString("utf8");if(!r.trim())throw new c(400,"invalid_request","body must be valid JSON");try{let t=JSON.parse(r);if(!t||"object"!=typeof t||Array.isArray(t))throw new c(400,"invalid_request","body must be valid JSON object");return t}catch{throw new c(400,"invalid_request","body must be valid JSON")}}async function D(t,e){let r=[],i=0;try{for await(let a of t){let t=Buffer.isBuffer(a)?a:Buffer.from(a);if((i+=t.length)>e)throw new c(413,"invalid_request","request body too large");r.push(t)}}catch(t){if(t instanceof c)throw t;throw new c(400,"invalid_request","invalid request body")}return Buffer.concat(r)}function C(t){if(null==t||""===t)return null;if(!(t&&"object"==typeof t&&!Array.isArray(t)))throw new c(400,"invalid_request","ios_runtime must be an object");let e=I(t.launch_url,"ios_runtime.launch_url",null),r=I(t.metro_bundle_url,"ios_runtime.metro_bundle_url",new Set(["http:","https:"])),i=function(t,e){let r=B(t,e);if(!r)return null;if(r.includes(":"))throw new c(400,"invalid_request",`${e} must not include a port`);if(!/^[A-Za-z0-9._-]{1,253}$/.test(r))throw new c(400,"invalid_request",`${e} must contain only host-safe characters`);return r}(t.metro_host,"ios_runtime.metro_host"),a=function(t,e){if(null==t||""===t)return null;let r=Number.parseInt(String(t),10);if(!Number.isInteger(r)||r<1||r>65535)throw new c(400,"invalid_request",`${e} must be an integer in range 1..65535`);return r}(t.metro_port,"ios_runtime.metro_port");return e||r||i||a?{launch_url:e||"",metro_bundle_url:r||"",metro_host:i||"",metro_port:a||0}:null}function q(t){if(!t)return null;if(t.metro_bundle_url){let e=new URL(t.metro_bundle_url);return{bundle_url:e.toString(),host:e.hostname,port:function(t){if(t.port){let e=Number.parseInt(t.port,10);if(Number.isInteger(e)&&e>=1&&e<=65535)return e}return"https:"===t.protocol?443:80}(e),status_url:M(e)}}if(t.metro_host&&t.metro_port){let e=new URL(`http://${t.metro_host}:${t.metro_port}/index.bundle?platform=ios&dev=true&minify=false`);return{bundle_url:e.toString(),host:e.hostname,port:Number.parseInt(e.port,10),status_url:M(e)}}return null}async function $({statusUrl:t,timeoutMs:e}){let r=Date.now();try{let i=await fetch(t,{signal:AbortSignal.timeout(e)});return{reachable:i.ok,status_code:i.status,latency_ms:Date.now()-r,detail:i.ok?"ok":`HTTP ${i.status}`}}catch(t){return{reachable:!1,status_code:0,latency_ms:Date.now()-r,detail:t instanceof Error?t.message:String(t)}}}function I(t,e,r){let i,a=B(t,e);if(!a)return null;try{i=new URL(a)}catch{throw new c(400,"invalid_request",`${e} must be an absolute URL`)}if(r&&!r.has(i.protocol))throw new c(400,"invalid_request",`${e} must use ${Array.from(r).join(" or ")}`);return i.toString()}function B(t,e){if(null==t||""===t)return"";if("string"!=typeof t)throw new c(400,"invalid_request",`${e} must be a string`);return t.trim()}function M(t){let e=new URL(t.toString()),r=e.pathname.endsWith("/")&&e.pathname.length>1?e.pathname.slice(0,-1):e.pathname,i=r;if(r.endsWith("/index.bundle"))i=r.slice(0,-13);else if(r.endsWith(".bundle")){let t=r.lastIndexOf("/");i=t>=0?r.slice(0,t):""}return e.pathname=`${i||""}/status`.replace(/\/{2,}/g,"/"),e.search="",e.hash="",e.toString()}function P(t){return!!t&&"object"==typeof t&&!Array.isArray(t)}function z(t,e){let r=j(t,e);if(!r)throw new c(400,"invalid_request",`${e} is required`);return r}function L(t,e,r){let i=z(t,e).toLowerCase();if(!r.includes(i))throw new c(400,"invalid_request",`${e} must be one of: ${r.join(", ")}`);return i}function j(t,e){if(null==t||""===t)return"";if("string"!=typeof t)throw new c(400,"invalid_request",`${e} must be a string`);return t.trim()}function G(t,e,r){if(null==t||""===t)return r;if("boolean"!=typeof t)throw new c(400,"invalid_request",`${e} must be a boolean`);return t}function U(t,e){let r=t[e];return"string"==typeof r?r.trim():Array.isArray(r)&&r.length>0?String(r[0]).trim():""}function k(t={}){return Y(t).server}function X(t={}){let{server:e,config:r,artifactStore:i}=Y(t);return function(t){let e=[t,"zip","unzip"],r=e.filter(t=>{var e,r;return!("string"==typeof(e=t)&&e.trim()&&0===u("sh",["-lc",`command -v ${(r=e,`'${r.replaceAll("'","'\\''")}'`)} >/dev/null 2>&1`],{stdio:"ignore"}).status)});if(r.length>0)throw Error(`agent-device-proxy missing required commands in PATH: ${r.join(", ")}`);process.stdout.write(`agent-device-proxy dependency preflight ok: ${e.join(", ")}
|
|
6
|
+
`)}(r.command),e.listen(r.port,r.host,()=>{process.stdout.write(`agent-device-proxy listening on http://${r.host}:${r.port}
|
|
7
|
+
`),process.stdout.write(`api health endpoint: http://${r.host}:${r.port}/api/health
|
|
8
|
+
`),process.stdout.write(`api exec endpoint: http://${r.host}:${r.port}/api/agent-device/exec
|
|
9
|
+
`),process.stdout.write(`api install endpoint: http://${r.host}:${r.port}/api/agent-device/install
|
|
10
|
+
`),process.stdout.write(`api artifacts endpoint: http://${r.host}:${r.port}/api/artifacts/upload
|
|
11
|
+
`),process.stdout.write(`api metro resolve endpoint: http://${r.host}:${r.port}/api/metro/resolve
|
|
12
|
+
`),process.stdout.write(`api metro probe endpoint: http://${r.host}:${r.port}/api/metro/probe
|
|
13
|
+
`),process.stdout.write(`agent-device command: ${r.command}
|
|
14
|
+
`),process.stdout.write(`root dir: ${r.rootDir}
|
|
15
|
+
`),process.stdout.write(`artifacts dir: ${r.artifactsDir}
|
|
16
|
+
`),r.workspaceRoot&&process.stdout.write(`workspace root constraint: ${r.workspaceRoot}
|
|
17
|
+
`)}),i.maybeCleanupArtifacts().catch(t=>{process.stderr.write(`agent-device-proxy artifact cleanup warning: ${t instanceof Error?t.message:String(t)}
|
|
18
|
+
`)}),e}function Y(n={}){let s,u,d,v,y,E,T,S,D,I,B,M,k,X,V,F,J,W=n.config||(s=process.env.AGENT_DEVICE_PROXY_HOST??"127.0.0.1",u=l("AGENT_DEVICE_PROXY_PORT",9123,{min:1,max:65535}),d=(process.env.AGENT_DEVICE_PROXY_DAEMON_BASE_URL??"").trim(),v=(process.env.AGENT_DEVICE_PROXY_DAEMON_AUTH_TOKEN??"").trim(),y=l("AGENT_DEVICE_PROXY_MAX_BODY_BYTES",1048576,{min:1024,max:0x1000000}),E=l("AGENT_PROXY_ARTIFACT_MAX_BYTES",0x20000000,{min:1048576,max:0xffffffff}),T=l("AGENT_DEVICE_PROXY_EXEC_TIMEOUT_MS",24e4,{min:1e3,max:36e5}),S=l("AGENT_PROXY_COMMAND_MAX_OUTPUT_BYTES",1048576,{min:4096,max:0x4000000}),D=l("AGENT_PROXY_ARTIFACT_UNZIP_TIMEOUT_MS",18e4,{min:1e3,max:18e5}),I=l("AGENT_PROXY_ARTIFACT_TTL_HOURS",24,{min:1,max:720}),B=l("AGENT_PROXY_ARTIFACT_MAX_TOTAL_BYTES",0x140000000,{min:1048576,max:0x1900000000}),M=function(t,e){let r=process.env[t];if(void 0===r||""===r)return e;let i=r.trim().toLowerCase();if("1"===i||"true"===i||"yes"===i||"on"===i)return!0;if("0"===i||"false"===i||"no"===i||"off"===i)return!1;throw Error(`${t} must be a boolean-like value`)}("AGENT_PROXY_STRICT_COMMAND_STATUS",!0),k=(process.env.AGENT_DEVICE_PROXY_BEARER_TOKEN??"").trim(),X=(process.env.AGENT_DEVICE_PROXY_COMMAND??"agent-device").trim()||"agent-device",V=e.resolve(process.env.AGENT_PROXY_ROOT_DIR??process.cwd()),F=function(t){let r=(t??"").trim();if(!r)return"";if(!e.isAbsolute(r))throw Error("AGENT_DEVICE_PROXY_WORKSPACE_ROOT must be absolute path");return e.resolve(r)}(process.env.AGENT_DEVICE_PROXY_WORKSPACE_ROOT??""),J=e.resolve(process.env.AGENT_PROXY_ARTIFACTS_DIR??e.join(V,".agent-device-proxy-artifacts")),{host:s,port:u,daemonBaseUrl:d,daemonAuthToken:v,maxJsonBodyBytes:y,maxArtifactBytes:E,commandTimeoutMs:T,commandMaxOutputBytes:S,unzipTimeoutMs:D,artifactTtlHours:I,artifactMaxTotalBytes:B,strictCommandStatus:M,bearerToken:k,command:X,rootDir:V,workspaceRoot:F,artifactsDir:J,baseCommandEnv:{PATH:function(t,r){let i=[e.join(r,"Library","Android","sdk","platform-tools"),e.join(r,"Library","Android","sdk","emulator"),"/opt/homebrew/bin","/usr/local/bin","/usr/bin","/bin"],a=new Set((t||"").split(e.delimiter).filter(Boolean));for(let t of i)o(t)&&a.add(t);return Array.from(a).join(e.delimiter)}(process.env.PATH||"",process.env.HOME||process.env.USERPROFILE||""),HOME:process.env.HOME||"",USER:process.env.USER||"",TMPDIR:process.env.TMPDIR||"/tmp",LANG:process.env.LANG||"en_US.UTF-8",LC_ALL:process.env.LC_ALL||"",LC_CTYPE:process.env.LC_CTYPE||"",SHELL:process.env.SHELL||"",TERM:process.env.TERM||"",ANDROID_HOME:process.env.ANDROID_HOME||"",ANDROID_SDK_ROOT:process.env.ANDROID_SDK_ROOT||"",JAVA_HOME:process.env.JAVA_HOME||"",XDG_RUNTIME_DIR:process.env.XDG_RUNTIME_DIR||""}});r(W.artifactsDir,{recursive:!0});let K=function(t,{runCommand:r}){let n=null,s=0;async function u(){let e=Date.now();if(n)return await n;e-s<6e4||(n=w(t).finally(()=>{s=Date.now(),n=null}),await n)}return{storeArtifactFromRequest:async function({req:n,headers:o}){let s;await u();let d=a(),l=e.join(t.artifactsDir,d),w=e.join(l,o.filename);await i.mkdir(l,{recursive:!0});try{s=await f(n,w,t.maxArtifactBytes)}catch(t){throw await g(l),t}if(o.expected_sha256&&o.expected_sha256!==s.sha256)throw await g(l),new c(400,"invalid_request","artifact sha256 mismatch");let v=w,y=!1,E=s.sizeBytes;if("app"===o.artifact_type){let a=e.join(l,"unpacked");await i.mkdir(a,{recursive:!0}),await _({archivePath:w,timeoutMs:t.unzipTimeoutMs,runCommand:r,env:t.baseCommandEnv});let n=await p({archivePath:w,timeoutMs:t.unzipTimeoutMs,runCommand:r,env:t.baseCommandEnv,maxOutputBytes:t.commandMaxOutputBytes});if(n>t.maxArtifactBytes)throw await g(l),new c(413,"invalid_request","unzipped app archive too large",{uncompressed_bytes:n,max_bytes:t.maxArtifactBytes});let o=await r({command:"unzip",argv:["-q",w,"-d",a],cwd:l,env:t.baseCommandEnv,timeoutMs:t.unzipTimeoutMs,maxOutputBytes:t.commandMaxOutputBytes});if(0!==o.exitCode)throw await g(l),new c(400,"invalid_request","failed to unzip app archive",{exit_code:o.exitCode,stdout:o.stdout,stderr:o.stderr});let u=await m(a);if(!u)throw await g(l),new c(400,"invalid_request","zip archive does not contain a .app bundle");let d=await h(a);if(d>t.maxArtifactBytes)throw await g(l),new c(413,"invalid_request","unzipped app archive too large",{uncompressed_bytes:d,max_bytes:t.maxArtifactBytes});E=s.sizeBytes+d,v=u,y=!0}let b={artifact_id:d,artifact_type:o.artifact_type,archive:o.archive,filename:o.filename,size_bytes:E,sha256:s.sha256,created_at:new Date().toISOString(),raw_path:w,ready_path:v,extracted:y};return await i.writeFile(e.join(l,"metadata.json"),JSON.stringify(b,null,2),"utf8"),b},readArtifactMetadata:async function(r){let a,n=String(r||"").trim();if(!/^[a-z0-9-]{8,128}$/i.test(n))throw new c(400,"invalid_request","artifact_id format is invalid");let s=e.join(t.artifactsDir,n,"metadata.json");if(!o(s))throw new c(404,"not_found","artifact_id not found");try{a=JSON.parse(await i.readFile(s,"utf8"))}catch{throw new c(500,"internal_error","artifact metadata is unreadable")}if(!a||"object"!=typeof a||!a.ready_path||!a.artifact_type)throw new c(500,"internal_error","artifact metadata is invalid");if(!o(a.ready_path))throw new c(404,"not_found","artifact file is missing");return a},maybeCleanupArtifacts:u}}(W,{runCommand:b});return{server:t.createServer(async(t,r)=>{var i,n;let s,u,d,l="string"==typeof(s=t.headers["x-request-id"])&&s.trim()?s.trim().slice(0,128):a(),{path:f,legacy:_}=(d=(u=new URL(t.url||"/","http://127.0.0.1")).pathname.startsWith("/v1/"),{path:O.get(u.pathname)||u.pathname,legacy:d});try{if("GET"===t.method&&"/agent-device/health"===f)return void await H({req:t,res:r,requestId:l,config:W,endpoint:"health"});if("GET"===t.method&&("/api/health"===f||"/healthz"===f)){let t={status:"ok",service:"agent-device-proxy",checked_at:new Date().toISOString()};if("/healthz"===f){r.statusCode=200,r.setHeader("content-type","application/json; charset=utf-8"),r.setHeader("x-request-id",l),r.end(JSON.stringify(t));return}x(r,200,t,l,_);return}if("POST"!==t.method)return void R(r,404,"not_found","not found",l,_);if((i=W.bearerToken)&&(t.headers.authorization||"")!==`Bearer ${i}`)return void R(r,401,"unauthorized","unauthorized",l,_);if("/agent-device/rpc"===f)return void await H({req:t,res:r,requestId:l,config:W,endpoint:"rpc"});if("/api/artifacts/upload"===f){let e=function(t){let e=L(U(t,"x-artifact-type"),"x-artifact-type",["app","apk"]),r=L(U(t,"x-artifact-archive"),"x-artifact-archive",["zip","raw"]),i=(U(t,"x-artifact-filename")||`${e}-${Date.now()}${"zip"===r?".zip":".bin"}`).replace(/[^a-zA-Z0-9._-]/g,"_").slice(-160)||`artifact-${Date.now()}`,a=U(t,"x-artifact-sha256").toLowerCase();if(a&&!/^[a-f0-9]{64}$/.test(a))throw new c(400,"invalid_request","x-artifact-sha256 must be a 64-char lowercase hex sha256");if("app"===e&&"zip"!==r)throw new c(400,"invalid_request","x-artifact-type=app requires x-artifact-archive=zip");if("apk"===e&&"raw"!==r)throw new c(400,"invalid_request","x-artifact-type=apk requires x-artifact-archive=raw");return{artifact_type:e,archive:r,filename:i,expected_sha256:a}}(t.headers),i=await K.storeArtifactFromRequest({req:t,headers:e});x(r,200,i,l,_);return}if("/api/metro/resolve"===f){let e=await N(t,W.maxJsonBodyBytes),i=C(e?.ios_runtime??e),a=q(i);if(!a)throw new c(400,"invalid_metro_runtime","ios_runtime metro hints are required (metro_bundle_url or metro_host+metro_port)");x(r,200,a,l,_);return}if("/api/metro/probe"===f){let e=await N(t,W.maxJsonBodyBytes),i=C(e?.ios_runtime??e),a=q(i);if(!a)throw new c(400,"invalid_metro_runtime","ios_runtime metro hints are required (metro_bundle_url or metro_host+metro_port)");let n=function(t){if(null==t||""===t)return 2500;let e=Number.parseInt(String(t),10);return!Number.isInteger(e)||e<100||e>6e4?2500:e}(e?.timeout_ms),o=await $({statusUrl:a.status_url,timeoutMs:n});x(r,200,{...a,...o},l,_);return}if("/api/agent-device/exec"===f){let i,s=await N(t,W.maxJsonBodyBytes),u=function(t){if(!P(t))throw new c(400,"invalid_request","body must be valid JSON object");if(!Array.isArray(t.argv)||t.argv.some(t=>"string"!=typeof t))throw new c(400,"invalid_request","argv must be an array of strings");if(0===t.argv.length)throw new c(400,"invalid_request","argv must contain at least one argument");let e={argv:t.argv,cwd:j(t.cwd,"cwd"),run_id:j(t.run_id,"run_id"),ios_session_id:j(t.ios_session_id,"ios_session_id"),tenant_id:j(t.tenant_id,"tenant_id"),ios_runtime:function(t,e){if(null==t||""===t)return null;if(!t||"object"!=typeof t||Array.isArray(t))throw new c(400,"invalid_request",`${e} must be an object`);return t}(t.ios_runtime,"ios_runtime")};if(e.tenant_id&&!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(e.tenant_id))throw new c(400,"invalid_request","tenant_id must match ^[a-z0-9][a-z0-9_-]{0,63}$");return e}(s),d={...u,ios_runtime:C(u.ios_runtime)},f=await b({command:W.command,argv:d.argv,cwd:function(t,r){if(!t)return r.rootDir;if(!e.isAbsolute(t))throw new c(400,"invalid_request","cwd must be absolute path");let i=e.resolve(t);if(r.workspaceRoot&&i!==r.workspaceRoot&&!i.startsWith(`${r.workspaceRoot}${e.sep}`))throw new c(400,"invalid_request","cwd is outside configured workspace root");if(!o(i))throw new c(400,"invalid_request","cwd does not exist");return i}(d.cwd,W),env:(n=W.baseCommandEnv,i={...n,AGENT_PROXY_REQUEST_ID:a()},d.tenant_id&&(i.AGENT_DEVICE_PROXY_TENANT_ID=d.tenant_id),d.run_id&&(i.AGENT_DEVICE_PROXY_RUN_ID=d.run_id),d.ios_session_id&&(i.AGENT_DEVICE_PROXY_IOS_SESSION_ID=d.ios_session_id),d.ios_runtime&&(d.ios_runtime.launch_url&&(i.AGENT_DEVICE_PROXY_IOS_LAUNCH_URL=d.ios_runtime.launch_url),d.ios_runtime.metro_bundle_url&&(i.AGENT_DEVICE_PROXY_METRO_BUNDLE_URL=d.ios_runtime.metro_bundle_url),d.ios_runtime.metro_host&&(i.AGENT_DEVICE_PROXY_METRO_HOST=d.ios_runtime.metro_host),d.ios_runtime.metro_port&&(i.AGENT_DEVICE_PROXY_METRO_PORT=String(d.ios_runtime.metro_port))),i),timeoutMs:W.commandTimeoutMs,maxOutputBytes:W.commandMaxOutputBytes});A({result:f,action:`agent-device exec (${d.argv[0]||"command"})`,strictCommandStatus:W.strictCommandStatus}),x(r,200,{exit_code:f.exitCode,stdout:f.stdout,stderr:f.stderr,timed_out:f.timedOut},l,_);return}if("/api/agent-device/install"===f){let e=await N(t,W.maxJsonBodyBytes),i=function(t){if(!P(t))throw new c(400,"invalid_request","body must be valid JSON object");let e=z(t.app,"app"),r=z(t.artifact_id,"artifact_id"),i=L(t.platform,"platform",["ios","android","apple"]),a=j(t.device,"device"),n=j(t.session,"session"),o=j(t.udid,"udid"),s=j(t.serial,"serial"),u=G(t.reinstall,"reinstall",!1);return{app:e,artifact_id:r,platform:i,device:a,session:n,udid:o,serial:s,reinstall:u,json:G(t.json,"json",!0)}}(e),n=await K.readArtifactMetadata(i.artifact_id),o=function(t,e){if("android"===t.platform&&"apk"!==e.artifact_type)throw new c(400,"invalid_request","android install requires artifact_type=apk");if(("ios"===t.platform||"apple"===t.platform)&&"app"!==e.artifact_type)throw new c(400,"invalid_request","ios install requires artifact_type=app");let r=[t.reinstall?"reinstall":"install",t.app,e.ready_path,"--platform",t.platform];return t.device&&r.push("--device",t.device),t.session&&r.push("--session",t.session),t.udid&&r.push("--udid",t.udid),t.serial&&r.push("--serial",t.serial),t.json&&r.push("--json"),r}(i,n),s=await b({command:W.command,argv:o,cwd:W.rootDir,env:{...W.baseCommandEnv,AGENT_PROXY_REQUEST_ID:a(),AGENT_PROXY_ARTIFACT_ID:i.artifact_id},timeoutMs:W.commandTimeoutMs,maxOutputBytes:W.commandMaxOutputBytes});A({result:s,action:"agent-device install",strictCommandStatus:W.strictCommandStatus}),x(r,200,{artifact_id:i.artifact_id,artifact_type:n.artifact_type,artifact_path:n.ready_path,exit_code:s.exitCode,stdout:s.stdout,stderr:s.stderr,output:function(t){try{return JSON.parse(t)}catch{return null}}(s.stdout)},l,_);return}R(r,404,"not_found","not found",l,_)}catch(e){let t;R(r,(t=function(t,e="internal error"){return t instanceof c?t:new c(500,"internal_error",t instanceof Error?t.message:e)}(e)).statusCode,t.code,t.message,l,_,t.details)}}),config:W,artifactStore:K}}async function H(t){var e,r;let{req:i,res:a,requestId:n,config:o,endpoint:s}=t;if(!o.daemonBaseUrl)throw new c(503,"daemon_unavailable","AGENT_DEVICE_PROXY_DAEMON_BASE_URL is required for daemon forwarding");let u=new URL(s,`${o.daemonBaseUrl.replace(/\/+$/,"")}/`),d=new Headers,l=i.headers["content-type"];"string"==typeof l&&l.trim()&&d.set("content-type",l);let f=o.daemonAuthToken||("string"!=typeof(e=i.headers.authorization)?"":e.startsWith("Bearer ")?e.slice(7):"")||("string"==typeof(r=i.headers["x-agent-device-token"])?r:"");f&&(d.set("authorization",`Bearer ${f}`),d.set("x-agent-device-token",f));let _="rpc"===s?new Uint8Array(await D(i,o.maxJsonBodyBytes)):void 0,p=await fetch(u,{method:i.method||("health"===s?"GET":"POST"),headers:d,..._?{body:_}:{}}),m=await p.text(),h=p.headers.get("content-type");a.statusCode=p.status,h?a.setHeader("content-type",h):a.setHeader("content-type","application/json; charset=utf-8"),a.setHeader("x-request-id",n),a.end(m)}export{k as createagentDeviceProxyServer,X as startAgentDeviceProxyServer};
|
package/dist/src/402.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{createHash,randomUUID}from"node:crypto";export{createReadStream,createWriteStream,existsSync,mkdirSync,promises}from"node:fs";export{default as node_path}from"node:path";
|
package/dist/src/403.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{spawn as t}from"./493.js";import{networkInterfaces as r}from"./47.js";async function e(e){var a,p,f,g,h,w;let M,_,b,y,v,j,$,S,T,x=(M=function(t,r){if("string"!=typeof t||!t.trim())throw Error(`${r} is required`);return t.trim()}((a=e).projectRoot,"projectRoot"),_=c(a.port,8081,1,65535),b=l(a.listenHost)||"0.0.0.0",y=l(a.statusHost)||"127.0.0.1",v=l(a.publicHost)||function(t,e){if(t&&"0.0.0.0"!==t&&"::"!==t)return t;if(e&&"127.0.0.1"!==e&&"::1"!==e)return e;for(let t of Object.values(r()))if(t){for(let r of t)if("IPv4"===r.family&&!r.internal)return r.address}return"127.0.0.1"}(b,y),j=d(a.statusPath,"/status"),$=d(a.bundlePath,"/index.bundle?platform=ios&dev=true&minify=false"),S=l(a.command)||"npx",T=Array.isArray(a.args)&&a.args.length>0?a.args.map(t=>String(t)):["react-native","start","--host",b,"--port",String(_)],{projectRoot:M,port:_,listenHost:b,statusHost:y,publicHost:v,statusPath:j,bundlePath:$,statusUrl:`http://${y}:${_}${j}`,bundleUrl:`http://${v}:${_}${$}`,command:S,args:T,env:!(!(p=a.env)||"object"!=typeof p||Array.isArray(p))&&Object.values(p).every(t=>"string"==typeof t)?a.env:{},readyMarker:l(a.readyMarker)||"packager-status:running",checkIntervalMs:c(a.checkIntervalMs,500,50,1e4),startupTimeoutMs:c(a.startupTimeoutMs,9e4,500,6e5),shutdownTimeoutMs:c(a.shutdownTimeoutMs,5e3,200,6e4),probeTimeoutMs:c(a.probeTimeoutMs,1500,100,6e4),reuseExisting:(f=a.reuseExisting,g=!0,null==f||""===f?g:!0===f||"true"===f||1===f||"1"===f)});if(x.reuseExisting&&await o(x))return n(x,{started:!1,reused:!0,pid:0,stop:async()=>{}});let k=[],E=t(x.command,x.args,{cwd:x.projectRoot,env:{...process.env,...x.env},stdio:["ignore","pipe","pipe"]}),R=!1,H=null;E.stdout.on("data",t=>{i(k,t.toString("utf8"))}),E.stderr.on("data",t=>{i(k,t.toString("utf8"))}),E.on("exit",t=>{R=!0,H=t});let I=Date.now();try{for(;Date.now()-I<x.startupTimeoutMs;){if(R)throw Error([`Metro exited before becoming ready (exit code: ${H??"unknown"}).`,s(k)].filter(Boolean).join("\n"));if(await o(x)){let t=async()=>{await u(E,x.shutdownTimeoutMs)};return n(x,{started:!0,reused:!1,pid:E.pid||0,stop:t})}await m(x.checkIntervalMs)}throw Error([`Timed out waiting for Metro readiness after ${x.startupTimeoutMs}ms.`,s(k)].filter(Boolean).join("\n"))}catch(r){let t;throw await u(E,x.shutdownTimeoutMs),h=r,w=x,t=h instanceof Error?h.message:String(h),Error(["failed to start metro runtime",`command: ${w.command} ${w.args.join(" ")}`,`project_root: ${w.projectRoot}`,`status_url: ${w.statusUrl}`,t].join("\n"))}}async function o(t){try{let r=await fetch(t.statusUrl,{signal:AbortSignal.timeout(t.probeTimeoutMs)});if(!r.ok)return!1;return(await r.text()).includes(t.readyMarker)}catch{return!1}}function n(t,r){return{started:r.started,reused:r.reused,pid:r.pid,command:t.command,args:t.args,project_root:t.projectRoot,listen_host:t.listenHost,status_host:t.statusHost,public_host:t.publicHost,port:t.port,status_url:t.statusUrl,bundle_url:t.bundleUrl,ios_runtime:{metro_host:t.publicHost,metro_port:t.port,metro_bundle_url:t.bundleUrl},stop:r.stop}}function i(t,r){let e=r.replace(/\u001B\[[0-9;]*[A-Za-z]/g,"").trim();if(e){for(let r of e.split(/\r?\n/)){let e=r.trim();e&&t.push(e)}for(;t.length>40;)t.shift()}}function s(t){return 0===t.length?"":`Recent Metro output:
|
|
2
|
+
${t.join("\n")}`}async function u(t,r){!t||null!==t.exitCode||t.killed||(t.kill("SIGTERM"),await Promise.race([a(t),m(r).then(()=>!1)])||(t.kill("SIGKILL"),await a(t)))}async function a(t){return await new Promise(r=>{null!==t.exitCode?r(!0):(t.once("exit",()=>r(!0)),t.once("error",()=>r(!0)))})}function l(t){return null==t||""===t?"":String(t).trim()}function c(t,r,e,o){if(null==t||""===t)return r;let n=Number.parseInt(String(t),10);return!Number.isInteger(n)||n<e||n>o?r:n}function d(t,r){let e=l(t)||r;return e.startsWith("/")?e:`/${e}`}function m(t){return new Promise(r=>setTimeout(r,t))}export{e as ensureMetroRuntime};
|
package/dist/src/47.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{default as node_os,networkInterfaces}from"node:os";
|
package/dist/src/493.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{spawn,spawnSync}from"node:child_process";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{createAgentDeviceProxyClient as _}from"./224.js";import{runAgentDeviceCommand as e}from"./111.js";function r(_){let e=process.env[_];return e&&e.trim()?e.trim():void 0}function t(_){let e=r(_);if(!e)return;let t=Number.parseInt(e,10);return Number.isInteger(t)?t:void 0}function T(_){let e=r(_);if(!e)return;let t=e.toLowerCase();return!!["1","true","yes","on"].includes(t)||!["0","false","no","off"].includes(t)&&void 0}(async function(){var E;let s,O=_({baseUrl:function(_){let e=r(_);if(!e)throw Error(`${_} is required`);return e}("AGENT_PROXY_BASE_URL"),...r("AGENT_PROXY_BEARER_TOKEN")?{bearerToken:r("AGENT_PROXY_BEARER_TOKEN")}:{},...t("AGENT_PROXY_TIMEOUT_MS")?{timeoutMs:t("AGENT_PROXY_TIMEOUT_MS")}:{}}),o=await e({argv:process.argv.slice(2),client:O,cwd:process.cwd(),metro:(E=process.cwd(),(s=r("AGENT_PROXY_METRO_PROJECT_ROOT"))?{projectRoot:s||E,...t("AGENT_PROXY_METRO_PORT")?{port:t("AGENT_PROXY_METRO_PORT")}:{},...r("AGENT_PROXY_METRO_LISTEN_HOST")?{listenHost:r("AGENT_PROXY_METRO_LISTEN_HOST")}:{},...r("AGENT_PROXY_METRO_STATUS_HOST")?{statusHost:r("AGENT_PROXY_METRO_STATUS_HOST")}:{},...r("AGENT_PROXY_METRO_PUBLIC_HOST")?{publicHost:r("AGENT_PROXY_METRO_PUBLIC_HOST")}:{},...t("AGENT_PROXY_METRO_STARTUP_TIMEOUT_MS")?{startupTimeoutMs:t("AGENT_PROXY_METRO_STARTUP_TIMEOUT_MS")}:{},...t("AGENT_PROXY_METRO_PROBE_TIMEOUT_MS")?{probeTimeoutMs:t("AGENT_PROXY_METRO_PROBE_TIMEOUT_MS")}:{},...void 0!==T("AGENT_PROXY_METRO_REUSE_EXISTING")?{reuseExisting:T("AGENT_PROXY_METRO_REUSE_EXISTING")}:{}}:null)});o.stdout&&(process.stdout.write(o.stdout),o.stdout.endsWith("\n")||process.stdout.write("\n")),o.stderr&&(process.stderr.write(o.stderr),o.stderr.endsWith("\n")||process.stderr.write("\n")),process.exitCode=o.exitCode})().catch(_=>{process.stderr.write(`${_ instanceof Error?_.message:String(_)}
|
|
3
|
+
`),process.exitCode=1});
|
package/dist/src/bin.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{createAgentDeviceProxyClient}from"./224.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{createAgentDeviceProxyClient}from"./224.js";export{createagentDeviceProxyServer,startAgentDeviceProxyServer}from"./36.js";export{ensureMetroRuntime}from"./403.js";export{runAgentDeviceCommand}from"./111.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{ensureMetroRuntime}from"./403.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{createagentDeviceProxyServer,startAgentDeviceProxyServer}from"./36.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-device-proxy",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"types": "./index.d.ts",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./index.d.ts",
|
|
9
|
+
"default": "./dist/src/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./client": {
|
|
12
|
+
"types": "./index.d.ts",
|
|
13
|
+
"default": "./dist/src/client.js"
|
|
14
|
+
},
|
|
15
|
+
"./server": {
|
|
16
|
+
"types": "./index.d.ts",
|
|
17
|
+
"default": "./dist/src/server.js"
|
|
18
|
+
},
|
|
19
|
+
"./metro-runtime": {
|
|
20
|
+
"types": "./index.d.ts",
|
|
21
|
+
"default": "./dist/src/metro-runtime.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"agent-device-proxy": "./dist/src/bin.js",
|
|
26
|
+
"agent-device": "./dist/src/agent-device.js"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "rslib build",
|
|
30
|
+
"lint": "tsc -p tsconfig.json --noEmit",
|
|
31
|
+
"start": "tsx src/agent-proxy-server.ts",
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"test:watch": "vitest"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@rslib/core": "^0.19.6",
|
|
37
|
+
"tsx": "^4.20.6",
|
|
38
|
+
"typescript": "^5.9.3",
|
|
39
|
+
"vitest": "^4.0.18"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist"
|
|
43
|
+
],
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=24"
|
|
46
|
+
}
|
|
47
|
+
}
|