agent-device 0.12.9 → 0.13.1

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 CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  # agent-device
10
10
 
11
- `agent-device` is a CLI for UI automation on iOS, tvOS, macOS, Android, and AndroidTV. It is designed for agent-driven workflows: inspect the UI, act on it deterministically, and keep that work session-aware and replayable.
11
+ `agent-device` is a CLI for UI automation and app observability on iOS, tvOS, macOS, Android, and AndroidTV. It is built for agent-driven workflows: inspect the UI, interact deterministically, collect logs/network/perf evidence when behavior breaks, and keep the whole flow session-aware and replayable.
12
12
 
13
13
  If you know Vercel's [agent-browser](https://github.com/vercel-labs/agent-browser), this project applies the same broad idea to mobile apps and devices.
14
14
 
@@ -19,6 +19,7 @@ If you know Vercel's [agent-browser](https://github.com/vercel-labs/agent-browse
19
19
  - Give agents a practical way to understand mobile UI state through structured snapshots.
20
20
  - Keep automation flows token-efficient enough for real agent loops.
21
21
  - Make common interactions reliable enough for repeated automation runs.
22
+ - Make debugging evidence easy to collect through logs, network inspection, and performance snapshots.
22
23
  - Keep automation grounded in sessions, selectors, and replayable flows instead of one-off scripts.
23
24
 
24
25
  ## Core Ideas
@@ -26,11 +27,32 @@ If you know Vercel's [agent-browser](https://github.com/vercel-labs/agent-browse
26
27
  - Sessions: open a target once, interact within that session, then close it cleanly.
27
28
  - Snapshots: inspect the current accessibility tree in a compact form and get current-screen refs for exploration.
28
29
  - Refs vs selectors: use refs for discovery, use selectors for durable replay and assertions.
30
+ - Observability: collect session logs, inspect recent HTTP traffic with `network dump`, and sample CPU/memory with `perf`.
29
31
  - Tests: run deterministic `.ad` scripts as a light e2e test suite.
30
32
  - Replay scripts: save `.ad` flows with `--save-script`, replay one script with `replay`, or run a folder/glob as a serial suite with `test`.
31
33
  `test` supports metadata-aware retries up to 3 additional attempts, per-test timeouts, flaky pass reporting, and runner-managed artifacts under `.agent-device/test-artifacts` by default. Each attempt writes `replay.ad` and `result.txt`; failed attempts also keep copied logs and artifacts when available.
32
34
  - Human docs vs agent skills: docs explain the system for people; skills provide compact operating guidance for agents.
33
35
 
36
+ ## Complementary Tooling
37
+
38
+ Use `agent-device` for on-device UI automation, screenshots/recordings, app logs, network inspection, and performance snapshots.
39
+
40
+ When the task needs the React Native component tree, props, state, hooks, or render profiling, use the bundled passthrough:
41
+
42
+ ```bash
43
+ agent-device react-devtools status
44
+ agent-device react-devtools get tree --depth 3
45
+ agent-device react-devtools profile start
46
+ agent-device react-devtools profile stop
47
+ agent-device react-devtools profile slow --limit 5
48
+ ```
49
+
50
+ `react-devtools` dynamically runs pinned `agent-react-devtools@0.4.0` commands 1:1, so `agent-device` covers both the device/app runtime layer and React component internals without making React DevTools part of the daemon.
51
+
52
+ When an Android session is connected through a remote bridge profile, `react-devtools` automatically opens a lease-scoped companion tunnel for the local DevTools daemon on port 8097 and cleans it up when the command exits.
53
+
54
+ Remote Android React DevTools assumes the React Native-bundled DevTools behavior in React Native 0.83+. Older browser/Chromium DevTools workflows are not assumed to exist inside remote sandboxes. Expo projects should be verified against the SDK's bundled React Native version before relying on this path; this release does not claim a separately verified Expo SDK version.
55
+
34
56
  ## Command Flow
35
57
 
36
58
  The canonical loop is:
@@ -69,6 +91,7 @@ For people:
69
91
  For agents:
70
92
 
71
93
  - [agent-device skill](skills/agent-device/SKILL.md)
94
+ - [react-devtools skill](skills/react-devtools/SKILL.md)
72
95
  - [dogfood skill](skills/dogfood/SKILL.md)
