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 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)
@@ -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};
@@ -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};
@@ -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";
@@ -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";
@@ -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});
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env tsx
2
+ import{startAgentDeviceProxyServer as _}from"./36.js";_();
@@ -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
+ }