73
96
  - [agent-device skill on ClawHub](https://clawhub.ai/okwasniewski/agent-device)
74
97
 
package/dist/src/1974.js CHANGED
@@ -1,2 +1,2 @@
1
- import e from"node:fs";import r from"node:path";import{createHash as t}from"node:crypto";import{fileURLToPath as n}from"node:url";import{runCmdDetached as o,runCmdSync as a}from"./9818.js";import{readProcessCommand as i,waitForProcessExit as s,isProcessAlive as l,readProcessStartTime as c}from"./3883.js";import{METRO_COMPANION_RUN_ARG as u,normalizeBaseUrl as d,buildBundleUrl as p,ENV_SERVER_BASE_URL as m,ENV_LOCAL_BASE_URL as f,ENV_STATE_PATH as h,ENV_LAUNCH_URL as g,ENV_SCOPE_TENANT_ID as y,ENV_SCOPE_RUN_ID as b,ENV_BEARER_TOKEN as S,ENV_SCOPE_LEASE_ID as w}from"./320.js";import{AppError as _}from"./9152.js";import{resolveUserPath as U}from"./3267.js";import{sleep as I}from"./4829.js";let E="metro-companion";function P(e){return t("sha256").update(e).digest("hex")}function k(e){return e?.trim()?e.trim():void 0}function v(e,t){let n=r.join(e,".agent-device");if(!t)return{statePath:r.join(n,"metro-companion.json"),logPath:r.join(n,"metro-companion.log")};let o=P(t).slice(0,12),a=r.join(n,E);return{statePath:r.join(a,`metro-companion-${o}.json`),logPath:r.join(a,`metro-companion-${o}.log`)}}function M(r){try{let t=JSON.parse(e.readFileSync(r,"utf8"));if(!Number.isInteger(t.pid)||0>=Number(t.pid)||"string"!=typeof t.serverBaseUrl||"string"!=typeof t.localBaseUrl||"string"!=typeof t.tokenHash||0===t.tokenHash.length)return null;let n=Array.isArray(t.consumers)?t.consumers.filter(e=>"string"==typeof e&&e.length>0):[];return{pid:Number(t.pid),startTime:"string"==typeof t.startTime?t.startTime:void 0,command:"string"==typeof t.command?t.command:void 0,serverBaseUrl:t.serverBaseUrl,localBaseUrl:t.localBaseUrl,launchUrl:k("string"==typeof t.launchUrl?t.launchUrl:void 0),bridgeScope:function(e){if(!(!e||"object"!=typeof e||Array.isArray(e))&&"string"==typeof e.tenantId&&"string"==typeof e.runId&&"string"==typeof e.leaseId)return{tenantId:e.tenantId,runId:e.runId,leaseId:e.leaseId}}(t.bridgeScope),tokenHash:t.tokenHash,consumers:n}}catch{return null}}function A(t,n){e.mkdirSync(r.dirname(t),{recursive:!0}),e.writeFileSync(t,`${JSON.stringify(n,null,2)}
2
- `,"utf8")}function N(r){try{let t=e.readdirSync(r);0===t.length&&e.rmdirSync(r)}catch{}}function R(t){let n=r.dirname(t.statePath),o=r.dirname(t.logPath);var a=t.statePath;try{e.unlinkSync(a)}catch{}var i=t.logPath;try{e.unlinkSync(i)}catch{}N(n),o!==n&&N(o),r.basename(n)===E&&N(r.dirname(n))}function $(e){return e.includes(u)}function x(e){return k(e.consumerKey)??k(e.profileKey)??null}function j(e,r){return!r||e.consumers.includes(r)?e:{...e,consumers:[...e.consumers,r]}}async function T(e){if(!l(e.pid))return;let r=i(e.pid);if(r&&$(r)){try{process.kill(e.pid,"SIGTERM")}catch(r){let e=r.code;if("ESRCH"===e||"EPERM"===e)return;throw r}if(!await s(e.pid,1e3)){try{process.kill(e.pid,"SIGKILL")}catch(r){let e=r.code;if("ESRCH"===e||"EPERM"===e)return;throw r}await s(e.pid,1e3)}}}async function B(t){let a=x(t),s=v(t.projectRoot,t.profileKey),p=M(s.statePath);if(p&&function(e,r){var t,n;if(!l(e.pid))return!1;if(e.startTime){let r=c(e.pid);if(!r||r!==e.startTime)return!1}let o=i(e.pid);return!!o&&!!$(o)&&!!e.bridgeScope&&e.serverBaseUrl===d(r.serverBaseUrl)&&e.localBaseUrl===d(r.localBaseUrl)&&e.launchUrl===k(r.launchUrl)&&(t=e.bridgeScope,n=r.bridgeScope,t.tenantId===n.tenantId&&t.runId===n.runId&&t.leaseId===n.leaseId)&&e.tokenHash===P(r.bearerToken)}(p,t)){let e=j(p,a);return e!==p&&A(s.statePath,e),{pid:p.pid,spawned:!1,statePath:s.statePath,logPath:s.logPath}}p&&(await T(p),R(s));let _=function(t,a){let s=function(){let t=n(import.meta.url),o=r.extname(t)||".js",a=r.join(r.dirname(t),`metro-companion${o}`);if(!e.existsSync(a))throw Error(`Metro companion entrypoint not found at ${a}. Rebuild the package to include the companion worker entry.`);return a}(),l=s.endsWith(".ts")?["--experimental-strip-types"]:[];e.mkdirSync(r.dirname(a),{recursive:!0});let p=e.openSync(a,"a"),_=0;try{let e;_=o(process.execPath,[...l,s,u],{env:((e={...t.env??process.env,[m]:d(t.serverBaseUrl),[S]:t.bearerToken,[f]:d(t.localBaseUrl),[h]:v(t.projectRoot,t.profileKey).statePath})[y]=t.bridgeScope.tenantId,e[b]=t.bridgeScope.runId,e[w]=t.bridgeScope.leaseId,t.launchUrl?.trim()?e[g]=t.launchUrl.trim():delete e[g],e),stdio:["ignore",p,p]})}finally{e.closeSync(p)}if(!Number.isInteger(_)||_<=0)throw Error("Failed to start Metro companion process.");return{pid:_,startTime:c(_)??void 0,command:i(_)??void 0,serverBaseUrl:d(t.serverBaseUrl),localBaseUrl:d(t.localBaseUrl),launchUrl:k(t.launchUrl),bridgeScope:t.bridgeScope,tokenHash:P(t.bearerToken),consumers:[]}}(t,s.logPath);return A(s.statePath,j(_,a)),{pid:_.pid,spawned:!0,statePath:s.statePath,logPath:s.logPath}}async function C(e){let r=x(e),t=v(e.projectRoot,e.profileKey),n=M(t.statePath);if(!n)return R(t),{stopped:!1,statePath:t.statePath};let o=r?{...n,consumers:n.consumers.filter(e=>e!==r)}:{...n,consumers:[]};return o.consumers.length>0?(A(t.statePath,o),{stopped:!1,statePath:t.statePath}):(await T(n),R(t),{stopped:!0,statePath:t.statePath})}function K(e){return"string"==typeof e&&e.trim()?d(e.trim()):""}function H(e){return"string"==typeof e&&e.trim()?e.trim():void 0}function D(e,r,t){return U(e,{env:r,cwd:t})}function O(r){try{return e.accessSync(r,e.constants.F_OK),!0}catch{return!1}}function J(e,r,t){if(null==e||""===e)return r;let n=Number.parseInt(String(e),10);return Number.isInteger(n)?Math.max(n,t):r}function L(e,r){return{platform:r,bundleUrl:p(e,r)}}function G(e,r){return{platform:r,metroHost:H(e?.metro_host),metroPort:e?.metro_port,bundleUrl:H(e?.metro_bundle_url),launchUrl:H(e?.launch_url)}}async function F(e){await I(e)}async function V(e,r,t={}){try{let n=await fetch(e,{headers:t,signal:AbortSignal.timeout(r)});return{ok:n.ok,status:n.status,body:await n.text()}}catch(t){if(t instanceof Error&&"TimeoutError"===t.name)throw Error(`Timed out fetching ${e} after ${r}ms`);throw t}}async function q(e,r){try{let t=await V(e,r);return t.ok&&t.body.includes("packager-status:running")}catch{return!1}}async function z(e){if(Number.isInteger(e)&&!(e<=0)){try{process.kill(e,"SIGTERM")}catch(r){let e=r.code;if("ESRCH"===e||"EPERM"===e)return;throw r}if(!await s(e,1e3)){try{process.kill(e,"SIGKILL")}catch(r){let e=r.code;if("ESRCH"===e||"EPERM"===e)return;throw r}await s(e,1e3)}}}function W(e,r){let t=Error(e);return t.retryable=r,t}function X(e,r){return!!(e>=500||408===e||425===e||429===e||JSON.stringify(r).includes("Metro companion is not connected"))}function Y(e){return!!(e&&"object"==typeof e&&"retryable"in e&&!0===e.retryable)}async function Q(e){let r;try{var t,n;r=await fetch(`${e.baseUrl}/api/metro/bridge`,{method:"POST",headers:(t=e.baseUrl,n=e.bearerToken,{Authorization:`Bearer ${n}`,"Content-Type":"application/json",...t.includes("ngrok")?{"ngrok-skip-browser-warning":"1"}:{}}),body:JSON.stringify({...e.scope,...e.runtime?{ios_runtime:e.runtime}:{},timeout_ms:e.timeoutMs}),signal:AbortSignal.timeout(e.timeoutMs)})}catch(r){if(r instanceof Error&&"TimeoutError"===r.name)throw W(`/api/metro/bridge timed out after ${e.timeoutMs}ms calling ${e.baseUrl}/api/metro/bridge`,!0);throw W(r instanceof Error?r.message:String(r),!0)}let o=function(e,r,t){if(!e)return{};try{let r=JSON.parse(e);if(!r||"object"!=typeof r||Array.isArray(r))throw Error("Expected a JSON object");return r}catch(a){let n=e.slice(0,200),o=a instanceof Error?a.message:String(a);throw W(`/api/metro/bridge returned invalid JSON (${r}) from ${t}: ${o}. body=${JSON.stringify(n)}`,X(r,e))}}(await r.text(),r.status,e.baseUrl);if(!r.ok)throw W(`/api/metro/bridge failed (${r.status}): ${JSON.stringify(o)}`,X(r.status,o));var a=o;let i=a.data??a;if(!i||"object"!=typeof i||Array.isArray(i))throw W("/api/metro/bridge returned malformed descriptor: Expected a JSON object.",!1);try{return{enabled:i.enabled,baseUrl:i.base_url,statusUrl:i.status_url??"",bundleUrl:i.bundle_url??"",iosRuntime:G(i.ios_runtime,"ios"),androidRuntime:G(i.android_runtime,"android"),upstream:{bundleUrl:i.upstream.bundle_url??"",host:i.upstream.host??"",port:i.upstream.port??0,statusUrl:i.upstream.status_url??""},probe:{reachable:i.probe.reachable,statusCode:i.probe.status_code,latencyMs:i.probe.latency_ms,detail:i.probe.detail}}}catch(e){throw W(`/api/metro/bridge returned malformed descriptor: ${e instanceof Error?e.message:String(e)}`,!1)}}function Z(e,r,t,n,o){let a=[`Metro bridge is required for this run but could not be configured via ${e}/api/metro/bridge.`];return r&&a.push(`bridgeError=${r}`),t?.probe.reachable===!1&&a.push(`bridgeProbe=${t.probe.detail||`unreachable (status ${t.probe.statusCode||0})`}`),n&&n!==r&&a.push(`initialBridgeError=${n}`),o&&a.push(`metroCompanionLog=${o}`),a.join(" ")}async function ee(e,r,t){let n=Date.now()+r;for(;Date.now()<n;){let r=Math.min(t,Math.max(n-Date.now(),1));if(await q(e,r))return!0;let o=Math.min(500,Math.max(n-Date.now(),0));o>0&&await F(o)}return!1}async function er(e){let r=Date.now()+e.startupTimeoutMs,t=null,n=null;for(;Date.now()<r;){try{let r=await Q({baseUrl:e.baseUrl,bearerToken:e.bearerToken,scope:e.scope,runtime:e.runtime,timeoutMs:e.probeTimeoutMs});if(!1!==r.probe.reachable)return r;t=r,n=null}catch(e){if(n=e instanceof Error?e.message:String(e),!Y(e))break}let o=Math.min(1e3,Math.max(r-Date.now(),0));o>0&&await F(o)}throw Error(Z(e.baseUrl,n,t,e.initialBridgeError,e.companionLogPath))}async function et(t={}){let n=t.env??process.env,i=process.cwd(),s=D(t.projectRoot??i,n,i),l=function(t,n){if("auto"!==n)return n;let o=function(t){let n=r.join(t,"package.json");if(!O(n))throw new _("INVALID_ARGS",`package.json not found at ${n}`);return JSON.parse(e.readFileSync(n,"utf8"))}(t);return"string"==typeof({...o.dependencies??{},...o.devDependencies??{}}).expo?"expo":"react-native"}(s,t.kind??"auto"),c=function(e){if(null==e||""===e)return 8081;let r=Number.parseInt(String(e),10);if(!Number.isInteger(r)||r<1||r>65535)throw new _("INVALID_ARGS",`Invalid Metro port: ${String(e)}. Use 1-65535.`);return r}(t.metroPort??8081),u=H(t.listenHost)??"0.0.0.0",d=H(t.statusHost)??"127.0.0.1",p=K(t.publicBaseUrl),m=J(t.startupTimeoutMs,18e4,3e4),f=J(t.probeTimeoutMs,1e4,1e3),h=t.reuseExisting??!0,g=t.installDependenciesIfNeeded??!0,y=t.runtimeFilePath?D(t.runtimeFilePath,n,i):null,b=D(t.logPath??r.join(s,".agent-device","metro.log"),n,i);if(!p&&!K(t.proxyBaseUrl))throw new _("INVALID_ARGS","metro prepare requires --public-base-url <url>.");let{proxyEnabled:S,proxyBaseUrl:w,proxyBearerToken:U}=function(e,r){if(e&&!r)throw new _("INVALID_ARGS","metro prepare requires proxy auth when --proxy-base-url is provided. Pass --bearer-token or set AGENT_DEVICE_PROXY_TOKEN.");if(!e&&r)throw new _("INVALID_ARGS","metro prepare requires --proxy-base-url when proxy auth is provided.");return{proxyEnabled:!!(e&&r),proxyBaseUrl:e,proxyBearerToken:r}}(K(t.proxyBaseUrl),H(t.proxyBearerToken)??""),I=S?function(e){if(!e?.tenantId||!e.runId||!e.leaseId)throw new _("INVALID_ARGS","metro prepare with proxy requires tenantId, runId, and leaseId bridge scope.");return e}(t.bridgeScope):null,E=g?function(t,n){if(function(r){try{return e.statSync(r).isDirectory()}catch{return!1}}(r.join(t,"node_modules")))return{installed:!1};let o=O(r.join(t,"pnpm-lock.yaml"))?{command:"pnpm",installArgs:["install"]}:O(r.join(t,"yarn.lock"))?{command:"yarn",installArgs:["install"]}:{command:"npm",installArgs:["install"]};return a(o.command,o.installArgs,{cwd:t,env:n}),{installed:!0,packageManager:o.command}}(s,n):{installed:!1},P=`http://${d}:${c}/status`,k=!1,v=!1,M=0;if(h&&await q(P,f))v=!0;else if(k=!0,M=function(t,n,a,i,s,l){let c="expo"===n?{command:"npx",installArgs:["expo","start","--host","lan","--port",String(a)]}:{command:"npx",installArgs:["react-native","start","--host",i,"--port",String(a)]};e.mkdirSync(r.dirname(s),{recursive:!0});let u=e.openSync(s,"a"),d=0;try{d=o(c.command,c.installArgs,{cwd:t,env:l,stdio:["ignore",u,u]})}finally{e.closeSync(u)}if(!Number.isInteger(d)||d<=0)throw Error("Failed to start Metro. Expected a detached child PID.");return{pid:d}}(s,l,c,u,b,n).pid,!await ee(P,m,f))throw await z(M).catch(()=>{}),Error(`Metro did not become ready at ${P} within ${m}ms. Check ${b}.`);let A=p?L(p,"ios"):{platform:"ios"},N=p?L(p,"android"):{platform:"android"},R=null,$=null;if(I)try{R=await Q({baseUrl:w,bearerToken:U,scope:I,timeoutMs:f})}catch(e){if(!Y(e))throw e;$=e instanceof Error?e.message:String(e)}if(I&&(!R||!1===R.probe.reachable)){let e;try{e=(await B({projectRoot:s,serverBaseUrl:w,bearerToken:U,bridgeScope:I,localBaseUrl:`http://${d}:${c}`,launchUrl:H(t.launchUrl),profileKey:H(t.companionProfileKey),consumerKey:H(t.companionConsumerKey),env:n})).logPath}catch(e){throw Error(Z(w,e instanceof Error?e.message:String(e),R,$))}try{R=await er({baseUrl:w,bearerToken:U,scope:I,probeTimeoutMs:f,startupTimeoutMs:m,initialBridgeError:$,companionLogPath:e})}catch(e){throw e instanceof Error?e:Error(String(e))}}I&&function(e,r){if(!r?.iosRuntime.bundleUrl)throw Error(Z(e,"bridge descriptor is missing ios_runtime.metro_bundle_url",r))}(w,R);let x=R?.iosRuntime??A,j=R?.androidRuntime??N,T={projectRoot:s,kind:l,dependenciesInstalled:E.installed,packageManager:E.packageManager??null,started:k,reused:v,pid:M,logPath:b,statusUrl:P,runtimeFilePath:y,iosRuntime:x,androidRuntime:j,bridge:R};return y&&(e.mkdirSync(r.dirname(y),{recursive:!0}),e.writeFileSync(y,JSON.stringify(T,null,2))),T}export{L as buildMetroRuntimeHints,B as ensureMetroCompanion,et as prepareMetroRuntime,C as stopMetroCompanion};
1
+ import e from"node:fs";import t from"node:path";import{createHash as r}from"node:crypto";import{fileURLToPath as n}from"node:url";import{runCmdDetached as o,runCmdSync as i}from"./9818.js";import{readProcessCommand as a,waitForProcessExit as s,resolveRuntimeTransportHints as l,isProcessAlive as u,readProcessStartTime as c}from"./8656.js";import{normalizeBaseUrl as d,ENV_REGISTER_PATH as m,ENV_UNREGISTER_PATH as p,ENV_LOCAL_BASE_URL as h,ENV_STATE_PATH as f,ENV_SESSION as g,ENV_BEARER_TOKEN as y,METRO_COMPANION_RUN_ARG as b,buildBundleUrl as S,ENV_SERVER_BASE_URL as w,ENV_LAUNCH_URL as P,ENV_SCOPE_TENANT_ID as v,ENV_SCOPE_RUN_ID as U,ENV_SCOPE_LEASE_ID as _,REACT_DEVTOOLS_COMPANION_RUN_ARG as I,ENV_DEVICE_PORT as E}from"./320.js";import{AppError as k}from"./9152.js";import{resolveUserPath as M}from"./3267.js";import{sleep as N}from"./4829.js";let A="metro-companion";function R(e){return r("sha256").update(e).digest("hex")}function $(e){return e?.trim()?e.trim():void 0}function x(e,r,n="metro",o){let i="react-devtools"===n?{stateFile:"react-devtools-companion.json",logFile:"react-devtools-companion.log",stateDir:"react-devtools-companion"}:{stateFile:"metro-companion.json",logFile:"metro-companion.log",stateDir:A},a=o??t.join(e,".agent-device");if(!r)return{statePath:t.join(a,i.stateFile),logPath:t.join(a,i.logFile)};let s=R(r).slice(0,12),l=t.join(a,i.stateDir);return{statePath:t.join(l,`${i.stateDir}-${s}.json`),logPath:t.join(l,`${i.stateDir}-${s}.log`)}}function j(t){try{let r=JSON.parse(e.readFileSync(t,"utf8"));if(!Number.isInteger(r.pid)||0>=Number(r.pid)||"string"!=typeof r.serverBaseUrl||"string"!=typeof r.localBaseUrl||"string"!=typeof r.tokenHash||0===r.tokenHash.length)return null;let n=Array.isArray(r.consumers)?r.consumers.filter(e=>"string"==typeof e&&e.length>0):[];return{pid:Number(r.pid),startTime:"string"==typeof r.startTime?r.startTime:void 0,command:"string"==typeof r.command?r.command:void 0,serverBaseUrl:r.serverBaseUrl,localBaseUrl:r.localBaseUrl,launchUrl:$("string"==typeof r.launchUrl?r.launchUrl:void 0),registerPath:$("string"==typeof r.registerPath?r.registerPath:void 0),unregisterPath:$("string"==typeof r.unregisterPath?r.unregisterPath:void 0),devicePort:Number.isInteger(r.devicePort)?Number(r.devicePort):void 0,session:$("string"==typeof r.session?r.session:void 0),bridgeScope:function(e){if(!(!e||"object"!=typeof e||Array.isArray(e))&&"string"==typeof e.tenantId&&"string"==typeof e.runId&&"string"==typeof e.leaseId)return{tenantId:e.tenantId,runId:e.runId,leaseId:e.leaseId}}(r.bridgeScope),tokenHash:r.tokenHash,consumers:n}}catch{return null}}function D(r,n){e.mkdirSync(t.dirname(r),{recursive:!0}),e.writeFileSync(r,`${JSON.stringify(n,null,2)}
2
+ `,"utf8")}function T(t){try{let r=e.readdirSync(t);0===r.length&&e.rmdirSync(t)}catch{}}function C(r){let n=t.dirname(r.statePath),o=t.dirname(r.logPath);var i=r.statePath;try{e.unlinkSync(i)}catch{}var a=r.logPath;try{e.unlinkSync(a)}catch{}T(n),o!==n&&T(o),t.basename(n)===A&&T(t.dirname(n))}function B(e){return e.includes(b)||e.includes(I)}function K(e){return $(e.consumerKey)??$(e.profileKey)??null}function H(e,t){return!t||e.consumers.includes(t)?e:{...e,consumers:[...e.consumers,t]}}async function O(e){if(!u(e.pid))return;let t=a(e.pid);if(t&&B(t)){try{process.kill(e.pid,"SIGTERM")}catch(t){let e=t.code;if("ESRCH"===e||"EPERM"===e)return;throw t}if(!await s(e.pid,1e3)){try{process.kill(e.pid,"SIGKILL")}catch(t){let e=t.code;if("ESRCH"===e||"EPERM"===e)return;throw t}await s(e.pid,1e3)}}}async function F(r){let i=K(r),s=x(r.projectRoot,r.profileKey,r.kind,r.stateDir),l=j(s.statePath);if(l&&function(e,t){var r,n;if(!u(e.pid))return!1;if(e.startTime){let t=c(e.pid);if(!t||t!==e.startTime)return!1}let o=a(e.pid);return!!o&&!!B(o)&&!!e.bridgeScope&&e.serverBaseUrl===d(t.serverBaseUrl)&&e.localBaseUrl===d(t.localBaseUrl)&&e.launchUrl===$(t.launchUrl)&&e.registerPath===$(t.registerPath)&&e.unregisterPath===$(t.unregisterPath)&&e.devicePort===t.devicePort&&e.session===$(t.session)&&(r=e.bridgeScope,n=t.bridgeScope,r.tenantId===n.tenantId&&r.runId===n.runId&&r.leaseId===n.leaseId)&&e.tokenHash===R(t.bearerToken)}(l,r)){let e=H(l,i);return e!==l&&D(s.statePath,e),{pid:l.pid,spawned:!1,statePath:s.statePath,logPath:s.logPath}}l&&(await O(l),C(s));let S=function(r,i){let s=function(){let r=n(import.meta.url),o=t.extname(r)||".js",i=t.join(t.dirname(r),`metro-companion${o}`);if(!e.existsSync(i))throw Error(`Metro companion entrypoint not found at ${i}. Rebuild the package to include the companion worker entry.`);return i}(),l=s.endsWith(".ts")?["--experimental-strip-types"]:[],u="react-devtools"===r.kind?I:b;e.mkdirSync(t.dirname(i),{recursive:!0});let S=e.openSync(i,"a"),k=0;try{let e;k=o(process.execPath,[...l,s,u],{env:((e={...r.env??process.env,[w]:d(r.serverBaseUrl),[y]:r.bearerToken,[h]:d(r.localBaseUrl),[f]:x(r.projectRoot,r.profileKey,r.kind,r.stateDir).statePath})[v]=r.bridgeScope.tenantId,e[U]=r.bridgeScope.runId,e[_]=r.bridgeScope.leaseId,r.launchUrl?.trim()?e[P]=r.launchUrl.trim():delete e[P],r.registerPath?.trim()?e[m]=r.registerPath.trim():delete e[m],r.unregisterPath?.trim()?e[p]=r.unregisterPath.trim():delete e[p],void 0!==r.devicePort?e[E]=String(r.devicePort):delete e[E],r.session?.trim()?e[g]=r.session.trim():delete e[g],e),stdio:["ignore",S,S]})}finally{e.closeSync(S)}if(!Number.isInteger(k)||k<=0)throw Error("Failed to start Metro companion process.");return{pid:k,startTime:c(k)??void 0,command:a(k)??void 0,serverBaseUrl:d(r.serverBaseUrl),localBaseUrl:d(r.localBaseUrl),launchUrl:$(r.launchUrl),registerPath:$(r.registerPath),unregisterPath:$(r.unregisterPath),devicePort:r.devicePort,session:$(r.session),bridgeScope:r.bridgeScope,tokenHash:R(r.bearerToken),consumers:[]}}(r,s.logPath);return D(s.statePath,H(S,i)),{pid:S.pid,spawned:!0,statePath:s.statePath,logPath:s.logPath}}async function L(e){let t=K(e),r=x(e.projectRoot,e.profileKey,e.kind,e.stateDir),n=j(r.statePath);if(!n)return C(r),{stopped:!1,statePath:r.statePath};let o=t?{...n,consumers:n.consumers.filter(e=>e!==t)}:{...n,consumers:[]};return o.consumers.length>0?(D(r.statePath,o),{stopped:!1,statePath:r.statePath}):(await O(n),C(r),{stopped:!0,statePath:r.statePath})}function G(e){return"string"==typeof e&&e.trim()?d(e.trim()):""}function J(e){return"string"==typeof e&&e.trim()?e.trim():void 0}function V(e,t,r){return M(e,{env:t,cwd:r})}function q(t){try{return e.accessSync(t,e.constants.F_OK),!0}catch{return!1}}function W(e,t,r){if(null==e||""===e)return t;let n=Number.parseInt(String(e),10);return Number.isInteger(n)?Math.max(n,r):t}function z(e,t){if(null==e||""===e)return t;let r=Number.parseInt(String(e),10);if(!Number.isInteger(r)||r<1||r>65535)throw new k("INVALID_ARGS",`Invalid Metro port: ${String(e)}. Use 1-65535.`);return r}function X(e,t){return{platform:t,bundleUrl:S(e,t)}}function Y(e,t){return{platform:t,metroHost:J(e?.metro_host),metroPort:e?.metro_port,bundleUrl:J(e?.metro_bundle_url),launchUrl:J(e?.launch_url)}}async function Q(e){await N(e)}async function Z(e,t,r={}){try{let n=await fetch(e,{headers:r,signal:AbortSignal.timeout(t)});return{ok:n.ok,status:n.status,body:await n.text()}}catch(r){if(r instanceof Error&&"TimeoutError"===r.name)throw Error(`Timed out fetching ${e} after ${t}ms`);throw r}}async function ee(e,t){try{let r=await Z(e,t);return r.ok&&r.body.includes("packager-status:running")}catch{return!1}}async function et(e){if(Number.isInteger(e)&&!(e<=0)){try{process.kill(e,"SIGTERM")}catch(t){let e=t.code;if("ESRCH"===e||"EPERM"===e)return;throw t}if(!await s(e,1e3)){try{process.kill(e,"SIGKILL")}catch(t){let e=t.code;if("ESRCH"===e||"EPERM"===e)return;throw t}await s(e,1e3)}}}function er(e,t){let r=Error(e);return r.retryable=t,r}function en(e,t){return!!(e>=500||408===e||425===e||429===e||JSON.stringify(t).includes("Metro companion is not connected"))}function eo(e){return!!(e&&"object"==typeof e&&"retryable"in e&&!0===e.retryable)}async function ei(e){let t;try{var r,n;t=await fetch(`${e.baseUrl}/api/metro/bridge`,{method:"POST",headers:(r=e.baseUrl,n=e.bearerToken,{Authorization:`Bearer ${n}`,"Content-Type":"application/json",...r.includes("ngrok")?{"ngrok-skip-browser-warning":"1"}:{}}),body:JSON.stringify({...e.scope,...e.runtime?{ios_runtime:e.runtime}:{},timeout_ms:e.timeoutMs}),signal:AbortSignal.timeout(e.timeoutMs)})}catch(t){if(t instanceof Error&&"TimeoutError"===t.name)throw er(`/api/metro/bridge timed out after ${e.timeoutMs}ms calling ${e.baseUrl}/api/metro/bridge`,!0);throw er(t instanceof Error?t.message:String(t),!0)}let o=function(e,t,r){if(!e)return{};try{let t=JSON.parse(e);if(!t||"object"!=typeof t||Array.isArray(t))throw Error("Expected a JSON object");return t}catch(i){let n=e.slice(0,200),o=i instanceof Error?i.message:String(i);throw er(`/api/metro/bridge returned invalid JSON (${t}) from ${r}: ${o}. body=${JSON.stringify(n)}`,en(t,e))}}(await t.text(),t.status,e.baseUrl);if(!t.ok)throw er(`/api/metro/bridge failed (${t.status}): ${JSON.stringify(o)}`,en(t.status,o));var i=o;let a=i.data??i;if(!a||"object"!=typeof a||Array.isArray(a))throw er("/api/metro/bridge returned malformed descriptor: Expected a JSON object.",!1);try{return{enabled:a.enabled,baseUrl:a.base_url,statusUrl:a.status_url??"",bundleUrl:a.bundle_url??"",iosRuntime:Y(a.ios_runtime,"ios"),androidRuntime:Y(a.android_runtime,"android"),upstream:{bundleUrl:a.upstream.bundle_url??"",host:a.upstream.host??"",port:a.upstream.port??0,statusUrl:a.upstream.status_url??""},probe:{reachable:a.probe.reachable,statusCode:a.probe.status_code,latencyMs:a.probe.latency_ms,detail:a.probe.detail}}}catch(e){throw er(`/api/metro/bridge returned malformed descriptor: ${e instanceof Error?e.message:String(e)}`,!1)}}function ea(e,t,r,n,o){let i=[`Metro bridge is required for this run but could not be configured via ${e}/api/metro/bridge.`];return t&&i.push(`bridgeError=${t}`),r?.probe.reachable===!1&&i.push(`bridgeProbe=${r.probe.detail||`unreachable (status ${r.probe.statusCode||0})`}`),n&&n!==t&&i.push(`initialBridgeError=${n}`),o&&i.push(`metroCompanionLog=${o}`),i.join(" ")}async function es(e,t,r){let n=Date.now()+t;for(;Date.now()<n;){let t=Math.min(r,Math.max(n-Date.now(),1));if(await ee(e,t))return!0;let o=Math.min(500,Math.max(n-Date.now(),0));o>0&&await Q(o)}return!1}async function el(e){let t=Date.now()+e.startupTimeoutMs,r=null,n=null;for(;Date.now()<t;){try{let t=await ei({baseUrl:e.baseUrl,bearerToken:e.bearerToken,scope:e.scope,runtime:e.runtime,timeoutMs:e.probeTimeoutMs});if(!1!==t.probe.reachable)return t;r=t,n=null}catch(e){if(n=e instanceof Error?e.message:String(e),!eo(e))break}let o=Math.min(1e3,Math.max(t-Date.now(),0));o>0&&await Q(o)}throw Error(ea(e.baseUrl,n,r,e.initialBridgeError,e.companionLogPath))}async function eu(r={}){let n=r.env??process.env,a=process.cwd(),s=V(r.projectRoot??a,n,a),l=function(r,n){if("auto"!==n)return n;let o=function(r){let n=t.join(r,"package.json");if(!q(n))throw new k("INVALID_ARGS",`package.json not found at ${n}`);return JSON.parse(e.readFileSync(n,"utf8"))}(r);return"string"==typeof({...o.dependencies??{},...o.devDependencies??{}}).expo?"expo":"react-native"}(s,r.kind??"auto"),u=z(r.metroPort??8081,8081),c=J(r.listenHost)??"0.0.0.0",d=J(r.statusHost)??"127.0.0.1",m=G(r.publicBaseUrl),p=W(r.startupTimeoutMs,18e4,3e4),h=W(r.probeTimeoutMs,1e4,1e3),f=r.reuseExisting??!0,g=r.installDependenciesIfNeeded??!0,y=r.runtimeFilePath?V(r.runtimeFilePath,n,a):null,b=V(r.logPath??t.join(s,".agent-device","metro.log"),n,a);if(!m&&!G(r.proxyBaseUrl))throw new k("INVALID_ARGS","metro prepare requires --public-base-url <url>.");let{proxyEnabled:S,proxyBaseUrl:w,proxyBearerToken:P}=function(e,t){if(e&&!t)throw new k("INVALID_ARGS","metro prepare requires proxy auth when --proxy-base-url is provided. Pass --bearer-token or set AGENT_DEVICE_PROXY_TOKEN.");if(!e&&t)throw new k("INVALID_ARGS","metro prepare requires --proxy-base-url when proxy auth is provided.");return{proxyEnabled:!!(e&&t),proxyBaseUrl:e,proxyBearerToken:t}}(G(r.proxyBaseUrl),J(r.proxyBearerToken)??""),v=S?function(e){if(!e?.tenantId||!e.runId||!e.leaseId)throw new k("INVALID_ARGS","metro prepare with proxy requires tenantId, runId, and leaseId bridge scope.");return e}(r.bridgeScope):null,U=g?function(r,n){if(function(t){try{return e.statSync(t).isDirectory()}catch{return!1}}(t.join(r,"node_modules")))return{installed:!1};let o=q(t.join(r,"pnpm-lock.yaml"))?{command:"pnpm",installArgs:["install"]}:q(t.join(r,"yarn.lock"))?{command:"yarn",installArgs:["install"]}:{command:"npm",installArgs:["install"]};return i(o.command,o.installArgs,{cwd:r,env:n}),{installed:!0,packageManager:o.command}}(s,n):{installed:!1},_=`http://${d}:${u}/status`,I=!1,E=!1,M=0;if(f&&await ee(_,h))E=!0;else if(I=!0,M=function(r,n,i,a,s,l){let u="expo"===n?{command:"npx",installArgs:["expo","start","--host","lan","--port",String(i)]}:{command:"npx",installArgs:["react-native","start","--host",a,"--port",String(i)]};e.mkdirSync(t.dirname(s),{recursive:!0});let c=e.openSync(s,"a"),d=0;try{d=o(u.command,u.installArgs,{cwd:r,env:l,stdio:["ignore",c,c]})}finally{e.closeSync(c)}if(!Number.isInteger(d)||d<=0)throw Error("Failed to start Metro. Expected a detached child PID.");return{pid:d}}(s,l,u,c,b,n).pid,!await es(_,p,h))throw await et(M).catch(()=>{}),Error(`Metro did not become ready at ${_} within ${p}ms. Check ${b}.`);let N=m?X(m,"ios"):{platform:"ios"},A=m?X(m,"android"):{platform:"android"},R=null,$=null;if(v)try{R=await ei({baseUrl:w,bearerToken:P,scope:v,timeoutMs:h})}catch(e){if(!eo(e))throw e;$=e instanceof Error?e.message:String(e)}if(v&&(!R||!1===R.probe.reachable)){let e;try{e=(await F({projectRoot:s,serverBaseUrl:w,bearerToken:P,bridgeScope:v,localBaseUrl:`http://${d}:${u}`,launchUrl:J(r.launchUrl),profileKey:J(r.companionProfileKey),consumerKey:J(r.companionConsumerKey),env:n})).logPath}catch(e){throw Error(ea(w,e instanceof Error?e.message:String(e),R,$))}try{R=await el({baseUrl:w,bearerToken:P,scope:v,probeTimeoutMs:h,startupTimeoutMs:p,initialBridgeError:$,companionLogPath:e})}catch(e){throw e instanceof Error?e:Error(String(e))}}v&&function(e,t){if(!t?.iosRuntime.bundleUrl)throw Error(ea(e,"bridge descriptor is missing ios_runtime.metro_bundle_url",t))}(w,R);let x=R?.iosRuntime??N,j=R?.androidRuntime??A,D={projectRoot:s,kind:l,dependenciesInstalled:U.installed,packageManager:U.packageManager??null,started:I,reused:E,pid:M,logPath:b,statusUrl:_,runtimeFilePath:y,iosRuntime:x,androidRuntime:j,bridge:R};return y&&(e.mkdirSync(t.dirname(y),{recursive:!0}),e.writeFileSync(y,JSON.stringify(D,null,2))),D}async function ec(e={}){let t=W(e.timeoutMs,1e4,1e3),r=function(e){var t;let r,n=J(e.bundleUrl)??e.runtime?.bundleUrl,o=!!J(e.bundleUrl),i=!!J(n),a=l({metroHost:J(e.metroHost)??(o?void 0:J(e.runtime?.metroHost))??(i?void 0:"localhost"),metroPort:void 0!==e.metroPort?z(e.metroPort,8081):o?void 0:e.runtime?.metroPort??(i?void 0:8081),bundleUrl:n});if(!a)throw new k("INVALID_ARGS","Unable to resolve Metro host and port for reload.");return t=function(e){let t=J(e);if(!t)return"/reload";let r=new URL(t).pathname.replace(/\/+$/,"");return r.endsWith("/index.bundle")?`${r.slice(0,-13)}/reload`:"/reload"}(n),(r=new URL(`${a.scheme}://localhost`)).hostname=a.host,r.port=String(a.port),r.pathname=t,r.toString()}(e),n=await Z(r,t);if(!n.ok)throw new k("COMMAND_FAILED",`Metro reload failed (${n.status}).`,{reloadUrl:r,status:n.status,body:n.body,hint:"Verify Metro is running and the target React Native app is connected to this Metro instance."});return{reloaded:!0,reloadUrl:r,status:n.status,body:n.body}}export{X as buildMetroRuntimeHints,F as ensureMetroCompanion,eu as prepareMetroRuntime,ec as reloadMetro,L as stopMetroCompanion};
package/dist/src/320.js CHANGED
@@ -1 +1 @@
1
- let E="--agent-device-run-metro-companion",_="AGENT_DEVICE_METRO_COMPANION_SERVER_BASE_URL",N="AGENT_DEVICE_METRO_COMPANION_BEARER_TOKEN",A="AGENT_DEVICE_METRO_COMPANION_LOCAL_BASE_URL",O="AGENT_DEVICE_METRO_COMPANION_LAUNCH_URL",e="AGENT_DEVICE_METRO_COMPANION_STATE_PATH",T="AGENT_DEVICE_METRO_COMPANION_SCOPE_TENANT_ID",C="AGENT_DEVICE_METRO_COMPANION_SCOPE_RUN_ID",R="AGENT_DEVICE_METRO_COMPANION_SCOPE_LEASE_ID";function I(E){let _=E.length;for(;_>0&&47===E.charCodeAt(_-1);)_-=1;return _===E.length?E:E.slice(0,_)}function r(E,_){let N=new URL(`${I(E)}/index.bundle`);return N.searchParams.set("platform",_),N.searchParams.set("dev","true"),N.searchParams.set("minify","false"),N.toString()}export{N as ENV_BEARER_TOKEN,O as ENV_LAUNCH_URL,A as ENV_LOCAL_BASE_URL,R as ENV_SCOPE_LEASE_ID,C as ENV_SCOPE_RUN_ID,T as ENV_SCOPE_TENANT_ID,_ as ENV_SERVER_BASE_URL,e as ENV_STATE_PATH,E as METRO_COMPANION_RUN_ARG,r as buildBundleUrl,I as normalizeBaseUrl};
1
+ let E="--agent-device-run-metro-companion",_="--agent-device-run-react-devtools-companion",N="AGENT_DEVICE_METRO_COMPANION_SERVER_BASE_URL",O="AGENT_DEVICE_METRO_COMPANION_BEARER_TOKEN",A="AGENT_DEVICE_METRO_COMPANION_LOCAL_BASE_URL",T="AGENT_DEVICE_METRO_COMPANION_LAUNCH_URL",R="AGENT_DEVICE_METRO_COMPANION_STATE_PATH",C="AGENT_DEVICE_METRO_COMPANION_SCOPE_TENANT_ID",I="AGENT_DEVICE_METRO_COMPANION_SCOPE_RUN_ID",e="AGENT_DEVICE_METRO_COMPANION_SCOPE_LEASE_ID",P="AGENT_DEVICE_METRO_COMPANION_REGISTER_PATH",V="AGENT_DEVICE_METRO_COMPANION_UNREGISTER_PATH",S="AGENT_DEVICE_METRO_COMPANION_DEVICE_PORT",M="AGENT_DEVICE_METRO_COMPANION_SESSION";function n(E){let _=E.length;for(;_>0&&47===E.charCodeAt(_-1);)_-=1;return _===E.length?E:E.slice(0,_)}function r(E,_){let N=new URL(`${n(E)}/index.bundle`);return N.searchParams.set("platform",_),N.searchParams.set("dev","true"),N.searchParams.set("minify","false"),N.toString()}export{O as ENV_BEARER_TOKEN,S as ENV_DEVICE_PORT,T as ENV_LAUNCH_URL,A as ENV_LOCAL_BASE_URL,P as ENV_REGISTER_PATH,e as ENV_SCOPE_LEASE_ID,I as ENV_SCOPE_RUN_ID,C as ENV_SCOPE_TENANT_ID,N as ENV_SERVER_BASE_URL,M as ENV_SESSION,R as ENV_STATE_PATH,V as ENV_UNREGISTER_PATH,E as METRO_COMPANION_RUN_ARG,_ as REACT_DEVTOOLS_COMPANION_RUN_ARG,r as buildBundleUrl,n as normalizeBaseUrl};