agent-device 0.2.0 → 0.2.2

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
@@ -1,6 +1,14 @@
1
+ <a href="https://www.callstack.com/open-source?utm_campaign=generic&utm_source=github&utm_medium=referral&utm_content=agent-device" align="center">
2
+ <picture>
3
+ <img alt="agent-device" src="website/docs/public/agent-device-banner.jpg">
4
+ </picture>
5
+ </a>
6
+
7
+ ---
8
+
1
9
  # agent-device
2
10
 
3
- CLI to control iOS and Android devices for AI agents influenced by Vercel’s [agent-browser](https://github.com/vercel/agent-browser).
11
+ CLI to control iOS and Android devices for AI agents influenced by Vercel’s [agent-browser](https://github.com/vercel-labs/agent-browser).
4
12
 
5
13
  The project is in early development and considered experimental. Pull requests are welcome!
6
14
 
@@ -26,12 +34,12 @@ npx agent-device open SampleApp
26
34
  ## Quick Start
27
35
 
28
36
  ```bash
29
- agent-device open Contacts --platform ios
30
- agent-device snapshot -i -c --platform ios
31
- agent-device click @e5 --platform ios
32
- agent-device fill @e6 "John" --platform ios
33
- agent-device fill @e7 "Doe" --platform ios
34
- agent-device click @e3 --platform ios
37
+ agent-device open Contacts --platform ios # creates session on iOS Simulator
38
+ agent-device snapshot
39
+ agent-device click @e5
40
+ agent-device fill @e6 "John"
41
+ agent-device fill @e7 "Doe"
42
+ agent-device click @e3
35
43
  agent-device close
36
44
  ```
37
45
 
@@ -55,7 +63,7 @@ Debug flow:
55
63
 
56
64
  ```bash
57
65
  agent-device trace start
58
- agent-device snapshot --backend xctest -s "Sample App"
66
+ agent-device snapshot -s "Sample App"
59
67
  agent-device find label "Wi-Fi" click
60
68
  agent-device trace stop ./trace.log
61
69
  ```
@@ -77,8 +85,8 @@ Coordinates:
77
85
 
78
86
  | Backend | Speed | Accuracy | Requirements |
79
87
  | --- | --- | --- | --- |
80
- | `ax` | Fast | Medium | Accessibility permission for the terminal app |
81
88
  | `xctest` | Fast | High | No Accessibility permission required |
89
+ | `ax` | Fast | Medium | Accessibility permission for the terminal app, not recommended |
82
90
 
83
91
  Notes:
84
92
  - Default backend is `xctest` on iOS.
@@ -90,6 +98,7 @@ Flags:
90
98
  - `--device <name>`
91
99
  - `--udid <udid>` (iOS)
92
100
  - `--serial <serial>` (Android)
101
+ - `--activity <component>` (Android; package/Activity or package/.Activity)
93
102
  - `--out <path>` (screenshot)
94
103
  - `--session <name>`
95
104
  - `--verbose` for daemon and runner logs
@@ -126,9 +135,9 @@ App state:
126
135
 
127
136
  - `agent-device trace start`
128
137
  - `agent-device trace stop ./trace.log`
129
- - The trace log includes AX snapshot stderr and XCTest runner logs for the session.
138
+ - The trace log includes snapshot logs and XCTest runner logs for the session.
130
139
  - Built-in retries cover transient runner connection failures, AX snapshot hiccups, and Android UI dumps.
131
- - For snapshot issues, compare `--backend ax` vs `--backend xctest` and scope with `-s "<label>"`.
140
+ - For snapshot issues (missing elements), compare with `--raw` flag for unaltered output and scope with `-s "<label>"`.
132
141
 
133
142
  ## App resolution
134
143
  - Bundle/package identifiers are accepted directly (e.g., `com.apple.Preferences`).
package/dist/src/bin.js CHANGED
@@ -1,8 +1,8 @@
1
- import{node_path as e,fileURLToPath as t,asAppError as r,pathToFileURL as n,runCmdDetached as s,node_fs as o,node_os as a,node_net as i,errors_AppError as l}from"./861.js";function c(e){process.stdout.write(`${JSON.stringify(e,null,2)}
1
+ import{node_path as e,fileURLToPath as t,asAppError as r,pathToFileURL as n,runCmdDetached as a,node_fs as s,node_os as o,node_net as i,errors_AppError as l}from"./861.js";function c(e){process.stdout.write(`${JSON.stringify(e,null,2)}
2
2
  `)}function u(e){let t=e.details?`
3
3
  ${JSON.stringify(e.details,null,2)}`:"";process.stderr.write(`Error (${e.code}): ${e.message}${t}
4
- `)}let d=e.join(a.homedir(),".agent-device"),p=e.join(d,"daemon.json"),f=function(){let e=process.env.AGENT_DEVICE_DAEMON_TIMEOUT_MS;if(!e)return 6e4;let t=Number(e);return Number.isFinite(t)?Math.max(1e3,Math.floor(t)):6e4}();async function m(e){let t=await h(),r={...e,token:t.token};return await b(t,r)}async function h(){let t=w(),r=function(){try{let t=v();return JSON.parse(o.readFileSync(e.join(t,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}();if(t&&t.version===r&&await y(t))return t;t&&(t.version!==r||!await y(t))&&o.existsSync(p)&&o.unlinkSync(p),await g();let n=Date.now();for(;Date.now()-n<5e3;){let e=w();if(e&&await y(e))return e;await new Promise(e=>setTimeout(e,100))}throw new l("COMMAND_FAILED","Failed to start daemon",{infoPath:p,hint:"Run pnpm build, or delete ~/.agent-device/daemon.json if stale."})}function w(){if(!o.existsSync(p))return null;try{let e=JSON.parse(o.readFileSync(p,"utf8"));if(!e.port||!e.token)return null;return e}catch{return null}}async function y(e){return new Promise(t=>{let r=i.createConnection({host:"127.0.0.1",port:e.port},()=>{r.destroy(),t(!0)});r.on("error",()=>{t(!1)})})}async function g(){let t=v(),r=e.join(t,"dist","src","daemon.js"),n=e.join(t,"src","daemon.ts"),a=o.existsSync(r);if(!a&&!o.existsSync(n))throw new l("COMMAND_FAILED","Daemon entry not found",{distPath:r,srcPath:n});let i=a?[r]:["--experimental-strip-types",n];s(process.execPath,i)}async function b(e,t){return new Promise((r,n)=>{let s=i.createConnection({host:"127.0.0.1",port:e.port},()=>{s.write(`${JSON.stringify(t)}
5
- `)}),o=setTimeout(()=>{s.destroy(),n(new l("COMMAND_FAILED","Daemon request timed out",{timeoutMs:f}))},f),a="";s.setEncoding("utf8"),s.on("data",e=>{let t=(a+=e).indexOf("\n");if(-1===t)return;let i=a.slice(0,t).trim();if(i)try{let e=JSON.parse(i);s.end(),clearTimeout(o),r(e)}catch(e){clearTimeout(o),n(e)}}),s.on("error",e=>{clearTimeout(o),n(e)})})}function v(){let r=e.dirname(t(import.meta.url)),n=r;for(let t=0;t<6;t+=1){let t=e.join(n,"package.json");if(o.existsSync(t))return n;n=e.dirname(n)}return r}async function $(t){let n=function(e){let t={json:!1,help:!1},r=[];for(let n=0;n<e.length;n+=1){let s=e[n];if("--json"===s){t.json=!0;continue}if("--help"===s||"-h"===s){t.help=!0;continue}if("--verbose"===s||"-v"===s){t.verbose=!0;continue}if("-i"===s){t.snapshotInteractiveOnly=!0;continue}if("-c"===s){t.snapshotCompact=!0;continue}if("--raw"===s){t.snapshotRaw=!0;continue}if("--no-record"===s){t.noRecord=!0;continue}if("--record-json"===s){t.recordJson=!0;continue}if("--user-installed"===s){t.appsFilter="user-installed";continue}if("--all"===s){t.appsFilter="all";continue}if("--metadata"===s){t.appsMetadata=!0;continue}if(s.startsWith("--backend")){let r=s.includes("=")?s.split("=")[1]:e[n+1];if(s.includes("=")||(n+=1),"ax"!==r&&"xctest"!==r)throw new l("INVALID_ARGS",`Invalid backend: ${r}`);t.snapshotBackend=r;continue}if(s.startsWith("--")){let[r,o]=s.split("="),a=o??e[n+1];switch(!o&&(n+=1),r){case"--platform":if("ios"!==a&&"android"!==a)throw new l("INVALID_ARGS",`Invalid platform: ${a}`);t.platform=a;break;case"--depth":{let e=Number(a);if(!Number.isFinite(e)||e<0)throw new l("INVALID_ARGS",`Invalid depth: ${a}`);t.snapshotDepth=Math.floor(e);break}case"--scope":t.snapshotScope=a;break;case"--device":t.device=a;break;case"--udid":t.udid=a;break;case"--serial":t.serial=a;break;case"--out":t.out=a;break;case"--session":t.session=a;break;default:throw new l("INVALID_ARGS",`Unknown flag: ${r}`)}continue}if("-d"===s){let r=e[n+1];n+=1;let s=Number(r);if(!Number.isFinite(s)||s<0)throw new l("INVALID_ARGS",`Invalid depth: ${r}`);t.snapshotDepth=Math.floor(s);continue}if("-s"===s){let r=e[n+1];n+=1,t.snapshotScope=r;continue}r.push(s)}return{command:r.shift()??null,positionals:r,flags:t}}(t);(n.flags.help||!n.command)&&(process.stdout.write(`agent-device <command> [args] [--json]
4
+ `)}let d=e.join(o.homedir(),".agent-device"),p=e.join(d,"daemon.json"),f=function(){let e=process.env.AGENT_DEVICE_DAEMON_TIMEOUT_MS;if(!e)return 6e4;let t=Number(e);return Number.isFinite(t)?Math.max(1e3,Math.floor(t)):6e4}();async function m(e){let t=await h(),r={...e,token:t.token};return await b(t,r)}async function h(){let t=y(),r=function(){try{let t=v();return JSON.parse(s.readFileSync(e.join(t,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}();if(t&&t.version===r&&await w(t))return t;t&&(t.version!==r||!await w(t))&&s.existsSync(p)&&s.unlinkSync(p),await g();let n=Date.now();for(;Date.now()-n<5e3;){let e=y();if(e&&await w(e))return e;await new Promise(e=>setTimeout(e,100))}throw new l("COMMAND_FAILED","Failed to start daemon",{infoPath:p,hint:"Run pnpm build, or delete ~/.agent-device/daemon.json if stale."})}function y(){if(!s.existsSync(p))return null;try{let e=JSON.parse(s.readFileSync(p,"utf8"));if(!e.port||!e.token)return null;return e}catch{return null}}async function w(e){return new Promise(t=>{let r=i.createConnection({host:"127.0.0.1",port:e.port},()=>{r.destroy(),t(!0)});r.on("error",()=>{t(!1)})})}async function g(){let t=v(),r=e.join(t,"dist","src","daemon.js"),n=e.join(t,"src","daemon.ts"),o=s.existsSync(r);if(!o&&!s.existsSync(n))throw new l("COMMAND_FAILED","Daemon entry not found",{distPath:r,srcPath:n});let i=o?[r]:["--experimental-strip-types",n];a(process.execPath,i)}async function b(e,t){return new Promise((r,n)=>{let a=i.createConnection({host:"127.0.0.1",port:e.port},()=>{a.write(`${JSON.stringify(t)}
5
+ `)}),s=setTimeout(()=>{a.destroy(),n(new l("COMMAND_FAILED","Daemon request timed out",{timeoutMs:f}))},f),o="";a.setEncoding("utf8"),a.on("data",e=>{let t=(o+=e).indexOf("\n");if(-1===t)return;let i=o.slice(0,t).trim();if(i)try{let e=JSON.parse(i);a.end(),clearTimeout(s),r(e)}catch(e){clearTimeout(s),n(e)}}),a.on("error",e=>{clearTimeout(s),n(e)})})}function v(){let r=e.dirname(t(import.meta.url)),n=r;for(let t=0;t<6;t+=1){let t=e.join(n,"package.json");if(s.existsSync(t))return n;n=e.dirname(n)}return r}async function $(t){let n=function(e){let t={json:!1,help:!1},r=[];for(let n=0;n<e.length;n+=1){let a=e[n];if("--json"===a){t.json=!0;continue}if("--help"===a||"-h"===a){t.help=!0;continue}if("--verbose"===a||"-v"===a){t.verbose=!0;continue}if("-i"===a){t.snapshotInteractiveOnly=!0;continue}if("-c"===a){t.snapshotCompact=!0;continue}if("--raw"===a){t.snapshotRaw=!0;continue}if("--no-record"===a){t.noRecord=!0;continue}if("--record-json"===a){t.recordJson=!0;continue}if("--user-installed"===a){t.appsFilter="user-installed";continue}if("--all"===a){t.appsFilter="all";continue}if("--metadata"===a){t.appsMetadata=!0;continue}if(a.startsWith("--backend")){let r=a.includes("=")?a.split("=")[1]:e[n+1];if(a.includes("=")||(n+=1),"ax"!==r&&"xctest"!==r)throw new l("INVALID_ARGS",`Invalid backend: ${r}`);t.snapshotBackend=r;continue}if(a.startsWith("--")){let[r,s]=a.split("="),o=s??e[n+1];switch(!s&&(n+=1),r){case"--platform":if("ios"!==o&&"android"!==o)throw new l("INVALID_ARGS",`Invalid platform: ${o}`);t.platform=o;break;case"--depth":{let e=Number(o);if(!Number.isFinite(e)||e<0)throw new l("INVALID_ARGS",`Invalid depth: ${o}`);t.snapshotDepth=Math.floor(e);break}case"--scope":t.snapshotScope=o;break;case"--device":t.device=o;break;case"--udid":t.udid=o;break;case"--serial":t.serial=o;break;case"--out":t.out=o;break;case"--session":t.session=o;break;case"--activity":t.activity=o;break;default:throw new l("INVALID_ARGS",`Unknown flag: ${r}`)}continue}if("-d"===a){let r=e[n+1];n+=1;let a=Number(r);if(!Number.isFinite(a)||a<0)throw new l("INVALID_ARGS",`Invalid depth: ${r}`);t.snapshotDepth=Math.floor(a);continue}if("-s"===a){let r=e[n+1];n+=1,t.snapshotScope=r;continue}r.push(a)}return{command:r.shift()??null,positionals:r,flags:t}}(t);(n.flags.help||!n.command)&&(process.stdout.write(`agent-device <command> [args] [--json]
6
6
 
7
7
  CLI to control iOS and Android devices for AI agents.
8
8
 
@@ -56,6 +56,7 @@ Flags:
56
56
  --device <name> Device name to target
57
57
  --udid <udid> iOS device UDID
58
58
  --serial <serial> Android device serial
59
+ --activity <component> Android activity to launch (package/Activity)
59
60
  --out <path> Output path for screenshots
60
61
  --session <name> Named session
61
62
  --verbose Stream daemon/runner logs
@@ -65,26 +66,26 @@ Flags:
65
66
  --user-installed Apps: list user-installed packages (Android only)
66
67
  --all Apps: list all packages (Android only)
67
68
 
68
- `),process.exit(+!n.flags.help));let{command:s,positionals:i,flags:d}=n,p=d.session??process.env.AGENT_DEVICE_SESSION??"default",f=d.verbose&&!d.json?function(){try{let t=e.join(a.homedir(),".agent-device","daemon.log"),r=0,n=!1,s=setInterval(()=>{if(n||!o.existsSync(t))return;let e=o.statSync(t);if(e.size<=r)return;let s=o.openSync(t,"r"),a=Buffer.alloc(e.size-r);o.readSync(s,a,0,a.length,r),o.closeSync(s),r=e.size,a.length>0&&process.stdout.write(a.toString("utf8"))},200);return()=>{n=!0,clearInterval(s)}}catch{return null}}():null;try{if("session"===s){let e=i[0]??"list";if("list"!==e)throw new l("INVALID_ARGS","session only supports list");let t=await m({session:p,command:"session_list",positionals:[],flags:{}});if(!t.ok)throw new l(t.error.code,t.error.message);d.json?c({success:!0,data:t.data??{}}):process.stdout.write(`${JSON.stringify(t.data??{},null,2)}
69
- `),f&&f();return}let e=await m({session:p,command:s,positionals:i,flags:d});if(e.ok){if(d.json){c({success:!0,data:e.data??{}}),f&&f();return}if("snapshot"===s){process.stdout.write(function(e,t={}){let r=e.nodes??[],n=!!e.truncated,s="string"==typeof e.appName?e.appName:void 0,o="string"==typeof e.appBundleId?e.appBundleId:void 0,a=[];s&&a.push(`Page: ${s}`),o&&a.push(`App: ${o}`);let i=`Snapshot: ${r.length} nodes${n?" (truncated)":""}`,l=a.length>0?`${a.join("\n")}
69
+ `),process.exit(+!n.flags.help));let{command:a,positionals:i,flags:d}=n,p=d.session??process.env.AGENT_DEVICE_SESSION??"default",f=d.verbose&&!d.json?function(){try{let t=e.join(o.homedir(),".agent-device","daemon.log"),r=0,n=!1,a=setInterval(()=>{if(n||!s.existsSync(t))return;let e=s.statSync(t);if(e.size<=r)return;let a=s.openSync(t,"r"),o=Buffer.alloc(e.size-r);s.readSync(a,o,0,o.length,r),s.closeSync(a),r=e.size,o.length>0&&process.stdout.write(o.toString("utf8"))},200);return()=>{n=!0,clearInterval(a)}}catch{return null}}():null;try{if("session"===a){let e=i[0]??"list";if("list"!==e)throw new l("INVALID_ARGS","session only supports list");let t=await m({session:p,command:"session_list",positionals:[],flags:{}});if(!t.ok)throw new l(t.error.code,t.error.message);d.json?c({success:!0,data:t.data??{}}):process.stdout.write(`${JSON.stringify(t.data??{},null,2)}
70
+ `),f&&f();return}let e=await m({session:p,command:a,positionals:i,flags:d});if(e.ok){if(d.json){c({success:!0,data:e.data??{}}),f&&f();return}if("snapshot"===a){process.stdout.write(function(e,t={}){let r=e.nodes??[],n=!!e.truncated,a="string"==typeof e.appName?e.appName:void 0,s="string"==typeof e.appBundleId?e.appBundleId:void 0,o=[];a&&o.push(`Page: ${a}`),s&&o.push(`App: ${s}`);let i=`Snapshot: ${r.length} nodes${n?" (truncated)":""}`,l=o.length>0?`${o.join("\n")}
70
71
  `:"";if(!Array.isArray(r)||0===r.length)return`${l}${i}
71
72
  `;if(t.raw){let e=r.map(e=>JSON.stringify(e));return`${l}${i}
72
73
  ${e.join("\n")}
73
- `}let c=[],u=[];for(let e of r){let t=e.depth??0;for(;c.length>0&&t<=c[c.length-1];)c.pop();let r=e.label?.trim()||e.value?.trim()||e.identifier?.trim()||"",n=function(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase();switch(t.startsWith("ax")&&(t=t.replace(/^ax/,"")),t){case"application":return"application";case"navigationbar":return"navigation-bar";case"tabbar":return"tab-bar";case"button":return"button";case"link":return"link";case"cell":return"cell";case"statictext":case"statictext":return"text";case"textfield":case"textfield":return"text-field";case"textview":case"textarea":return"text-view";case"switch":return"switch";case"slider":return"slider";case"image":return"image";case"table":return"list";case"collectionview":return"collection";case"searchfield":return"search";case"segmentedcontrol":return"segmented-control";case"group":return"group";case"window":return"window";case"checkbox":return"checkbox";case"radio":return"radio";case"menuitem":return"menu-item";case"toolbar":return"toolbar";case"scrollarea":return"scroll-area";case"table":return"table";default:return t||"element"}}(e.type??"Element"),s="group"===n&&!r;s&&c.push(t);let o=s?t:Math.max(0,t-c.length),a=" ".repeat(o),i=e.ref?`@${e.ref}`:"",l=[!1===e.enabled?"disabled":null].filter(Boolean).join(", "),d=l?` [${l}]`:"",p=r?` "${r}"`:"";if(s){u.push(`${a}${i} [${n}]${d}`.trimEnd());continue}u.push(`${a}${i} [${n}]${p}${d}`.trimEnd())}return`${l}${i}
74
+ `}let c=[],u=[];for(let e of r){let t=e.depth??0;for(;c.length>0&&t<=c[c.length-1];)c.pop();let r=e.label?.trim()||e.value?.trim()||e.identifier?.trim()||"",n=function(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase();switch(t.startsWith("ax")&&(t=t.replace(/^ax/,"")),t){case"application":return"application";case"navigationbar":return"navigation-bar";case"tabbar":return"tab-bar";case"button":return"button";case"link":return"link";case"cell":return"cell";case"statictext":case"statictext":return"text";case"textfield":case"textfield":return"text-field";case"textview":case"textarea":return"text-view";case"switch":return"switch";case"slider":return"slider";case"image":return"image";case"table":return"list";case"collectionview":return"collection";case"searchfield":return"search";case"segmentedcontrol":return"segmented-control";case"group":return"group";case"window":return"window";case"checkbox":return"checkbox";case"radio":return"radio";case"menuitem":return"menu-item";case"toolbar":return"toolbar";case"scrollarea":return"scroll-area";case"table":return"table";default:return t||"element"}}(e.type??"Element"),a="group"===n&&!r;a&&c.push(t);let s=a?t:Math.max(0,t-c.length),o=" ".repeat(s),i=e.ref?`@${e.ref}`:"",l=[!1===e.enabled?"disabled":null].filter(Boolean).join(", "),d=l?` [${l}]`:"",p=r?` "${r}"`:"";if(a){u.push(`${o}${i} [${n}]${d}`.trimEnd());continue}u.push(`${o}${i} [${n}]${p}${d}`.trimEnd())}return`${l}${i}
74
75
  ${u.join("\n")}
75
- `}(e.data??{},{raw:d.snapshotRaw})),f&&f();return}if("get"===s){let t=i[0];if("text"===t){let t=e.data?.text??"";process.stdout.write(`${t}
76
+ `}(e.data??{},{raw:d.snapshotRaw})),f&&f();return}if("get"===a){let t=i[0];if("text"===t){let t=e.data?.text??"";process.stdout.write(`${t}
76
77
  `),f&&f();return}if("attrs"===t){let t=e.data?.node??{};process.stdout.write(`${JSON.stringify(t,null,2)}
77
- `),f&&f();return}}if("find"===s){let t=e.data;if("string"==typeof t?.text){process.stdout.write(`${t.text}
78
+ `),f&&f();return}}if("find"===a){let t=e.data;if("string"==typeof t?.text){process.stdout.write(`${t.text}
78
79
  `),f&&f();return}if("boolean"==typeof t?.found){process.stdout.write(`Found: ${t.found}
79
80
  `),f&&f();return}if(t?.node){process.stdout.write(`${JSON.stringify(t.node,null,2)}
80
- `),f&&f();return}}if("click"===s){let t=e.data?.ref??"",r=e.data?.x,n=e.data?.y;t&&"number"==typeof r&&"number"==typeof n&&process.stdout.write(`Clicked @${t} (${r}, ${n})
81
- `),f&&f();return}if(e.data&&"object"==typeof e.data){let t=e.data;if("devices"===s){let e=(Array.isArray(t.devices)?t.devices:[]).map(e=>{let t=e?.name??e?.id??"unknown",r=e?.platform??"unknown",n=e?.kind?` ${e.kind}`:"",s="boolean"==typeof e?.booted?` booted=${e.booted}`:"";return`${t} (${r}${n})${s}`});process.stdout.write(`${e.join("\n")}
82
- `),f&&f();return}if("apps"===s){let e=(Array.isArray(t.apps)?t.apps:[]).map(e=>{if("string"==typeof e)return e;if(e&&"object"==typeof e){let t=e.bundleId??e.package,r=e.name??e.label;return r&&t?`${r} (${t})`:t&&"boolean"==typeof e.launchable?`${t} (launchable=${e.launchable})`:t?String(t):JSON.stringify(e)}return String(e)});process.stdout.write(`${e.join("\n")}
83
- `),f&&f();return}if("appstate"===s){let e=t?.platform,r=t?.appBundleId,n=t?.appName,s=t?.source,o=t?.package,a=t?.activity;if("ios"===e){process.stdout.write(`Foreground app: ${n??r}
81
+ `),f&&f();return}}if("click"===a){let t=e.data?.ref??"",r=e.data?.x,n=e.data?.y;t&&"number"==typeof r&&"number"==typeof n&&process.stdout.write(`Clicked @${t} (${r}, ${n})
82
+ `),f&&f();return}if(e.data&&"object"==typeof e.data){let t=e.data;if("devices"===a){let e=(Array.isArray(t.devices)?t.devices:[]).map(e=>{let t=e?.name??e?.id??"unknown",r=e?.platform??"unknown",n=e?.kind?` ${e.kind}`:"",a="boolean"==typeof e?.booted?` booted=${e.booted}`:"";return`${t} (${r}${n})${a}`});process.stdout.write(`${e.join("\n")}
83
+ `),f&&f();return}if("apps"===a){let e=(Array.isArray(t.apps)?t.apps:[]).map(e=>{if("string"==typeof e)return e;if(e&&"object"==typeof e){let t=e.bundleId??e.package,r=e.name??e.label;return r&&t?`${r} (${t})`:t&&"boolean"==typeof e.launchable?`${t} (launchable=${e.launchable})`:t?String(t):JSON.stringify(e)}return String(e)});process.stdout.write(`${e.join("\n")}
84
+ `),f&&f();return}if("appstate"===a){let e=t?.platform,r=t?.appBundleId,n=t?.appName,a=t?.source,s=t?.package,o=t?.activity;if("ios"===e){process.stdout.write(`Foreground app: ${n??r}
84
85
  `),r&&process.stdout.write(`Bundle: ${r}
85
- `),s&&process.stdout.write(`Source: ${s}
86
- `),f&&f();return}if("android"===e){process.stdout.write(`Foreground app: ${o??"unknown"}
87
- `),a&&process.stdout.write(`Activity: ${a}
86
+ `),a&&process.stdout.write(`Source: ${a}
87
+ `),f&&f();return}if("android"===e){process.stdout.write(`Foreground app: ${s??"unknown"}
88
+ `),o&&process.stdout.write(`Activity: ${o}
88
89
  `),f&&f();return}}}f&&f();return}throw new l(e.error.code,e.error.message,e.error.details)}catch(t){let e=r(t);if(d.json)c({success:!1,error:{code:e.code,message:e.message,details:e.details}});else if(u(e),d.verbose)try{let e=await import("node:fs"),t=await import("node:os"),r=(await import("node:path")).join(t.homedir(),".agent-device","daemon.log");if(e.existsSync(r)){let t=e.readFileSync(r,"utf8").split("\n"),n=t.slice(Math.max(0,t.length-200)).join("\n");n.trim().length>0&&process.stderr.write(`
89
90
  [daemon log]
90
91
  ${n}
@@ -1,9 +1,9 @@
1
- let e,t;import a from"node:crypto";import{isCancel as i,select as o}from"@clack/prompts";import{node_path as n,runCmdStreaming as r,promises as s,asAppError as l,fileURLToPath as c,runCmdBackground as u,node_fs as d,node_os as p,errors_AppError as f,runCmd as h,node_net as m,whichCmd as w}from"./861.js";async function g(e,t){let a=e,n=e=>e.toLowerCase().replace(/_/g," ").replace(/\s+/g," ").trim();if(t.platform&&(a=a.filter(e=>e.platform===t.platform)),t.udid){let e=a.find(e=>e.id===t.udid&&"ios"===e.platform);if(!e)throw new f("DEVICE_NOT_FOUND",`No iOS device with UDID ${t.udid}`);return e}if(t.serial){let e=a.find(e=>e.id===t.serial&&"android"===e.platform);if(!e)throw new f("DEVICE_NOT_FOUND",`No Android device with serial ${t.serial}`);return e}if(t.deviceName){let e=n(t.deviceName),i=a.find(t=>n(t.name)===e);if(!i)throw new f("DEVICE_NOT_FOUND",`No device named ${t.deviceName}`);return i}if(1===a.length)return a[0];if(0===a.length)throw new f("DEVICE_NOT_FOUND","No devices found",{selector:t});let r=a.filter(e=>e.booted);if(1===r.length)return r[0];if(!process.env.CI&&process.stdin.isTTY&&process.stdout.isTTY){let e=await o({message:"Multiple devices available. Choose a device to continue:",options:(r.length>0?r:a).map(e=>({label:`${e.name} (${e.platform}${e.kind?`, ${e.kind}`:""}${e.booted?", booted":""})`,value:e.id}))});if(i(e))throw new f("INVALID_ARGS","Device selection cancelled");if(e){let t=a.find(t=>t.id===e);if(t)return t}}return r[0]??a[0]}async function v(){if(!await w("adb"))throw new f("TOOL_MISSING","adb not found in PATH");let e=(await h("adb",["devices","-l"])).stdout.split("\n").map(e=>e.trim()),t=[];for(let a of e){if(!a||a.startsWith("List of devices"))continue;let e=a.split(/\s+/),i=e[0];if("device"!==e[1])continue;let o=(e.find(e=>e.startsWith("model:"))??"").replace("model:","").replace(/_/g," ").trim()||i;if(i.startsWith("emulator-")){let e=await h("adb",["-s",i,"emu","avd","name"],{allowFailure:!0}),t=e.stdout.trim();0===e.exitCode&&t&&(o=t.replace(/_/g," "))}let n=await y(i);t.push({platform:"android",id:i,name:o,kind:i.startsWith("emulator-")?"emulator":"device",booted:n})}return t}async function y(e){try{let t=await h("adb",["-s",e,"shell","getprop","sys.boot_completed"],{allowFailure:!0});return"1"===t.stdout.trim()}catch{return!1}}async function I(e,t=6e4){let a=Date.now();for(;Date.now()-a<t;){if(await y(e))return;await new Promise(e=>setTimeout(e,1e3))}throw new f("COMMAND_FAILED","Android device did not finish booting in time",{serial:e,timeoutMs:t})}async function N(e,t={}){let a,i=t.attempts??3,o=t.baseDelayMs??200,n=t.maxDelayMs??2e3,r=t.jitter??.2;for(let s=1;s<=i;s+=1)try{return await e()}catch(l){if(a=l,s>=i||t.shouldRetry&&!t.shouldRetry(l,s))break;let e=function(e,t,a,i){let o=Math.min(t,e*2**(i-1));return Math.max(0,o+o*a*(2*Math.random()-1))}(o,n,r,s);await function(e){return new Promise(t=>setTimeout(t,e))}(e)}if(a)throw a;throw new f("COMMAND_FAILED","retry failed")}let A={settings:{type:"intent",value:"android.settings.SETTINGS"}};function b(e,t){return["-s",e.id,...t]}async function S(e,t){let a=t.trim();if(a.includes("."))return{type:"package",value:a};let i=A[a.toLowerCase()];if(i)return i;let o=(await h("adb",b(e,["shell","pm","list","packages"]))).stdout.split("\n").map(e=>e.replace("package:","").trim()).filter(Boolean).filter(e=>e.toLowerCase().includes(a.toLowerCase()));if(1===o.length)return{type:"package",value:o[0]};if(o.length>1)throw new f("INVALID_ARGS",`Multiple packages matched "${t}"`,{matches:o});throw new f("APP_NOT_INSTALLED",`No package found matching "${t}"`)}async function D(e,t="launchable"){if("launchable"===t){let t=await h("adb",b(e,["shell","cmd","package","query-activities","--brief","-a","android.intent.action.MAIN","-c","android.intent.category.LAUNCHER"]),{allowFailure:!0});if(0===t.exitCode&&t.stdout.trim().length>0){let e=new Set;for(let a of t.stdout.split("\n")){let t=a.trim();if(!t)continue;let i=t.split(/\s+/)[0],o=i.includes("/")?i.split("/")[0]:i;o&&e.add(o)}if(e.size>0)return Array.from(e)}}return(await h("adb",b(e,"user-installed"===t?["shell","pm","list","packages","-3"]:["shell","pm","list","packages"]))).stdout.split("\n").map(e=>e.replace("package:","").trim()).filter(Boolean)}async function k(e,t="launchable"){let a=await D(e,t),i=new Set("launchable"===t?a:await D(e,"launchable"));return a.map(e=>({package:e,launchable:i.has(e)}))}async function P(e){let t=await O(e,[["shell","dumpsys","window","windows"],["shell","dumpsys","window"]]);if(t)return t;let a=await O(e,[["shell","dumpsys","activity","activities"],["shell","dumpsys","activity"]]);return a||{}}async function O(e,t){for(let a of t){let t=function(e){for(let t of[/mCurrentFocus=Window\{[^}]*\s([\w.]+)\/([\w.$]+)/,/mFocusedApp=AppWindowToken\{[^}]*\s([\w.]+)\/([\w.$]+)/,/mResumedActivity:.*?\s([\w.]+)\/([\w.$]+)/,/ResumedActivity:.*?\s([\w.]+)\/([\w.$]+)/]){let a=t.exec(e);if(a)return{package:a[1],activity:a[2]}}return null}((await h("adb",b(e,a),{allowFailure:!0})).stdout??"");if(t)return t}return null}async function x(e,t){e.booted||await I(e.id);let a=await S(e,t);"intent"===a.type?await h("adb",b(e,["shell","am","start","-a",a.value])):await h("adb",b(e,["shell","monkey","-p",a.value,"-c","android.intent.category.LAUNCHER","1"]))}async function _(e){e.booted||await I(e.id)}async function L(e,t){if("settings"===t.trim().toLowerCase())return void await h("adb",b(e,["shell","am","force-stop","com.android.settings"]));let a=await S(e,t);if("intent"===a.type)throw new f("INVALID_ARGS","Close requires a package name, not an intent");await h("adb",b(e,["shell","am","force-stop",a.value]))}async function E(e,t,a){await h("adb",b(e,["shell","input","tap",String(t),String(a)]))}async function R(e){await h("adb",b(e,["shell","input","keyevent","4"]))}async function C(e){await h("adb",b(e,["shell","input","keyevent","3"]))}async function M(e){await h("adb",b(e,["shell","input","keyevent","187"]))}async function T(e,t,a,i=800){await h("adb",b(e,["shell","input","swipe",String(t),String(a),String(t),String(a),String(i)]))}async function F(e,t){let a=t.replace(/ /g,"%s");await h("adb",b(e,["shell","input","text",a]))}async function B(e,t,a){await E(e,t,a)}async function $(e,t,a,i){await B(e,t,a),await F(e,i)}async function U(e,t,a=.6){let{width:i,height:o}=await W(e),n=Math.floor(i*a),r=Math.floor(o*a),s=Math.floor(i/2),l=Math.floor(o/2),c=s,u=l,d=s,p=l;switch(t){case"up":u=l-Math.floor(r/2),p=l+Math.floor(r/2);break;case"down":u=l+Math.floor(r/2),p=l-Math.floor(r/2);break;case"left":c=s-Math.floor(n/2),d=s+Math.floor(n/2);break;case"right":c=s+Math.floor(n/2),d=s-Math.floor(n/2);break;default:throw new f("INVALID_ARGS",`Unknown direction: ${t}`)}await h("adb",b(e,["shell","input","swipe",String(c),String(u),String(d),String(p),"300"]))}async function V(e,t){for(let a=0;a<8;a+=1){let a="";try{a=await X(e)}catch(t){let e=t instanceof Error?t.message:String(t);throw new f("UNSUPPORTED_OPERATION",`uiautomator dump failed: ${e}`)}if(function(e,t){let a=t.toLowerCase(),i=/<node[^>]+>/g,o=i.exec(e);for(;o;){let t=o[0],n=/text="([^"]*)"/.exec(t),r=/content-desc="([^"]*)"/.exec(t),s=(n?.[1]??"").toLowerCase(),l=(r?.[1]??"").toLowerCase();if(s.includes(a)||l.includes(a)){let e=/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/.exec(t);if(e){let t=Number(e[1]),a=Number(e[2]);return{x:Math.floor((t+Number(e[3]))/2),y:Math.floor((a+Number(e[4]))/2)}}return{x:0,y:0}}o=i.exec(e)}return null}(a,t))return;await U(e,"down",.5)}throw new f("COMMAND_FAILED",`Could not find element containing "${t}" after scrolling`)}async function j(e,t){let a=await h("adb",b(e,["exec-out","screencap","-p"]),{binaryStdout:!0});if(!a.stdoutBuffer)throw new f("COMMAND_FAILED","Failed to capture screenshot");await s.writeFile(t,a.stdoutBuffer)}async function G(e,t,a){let i=t.toLowerCase(),o=function(e){let t=e.toLowerCase();if("on"===t||"true"===t||"1"===t)return!0;if("off"===t||"false"===t||"0"===t)return!1;throw new f("INVALID_ARGS",`Invalid setting state: ${e}`)}(a);switch(i){case"wifi":return void await h("adb",b(e,["shell","svc","wifi",o?"enable":"disable"]));case"airplane":await h("adb",b(e,["shell","settings","put","global","airplane_mode_on",o?"1":"0"])),await h("adb",b(e,["shell","am","broadcast","-a","android.intent.action.AIRPLANE_MODE","--ez","state",o?"true":"false"]));return;case"location":return void await h("adb",b(e,["shell","settings","put","secure","location_mode",o?"3":"0"]));default:throw new f("INVALID_ARGS",`Unsupported setting: ${t}`)}}async function q(e,t={}){return function(e,t,a){let i=function(e){let t={type:null,label:null,value:null,identifier:null,depth:-1,children:[]},a=[t],i=/<node\b[^>]*>|<\/node>/g,o=i.exec(e);for(;o;){let t=o[0];if(t.startsWith("</node")){a.length>1&&a.pop(),o=i.exec(e);continue}let n=function(e){let t=t=>{let a=RegExp(`${t}="([^"]*)"`).exec(e);return a?a[1]:null},a=e=>{let a=t(e);if(null!==a)return"true"===a};return{text:t("text"),desc:t("content-desc"),resourceId:t("resource-id"),className:t("class"),bounds:t("bounds"),clickable:a("clickable"),enabled:a("enabled"),focusable:a("focusable")}}(t),r=function(e){if(!e)return;let t=/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/.exec(e);if(!t)return;let a=Number(t[1]),i=Number(t[2]);return{x:a,y:i,width:Math.max(0,Number(t[3])-a),height:Math.max(0,Number(t[4])-i)}}(n.bounds),s=a[a.length-1],l={type:n.className,label:n.text||n.desc,value:n.text,identifier:n.resourceId,rect:r,enabled:n.enabled,hittable:n.clickable??n.focusable,depth:s.depth+1,parentIndex:void 0,children:[]};s.children.push(l),t.endsWith("/>")||a.push(l),o=i.exec(e)}return t}(e),o=[],n=!1,r=a.depth??1/0,s=a.scope?function(e,t){let a=t.toLowerCase(),i=[...e.children];for(;i.length>0;){let e=i.shift(),t=e.label?.toLowerCase()??"",o=e.value?.toLowerCase()??"",n=e.identifier?.toLowerCase()??"";if(t.includes(a)||o.includes(a)||n.includes(a))return e;i.push(...e.children)}return null}(i,a.scope):null,l=s?[s]:i.children,c=(e,t)=>{if(o.length>=800){n=!0;return}if(!(t>r)){for(let i of((a.raw||function(e,t){if(t.interactiveOnly)return!!e.hittable;if(t.compact){let t=!!(e.label&&e.label.trim().length>0),a=!!(e.identifier&&e.identifier.trim().length>0);return t||a||!!e.hittable}return!0}(e,a))&&o.push({index:o.length,type:e.type??void 0,label:e.label??void 0,value:e.value??void 0,identifier:e.identifier??void 0,rect:e.rect,enabled:e.enabled,hittable:e.hittable,depth:t,parentIndex:e.parentIndex}),e.children))if(c(i,t+1),n)return}};for(let e of l)if(c(e,0),n)break;return n?{nodes:o,truncated:n}:{nodes:o}}(await X(e),800,t)}async function J(){if(!await w("adb"))throw new f("TOOL_MISSING","adb not found in PATH")}async function W(e){let t=(await h("adb",b(e,["shell","wm","size"]))).stdout.match(/Physical size:\s*(\d+)x(\d+)/);if(!t)throw new f("COMMAND_FAILED","Unable to read screen size");return{width:Number(t[1]),height:Number(t[2])}}async function X(e){return N(()=>z(e),{shouldRetry:H})}async function z(e){return await h("adb",b(e,["shell","uiautomator","dump","/sdcard/window_dump.xml"])),(await h("adb",b(e,["shell","cat","/sdcard/window_dump.xml"]))).stdout}function H(e){if(!(e instanceof f)||"COMMAND_FAILED"!==e.code)return!1;let t=`${e.details?.stderr??""}`.toLowerCase();return!!(t.includes("device offline")||t.includes("device not found")||t.includes("transport error")||t.includes("connection reset")||t.includes("broken pipe")||t.includes("timed out"))}async function Z(){if("darwin"!==process.platform)throw new f("UNSUPPORTED_PLATFORM","iOS tools are only available on macOS");if(!await w("xcrun"))throw new f("TOOL_MISSING","xcrun not found in PATH");let e=[],t=await h("xcrun",["simctl","list","devices","-j"]);try{let a=JSON.parse(t.stdout);for(let t of Object.values(a.devices))for(let a of t)a.isAvailable&&e.push({platform:"ios",id:a.udid,name:a.name,kind:"simulator",booted:"Booted"===a.state})}catch(e){throw new f("COMMAND_FAILED","Failed to parse simctl devices JSON",void 0,e)}if(await w("xcrun"))try{let t=await h("xcrun",["devicectl","list","devices","--json"]);for(let a of JSON.parse(t.stdout).devices??[])a.platform?.toLowerCase().includes("ios")&&e.push({platform:"ios",id:a.identifier,name:a.name,kind:"device",booted:!0})}catch{}return e}let Y={settings:"com.apple.Preferences"};async function K(e,t){let a=t.trim();if(a.includes("."))return a;let i=Y[a.toLowerCase()];if(i)return i;if("simulator"===e.kind){let i=(await ep(e)).filter(e=>e.name.toLowerCase()===a.toLowerCase());if(1===i.length)return i[0].bundleId;if(i.length>1)throw new f("INVALID_ARGS",`Multiple apps matched "${t}"`,{matches:i})}throw new f("APP_NOT_INSTALLED",`No app found matching "${t}"`)}async function Q(e,t){let a=await K(e,t);if("simulator"===e.kind){await ef(e),await h("open",["-a","Simulator"],{allowFailure:!0}),await h("xcrun",["simctl","launch",e.id,a]);return}await h("xcrun",["devicectl","device","process","launch","--device",e.id,a])}async function ee(e){"simulator"!==e.kind||"Booted"!==await eh(e.id)&&(await ef(e),await h("open",["-a","Simulator"],{allowFailure:!0}))}async function et(e,t){let a=await K(e,t);if("simulator"===e.kind){await ef(e);let t=await h("xcrun",["simctl","terminate",e.id,a],{allowFailure:!0});if(0!==t.exitCode){if(t.stderr.toLowerCase().includes("found nothing to terminate"))return;throw new f("COMMAND_FAILED",`xcrun exited with code ${t.exitCode}`,{cmd:"xcrun",args:["simctl","terminate",e.id,a],stdout:t.stdout,stderr:t.stderr,exitCode:t.exitCode})}return}await h("xcrun",["devicectl","device","process","terminate","--device",e.id,a])}async function ea(e,t,a){throw ed(e,"press"),new f("UNSUPPORTED_OPERATION","simctl io tap is not available; use the XCTest runner for input")}async function ei(e,t,a,i=800){throw ed(e,"long-press"),new f("UNSUPPORTED_OPERATION","long-press is not supported on iOS simulators without XCTest runner support")}async function eo(e,t,a){await ea(e,t,a)}async function en(e,t){throw ed(e,"type"),new f("UNSUPPORTED_OPERATION","simctl io keyboard is not available; use the XCTest runner for input")}async function er(e,t,a,i){await eo(e,t,a),await en(e,i)}async function es(e,t,a=.6){throw ed(e,"scroll"),new f("UNSUPPORTED_OPERATION","simctl io swipe is not available; use the XCTest runner for input")}async function el(e){throw new f("UNSUPPORTED_OPERATION",`scrollintoview is not supported on iOS without UI automation (${e})`)}async function ec(e,t){if("simulator"===e.kind){await ef(e),await h("xcrun",["simctl","io",e.id,"screenshot",t]);return}await h("xcrun",["devicectl","device","screenshot","--device",e.id,t])}async function eu(e,t,a,i){ed(e,"settings"),await ef(e);let o=t.toLowerCase(),n=function(e){let t=e.toLowerCase();if("on"===t||"true"===t||"1"===t)return!0;if("off"===t||"false"===t||"0"===t)return!1;throw new f("INVALID_ARGS",`Invalid setting state: ${e}`)}(a);switch(o){case"wifi":return void await h("xcrun",["simctl","status_bar",e.id,"override","--wifiMode",n?"active":"failed"]);case"airplane":n?await h("xcrun",["simctl","status_bar",e.id,"override","--dataNetwork","hide","--wifiMode","failed","--wifiBars","0","--cellularMode","failed","--cellularBars","0","--operatorName",""]):await h("xcrun",["simctl","status_bar",e.id,"clear"]);return;case"location":if(!i)throw new f("INVALID_ARGS","location setting requires an active app in session");await h("xcrun",["simctl","privacy",e.id,n?"grant":"revoke","location",i]);return;default:throw new f("INVALID_ARGS",`Unsupported setting: ${t}`)}}function ed(e,t){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION",`${t} is only supported on iOS simulators in v1`)}async function ep(e){let t=(await h("xcrun",["simctl","listapps",e.id],{allowFailure:!0})).stdout.trim();if(!t)return[];let a=null;if(t.startsWith("{"))try{a=JSON.parse(t)}catch{a=null}if(!a&&t.startsWith("{"))try{let e=await h("plutil",["-convert","json","-o","-","-"],{allowFailure:!0,stdin:t});0===e.exitCode&&e.stdout.trim().startsWith("{")&&(a=JSON.parse(e.stdout))}catch{a=null}return a?Object.entries(a).map(([e,t])=>({bundleId:e,name:t.CFBundleDisplayName??t.CFBundleName??e})):[]}async function ef(e){"simulator"!==e.kind||"Booted"!==await eh(e.id)&&(await h("xcrun",["simctl","boot",e.id],{allowFailure:!0}),await h("xcrun",["simctl","bootstatus",e.id,"-b"],{allowFailure:!0}))}async function eh(e){let t=await h("xcrun",["simctl","list","devices","-j"],{allowFailure:!0});if(0!==t.exitCode)return null;try{let a=JSON.parse(t.stdout);for(let t of Object.values(a.devices??{})){let a=t.find(t=>t.udid===e);if(a)return a.state}}catch{}return null}let em=new Map;async function ew(e,t,a={}){var i;return"snapshot"===(i=t.command)||"findText"===i||"listTappables"===i||"alert"===i?N(()=>eg(e,t,a),{shouldRetry:eS}):eg(e,t,a)}async function eg(e,t,a={}){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","iOS runner only supports simulators in v1");try{let i=await eI(e,a),o=await eD(e,i.port,t,a.logPath),n=await o.text(),r={};try{r=JSON.parse(n)}catch{throw new f("COMMAND_FAILED","Invalid runner response",{text:n})}if(!r.ok)throw new f("COMMAND_FAILED",r.error?.message??"Runner error",{runner:r,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:a.logPath});return r.data??{}}catch(o){let i=o instanceof f?o:new f("COMMAND_FAILED",String(o));if("COMMAND_FAILED"===i.code&&"string"==typeof i.message&&i.message.includes("Runner did not accept connection")){await ev(e.id);let i=await eI(e,a),o=await eD(e,i.port,t,a.logPath),n=await o.text(),r={};try{r=JSON.parse(n)}catch{throw new f("COMMAND_FAILED","Invalid runner response",{text:n})}if(!r.ok)throw new f("COMMAND_FAILED",r.error?.message??"Runner error",{runner:r,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:a.logPath});return r.data??{}}throw o}}async function ev(e){let t=em.get(e);if(t){try{await eD(t.device,t.port,{command:"shutdown"})}catch{}try{await t.testPromise}catch{}ex(t.xctestrunPath),ex(t.jsonPath),em.delete(e)}}async function ey(e){await h("xcrun",["simctl","bootstatus",e,"-b"],{allowFailure:!0})}async function eI(e,t){let a=em.get(e.id);if(a)return a;await ey(e.id);let i=await eN(e.id,t),o=await eP(),n=process.env.AGENT_DEVICE_RUNNER_TIMEOUT??"300",{xctestrunPath:s,jsonPath:l}=await eO(i,{AGENT_DEVICE_RUNNER_PORT:String(o),AGENT_DEVICE_RUNNER_TIMEOUT:n},`session-${e.id}-${o}`),c=r("xcodebuild",["test-without-building","-only-testing","AgentDeviceRunnerUITests/RunnerTests/testCommand","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-xctestrun",s,"-destination",`platform=iOS Simulator,id=${e.id}`],{onStdoutChunk:e=>{eb(e,t.logPath,t.traceLogPath,t.verbose)},onStderrChunk:e=>{eb(e,t.logPath,t.traceLogPath,t.verbose)},allowFailure:!0,env:{...process.env,AGENT_DEVICE_RUNNER_PORT:String(o),AGENT_DEVICE_RUNNER_TIMEOUT:n}}),u={device:e,deviceId:e.id,port:o,xctestrunPath:s,jsonPath:l,testPromise:c};return em.set(e.id,u),u}async function eN(e,t){let a,i=n.join(p.homedir(),".agent-device","ios-runner"),o=n.join(i,"derived");if((a=process.env.AGENT_DEVICE_IOS_CLEAN_DERIVED)&&["1","true","yes","on"].includes(a.toLowerCase()))try{d.rmSync(o,{recursive:!0,force:!0})}catch{}let s=eA(o);if(s)return s;let l=function(){let e=n.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=n.join(t,"package.json");if(d.existsSync(e))return t;t=n.dirname(t)}return e}(),u=n.join(l,"ios-runner","AgentDeviceRunner","AgentDeviceRunner.xcodeproj");if(!d.existsSync(u))throw new f("COMMAND_FAILED","iOS runner project not found",{projectPath:u});try{await r("xcodebuild",["build-for-testing","-project",u,"-scheme","AgentDeviceRunner","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-destination",`platform=iOS Simulator,id=${e}`,"-derivedDataPath",o],{onStdoutChunk:e=>{eb(e,t.logPath,t.traceLogPath,t.verbose)},onStderrChunk:e=>{eb(e,t.logPath,t.traceLogPath,t.verbose)}})}catch(a){let e=a instanceof f?a:new f("COMMAND_FAILED",String(a));throw new f("COMMAND_FAILED","xcodebuild build-for-testing failed",{error:e.message,details:e.details,logPath:t.logPath})}let h=eA(o);if(!h)throw new f("COMMAND_FAILED","Failed to locate .xctestrun after build");return h}function eA(e){if(!d.existsSync(e))return null;let t=[],a=[e];for(;a.length>0;){let e=a.pop();for(let i of d.readdirSync(e,{withFileTypes:!0})){let o=n.join(e,i.name);if(i.isDirectory()){a.push(o);continue}if(i.isFile()&&i.name.endsWith(".xctestrun"))try{let e=d.statSync(o);t.push({path:o,mtimeMs:e.mtimeMs})}catch{}}}return 0===t.length?null:(t.sort((e,t)=>t.mtimeMs-e.mtimeMs),t[0]?.path??null)}function eb(e,t,a,i){t&&d.appendFileSync(t,e),a&&d.appendFileSync(a,e),i&&process.stderr.write(e)}function eS(e){if(!(e instanceof f)||"COMMAND_FAILED"!==e.code)return!1;let t=`${e.message??""}`.toLowerCase();return!!(t.includes("runner did not accept connection")||t.includes("fetch failed")||t.includes("econnrefused")||t.includes("socket hang up"))}async function eD(e,t,a,i){let o=Date.now(),n=null;for(;Date.now()-o<15e3;)try{return await fetch(`http://127.0.0.1:${t}/command`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})}catch(e){n=e,await new Promise(e=>setTimeout(e,100))}if("simulator"===e.kind){let i=await ek(e.id,t,a);return new Response(i.body,{status:i.status})}throw new f("COMMAND_FAILED","Runner did not accept connection",{port:t,logPath:i,lastError:n?String(n):void 0})}async function ek(e,t,a){let i=JSON.stringify(a),o=await h("xcrun",["simctl","spawn",e,"/usr/bin/curl","-s","-X","POST","-H","Content-Type: application/json","--data",i,`http://127.0.0.1:${t}/command`],{allowFailure:!0}),n=o.stdout;if(0!==o.exitCode)throw new f("COMMAND_FAILED","Runner did not accept connection (simctl spawn)",{port:t,stdout:o.stdout,stderr:o.stderr,exitCode:o.exitCode});return{status:200,body:n}}async function eP(){return await new Promise((e,t)=>{let a=m.createServer();a.listen(0,"127.0.0.1",()=>{let i=a.address();a.close(),"object"==typeof i&&i?.port?e(i.port):t(new f("COMMAND_FAILED","Failed to allocate port"))}),a.on("error",t)})}async function eO(e,t,a){let i,o=n.dirname(e),r=a.replace(/[^a-zA-Z0-9._-]/g,"_"),s=n.join(o,`AgentDeviceRunner.env.${r}.json`),l=n.join(o,`AgentDeviceRunner.env.${r}.xctestrun`),c=await h("plutil",["-convert","json","-o","-",e],{allowFailure:!0});if(0!==c.exitCode||!c.stdout.trim())throw new f("COMMAND_FAILED","Failed to read xctestrun plist",{xctestrunPath:e,stderr:c.stderr});try{i=JSON.parse(c.stdout)}catch(t){throw new f("COMMAND_FAILED","Failed to parse xctestrun JSON",{xctestrunPath:e,error:String(t)})}let u=e=>{e.EnvironmentVariables={...e.EnvironmentVariables??{},...t},e.UITestEnvironmentVariables={...e.UITestEnvironmentVariables??{},...t},e.UITargetAppEnvironmentVariables={...e.UITargetAppEnvironmentVariables??{},...t},e.TestingEnvironmentVariables={...e.TestingEnvironmentVariables??{},...t}},p=i.TestConfigurations;if(Array.isArray(p))for(let e of p){if(!e||"object"!=typeof e)continue;let t=e.TestTargets;if(Array.isArray(t))for(let e of t)e&&"object"==typeof e&&u(e)}for(let[e,t]of Object.entries(i))t&&"object"==typeof t&&t.TestBundlePath&&(u(t),i[e]=t);d.writeFileSync(s,JSON.stringify(i,null,2));let m=await h("plutil",["-convert","xml1","-o",l,s],{allowFailure:!0});if(0!==m.exitCode)throw new f("COMMAND_FAILED","Failed to write xctestrun plist",{tmpXctestrunPath:l,stderr:m.stderr});return{xctestrunPath:l,jsonPath:s}}function ex(e){try{d.existsSync(e)&&d.unlinkSync(e)}catch{}}async function e_(e,t={}){let a,i;if("ios"!==e.platform||"simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","AX snapshot is only supported on iOS simulators");let o=await eL(),n=await N(async()=>{var e,a;let i,n,r,s=await h(o,[],{allowFailure:!0});if(t.traceLogPath&&(e=t.traceLogPath,i=((a=s).stdout??"").toString(),n=(a.stderr??"").toString(),r=`
2
- [axsnapshot] exit=${a.exitCode} stdoutBytes=${i.length} stderrBytes=${n.length}
3
- `,d.appendFileSync(e,r),(0!==a.exitCode||n.length>0)&&(n.length>0&&d.appendFileSync(e,`${n}
1
+ let e,t;import a from"node:crypto";import{isCancel as i,select as n}from"@clack/prompts";import{node_path as r,runCmdStreaming as o,promises as s,asAppError as l,fileURLToPath as c,runCmdBackground as u,node_fs as d,node_os as p,errors_AppError as f,runCmd as h,node_net as m,whichCmd as w}from"./861.js";async function g(e,t){let a=e,r=e=>e.toLowerCase().replace(/_/g," ").replace(/\s+/g," ").trim();if(t.platform&&(a=a.filter(e=>e.platform===t.platform)),t.udid){let e=a.find(e=>e.id===t.udid&&"ios"===e.platform);if(!e)throw new f("DEVICE_NOT_FOUND",`No iOS device with UDID ${t.udid}`);return e}if(t.serial){let e=a.find(e=>e.id===t.serial&&"android"===e.platform);if(!e)throw new f("DEVICE_NOT_FOUND",`No Android device with serial ${t.serial}`);return e}if(t.deviceName){let e=r(t.deviceName),i=a.find(t=>r(t.name)===e);if(!i)throw new f("DEVICE_NOT_FOUND",`No device named ${t.deviceName}`);return i}if(1===a.length)return a[0];if(0===a.length)throw new f("DEVICE_NOT_FOUND","No devices found",{selector:t});let o=a.filter(e=>e.booted);if(1===o.length)return o[0];if(!process.env.CI&&process.stdin.isTTY&&process.stdout.isTTY){let e=await n({message:"Multiple devices available. Choose a device to continue:",options:(o.length>0?o:a).map(e=>({label:`${e.name} (${e.platform}${e.kind?`, ${e.kind}`:""}${e.booted?", booted":""})`,value:e.id}))});if(i(e))throw new f("INVALID_ARGS","Device selection cancelled");if(e){let t=a.find(t=>t.id===e);if(t)return t}}return o[0]??a[0]}async function v(){if(!await w("adb"))throw new f("TOOL_MISSING","adb not found in PATH");let e=(await h("adb",["devices","-l"])).stdout.split("\n").map(e=>e.trim()),t=[];for(let a of e){if(!a||a.startsWith("List of devices"))continue;let e=a.split(/\s+/),i=e[0];if("device"!==e[1])continue;let n=(e.find(e=>e.startsWith("model:"))??"").replace("model:","").replace(/_/g," ").trim()||i;if(i.startsWith("emulator-")){let e=await h("adb",["-s",i,"emu","avd","name"],{allowFailure:!0}),t=e.stdout.trim();0===e.exitCode&&t&&(n=t.replace(/_/g," "))}let r=await y(i);t.push({platform:"android",id:i,name:n,kind:i.startsWith("emulator-")?"emulator":"device",booted:r})}return t}async function y(e){try{let t=await h("adb",["-s",e,"shell","getprop","sys.boot_completed"],{allowFailure:!0});return"1"===t.stdout.trim()}catch{return!1}}async function I(e,t=6e4){let a=Date.now();for(;Date.now()-a<t;){if(await y(e))return;await new Promise(e=>setTimeout(e,1e3))}throw new f("COMMAND_FAILED","Android device did not finish booting in time",{serial:e,timeoutMs:t})}async function N(e,t={}){let a,i=t.attempts??3,n=t.baseDelayMs??200,r=t.maxDelayMs??2e3,o=t.jitter??.2;for(let s=1;s<=i;s+=1)try{return await e()}catch(l){if(a=l,s>=i||t.shouldRetry&&!t.shouldRetry(l,s))break;let e=function(e,t,a,i){let n=Math.min(t,e*2**(i-1));return Math.max(0,n+n*a*(2*Math.random()-1))}(n,r,o,s);await function(e){return new Promise(t=>setTimeout(t,e))}(e)}if(a)throw a;throw new f("COMMAND_FAILED","retry failed")}let A={settings:{type:"intent",value:"android.settings.SETTINGS"}};function b(e,t){return["-s",e.id,...t]}async function S(e,t){let a=t.trim();if(a.includes("."))return{type:"package",value:a};let i=A[a.toLowerCase()];if(i)return i;let n=(await h("adb",b(e,["shell","pm","list","packages"]))).stdout.split("\n").map(e=>e.replace("package:","").trim()).filter(Boolean).filter(e=>e.toLowerCase().includes(a.toLowerCase()));if(1===n.length)return{type:"package",value:n[0]};if(n.length>1)throw new f("INVALID_ARGS",`Multiple packages matched "${t}"`,{matches:n});throw new f("APP_NOT_INSTALLED",`No package found matching "${t}"`)}async function D(e,t="launchable"){if("launchable"===t){let t=await h("adb",b(e,["shell","cmd","package","query-activities","--brief","-a","android.intent.action.MAIN","-c","android.intent.category.LAUNCHER"]),{allowFailure:!0});if(0===t.exitCode&&t.stdout.trim().length>0){let e=new Set;for(let a of t.stdout.split("\n")){let t=a.trim();if(!t)continue;let i=t.split(/\s+/)[0],n=i.includes("/")?i.split("/")[0]:i;n&&e.add(n)}if(e.size>0)return Array.from(e)}}return(await h("adb",b(e,"user-installed"===t?["shell","pm","list","packages","-3"]:["shell","pm","list","packages"]))).stdout.split("\n").map(e=>e.replace("package:","").trim()).filter(Boolean)}async function k(e,t="launchable"){let a=await D(e,t),i=new Set("launchable"===t?a:await D(e,"launchable"));return a.map(e=>({package:e,launchable:i.has(e)}))}async function P(e){let t=await O(e,[["shell","dumpsys","window","windows"],["shell","dumpsys","window"]]);if(t)return t;let a=await O(e,[["shell","dumpsys","activity","activities"],["shell","dumpsys","activity"]]);return a||{}}async function O(e,t){for(let a of t){let t=function(e){for(let t of[/mCurrentFocus=Window\{[^}]*\s([\w.]+)\/([\w.$]+)/,/mFocusedApp=AppWindowToken\{[^}]*\s([\w.]+)\/([\w.$]+)/,/mResumedActivity:.*?\s([\w.]+)\/([\w.$]+)/,/ResumedActivity:.*?\s([\w.]+)\/([\w.$]+)/]){let a=t.exec(e);if(a)return{package:a[1],activity:a[2]}}return null}((await h("adb",b(e,a),{allowFailure:!0})).stdout??"");if(t)return t}return null}async function x(e,t,a){e.booted||await I(e.id);let i=await S(e,t);if("intent"===i.type){if(a)throw new f("INVALID_ARGS","Activity override requires a package name, not an intent");await h("adb",b(e,["shell","am","start","-a",i.value]));return}if(a){let t=a.includes("/")?a:`${i.value}/${a.startsWith(".")?a:`.${a}`}`;await h("adb",b(e,["shell","am","start","-a","android.intent.action.MAIN","-c","android.intent.category.DEFAULT","-c","android.intent.category.LAUNCHER","-n",t]));return}await h("adb",b(e,["shell","am","start","-a","android.intent.action.MAIN","-c","android.intent.category.DEFAULT","-c","android.intent.category.LAUNCHER","-p",i.value]))}async function L(e){e.booted||await I(e.id)}async function _(e,t){if("settings"===t.trim().toLowerCase())return void await h("adb",b(e,["shell","am","force-stop","com.android.settings"]));let a=await S(e,t);if("intent"===a.type)throw new f("INVALID_ARGS","Close requires a package name, not an intent");await h("adb",b(e,["shell","am","force-stop",a.value]))}async function E(e,t,a){await h("adb",b(e,["shell","input","tap",String(t),String(a)]))}async function R(e){await h("adb",b(e,["shell","input","keyevent","4"]))}async function C(e){await h("adb",b(e,["shell","input","keyevent","3"]))}async function M(e){await h("adb",b(e,["shell","input","keyevent","187"]))}async function T(e,t,a,i=800){await h("adb",b(e,["shell","input","swipe",String(t),String(a),String(t),String(a),String(i)]))}async function F(e,t){let a=t.replace(/ /g,"%s");await h("adb",b(e,["shell","input","text",a]))}async function B(e,t,a){await E(e,t,a)}async function $(e,t,a,i){await B(e,t,a),await F(e,i)}async function U(e,t,a=.6){let{width:i,height:n}=await W(e),r=Math.floor(i*a),o=Math.floor(n*a),s=Math.floor(i/2),l=Math.floor(n/2),c=s,u=l,d=s,p=l;switch(t){case"up":u=l-Math.floor(o/2),p=l+Math.floor(o/2);break;case"down":u=l+Math.floor(o/2),p=l-Math.floor(o/2);break;case"left":c=s-Math.floor(r/2),d=s+Math.floor(r/2);break;case"right":c=s+Math.floor(r/2),d=s-Math.floor(r/2);break;default:throw new f("INVALID_ARGS",`Unknown direction: ${t}`)}await h("adb",b(e,["shell","input","swipe",String(c),String(u),String(d),String(p),"300"]))}async function V(e,t){for(let a=0;a<8;a+=1){let a="";try{a=await X(e)}catch(t){let e=t instanceof Error?t.message:String(t);throw new f("UNSUPPORTED_OPERATION",`uiautomator dump failed: ${e}`)}if(function(e,t){let a=t.toLowerCase(),i=/<node[^>]+>/g,n=i.exec(e);for(;n;){let t=n[0],r=/text="([^"]*)"/.exec(t),o=/content-desc="([^"]*)"/.exec(t),s=(r?.[1]??"").toLowerCase(),l=(o?.[1]??"").toLowerCase();if(s.includes(a)||l.includes(a)){let e=/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/.exec(t);if(e){let t=Number(e[1]),a=Number(e[2]);return{x:Math.floor((t+Number(e[3]))/2),y:Math.floor((a+Number(e[4]))/2)}}return{x:0,y:0}}n=i.exec(e)}return null}(a,t))return;await U(e,"down",.5)}throw new f("COMMAND_FAILED",`Could not find element containing "${t}" after scrolling`)}async function j(e,t){let a=await h("adb",b(e,["exec-out","screencap","-p"]),{binaryStdout:!0});if(!a.stdoutBuffer)throw new f("COMMAND_FAILED","Failed to capture screenshot");await s.writeFile(t,a.stdoutBuffer)}async function G(e,t,a){let i=t.toLowerCase(),n=function(e){let t=e.toLowerCase();if("on"===t||"true"===t||"1"===t)return!0;if("off"===t||"false"===t||"0"===t)return!1;throw new f("INVALID_ARGS",`Invalid setting state: ${e}`)}(a);switch(i){case"wifi":return void await h("adb",b(e,["shell","svc","wifi",n?"enable":"disable"]));case"airplane":await h("adb",b(e,["shell","settings","put","global","airplane_mode_on",n?"1":"0"])),await h("adb",b(e,["shell","am","broadcast","-a","android.intent.action.AIRPLANE_MODE","--ez","state",n?"true":"false"]));return;case"location":return void await h("adb",b(e,["shell","settings","put","secure","location_mode",n?"3":"0"]));default:throw new f("INVALID_ARGS",`Unsupported setting: ${t}`)}}async function q(e,t={}){return function(e,t,a){let i=function(e){let t={type:null,label:null,value:null,identifier:null,depth:-1,children:[]},a=[t],i=/<node\b[^>]*>|<\/node>/g,n=i.exec(e);for(;n;){let t=n[0];if(t.startsWith("</node")){a.length>1&&a.pop(),n=i.exec(e);continue}let r=function(e){let t=t=>{let a=RegExp(`${t}="([^"]*)"`).exec(e);return a?a[1]:null},a=e=>{let a=t(e);if(null!==a)return"true"===a};return{text:t("text"),desc:t("content-desc"),resourceId:t("resource-id"),className:t("class"),bounds:t("bounds"),clickable:a("clickable"),enabled:a("enabled"),focusable:a("focusable")}}(t),o=function(e){if(!e)return;let t=/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/.exec(e);if(!t)return;let a=Number(t[1]),i=Number(t[2]);return{x:a,y:i,width:Math.max(0,Number(t[3])-a),height:Math.max(0,Number(t[4])-i)}}(r.bounds),s=a[a.length-1],l={type:r.className,label:r.text||r.desc,value:r.text,identifier:r.resourceId,rect:o,enabled:r.enabled,hittable:r.clickable??r.focusable,depth:s.depth+1,parentIndex:void 0,children:[]};s.children.push(l),t.endsWith("/>")||a.push(l),n=i.exec(e)}return t}(e),n=[],r=!1,o=a.depth??1/0,s=a.scope?function(e,t){let a=t.toLowerCase(),i=[...e.children];for(;i.length>0;){let e=i.shift(),t=e.label?.toLowerCase()??"",n=e.value?.toLowerCase()??"",r=e.identifier?.toLowerCase()??"";if(t.includes(a)||n.includes(a)||r.includes(a))return e;i.push(...e.children)}return null}(i,a.scope):null,l=s?[s]:i.children,c=(e,t,i)=>{if(n.length>=800){r=!0;return}if(t>o)return;let s=!!a.raw||function(e,t){if(t.interactiveOnly)return!!e.hittable;if(t.compact){let t=!!(e.label&&e.label.trim().length>0),a=!!(e.identifier&&e.identifier.trim().length>0);return t||a||!!e.hittable}return!0}(e,a),l=i;for(let a of(s&&(l=n.length,n.push({index:l,type:e.type??void 0,label:e.label??void 0,value:e.value??void 0,identifier:e.identifier??void 0,rect:e.rect,enabled:e.enabled,hittable:e.hittable,depth:t,parentIndex:i})),e.children))if(c(a,t+1,l),r)return};for(let e of l)if(c(e,0,void 0),r)break;return r?{nodes:n,truncated:r}:{nodes:n}}(await X(e),800,t)}async function J(){if(!await w("adb"))throw new f("TOOL_MISSING","adb not found in PATH")}async function W(e){let t=(await h("adb",b(e,["shell","wm","size"]))).stdout.match(/Physical size:\s*(\d+)x(\d+)/);if(!t)throw new f("COMMAND_FAILED","Unable to read screen size");return{width:Number(t[1]),height:Number(t[2])}}async function X(e){return N(()=>z(e),{shouldRetry:H})}async function z(e){return await h("adb",b(e,["shell","uiautomator","dump","/sdcard/window_dump.xml"])),(await h("adb",b(e,["shell","cat","/sdcard/window_dump.xml"]))).stdout}function H(e){if(!(e instanceof f)||"COMMAND_FAILED"!==e.code)return!1;let t=`${e.details?.stderr??""}`.toLowerCase();return!!(t.includes("device offline")||t.includes("device not found")||t.includes("transport error")||t.includes("connection reset")||t.includes("broken pipe")||t.includes("timed out"))}async function Z(){if("darwin"!==process.platform)throw new f("UNSUPPORTED_PLATFORM","iOS tools are only available on macOS");if(!await w("xcrun"))throw new f("TOOL_MISSING","xcrun not found in PATH");let e=[],t=await h("xcrun",["simctl","list","devices","-j"]);try{let a=JSON.parse(t.stdout);for(let t of Object.values(a.devices))for(let a of t)a.isAvailable&&e.push({platform:"ios",id:a.udid,name:a.name,kind:"simulator",booted:"Booted"===a.state})}catch(e){throw new f("COMMAND_FAILED","Failed to parse simctl devices JSON",void 0,e)}if(await w("xcrun"))try{let t=await h("xcrun",["devicectl","list","devices","--json"]);for(let a of JSON.parse(t.stdout).devices??[])a.platform?.toLowerCase().includes("ios")&&e.push({platform:"ios",id:a.identifier,name:a.name,kind:"device",booted:!0})}catch{}return e}let Y={settings:"com.apple.Preferences"};async function K(e,t){let a=t.trim();if(a.includes("."))return a;let i=Y[a.toLowerCase()];if(i)return i;if("simulator"===e.kind){let i=(await ep(e)).filter(e=>e.name.toLowerCase()===a.toLowerCase());if(1===i.length)return i[0].bundleId;if(i.length>1)throw new f("INVALID_ARGS",`Multiple apps matched "${t}"`,{matches:i})}throw new f("APP_NOT_INSTALLED",`No app found matching "${t}"`)}async function Q(e,t){let a=await K(e,t);if("simulator"===e.kind){await ef(e),await h("open",["-a","Simulator"],{allowFailure:!0}),await h("xcrun",["simctl","launch",e.id,a]);return}await h("xcrun",["devicectl","device","process","launch","--device",e.id,a])}async function ee(e){"simulator"!==e.kind||"Booted"!==await eh(e.id)&&(await ef(e),await h("open",["-a","Simulator"],{allowFailure:!0}))}async function et(e,t){let a=await K(e,t);if("simulator"===e.kind){await ef(e);let t=await h("xcrun",["simctl","terminate",e.id,a],{allowFailure:!0});if(0!==t.exitCode){if(t.stderr.toLowerCase().includes("found nothing to terminate"))return;throw new f("COMMAND_FAILED",`xcrun exited with code ${t.exitCode}`,{cmd:"xcrun",args:["simctl","terminate",e.id,a],stdout:t.stdout,stderr:t.stderr,exitCode:t.exitCode})}return}await h("xcrun",["devicectl","device","process","terminate","--device",e.id,a])}async function ea(e,t,a){throw ed(e,"press"),new f("UNSUPPORTED_OPERATION","simctl io tap is not available; use the XCTest runner for input")}async function ei(e,t,a,i=800){throw ed(e,"long-press"),new f("UNSUPPORTED_OPERATION","long-press is not supported on iOS simulators without XCTest runner support")}async function en(e,t,a){await ea(e,t,a)}async function er(e,t){throw ed(e,"type"),new f("UNSUPPORTED_OPERATION","simctl io keyboard is not available; use the XCTest runner for input")}async function eo(e,t,a,i){await en(e,t,a),await er(e,i)}async function es(e,t,a=.6){throw ed(e,"scroll"),new f("UNSUPPORTED_OPERATION","simctl io swipe is not available; use the XCTest runner for input")}async function el(e){throw new f("UNSUPPORTED_OPERATION",`scrollintoview is not supported on iOS without UI automation (${e})`)}async function ec(e,t){if("simulator"===e.kind){await ef(e),await h("xcrun",["simctl","io",e.id,"screenshot",t]);return}await h("xcrun",["devicectl","device","screenshot","--device",e.id,t])}async function eu(e,t,a,i){ed(e,"settings"),await ef(e);let n=t.toLowerCase(),r=function(e){let t=e.toLowerCase();if("on"===t||"true"===t||"1"===t)return!0;if("off"===t||"false"===t||"0"===t)return!1;throw new f("INVALID_ARGS",`Invalid setting state: ${e}`)}(a);switch(n){case"wifi":return void await h("xcrun",["simctl","status_bar",e.id,"override","--wifiMode",r?"active":"failed"]);case"airplane":r?await h("xcrun",["simctl","status_bar",e.id,"override","--dataNetwork","hide","--wifiMode","failed","--wifiBars","0","--cellularMode","failed","--cellularBars","0","--operatorName",""]):await h("xcrun",["simctl","status_bar",e.id,"clear"]);return;case"location":if(!i)throw new f("INVALID_ARGS","location setting requires an active app in session");await h("xcrun",["simctl","privacy",e.id,r?"grant":"revoke","location",i]);return;default:throw new f("INVALID_ARGS",`Unsupported setting: ${t}`)}}function ed(e,t){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION",`${t} is only supported on iOS simulators in v1`)}async function ep(e){let t=(await h("xcrun",["simctl","listapps",e.id],{allowFailure:!0})).stdout.trim();if(!t)return[];let a=null;if(t.startsWith("{"))try{a=JSON.parse(t)}catch{a=null}if(!a&&t.startsWith("{"))try{let e=await h("plutil",["-convert","json","-o","-","-"],{allowFailure:!0,stdin:t});0===e.exitCode&&e.stdout.trim().startsWith("{")&&(a=JSON.parse(e.stdout))}catch{a=null}return a?Object.entries(a).map(([e,t])=>({bundleId:e,name:t.CFBundleDisplayName??t.CFBundleName??e})):[]}async function ef(e){"simulator"!==e.kind||"Booted"!==await eh(e.id)&&(await h("xcrun",["simctl","boot",e.id],{allowFailure:!0}),await h("xcrun",["simctl","bootstatus",e.id,"-b"],{allowFailure:!0}))}async function eh(e){let t=await h("xcrun",["simctl","list","devices","-j"],{allowFailure:!0});if(0!==t.exitCode)return null;try{let a=JSON.parse(t.stdout);for(let t of Object.values(a.devices??{})){let a=t.find(t=>t.udid===e);if(a)return a.state}}catch{}return null}let em=new Map;async function ew(e,t,a={}){var i;return"snapshot"===(i=t.command)||"findText"===i||"listTappables"===i||"alert"===i?N(()=>eg(e,t,a),{shouldRetry:eS}):eg(e,t,a)}async function eg(e,t,a={}){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","iOS runner only supports simulators in v1");try{let i=await eI(e,a),n=await eD(e,i.port,t,a.logPath),r=await n.text(),o={};try{o=JSON.parse(r)}catch{throw new f("COMMAND_FAILED","Invalid runner response",{text:r})}if(!o.ok)throw new f("COMMAND_FAILED",o.error?.message??"Runner error",{runner:o,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:a.logPath});return o.data??{}}catch(n){let i=n instanceof f?n:new f("COMMAND_FAILED",String(n));if("COMMAND_FAILED"===i.code&&"string"==typeof i.message&&i.message.includes("Runner did not accept connection")){await ev(e.id);let i=await eI(e,a),n=await eD(e,i.port,t,a.logPath),r=await n.text(),o={};try{o=JSON.parse(r)}catch{throw new f("COMMAND_FAILED","Invalid runner response",{text:r})}if(!o.ok)throw new f("COMMAND_FAILED",o.error?.message??"Runner error",{runner:o,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:a.logPath});return o.data??{}}throw n}}async function ev(e){let t=em.get(e);if(t){try{await eD(t.device,t.port,{command:"shutdown"})}catch{}try{await t.testPromise}catch{}ex(t.xctestrunPath),ex(t.jsonPath),em.delete(e)}}async function ey(e){await h("xcrun",["simctl","bootstatus",e,"-b"],{allowFailure:!0})}async function eI(e,t){let a=em.get(e.id);if(a)return a;await ey(e.id);let i=await eN(e.id,t),n=await eP(),r=process.env.AGENT_DEVICE_RUNNER_TIMEOUT??"0",{xctestrunPath:s,jsonPath:l}=await eO(i,{AGENT_DEVICE_RUNNER_PORT:String(n),AGENT_DEVICE_RUNNER_TIMEOUT:r},`session-${e.id}-${n}`),c=o("xcodebuild",["test-without-building","-only-testing","AgentDeviceRunnerUITests/RunnerTests/testCommand","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-xctestrun",s,"-destination",`platform=iOS Simulator,id=${e.id}`],{onStdoutChunk:e=>{eb(e,t.logPath,t.traceLogPath,t.verbose)},onStderrChunk:e=>{eb(e,t.logPath,t.traceLogPath,t.verbose)},allowFailure:!0,env:{...process.env,AGENT_DEVICE_RUNNER_PORT:String(n),AGENT_DEVICE_RUNNER_TIMEOUT:r}}),u={device:e,deviceId:e.id,port:n,xctestrunPath:s,jsonPath:l,testPromise:c};return em.set(e.id,u),u}async function eN(e,t){let a,i=r.join(p.homedir(),".agent-device","ios-runner"),n=r.join(i,"derived");if((a=process.env.AGENT_DEVICE_IOS_CLEAN_DERIVED)&&["1","true","yes","on"].includes(a.toLowerCase()))try{d.rmSync(n,{recursive:!0,force:!0})}catch{}let s=eA(n);if(s)return s;let l=function(){let e=r.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=r.join(t,"package.json");if(d.existsSync(e))return t;t=r.dirname(t)}return e}(),u=r.join(l,"ios-runner","AgentDeviceRunner","AgentDeviceRunner.xcodeproj");if(!d.existsSync(u))throw new f("COMMAND_FAILED","iOS runner project not found",{projectPath:u});try{await o("xcodebuild",["build-for-testing","-project",u,"-scheme","AgentDeviceRunner","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-destination",`platform=iOS Simulator,id=${e}`,"-derivedDataPath",n],{onStdoutChunk:e=>{eb(e,t.logPath,t.traceLogPath,t.verbose)},onStderrChunk:e=>{eb(e,t.logPath,t.traceLogPath,t.verbose)}})}catch(a){let e=a instanceof f?a:new f("COMMAND_FAILED",String(a));throw new f("COMMAND_FAILED","xcodebuild build-for-testing failed",{error:e.message,details:e.details,logPath:t.logPath})}let h=eA(n);if(!h)throw new f("COMMAND_FAILED","Failed to locate .xctestrun after build");return h}function eA(e){if(!d.existsSync(e))return null;let t=[],a=[e];for(;a.length>0;){let e=a.pop();for(let i of d.readdirSync(e,{withFileTypes:!0})){let n=r.join(e,i.name);if(i.isDirectory()){a.push(n);continue}if(i.isFile()&&i.name.endsWith(".xctestrun"))try{let e=d.statSync(n);t.push({path:n,mtimeMs:e.mtimeMs})}catch{}}}return 0===t.length?null:(t.sort((e,t)=>t.mtimeMs-e.mtimeMs),t[0]?.path??null)}function eb(e,t,a,i){t&&d.appendFileSync(t,e),a&&d.appendFileSync(a,e),i&&process.stderr.write(e)}function eS(e){if(!(e instanceof f)||"COMMAND_FAILED"!==e.code)return!1;let t=`${e.message??""}`.toLowerCase();return!!(t.includes("runner did not accept connection")||t.includes("fetch failed")||t.includes("econnrefused")||t.includes("socket hang up"))}async function eD(e,t,a,i){let n=Date.now(),r=null;for(;Date.now()-n<15e3;)try{return await fetch(`http://127.0.0.1:${t}/command`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})}catch(e){r=e,await new Promise(e=>setTimeout(e,100))}if("simulator"===e.kind){let i=await ek(e.id,t,a);return new Response(i.body,{status:i.status})}throw new f("COMMAND_FAILED","Runner did not accept connection",{port:t,logPath:i,lastError:r?String(r):void 0})}async function ek(e,t,a){let i=JSON.stringify(a),n=await h("xcrun",["simctl","spawn",e,"/usr/bin/curl","-s","-X","POST","-H","Content-Type: application/json","--data",i,`http://127.0.0.1:${t}/command`],{allowFailure:!0}),r=n.stdout;if(0!==n.exitCode)throw new f("COMMAND_FAILED","Runner did not accept connection (simctl spawn)",{port:t,stdout:n.stdout,stderr:n.stderr,exitCode:n.exitCode});return{status:200,body:r}}async function eP(){return await new Promise((e,t)=>{let a=m.createServer();a.listen(0,"127.0.0.1",()=>{let i=a.address();a.close(),"object"==typeof i&&i?.port?e(i.port):t(new f("COMMAND_FAILED","Failed to allocate port"))}),a.on("error",t)})}async function eO(e,t,a){let i,n=r.dirname(e),o=a.replace(/[^a-zA-Z0-9._-]/g,"_"),s=r.join(n,`AgentDeviceRunner.env.${o}.json`),l=r.join(n,`AgentDeviceRunner.env.${o}.xctestrun`),c=await h("plutil",["-convert","json","-o","-",e],{allowFailure:!0});if(0!==c.exitCode||!c.stdout.trim())throw new f("COMMAND_FAILED","Failed to read xctestrun plist",{xctestrunPath:e,stderr:c.stderr});try{i=JSON.parse(c.stdout)}catch(t){throw new f("COMMAND_FAILED","Failed to parse xctestrun JSON",{xctestrunPath:e,error:String(t)})}let u=e=>{e.EnvironmentVariables={...e.EnvironmentVariables??{},...t},e.UITestEnvironmentVariables={...e.UITestEnvironmentVariables??{},...t},e.UITargetAppEnvironmentVariables={...e.UITargetAppEnvironmentVariables??{},...t},e.TestingEnvironmentVariables={...e.TestingEnvironmentVariables??{},...t}},p=i.TestConfigurations;if(Array.isArray(p))for(let e of p){if(!e||"object"!=typeof e)continue;let t=e.TestTargets;if(Array.isArray(t))for(let e of t)e&&"object"==typeof e&&u(e)}for(let[e,t]of Object.entries(i))t&&"object"==typeof t&&t.TestBundlePath&&(u(t),i[e]=t);d.writeFileSync(s,JSON.stringify(i,null,2));let m=await h("plutil",["-convert","xml1","-o",l,s],{allowFailure:!0});if(0!==m.exitCode)throw new f("COMMAND_FAILED","Failed to write xctestrun plist",{tmpXctestrunPath:l,stderr:m.stderr});return{xctestrunPath:l,jsonPath:s}}function ex(e){try{d.existsSync(e)&&d.unlinkSync(e)}catch{}}async function eL(e,t={}){let a,i;if("ios"!==e.platform||"simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","AX snapshot is only supported on iOS simulators");let n=await e_(),r=await N(async()=>{var e,a;let i,r,o,s=await h(n,[],{allowFailure:!0});if(t.traceLogPath&&(e=t.traceLogPath,i=((a=s).stdout??"").toString(),r=(a.stderr??"").toString(),o=`
2
+ [axsnapshot] exit=${a.exitCode} stdoutBytes=${i.length} stderrBytes=${r.length}
3
+ `,d.appendFileSync(e,o),(0!==a.exitCode||r.length>0)&&(r.length>0&&d.appendFileSync(e,`${r}
4
4
  `),0!==a.exitCode&&i.length>0&&d.appendFileSync(e,`${i}
5
- `))),0!==s.exitCode){let e,t,a=(s.stderr??"").toString(),i=(e=a.toLowerCase()).includes("accessibility permission")?" Enable Accessibility for your terminal in System Settings > Privacy & Security > Accessibility, or use --backend xctest (slower snapshots via XCTest).":e.includes("could not find ios app content")?" AX snapshot sometimes caches empty content. Try restarting the Simulator app.":"",o=!!((t=a.toLowerCase()).includes("could not find ios app content")||t.includes("timeout"));throw new f("COMMAND_FAILED","AX snapshot failed",{stderr:`${a}${i}`,stdout:s.stdout,retryable:o})}return s},{shouldRetry:e=>{var t;return(t=e)instanceof f&&"COMMAND_FAILED"===t.code&&t.details?.retryable===!0}});try{let e=JSON.parse(n.stdout);if(e&&"object"==typeof e&&"root"in e){if(!e.root)throw Error("AX snapshot missing root");a=e.root,i=e.windowFrame??void 0}else a=e}catch(e){throw new f("COMMAND_FAILED","Invalid AX snapshot JSON",{error:String(e)})}let r=a.frame??i,s=[],l=[],c=(e,t)=>{e.frame&&s.push(e.frame);let a=e.frame&&r?{x:e.frame.x-r.x,y:e.frame.y-r.y,width:e.frame.width,height:e.frame.height}:e.frame;for(let i of(l.push({...e,frame:a,children:void 0,depth:t}),e.children??[]))c(i,t+1)};return c(a,0),{nodes:(function(e,t,a){if(!t||0===a.length)return e;let i=1/0,o=1/0;for(let e of a)e.x<i&&(i=e.x),e.y<o&&(o=e.y);return i<=5&&o<=5?e.map(e=>({...e,frame:e.frame?{x:e.frame.x+t.x,y:e.frame.y+t.y,width:e.frame.width,height:e.frame.height}:void 0})):e})(l,r,s).map((e,t)=>({index:t,type:e.subrole??e.role,label:e.label,value:e.value,identifier:e.identifier,rect:e.frame?{x:e.frame.x,y:e.frame.y,width:e.frame.width,height:e.frame.height}:void 0,depth:e.depth}))}}async function eL(){let e=function(){let e=n.dirname(c(import.meta.url));for(let t=0;t<6;t+=1){let t=n.join(e,"package.json");if(d.existsSync(t))return e;e=n.dirname(e)}return process.cwd()}(),t=n.join(e,"ios-runner","AXSnapshot"),a=process.env.AGENT_DEVICE_AX_BINARY;if(a&&d.existsSync(a))return a;let i=n.join(e,"dist","bin","axsnapshot");if(d.existsSync(i))return i;let o=n.join(t,".build","release","axsnapshot");if(d.existsSync(o))return o;let r=await h("swift",["build","-c","release"],{cwd:t,allowFailure:!0});if(0!==r.exitCode||!d.existsSync(o))throw new f("COMMAND_FAILED","Failed to build AX snapshot tool",{stderr:r.stderr,stdout:r.stdout});return o}async function eE(e){let t={platform:e.platform,deviceName:e.device,udid:e.udid,serial:e.serial};if("android"===t.platform){await J();let e=await v();return await g(e,t)}if("ios"===t.platform){let e=await Z();return await g(e,t)}let a=[];try{a.push(...await v())}catch{}try{a.push(...await Z())}catch{}return await g(a,t)}async function eR(e,t,a,i,o){let n=function(e){switch(e.platform){case"android":return{open:t=>x(e,t),openDevice:()=>_(e),close:t=>L(e,t),tap:(t,a)=>E(e,t,a),longPress:(t,a,i)=>T(e,t,a,i),focus:(t,a)=>B(e,t,a),type:t=>F(e,t),fill:(t,a,i)=>$(e,t,a,i),scroll:(t,a)=>U(e,t,a),scrollIntoView:t=>V(e,t),screenshot:t=>j(e,t)};case"ios":return{open:t=>Q(e,t),openDevice:()=>ee(e),close:t=>et(e,t),tap:(t,a)=>ea(e,t,a),longPress:(t,a,i)=>ei(e,t,a,i),focus:(t,a)=>eo(e,t,a),type:t=>en(e,t),fill:(t,a,i)=>er(e,t,a,i),scroll:(t,a)=>es(e,t,a),scrollIntoView:e=>el(e),screenshot:t=>ec(e,t)};default:throw new f("UNSUPPORTED_PLATFORM",`Unsupported platform: ${e.platform}`)}}(e);switch(t){case"open":{let e=a[0];if(!e)return await n.openDevice(),{app:null};return await n.open(e),{app:e}}case"close":{let e=a[0];if(!e)return{closed:"session"};return await n.close(e),{app:e}}case"press":{let[t,i]=a.map(Number);if(Number.isNaN(t)||Number.isNaN(i))throw new f("INVALID_ARGS","press requires x y");return"ios"===e.platform&&"simulator"===e.kind?await ew(e,{command:"tap",x:t,y:i,appBundleId:o?.appBundleId},{verbose:o?.verbose,logPath:o?.logPath,traceLogPath:o?.traceLogPath}):await n.tap(t,i),{x:t,y:i}}case"long-press":{let e=Number(a[0]),t=Number(a[1]),i=a[2]?Number(a[2]):void 0;if(Number.isNaN(e)||Number.isNaN(t))throw new f("INVALID_ARGS","long-press requires x y [durationMs]");return await n.longPress(e,t,i),{x:e,y:t,durationMs:i}}case"focus":{let[t,i]=a.map(Number);if(Number.isNaN(t)||Number.isNaN(i))throw new f("INVALID_ARGS","focus requires x y");return"ios"===e.platform&&"simulator"===e.kind?await ew(e,{command:"tap",x:t,y:i,appBundleId:o?.appBundleId},{verbose:o?.verbose,logPath:o?.logPath,traceLogPath:o?.traceLogPath}):await n.focus(t,i),{x:t,y:i}}case"type":{let t=a.join(" ");if(!t)throw new f("INVALID_ARGS","type requires text");return"ios"===e.platform&&"simulator"===e.kind?await ew(e,{command:"type",text:t,appBundleId:o?.appBundleId},{verbose:o?.verbose,logPath:o?.logPath,traceLogPath:o?.traceLogPath}):await n.type(t),{text:t}}case"fill":{let t=Number(a[0]),i=Number(a[1]),r=a.slice(2).join(" ");if(Number.isNaN(t)||Number.isNaN(i)||!r)throw new f("INVALID_ARGS","fill requires x y text");return"ios"===e.platform&&"simulator"===e.kind?(await ew(e,{command:"tap",x:t,y:i,appBundleId:o?.appBundleId},{verbose:o?.verbose,logPath:o?.logPath,traceLogPath:o?.traceLogPath}),await ew(e,{command:"type",text:r,appBundleId:o?.appBundleId},{verbose:o?.verbose,logPath:o?.logPath,traceLogPath:o?.traceLogPath})):await n.fill(t,i,r),{x:t,y:i,text:r}}case"scroll":{let t=a[0],i=a[1]?Number(a[1]):void 0;if(!t)throw new f("INVALID_ARGS","scroll requires direction");if("ios"===e.platform&&"simulator"===e.kind){if(!["up","down","left","right"].includes(t))throw new f("INVALID_ARGS",`Unknown direction: ${t}`);let a=function(e){switch(e){case"up":return"down";case"down":return"up";case"left":return"right";case"right":return"left"}}(t);await ew(e,{command:"swipe",direction:a,appBundleId:o?.appBundleId},{verbose:o?.verbose,logPath:o?.logPath,traceLogPath:o?.traceLogPath})}else await n.scroll(t,i);return{direction:t,amount:i}}case"scrollintoview":{let t=a.join(" ").trim();if(!t)throw new f("INVALID_ARGS","scrollintoview requires text");if("ios"===e.platform&&"simulator"===e.kind){for(let a=0;a<8;a+=1){let i=await ew(e,{command:"findText",text:t,appBundleId:o?.appBundleId},{verbose:o?.verbose,logPath:o?.logPath,traceLogPath:o?.traceLogPath});if(i?.found)return{text:t,attempts:a+1};await ew(e,{command:"swipe",direction:"up",appBundleId:o?.appBundleId},{verbose:o?.verbose,logPath:o?.logPath,traceLogPath:o?.traceLogPath}),await new Promise(e=>setTimeout(e,300))}throw new f("COMMAND_FAILED",`scrollintoview could not find text: ${t}`)}return await n.scrollIntoView(t),{text:t}}case"screenshot":{let e=i??`./screenshot-${Date.now()}.png`;return await n.screenshot(e),{path:e}}case"back":if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","back is only supported on iOS simulators in v1");return await ew(e,{command:"back",appBundleId:o?.appBundleId},{verbose:o?.verbose,logPath:o?.logPath,traceLogPath:o?.traceLogPath}),{action:"back"}}return await R(e),{action:"back"};case"home":if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","home is only supported on iOS simulators in v1");return await ew(e,{command:"home",appBundleId:o?.appBundleId},{verbose:o?.verbose,logPath:o?.logPath,traceLogPath:o?.traceLogPath}),{action:"home"}}return await C(e),{action:"home"};case"app-switcher":if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","app-switcher is only supported on iOS simulators in v1");return await ew(e,{command:"appSwitcher",appBundleId:o?.appBundleId},{verbose:o?.verbose,logPath:o?.logPath,traceLogPath:o?.traceLogPath}),{action:"app-switcher"}}return await M(e),{action:"app-switcher"};case"settings":{let[t,i,n]=a;if("ios"===e.platform)return await eu(e,t,i,n??o?.appBundleId),{setting:t,state:i};return await G(e,t,i),{setting:t,state:i}}case"snapshot":{let t=o?.snapshotBackend??"xctest";if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","snapshot is only supported on iOS simulators in v1");if("ax"===t)return{nodes:(await e_(e,{traceLogPath:o?.traceLogPath})).nodes??[],truncated:!1,backend:"ax"};let a=await ew(e,{command:"snapshot",appBundleId:o?.appBundleId,interactiveOnly:o?.snapshotInteractiveOnly,compact:o?.snapshotCompact,depth:o?.snapshotDepth,scope:o?.snapshotScope,raw:o?.snapshotRaw},{verbose:o?.verbose,logPath:o?.logPath,traceLogPath:o?.traceLogPath}),i=a.nodes??[];if(0===i.length)try{return{nodes:(await e_(e,{traceLogPath:o?.traceLogPath})).nodes??[],truncated:!1,backend:"ax"}}catch{}return{nodes:i,truncated:a.truncated??!1,backend:"xctest"}}let a=await q(e,{interactiveOnly:o?.snapshotInteractiveOnly,compact:o?.snapshotCompact,depth:o?.snapshotDepth,scope:o?.snapshotScope,raw:o?.snapshotRaw});return{nodes:a.nodes??[],truncated:a.truncated??!1,backend:"android"}}default:throw new f("INVALID_ARGS",`Unknown command: ${t}`)}}function eC(e){return e.map((e,t)=>({...e,ref:`e${t+1}`}))}function eM(e){let t=e.trim();return t.startsWith("@")?t.slice(1)||null:t.startsWith("e")?t:null}function eT(e,t){return e.find(e=>e.ref===t)??null}function eF(e){return{x:Math.round(e.x+e.width/2),y:Math.round(e.y+e.height/2)}}function eB(e,t,a,i={}){let o=eU(a);if(!o)return null;let n=null;for(let a of e){if(i.requireRect&&!a.rect)continue;let e=function(e,t,a){switch(t){case"role":return function(e,t){let a=function(e){let t=e.trim();return t?((t=(t.split(".").pop()??t).replace(/XCUIElementType/gi,"").toLowerCase()).startsWith("ax")&&(t=t.replace(/^ax/,"")),t):""}(e??"");return a?a===t?2:+!!a.includes(t):0}(e.type,a);case"label":return e$(e.label,a);case"value":return e$(e.value,a);case"id":return e$(e.identifier,a);default:return Math.max(e$(e.label,a),e$(e.value,a),e$(e.identifier,a))}}(a,t,o);if(!(e<=0)&&(!n||e>n.score)&&(n={node:a,score:e},e>=2))break}return n?.node??null}function e$(e,t){let a=eU(e??"");return a?a===t?2:+!!a.includes(t):0}function eU(e){return e.trim().toLowerCase().replace(/\s+/g," ")}let eV=new Map,ej=n.join(p.homedir(),".agent-device"),eG=n.join(ej,"daemon.json"),eq=n.join(ej,"daemon.log"),eJ=n.join(ej,"sessions"),eW=function(){try{let e=function(){let e=n.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=n.join(t,"package.json");if(d.existsSync(e))return t;t=n.dirname(t)}return e}();return JSON.parse(d.readFileSync(n.join(e,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}(),eX=a.randomBytes(24).toString("hex");function ez(e,t,a){return{appBundleId:t,verbose:e?.verbose,logPath:eq,traceLogPath:a,snapshotInteractiveOnly:e?.snapshotInteractiveOnly,snapshotCompact:e?.snapshotCompact,snapshotDepth:e?.snapshotDepth,snapshotScope:e?.snapshotScope,snapshotRaw:e?.snapshotRaw,snapshotBackend:e?.snapshotBackend}}async function eH(e){if(e.token!==eX)return{ok:!1,error:{code:"UNAUTHORIZED",message:"Invalid token"}};let t=e.command,a=e.session||"default";if("session_list"===t)return{ok:!0,data:{sessions:Array.from(eV.values()).map(e=>({name:e.name,platform:e.device.platform,device:e.device.name,id:e.device.id,createdAt:e.createdAt}))}};if("devices"===t)try{let t=[];if(e.flags?.platform==="android"){let{listAndroidDevices:e}=await Promise.resolve().then(()=>({listAndroidDevices:v}));t.push(...await e())}else if(e.flags?.platform==="ios"){let{listIosDevices:e}=await Promise.resolve().then(()=>({listIosDevices:Z}));t.push(...await e())}else{let{listAndroidDevices:e}=await Promise.resolve().then(()=>({listAndroidDevices:v})),{listIosDevices:a}=await Promise.resolve().then(()=>({listIosDevices:Z}));try{t.push(...await e())}catch{}try{t.push(...await a())}catch{}}return{ok:!0,data:{devices:t}}}catch(t){let e=l(t);return{ok:!1,error:{code:e.code,message:e.message,details:e.details}}}if("apps"===t){let t=eV.get(a),i=e.flags??{};if(!t&&!i.platform&&!i.device&&!i.udid&&!i.serial)return{ok:!1,error:{code:"INVALID_ARGS",message:"apps requires an active session or an explicit device selector (e.g. --platform ios)."}};let o=t?.device??await eE(i);if(await e4(o),"ios"===o.platform){if("simulator"!==o.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"apps list is only supported on iOS simulators"}};let{listSimulatorApps:t}=await Promise.resolve().then(()=>({listSimulatorApps:ep})),a=await t(o);return e.flags?.appsMetadata?{ok:!0,data:{apps:a}}:{ok:!0,data:{apps:a.map(e=>e.name&&e.name!==e.bundleId?`${e.name} (${e.bundleId})`:e.bundleId)}}}let{listAndroidApps:n,listAndroidAppsMetadata:r}=await Promise.resolve().then(()=>({listAndroidApps:D,listAndroidAppsMetadata:k}));return e.flags?.appsMetadata?{ok:!0,data:{apps:await r(o,e.flags?.appsFilter)}}:{ok:!0,data:{apps:await n(o,e.flags?.appsFilter)}}}if("appstate"===t){let t=eV.get(a),i=e.flags??{},o=t?.device??await eE(i);if(await e4(o),"ios"===o.platform){if(t?.appBundleId)return{ok:!0,data:{platform:"ios",appBundleId:t.appBundleId,appName:t.appName??t.appBundleId,source:"session"}};let a=await eY(o,t?.trace?.outPath,e.flags);return{ok:!0,data:{platform:"ios",appName:a.appName,appBundleId:a.appBundleId,source:a.source}}}let{getAndroidAppState:n}=await Promise.resolve().then(()=>({getAndroidAppState:P})),r=await n(o);return{ok:!0,data:{platform:"android",package:r.package,activity:r.activity}}}if("open"===t){let i;if(eV.has(a)){let i,o=eV.get(a),n=e.positionals?.[0];if(!o||!n)return{ok:!1,error:{code:"INVALID_ARGS",message:"Session already active. Close it first or pass a new --session name."}};if("ios"===o.device.platform)try{let{resolveIosApp:e}=await Promise.resolve().then(()=>({resolveIosApp:K}));i=await e(o.device,n)}catch{i=void 0}await eR(o.device,"open",e.positionals??[],e.flags?.out,{...ez(e.flags,i)});let r={...o,appBundleId:i,appName:n,snapshot:void 0};return eZ(r,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:a,appName:n,appBundleId:i}}),eV.set(a,r),{ok:!0,data:{session:a,appName:n,appBundleId:i}}}let o=await eE(e.flags??{});await e4(o);let n=Array.from(eV.values()).find(e=>e.device.id===o.id);if(n)return{ok:!1,error:{code:"DEVICE_IN_USE",message:`Device is already in use by session "${n.name}".`,details:{session:n.name,deviceId:o.id,deviceName:o.name}}};let r=e.positionals?.[0];if("ios"===o.platform)try{let{resolveIosApp:t}=await Promise.resolve().then(()=>({resolveIosApp:K}));i=await t(o,e.positionals?.[0]??"")}catch{i=void 0}await eR(o,"open",e.positionals??[],e.flags?.out,{...ez(e.flags,i)});let s={name:a,device:o,createdAt:Date.now(),appBundleId:i,appName:r,actions:[]};return eZ(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:a}}),eV.set(a,s),{ok:!0,data:{session:a}}}if("replay"===t){let t=e.positionals?.[0];if(!t)return{ok:!1,error:{code:"INVALID_ARGS",message:"replay requires a path"}};try{let e=e0(t),i=JSON.parse(d.readFileSync(e,"utf8")),o=i.optimizedActions??i.actions??[];for(let e of o)e&&"replay"!==e.command&&await eH({token:eX,session:a,command:e.command,positionals:e.positionals??[],flags:e.flags??{}});return{ok:!0,data:{replayed:o.length,session:a}}}catch(t){let e=l(t);return{ok:!1,error:{code:e.code,message:e.message}}}}if("close"===t){let i=eV.get(a);return i?(e.positionals&&e.positionals.length>0&&await eR(i.device,"close",e.positionals??[],e.flags?.out,{...ez(e.flags,i.appBundleId,i.trace?.outPath)}),"ios"===i.device.platform&&"simulator"===i.device.kind&&await ev(i.device.id),eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:a}}),eQ(i),eV.delete(a),{ok:!0,data:{session:a}}):{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session"}}}if("snapshot"===t){let i=eV.get(a),o=i?.device??await eE(e.flags??{});i||await e4(o);let n=i?.appBundleId,r=e.flags?.snapshotScope;if(r&&r.trim().startsWith("@")){if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"Ref scope requires an existing snapshot in session."}};let e=eM(r.trim());if(!e)return{ok:!1,error:{code:"INVALID_ARGS",message:`Invalid ref scope: ${r}`}};let t=eT(i.snapshot.nodes,e),a=t?e3(t,i.snapshot.nodes):void 0;if(!a)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${r} not found or has no label`}};r=a}let s=await eR(o,"snapshot",[],e.flags?.out,{...ez({...e.flags,snapshotScope:r},n,i?.trace?.outPath)}),l=s?.nodes??[],c=eC(e.flags?.snapshotRaw?l:e6(l)),u={nodes:c,truncated:s?.truncated,createdAt:Date.now(),backend:s?.backend},d={name:a,device:o,createdAt:i?.createdAt??Date.now(),appBundleId:i?.appBundleId??n,snapshot:u,actions:i?.actions??[],appName:i?.appName};return eZ(d,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{nodes:c.length,truncated:s?.truncated??!1}}),eV.set(a,d),{ok:!0,data:{nodes:c,truncated:s?.truncated??!1,appName:d.appBundleId?d.appName??d.appBundleId:void 0,appBundleId:d.appBundleId}}}if("wait"===t){let i=eV.get(a),o=i?.device??await eE(e.flags??{});i||await e4(o);let n=e.positionals??[];if(0===n.length)return{ok:!1,error:{code:"INVALID_ARGS",message:"wait requires a duration or text"}};let r=e=>{if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null},s=r(n[0]);if(null!==s)return await new Promise(e=>setTimeout(e,s)),i&&eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{waitedMs:s}}),{ok:!0,data:{waitedMs:s}};let l="",c=null;if("text"===n[0])l=null!==(c=r(n[n.length-1]))?n.slice(1,-1).join(" "):n.slice(1).join(" ");else if(n[0].startsWith("@")){if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"Ref wait requires an existing snapshot in session."}};let e=eM(n[0]);if(!e)return{ok:!1,error:{code:"INVALID_ARGS",message:`Invalid ref: ${n[0]}`}};let t=eT(i.snapshot.nodes,e),a=t?e3(t,i.snapshot.nodes):void 0;if(!a)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${n[0]} not found or has no label`}};c=r(n[n.length-1]),l=a}else l=null!==(c=r(n[n.length-1]))?n.slice(0,-1).join(" "):n.join(" ");if(!(l=l.trim()))return{ok:!1,error:{code:"INVALID_ARGS",message:"wait requires text"}};let u=c??1e4,d=Date.now();for(;Date.now()-d<u;){if("ios"===o.platform&&"simulator"===o.kind){let a=await ew(o,{command:"findText",text:l,appBundleId:i?.appBundleId},{verbose:e.flags?.verbose,logPath:eq,traceLogPath:i?.trace?.outPath});if(a?.found)return i&&eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{text:l,waitedMs:Date.now()-d}}),{ok:!0,data:{text:l,waitedMs:Date.now()-d}}}else if("android"!==o.platform)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"wait is not supported on this device"}};else if(e2(eC((await q(o,{scope:l})).nodes??[]),l))return i&&eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{text:l,waitedMs:Date.now()-d}}),{ok:!0,data:{text:l,waitedMs:Date.now()-d}};await new Promise(e=>setTimeout(e,300))}return{ok:!1,error:{code:"COMMAND_FAILED",message:`wait timed out for text: ${l}`}}}if("alert"===t){let i=eV.get(a),o=i?.device??await eE(e.flags??{});i||await e4(o);let n=(e.positionals?.[0]??"get").toLowerCase();if("ios"!==o.platform||"simulator"!==o.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"alert is only supported on iOS simulators in v1"}};if("wait"===n){let a=(e=>{if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null})(e.positionals?.[1])??1e4,n=Date.now();for(;Date.now()-n<a;){try{let a=await ew(o,{command:"alert",action:"get",appBundleId:i?.appBundleId},{verbose:e.flags?.verbose,logPath:eq,traceLogPath:i?.trace?.outPath});return i&&eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:a}),{ok:!0,data:a}}catch{}await new Promise(e=>setTimeout(e,300))}return{ok:!1,error:{code:"COMMAND_FAILED",message:"alert wait timed out"}}}let r=await ew(o,{command:"alert",action:"accept"===n||"dismiss"===n?n:"get",appBundleId:i?.appBundleId},{verbose:e.flags?.verbose,logPath:eq,traceLogPath:i?.trace?.outPath});return i&&eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:r}),{ok:!0,data:r}}if("record"===t){let i=(e.positionals?.[0]??"").toLowerCase();if(!["start","stop"].includes(i))return{ok:!1,error:{code:"INVALID_ARGS",message:"record requires start|stop"}};let o=eV.get(a),r=o?.device??await eE(e.flags??{});o||await e4(r);let s=o??{name:a,device:r,createdAt:Date.now(),actions:[]};if("start"===i){if(s.recording)return{ok:!1,error:{code:"INVALID_ARGS",message:"recording already in progress"}};let i=e.positionals?.[1]??`./recording-${Date.now()}.mp4`,o=n.resolve(i),l=n.dirname(o);if(d.existsSync(l)||d.mkdirSync(l,{recursive:!0}),"ios"===r.platform){if("simulator"!==r.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"record is only supported on iOS simulators in v1"}};let{child:e,wait:t}=u("xcrun",["simctl","io",r.id,"recordVideo",o],{allowFailure:!0});s.recording={platform:"ios",outPath:o,child:e,wait:t}}else{let e=`/sdcard/agent-device-recording-${Date.now()}.mp4`,{child:t,wait:a}=u("adb",["-s",r.id,"shell","screenrecord",e],{allowFailure:!0});s.recording={platform:"android",outPath:o,remotePath:e,child:t,wait:a}}return eV.set(a,s),eZ(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"start"}}),{ok:!0,data:{recording:"started",outPath:i}}}if(!s.recording)return{ok:!1,error:{code:"INVALID_ARGS",message:"no active recording"}};let l=s.recording;l.child.kill("SIGINT");try{await l.wait}catch{}if("android"===l.platform&&l.remotePath)try{await h("adb",["-s",r.id,"pull",l.remotePath,l.outPath],{allowFailure:!0}),await h("adb",["-s",r.id,"shell","rm","-f",l.remotePath],{allowFailure:!0})}catch{}return s.recording=void 0,eZ(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"stop",outPath:l.outPath}}),{ok:!0,data:{recording:"stopped",outPath:l.outPath}}}if("trace"===t){let i=(e.positionals?.[0]??"").toLowerCase();if(!["start","stop"].includes(i))return{ok:!1,error:{code:"INVALID_ARGS",message:"trace requires start|stop"}};let o=eV.get(a);if(!o)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session"}};if("start"===i){let a,i;if(o.trace)return{ok:!1,error:{code:"INVALID_ARGS",message:"trace already in progress"}};let r=e0(e.positionals?.[1]??(a=o.name.replace(/[^a-zA-Z0-9._-]/g,"_"),i=new Date().toISOString().replace(/[:.]/g,"-"),n.join(eJ,`${a}-${i}.trace.log`)));return d.mkdirSync(n.dirname(r),{recursive:!0}),d.appendFileSync(r,""),o.trace={outPath:r,startedAt:Date.now()},eZ(o,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"start",outPath:r}}),{ok:!0,data:{trace:"started",outPath:r}}}if(!o.trace)return{ok:!1,error:{code:"INVALID_ARGS",message:"no active trace"}};let r=o.trace.outPath;if(e.positionals?.[1]){let t=e0(e.positionals[1]);d.mkdirSync(n.dirname(t),{recursive:!0}),d.existsSync(r)?d.renameSync(r,t):d.appendFileSync(t,""),r=t}return o.trace=void 0,eZ(o,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"stop",outPath:r}}),{ok:!0,data:{trace:"stopped",outPath:r}}}if("settings"===t){let i=e.positionals?.[0],o=e.positionals?.[1];if(!i||!o)return{ok:!1,error:{code:"INVALID_ARGS",message:"settings requires <wifi|airplane|location> <on|off>"}};let n=eV.get(a),r=n?.device??await eE(e.flags??{});n||await e4(r);let s=n?.appBundleId,l=await eR(r,"settings",[i,o,s??""],e.flags?.out,{...ez(e.flags,s,n?.trace?.outPath)});return n&&eZ(n,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:l??{setting:i,state:o}}),{ok:!0,data:l??{setting:i,state:o}}}if("find"===t){let o=e.positionals??[];if(0===o.length)return{ok:!1,error:{code:"INVALID_ARGS",message:"find requires a locator or text"}};let{locator:n,query:r,action:s,value:l,timeoutMs:c}=function(e){let t="any",a=0;["text","label","value","role","id"].includes(e[0])&&(t=e[0],a=1);let i=e[a]??"",o=e.slice(a+1);if(0===o.length)return{locator:t,query:i,action:"click"};let n=o[0].toLowerCase();if("get"===n){let e=o[1]?.toLowerCase();if("text"===e)return{locator:t,query:i,action:"get_text"};if("attrs"===e)return{locator:t,query:i,action:"get_attrs"};throw new f("INVALID_ARGS","find get only supports text or attrs")}if("wait"===n)return{locator:t,query:i,action:"wait",timeoutMs:function(e){if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null}(o[1])??void 0};if("exists"===n)return{locator:t,query:i,action:"exists"};if("click"===n)return{locator:t,query:i,action:"click"};if("focus"===n)return{locator:t,query:i,action:"focus"};if("fill"===n)return{locator:t,query:i,action:"fill",value:o.slice(1).join(" ")};if("type"===n)return{locator:t,query:i,action:"type",value:o.slice(1).join(" ")};throw new f("INVALID_ARGS",`Unsupported find action: ${o[0]}`)}(o);if(!r)return{ok:!1,error:{code:"INVALID_ARGS",message:"find requires a value"}};let u=eV.get(a);if(!u&&"exists"!==s&&"wait"!==s&&"get_text"!==s&&"get_attrs"!==s)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session. Run open first."}};let d=u?.device??await eE(e.flags??{});u||await e4(d);let p=u?.appBundleId,h="role"!==n?r:void 0,m="click"===s||"focus"===s||"fill"===s||"type"===s,w=0,g=null,v=async()=>{let t=Date.now();if(g&&t-w<750)return{nodes:g};let i=await eR(d,"snapshot",[],e.flags?.out,{...ez({...e.flags,snapshotScope:h,snapshotInteractiveOnly:m,snapshotCompact:m},p,u?.trace?.outPath)}),o=i?.nodes??[],n=eC(e.flags?.snapshotRaw?o:e6(o));return w=t,g=n,u&&(u.snapshot={nodes:n,truncated:i?.truncated,createdAt:Date.now(),backend:i?.backend},eV.set(a,u)),{nodes:n,truncated:i?.truncated,backend:i?.backend}};if("wait"===s){let a=c??1e4,i=Date.now();for(;Date.now()-i<a;){let{nodes:a}=await v();if(eB(a,n,r,{requireRect:!1}))return u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{found:!0,waitedMs:Date.now()-i}}),{ok:!0,data:{found:!0,waitedMs:Date.now()-i}};await new Promise(e=>setTimeout(e,300))}return{ok:!1,error:{code:"COMMAND_FAILED",message:"find wait timed out"}}}let{nodes:y}=await v(),I=eB(y,n,r,{requireRect:m});if(!I)return{ok:!1,error:{code:"COMMAND_FAILED",message:"find did not match any element"}};let N=`@${I.ref}`,A={...e.flags??{},noRecord:!0};if("exists"===s)return u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{found:!0}}),{ok:!0,data:{found:!0}};if("get_text"===s){var i;let a=[(i=I).label,i.value,i.identifier].map(e=>"string"==typeof e?e.trim():"").filter(e=>e.length>0)[0]??"";return u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:N,action:"get text",text:a}}),{ok:!0,data:{ref:N,text:a,node:I}}}if("get_attrs"===s)return u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:N,action:"get attrs"}}),{ok:!0,data:{ref:N,node:I}};if("click"===s){let i=await eH({token:eX,session:a,command:"click",positionals:[N],flags:A});return i.ok&&u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:N,action:"click"}}),i}if("fill"===s){if(!l)return{ok:!1,error:{code:"INVALID_ARGS",message:"find fill requires text"}};let i=await eH({token:eX,session:a,command:"fill",positionals:[N,l],flags:A});return i.ok&&u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:N,action:"fill"}}),i}if("focus"===s){let a=I.rect?eF(I.rect):null;if(!a)return{ok:!1,error:{code:"COMMAND_FAILED",message:"matched element has no bounds"}};let i=await eR(d,"focus",[String(a.x),String(a.y)],e.flags?.out,{...ez(e.flags,u?.appBundleId,u?.trace?.outPath)});return u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:N,action:"focus"}}),{ok:!0,data:i??{ref:N}}}if("type"===s){if(!l)return{ok:!1,error:{code:"INVALID_ARGS",message:"find type requires text"}};let a=I.rect?eF(I.rect):null;if(!a)return{ok:!1,error:{code:"COMMAND_FAILED",message:"matched element has no bounds"}};await eR(d,"focus",[String(a.x),String(a.y)],e.flags?.out,{...ez(e.flags,u?.appBundleId,u?.trace?.outPath)});let i=await eR(d,"type",[l],e.flags?.out,{...ez(e.flags,u?.appBundleId,u?.trace?.outPath)});return u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:N,action:"type"}}),{ok:!0,data:i??{ref:N}}}}if("click"===t){let i=eV.get(a);if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let o=e.positionals?.[0]??"",n=eM(o);if(!n)return{ok:!1,error:{code:"INVALID_ARGS",message:"click requires a ref like @e2"}};let r=eT(i.snapshot.nodes,n);if(!r?.rect&&e.positionals.length>1){let t=e.positionals.slice(1).join(" ").trim();t.length>0&&(r=e2(i.snapshot.nodes,t))}if(!r?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${o} not found or has no bounds`}};let s=e3(r,i.snapshot.nodes),{x:l,y:c}=eF(r.rect);return await eR(i.device,"press",[String(l),String(c)],e.flags?.out,{...ez(e.flags,i.appBundleId,i.trace?.outPath)}),eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:n,x:l,y:c,refLabel:s}}),{ok:!0,data:{ref:n,x:l,y:c}}}if("fill"===t){let i=eV.get(a);if(e.positionals?.[0]?.startsWith("@")){let a;if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let o=eM(e.positionals[0]);if(!o)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires a ref like @e2"}};let n=e.positionals.length>=3?e.positionals[1]:"",r=e.positionals.length>=3?e.positionals.slice(2).join(" "):e.positionals.slice(1).join(" ");if(!r)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires text after ref"}};let s=eT(i.snapshot.nodes,o);if(!s?.rect&&n&&(s=e2(i.snapshot.nodes,n)),!s?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${e.positionals[0]} not found or has no bounds`}};let l=e3(s,i.snapshot.nodes),c=s.label?.trim();if("ios"===i.device.platform&&"simulator"===i.device.kind&&("textfield"===(a=e7(s.type??""))||"textview"===a||"searchfield"===a||"textarea"===a)){let a=s.rect?eF(s.rect):null;return a?(await eR(i.device,"focus",[String(a.x),String(a.y)],e.flags?.out,{...ez(e.flags,i.appBundleId,i.trace?.outPath)}),await ew(i.device,{command:"type",text:r,appBundleId:i.appBundleId},{verbose:e.flags?.verbose,logPath:eq,traceLogPath:i?.trace?.outPath}),eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:o,refLabel:l??c,action:"fill",text:r}}),{ok:!0,data:{ref:o}}):{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${e.positionals[0]} not found or has no bounds`}}}let{x:u,y:d}=eF(s.rect),p=await eR(i.device,"fill",[String(u),String(d),r],e.flags?.out,{...ez(e.flags,i.appBundleId,i.trace?.outPath)});return eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:p??{ref:o,x:u,y:d,refLabel:l}}),{ok:!0,data:p??{ref:o,x:u,y:d}}}}if("get"===t){let i=e.positionals?.[0],o=e.positionals?.[1];if("text"!==i&&"attrs"!==i)return{ok:!1,error:{code:"INVALID_ARGS",message:"get only supports text or attrs"}};let n=eV.get(a);if(!n?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let r=eM(o??"");if(!r)return{ok:!1,error:{code:"INVALID_ARGS",message:"get text requires a ref like @e2"}};let s=eT(n.snapshot.nodes,r);if(!s&&e.positionals.length>2){let t=e.positionals.slice(2).join(" ").trim();t.length>0&&(s=e2(n.snapshot.nodes,t))}if(!s)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${o} not found`}};if("attrs"===i)return eZ(n,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:r}}),{ok:!0,data:{ref:r,node:s}};let l=[s.label,s.value,s.identifier].map(e=>"string"==typeof e?e.trim():"").filter(e=>e.length>0)[0]??"";return eZ(n,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:r,text:l,refLabel:l||void 0}}),{ok:!0,data:{ref:r,text:l,node:s}}}let o=eV.get(a);if(!o)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session. Run open first."}};let r=await eR(o.device,t,e.positionals??[],e.flags?.out,{...ez(e.flags,o.appBundleId,o.trace?.outPath)});return eZ(o,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:r??{}}),{ok:!0,data:r??{}}}function eZ(e,t){t.flags?.noRecord||e.actions.push({ts:Date.now(),command:t.command,positionals:t.positionals,flags:function(e){if(!e)return{};let{platform:t,device:a,udid:i,serial:o,out:n,verbose:r,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:u,snapshotRaw:d,snapshotBackend:p,appsMetadata:f,noRecord:h,recordJson:m}=e;return{platform:t,device:a,udid:i,serial:o,out:n,verbose:r,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:u,snapshotRaw:d,snapshotBackend:p,appsMetadata:f,noRecord:h,recordJson:m}}(t.flags),result:t.result})}async function eY(e,t,a){let i=eK(await eR(e,"snapshot",[],a?.out,{...ez({...a,snapshotDepth:1,snapshotCompact:!0,snapshotBackend:"ax"},void 0,t)}));if(i?.appName||i?.appBundleId)return{appName:i.appName??i.appBundleId??"unknown",appBundleId:i.appBundleId,source:"snapshot-ax"};let o=eK(await eR(e,"snapshot",[],a?.out,{...ez({...a,snapshotDepth:1,snapshotCompact:!0,snapshotBackend:"xctest"},void 0,t)}));return{appName:o?.appName??o?.appBundleId??"unknown",appBundleId:o?.appBundleId,source:"snapshot-xctest"}}function eK(e){let t=eC(e?.nodes??[]),a=t.find(e=>"application"===e7(e.type??""))??t[0];if(!a)return null;let i=a.label?.trim(),o=a.identifier?.trim();return i||o?{appName:i||void 0,appBundleId:o||void 0}:null}function eQ(e){try{d.existsSync(eJ)||d.mkdirSync(eJ,{recursive:!0});let t=e.name.replace(/[^a-zA-Z0-9._-]/g,"_"),a=new Date(e.createdAt).toISOString().replace(/[:.]/g,"-"),i=n.join(eJ,`${t}-${a}.ad`),o=n.join(eJ,`${t}-${a}.json`),r={name:e.name,device:e.device,createdAt:e.createdAt,appBundleId:e.appBundleId,actions:e.actions,optimizedActions:function(e){let t=[];for(let a of e.actions)if("snapshot"!==a.command){if("click"===a.command||"fill"===a.command||"get"===a.command){let i=a.result?.refLabel;"string"==typeof i&&i.trim().length>0&&t.push({ts:a.ts,command:"snapshot",positionals:[],flags:{platform:e.device.platform,snapshotInteractiveOnly:!0,snapshotCompact:!0,snapshotScope:i.trim()},result:{scope:i.trim()}})}t.push(a)}return t}(e)},s=function(e,t){let a=[],i=e.device.name.replace(/"/g,'\\"'),o=e.device.kind?` kind=${e.device.kind}`:"";for(let n of(a.push(`context platform=${e.device.platform} device="${i}"${o} theme=unknown`),t))n.flags?.noRecord||a.push(function(e){let t=[e.command];if("click"===e.command){let a=e.positionals?.[0];if(a){t.push(e1(a));let i=e.result?.refLabel;return"string"==typeof i&&i.trim().length>0&&t.push(e1(i)),t.join(" ")}}if("fill"===e.command){let a=e.positionals?.[0];if(a&&a.startsWith("@")){t.push(e1(a));let i=e.result?.refLabel,o=e.positionals.slice(1).join(" ");return"string"==typeof i&&i.trim().length>0&&t.push(e1(i)),o&&t.push(e1(o)),t.join(" ")}}if("get"===e.command){let a=e.positionals?.[0],i=e.positionals?.[1];if(a&&i){t.push(e1(a)),t.push(e1(i));let o=e.result?.refLabel;return"string"==typeof o&&o.trim().length>0&&t.push(e1(o)),t.join(" ")}}if("snapshot"===e.command)return e.flags?.snapshotInteractiveOnly&&t.push("-i"),e.flags?.snapshotCompact&&t.push("-c"),"number"==typeof e.flags?.snapshotDepth&&t.push("-d",String(e.flags.snapshotDepth)),e.flags?.snapshotScope&&t.push("-s",e1(e.flags.snapshotScope)),e.flags?.snapshotRaw&&t.push("--raw"),e.flags?.snapshotBackend&&t.push("--backend",e.flags.snapshotBackend),t.join(" ");for(let a of e.positionals??[])t.push(e1(a));return t.join(" ")}(n));return`${a.join("\n")}
6
- `}(e,r.optimizedActions);d.writeFileSync(i,s),e.actions.some(e=>e.flags?.recordJson)&&d.writeFileSync(o,JSON.stringify(r,null,2))}catch{}}function e0(e){return e.startsWith("~/")?n.join(p.homedir(),e.slice(2)):n.resolve(e)}function e1(e){let t=e.trim();return t.startsWith("@")||/^-?\d+(\.\d+)?$/.test(t)?t:JSON.stringify(t)}function e2(e,t){let a=t.toLowerCase();return e.find(e=>{let t=(e.label??"").toLowerCase(),i=(e.value??"").toLowerCase(),o=(e.identifier??"").toLowerCase();return t.includes(a)||i.includes(a)||o.includes(a)})??null}function e3(e,t){let a=[e.label,e.value,e.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0);return a&&e8(a)?a:function(e,t){if(!e.rect)return;let a=e.rect.y+e.rect.height/2,i=null;for(let e of t){if(!e.rect)continue;let t=[e.label,e.value,e.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0);if(!t||!e8(t))continue;let o=Math.abs(e.rect.y+e.rect.height/2-a);(!i||o<i.distance)&&(i={label:t,distance:o})}return i?.label}(e,t)??(a&&e8(a)?a:void 0)}function e8(e){let t=e.trim();return!(!t||/^(true|false)$/i.test(t)||/^\d+$/.test(t))}async function e4(e){if("ios"===e.platform&&"simulator"===e.kind){let{ensureBootedSimulator:t}=await Promise.resolve().then(()=>({ensureBootedSimulator:ef}));await t(e);return}if("android"===e.platform){let{waitForAndroidBoot:t}=await Promise.resolve().then(()=>({waitForAndroidBoot:I}));await t(e.id)}}function e6(e){let t=[],a=[];for(let i of e){let e=i.depth??0;for(;t.length>0&&e<=t[t.length-1];)t.pop();let o=e7(i.type??""),n=[i.label,i.value,i.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0),r=!!n&&e8(n);if(("group"===o||"ioscontentgroup"===o)&&!r){t.push(e);continue}let s=Math.max(0,e-t.length);a.push({...i,depth:s})}return a}function e7(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase();return t.startsWith("ax")&&(t=t.replace(/^ax/,"")),t}(e=m.createServer(e=>{let t="";e.setEncoding("utf8"),e.on("data",async a=>{let i=(t+=a).indexOf("\n");for(;-1!==i;){let a,o=t.slice(0,i).trim();if(t=t.slice(i+1),0===o.length){i=t.indexOf("\n");continue}try{let e=JSON.parse(o);a=await eH(e)}catch(t){let e=l(t);a={ok:!1,error:{code:e.code,message:e.message,details:e.details}}}e.write(`${JSON.stringify(a)}
5
+ `))),0!==s.exitCode){let e,t,a=(s.stderr??"").toString(),i=(e=a.toLowerCase()).includes("accessibility permission")?" Enable Accessibility for your terminal in System Settings > Privacy & Security > Accessibility, or use --backend xctest (slower snapshots via XCTest).":e.includes("could not find ios app content")?" AX snapshot sometimes caches empty content. Try restarting the Simulator app.":"",n=!!((t=a.toLowerCase()).includes("could not find ios app content")||t.includes("timeout"));throw new f("COMMAND_FAILED","AX snapshot failed",{stderr:`${a}${i}`,stdout:s.stdout,retryable:n})}return s},{shouldRetry:e=>{var t;return(t=e)instanceof f&&"COMMAND_FAILED"===t.code&&t.details?.retryable===!0}});try{let e=JSON.parse(r.stdout);if(e&&"object"==typeof e&&"root"in e){if(!e.root)throw Error("AX snapshot missing root");a=e.root,i=e.windowFrame??void 0}else a=e}catch(e){throw new f("COMMAND_FAILED","Invalid AX snapshot JSON",{error:String(e)})}let o=a.frame??i,s=[],l=[],c=(e,t)=>{e.frame&&s.push(e.frame);let a=e.frame&&o?{x:e.frame.x-o.x,y:e.frame.y-o.y,width:e.frame.width,height:e.frame.height}:e.frame;for(let i of(l.push({...e,frame:a,children:void 0,depth:t}),e.children??[]))c(i,t+1)};return c(a,0),{nodes:(function(e,t,a){if(!t||0===a.length)return e;let i=1/0,n=1/0;for(let e of a)e.x<i&&(i=e.x),e.y<n&&(n=e.y);return i<=5&&n<=5?e.map(e=>({...e,frame:e.frame?{x:e.frame.x+t.x,y:e.frame.y+t.y,width:e.frame.width,height:e.frame.height}:void 0})):e})(l,o,s).map((e,t)=>({index:t,type:e.subrole??e.role,label:e.label,value:e.value,identifier:e.identifier,rect:e.frame?{x:e.frame.x,y:e.frame.y,width:e.frame.width,height:e.frame.height}:void 0,depth:e.depth}))}}async function e_(){let e=function(){let e=r.dirname(c(import.meta.url));for(let t=0;t<6;t+=1){let t=r.join(e,"package.json");if(d.existsSync(t))return e;e=r.dirname(e)}return process.cwd()}(),t=r.join(e,"ios-runner","AXSnapshot"),a=process.env.AGENT_DEVICE_AX_BINARY;if(a&&d.existsSync(a))return a;let i=r.join(e,"dist","bin","axsnapshot");if(d.existsSync(i))return i;let n=r.join(t,".build","release","axsnapshot");if(d.existsSync(n))return n;let o=await h("swift",["build","-c","release"],{cwd:t,allowFailure:!0});if(0!==o.exitCode||!d.existsSync(n))throw new f("COMMAND_FAILED","Failed to build AX snapshot tool",{stderr:o.stderr,stdout:o.stdout});return n}async function eE(e){let t={platform:e.platform,deviceName:e.device,udid:e.udid,serial:e.serial};if("android"===t.platform){await J();let e=await v();return await g(e,t)}if("ios"===t.platform){let e=await Z();return await g(e,t)}let a=[];try{a.push(...await v())}catch{}try{a.push(...await Z())}catch{}return await g(a,t)}async function eR(e,t,a,i,n){let r=function(e){switch(e.platform){case"android":return{open:(t,a)=>x(e,t,a?.activity),openDevice:()=>L(e),close:t=>_(e,t),tap:(t,a)=>E(e,t,a),longPress:(t,a,i)=>T(e,t,a,i),focus:(t,a)=>B(e,t,a),type:t=>F(e,t),fill:(t,a,i)=>$(e,t,a,i),scroll:(t,a)=>U(e,t,a),scrollIntoView:t=>V(e,t),screenshot:t=>j(e,t)};case"ios":return{open:t=>Q(e,t),openDevice:()=>ee(e),close:t=>et(e,t),tap:(t,a)=>ea(e,t,a),longPress:(t,a,i)=>ei(e,t,a,i),focus:(t,a)=>en(e,t,a),type:t=>er(e,t),fill:(t,a,i)=>eo(e,t,a,i),scroll:(t,a)=>es(e,t,a),scrollIntoView:e=>el(e),screenshot:t=>ec(e,t)};default:throw new f("UNSUPPORTED_PLATFORM",`Unsupported platform: ${e.platform}`)}}(e);switch(t){case"open":{let e=a[0];if(!e)return await r.openDevice(),{app:null};return await r.open(e,{activity:n?.activity}),{app:e}}case"close":{let e=a[0];if(!e)return{closed:"session"};return await r.close(e),{app:e}}case"press":{let[t,i]=a.map(Number);if(Number.isNaN(t)||Number.isNaN(i))throw new f("INVALID_ARGS","press requires x y");return"ios"===e.platform&&"simulator"===e.kind?await ew(e,{command:"tap",x:t,y:i,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}):await r.tap(t,i),{x:t,y:i}}case"long-press":{let e=Number(a[0]),t=Number(a[1]),i=a[2]?Number(a[2]):void 0;if(Number.isNaN(e)||Number.isNaN(t))throw new f("INVALID_ARGS","long-press requires x y [durationMs]");return await r.longPress(e,t,i),{x:e,y:t,durationMs:i}}case"focus":{let[t,i]=a.map(Number);if(Number.isNaN(t)||Number.isNaN(i))throw new f("INVALID_ARGS","focus requires x y");return"ios"===e.platform&&"simulator"===e.kind?await ew(e,{command:"tap",x:t,y:i,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}):await r.focus(t,i),{x:t,y:i}}case"type":{let t=a.join(" ");if(!t)throw new f("INVALID_ARGS","type requires text");return"ios"===e.platform&&"simulator"===e.kind?await ew(e,{command:"type",text:t,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}):await r.type(t),{text:t}}case"fill":{let t=Number(a[0]),i=Number(a[1]),o=a.slice(2).join(" ");if(Number.isNaN(t)||Number.isNaN(i)||!o)throw new f("INVALID_ARGS","fill requires x y text");return"ios"===e.platform&&"simulator"===e.kind?(await ew(e,{command:"tap",x:t,y:i,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),await ew(e,{command:"type",text:o,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath})):await r.fill(t,i,o),{x:t,y:i,text:o}}case"scroll":{let t=a[0],i=a[1]?Number(a[1]):void 0;if(!t)throw new f("INVALID_ARGS","scroll requires direction");if("ios"===e.platform&&"simulator"===e.kind){if(!["up","down","left","right"].includes(t))throw new f("INVALID_ARGS",`Unknown direction: ${t}`);let a=function(e){switch(e){case"up":return"down";case"down":return"up";case"left":return"right";case"right":return"left"}}(t);await ew(e,{command:"swipe",direction:a,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath})}else await r.scroll(t,i);return{direction:t,amount:i}}case"scrollintoview":{let t=a.join(" ").trim();if(!t)throw new f("INVALID_ARGS","scrollintoview requires text");if("ios"===e.platform&&"simulator"===e.kind){for(let a=0;a<8;a+=1){let i=await ew(e,{command:"findText",text:t,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath});if(i?.found)return{text:t,attempts:a+1};await ew(e,{command:"swipe",direction:"up",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),await new Promise(e=>setTimeout(e,300))}throw new f("COMMAND_FAILED",`scrollintoview could not find text: ${t}`)}return await r.scrollIntoView(t),{text:t}}case"pinch":{let t=Number(a[0]),i=a[1]?Number(a[1]):void 0,r=a[2]?Number(a[2]):void 0;if(Number.isNaN(t)||t<=0)throw new f("INVALID_ARGS","pinch requires scale > 0");if("ios"===e.platform&&"simulator"===e.kind)await ew(e,{command:"pinch",scale:t,x:i,y:r,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath});else throw new f("UNSUPPORTED_OPERATION","pinch is only supported on iOS simulators");return{scale:t,x:i,y:r}}case"screenshot":{let e=i??`./screenshot-${Date.now()}.png`;return await r.screenshot(e),{path:e}}case"back":if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","back is only supported on iOS simulators in v1");return await ew(e,{command:"back",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),{action:"back"}}return await R(e),{action:"back"};case"home":if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","home is only supported on iOS simulators in v1");return await ew(e,{command:"home",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),{action:"home"}}return await C(e),{action:"home"};case"app-switcher":if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","app-switcher is only supported on iOS simulators in v1");return await ew(e,{command:"appSwitcher",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),{action:"app-switcher"}}return await M(e),{action:"app-switcher"};case"settings":{let[t,i,r]=a;if("ios"===e.platform)return await eu(e,t,i,r??n?.appBundleId),{setting:t,state:i};return await G(e,t,i),{setting:t,state:i}}case"snapshot":{let t=n?.snapshotBackend??"xctest";if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","snapshot is only supported on iOS simulators in v1");if("ax"===t)return{nodes:(await eL(e,{traceLogPath:n?.traceLogPath})).nodes??[],truncated:!1,backend:"ax"};let a=await ew(e,{command:"snapshot",appBundleId:n?.appBundleId,interactiveOnly:n?.snapshotInteractiveOnly,compact:n?.snapshotCompact,depth:n?.snapshotDepth,scope:n?.snapshotScope,raw:n?.snapshotRaw},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),i=a.nodes??[];if(0===i.length)try{return{nodes:(await eL(e,{traceLogPath:n?.traceLogPath})).nodes??[],truncated:!1,backend:"ax"}}catch{}return{nodes:i,truncated:a.truncated??!1,backend:"xctest"}}let a=await q(e,{interactiveOnly:n?.snapshotInteractiveOnly,compact:n?.snapshotCompact,depth:n?.snapshotDepth,scope:n?.snapshotScope,raw:n?.snapshotRaw});return{nodes:a.nodes??[],truncated:a.truncated??!1,backend:"android"}}default:throw new f("INVALID_ARGS",`Unknown command: ${t}`)}}function eC(e){return e.map((e,t)=>({...e,ref:`e${t+1}`}))}function eM(e){let t=e.trim();return t.startsWith("@")?t.slice(1)||null:t.startsWith("e")?t:null}function eT(e,t){return e.find(e=>e.ref===t)??null}function eF(e){return{x:Math.round(e.x+e.width/2),y:Math.round(e.y+e.height/2)}}function eB(e,t,a,i={}){let n=eU(a);if(!n)return null;let r=null;for(let a of e){if(i.requireRect&&!a.rect)continue;let e=function(e,t,a){switch(t){case"role":return function(e,t){let a=function(e){let t=e.trim();return t?((t=(t.split(".").pop()??t).replace(/XCUIElementType/gi,"").toLowerCase()).startsWith("ax")&&(t=t.replace(/^ax/,"")),t):""}(e??"");return a?a===t?2:+!!a.includes(t):0}(e.type,a);case"label":return e$(e.label,a);case"value":return e$(e.value,a);case"id":return e$(e.identifier,a);default:return Math.max(e$(e.label,a),e$(e.value,a),e$(e.identifier,a))}}(a,t,n);if(!(e<=0)&&(!r||e>r.score)&&(r={node:a,score:e},e>=2))break}return r?.node??null}function e$(e,t){let a=eU(e??"");return a?a===t?2:+!!a.includes(t):0}function eU(e){return e.trim().toLowerCase().replace(/\s+/g," ")}let eV=new Map,ej=r.join(p.homedir(),".agent-device"),eG=r.join(ej,"daemon.json"),eq=r.join(ej,"daemon.log"),eJ=r.join(ej,"sessions"),eW=function(){try{let e=function(){let e=r.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=r.join(t,"package.json");if(d.existsSync(e))return t;t=r.dirname(t)}return e}();return JSON.parse(d.readFileSync(r.join(e,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}(),eX=a.randomBytes(24).toString("hex");function ez(e,t,a){return{appBundleId:t,activity:e?.activity,verbose:e?.verbose,logPath:eq,traceLogPath:a,snapshotInteractiveOnly:e?.snapshotInteractiveOnly,snapshotCompact:e?.snapshotCompact,snapshotDepth:e?.snapshotDepth,snapshotScope:e?.snapshotScope,snapshotRaw:e?.snapshotRaw,snapshotBackend:e?.snapshotBackend}}async function eH(e){if(e.token!==eX)return{ok:!1,error:{code:"UNAUTHORIZED",message:"Invalid token"}};let t=e.command,a=e.session||"default";if("session_list"===t)return{ok:!0,data:{sessions:Array.from(eV.values()).map(e=>({name:e.name,platform:e.device.platform,device:e.device.name,id:e.device.id,createdAt:e.createdAt}))}};if("devices"===t)try{let t=[];if(e.flags?.platform==="android"){let{listAndroidDevices:e}=await Promise.resolve().then(()=>({listAndroidDevices:v}));t.push(...await e())}else if(e.flags?.platform==="ios"){let{listIosDevices:e}=await Promise.resolve().then(()=>({listIosDevices:Z}));t.push(...await e())}else{let{listAndroidDevices:e}=await Promise.resolve().then(()=>({listAndroidDevices:v})),{listIosDevices:a}=await Promise.resolve().then(()=>({listIosDevices:Z}));try{t.push(...await e())}catch{}try{t.push(...await a())}catch{}}return{ok:!0,data:{devices:t}}}catch(t){let e=l(t);return{ok:!1,error:{code:e.code,message:e.message,details:e.details}}}if("apps"===t){let t=eV.get(a),i=e.flags??{};if(!t&&!i.platform&&!i.device&&!i.udid&&!i.serial)return{ok:!1,error:{code:"INVALID_ARGS",message:"apps requires an active session or an explicit device selector (e.g. --platform ios)."}};let n=t?.device??await eE(i);if(await e4(n),"ios"===n.platform){if("simulator"!==n.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"apps list is only supported on iOS simulators"}};let{listSimulatorApps:t}=await Promise.resolve().then(()=>({listSimulatorApps:ep})),a=await t(n);return e.flags?.appsMetadata?{ok:!0,data:{apps:a}}:{ok:!0,data:{apps:a.map(e=>e.name&&e.name!==e.bundleId?`${e.name} (${e.bundleId})`:e.bundleId)}}}let{listAndroidApps:r,listAndroidAppsMetadata:o}=await Promise.resolve().then(()=>({listAndroidApps:D,listAndroidAppsMetadata:k}));return e.flags?.appsMetadata?{ok:!0,data:{apps:await o(n,e.flags?.appsFilter)}}:{ok:!0,data:{apps:await r(n,e.flags?.appsFilter)}}}if("appstate"===t){let t=eV.get(a),i=e.flags??{},n=t?.device??await eE(i);if(await e4(n),"ios"===n.platform){if(t?.appBundleId)return{ok:!0,data:{platform:"ios",appBundleId:t.appBundleId,appName:t.appName??t.appBundleId,source:"session"}};let a=await eY(n,t?.trace?.outPath,e.flags);return{ok:!0,data:{platform:"ios",appName:a.appName,appBundleId:a.appBundleId,source:a.source}}}let{getAndroidAppState:r}=await Promise.resolve().then(()=>({getAndroidAppState:P})),o=await r(n);return{ok:!0,data:{platform:"android",package:o.package,activity:o.activity}}}if("open"===t){let i;if(eV.has(a)){let i,n=eV.get(a),r=e.positionals?.[0];if(!n||!r)return{ok:!1,error:{code:"INVALID_ARGS",message:"Session already active. Close it first or pass a new --session name."}};if("ios"===n.device.platform)try{let{resolveIosApp:e}=await Promise.resolve().then(()=>({resolveIosApp:K}));i=await e(n.device,r)}catch{i=void 0}await eR(n.device,"open",e.positionals??[],e.flags?.out,{...ez(e.flags,i)});let o={...n,appBundleId:i,appName:r,snapshot:void 0};return eZ(o,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:a,appName:r,appBundleId:i}}),eV.set(a,o),{ok:!0,data:{session:a,appName:r,appBundleId:i}}}let n=await eE(e.flags??{});await e4(n);let r=Array.from(eV.values()).find(e=>e.device.id===n.id);if(r)return{ok:!1,error:{code:"DEVICE_IN_USE",message:`Device is already in use by session "${r.name}".`,details:{session:r.name,deviceId:n.id,deviceName:n.name}}};let o=e.positionals?.[0];if("ios"===n.platform)try{let{resolveIosApp:t}=await Promise.resolve().then(()=>({resolveIosApp:K}));i=await t(n,e.positionals?.[0]??"")}catch{i=void 0}await eR(n,"open",e.positionals??[],e.flags?.out,{...ez(e.flags,i)});let s={name:a,device:n,createdAt:Date.now(),appBundleId:i,appName:o,actions:[]};return eZ(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:a}}),eV.set(a,s),{ok:!0,data:{session:a}}}if("replay"===t){let t=e.positionals?.[0];if(!t)return{ok:!1,error:{code:"INVALID_ARGS",message:"replay requires a path"}};try{let e=e0(t),i=JSON.parse(d.readFileSync(e,"utf8")),n=i.optimizedActions??i.actions??[];for(let e of n)e&&"replay"!==e.command&&await eH({token:eX,session:a,command:e.command,positionals:e.positionals??[],flags:e.flags??{}});return{ok:!0,data:{replayed:n.length,session:a}}}catch(t){let e=l(t);return{ok:!1,error:{code:e.code,message:e.message}}}}if("close"===t){let i=eV.get(a);return i?(e.positionals&&e.positionals.length>0&&await eR(i.device,"close",e.positionals??[],e.flags?.out,{...ez(e.flags,i.appBundleId,i.trace?.outPath)}),"ios"===i.device.platform&&"simulator"===i.device.kind&&await ev(i.device.id),eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:a}}),eQ(i),eV.delete(a),{ok:!0,data:{session:a}}):{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session"}}}if("snapshot"===t){let i=eV.get(a),n=i?.device??await eE(e.flags??{});i||await e4(n);let r=i?.appBundleId,o=e.flags?.snapshotScope;if(o&&o.trim().startsWith("@")){if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"Ref scope requires an existing snapshot in session."}};let e=eM(o.trim());if(!e)return{ok:!1,error:{code:"INVALID_ARGS",message:`Invalid ref scope: ${o}`}};let t=eT(i.snapshot.nodes,e),a=t?e3(t,i.snapshot.nodes):void 0;if(!a)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${o} not found or has no label`}};o=a}let s=await eR(n,"snapshot",[],e.flags?.out,{...ez({...e.flags,snapshotScope:o},r,i?.trace?.outPath)}),l=s?.nodes??[],c=eC(e.flags?.snapshotRaw?l:e6(l)),u={nodes:c,truncated:s?.truncated,createdAt:Date.now(),backend:s?.backend},d={name:a,device:n,createdAt:i?.createdAt??Date.now(),appBundleId:i?.appBundleId??r,snapshot:u,actions:i?.actions??[],appName:i?.appName};return eZ(d,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{nodes:c.length,truncated:s?.truncated??!1}}),eV.set(a,d),{ok:!0,data:{nodes:c,truncated:s?.truncated??!1,appName:d.appBundleId?d.appName??d.appBundleId:void 0,appBundleId:d.appBundleId}}}if("wait"===t){let i=eV.get(a),n=i?.device??await eE(e.flags??{});i||await e4(n);let r=e.positionals??[];if(0===r.length)return{ok:!1,error:{code:"INVALID_ARGS",message:"wait requires a duration or text"}};let o=e=>{if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null},s=o(r[0]);if(null!==s)return await new Promise(e=>setTimeout(e,s)),i&&eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{waitedMs:s}}),{ok:!0,data:{waitedMs:s}};let l="",c=null;if("text"===r[0])l=null!==(c=o(r[r.length-1]))?r.slice(1,-1).join(" "):r.slice(1).join(" ");else if(r[0].startsWith("@")){if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"Ref wait requires an existing snapshot in session."}};let e=eM(r[0]);if(!e)return{ok:!1,error:{code:"INVALID_ARGS",message:`Invalid ref: ${r[0]}`}};let t=eT(i.snapshot.nodes,e),a=t?e3(t,i.snapshot.nodes):void 0;if(!a)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${r[0]} not found or has no label`}};c=o(r[r.length-1]),l=a}else l=null!==(c=o(r[r.length-1]))?r.slice(0,-1).join(" "):r.join(" ");if(!(l=l.trim()))return{ok:!1,error:{code:"INVALID_ARGS",message:"wait requires text"}};let u=c??1e4,d=Date.now();for(;Date.now()-d<u;){if("ios"===n.platform&&"simulator"===n.kind){let a=await ew(n,{command:"findText",text:l,appBundleId:i?.appBundleId},{verbose:e.flags?.verbose,logPath:eq,traceLogPath:i?.trace?.outPath});if(a?.found)return i&&eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{text:l,waitedMs:Date.now()-d}}),{ok:!0,data:{text:l,waitedMs:Date.now()-d}}}else if("android"!==n.platform)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"wait is not supported on this device"}};else if(e2(eC((await q(n,{scope:l})).nodes??[]),l))return i&&eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{text:l,waitedMs:Date.now()-d}}),{ok:!0,data:{text:l,waitedMs:Date.now()-d}};await new Promise(e=>setTimeout(e,300))}return{ok:!1,error:{code:"COMMAND_FAILED",message:`wait timed out for text: ${l}`}}}if("alert"===t){let i=eV.get(a),n=i?.device??await eE(e.flags??{});i||await e4(n);let r=(e.positionals?.[0]??"get").toLowerCase();if("ios"!==n.platform||"simulator"!==n.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"alert is only supported on iOS simulators in v1"}};if("wait"===r){let a=(e=>{if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null})(e.positionals?.[1])??1e4,r=Date.now();for(;Date.now()-r<a;){try{let a=await ew(n,{command:"alert",action:"get",appBundleId:i?.appBundleId},{verbose:e.flags?.verbose,logPath:eq,traceLogPath:i?.trace?.outPath});return i&&eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:a}),{ok:!0,data:a}}catch{}await new Promise(e=>setTimeout(e,300))}return{ok:!1,error:{code:"COMMAND_FAILED",message:"alert wait timed out"}}}let o=await ew(n,{command:"alert",action:"accept"===r||"dismiss"===r?r:"get",appBundleId:i?.appBundleId},{verbose:e.flags?.verbose,logPath:eq,traceLogPath:i?.trace?.outPath});return i&&eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:o}),{ok:!0,data:o}}if("record"===t){let i=(e.positionals?.[0]??"").toLowerCase();if(!["start","stop"].includes(i))return{ok:!1,error:{code:"INVALID_ARGS",message:"record requires start|stop"}};let n=eV.get(a),o=n?.device??await eE(e.flags??{});n||await e4(o);let s=n??{name:a,device:o,createdAt:Date.now(),actions:[]};if("start"===i){if(s.recording)return{ok:!1,error:{code:"INVALID_ARGS",message:"recording already in progress"}};let i=e.positionals?.[1]??`./recording-${Date.now()}.mp4`,n=r.resolve(i),l=r.dirname(n);if(d.existsSync(l)||d.mkdirSync(l,{recursive:!0}),"ios"===o.platform){if("simulator"!==o.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"record is only supported on iOS simulators in v1"}};let{child:e,wait:t}=u("xcrun",["simctl","io",o.id,"recordVideo",n],{allowFailure:!0});s.recording={platform:"ios",outPath:n,child:e,wait:t}}else{let e=`/sdcard/agent-device-recording-${Date.now()}.mp4`,{child:t,wait:a}=u("adb",["-s",o.id,"shell","screenrecord",e],{allowFailure:!0});s.recording={platform:"android",outPath:n,remotePath:e,child:t,wait:a}}return eV.set(a,s),eZ(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"start"}}),{ok:!0,data:{recording:"started",outPath:i}}}if(!s.recording)return{ok:!1,error:{code:"INVALID_ARGS",message:"no active recording"}};let l=s.recording;l.child.kill("SIGINT");try{await l.wait}catch{}if("android"===l.platform&&l.remotePath)try{await h("adb",["-s",o.id,"pull",l.remotePath,l.outPath],{allowFailure:!0}),await h("adb",["-s",o.id,"shell","rm","-f",l.remotePath],{allowFailure:!0})}catch{}return s.recording=void 0,eZ(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"stop",outPath:l.outPath}}),{ok:!0,data:{recording:"stopped",outPath:l.outPath}}}if("trace"===t){let i=(e.positionals?.[0]??"").toLowerCase();if(!["start","stop"].includes(i))return{ok:!1,error:{code:"INVALID_ARGS",message:"trace requires start|stop"}};let n=eV.get(a);if(!n)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session"}};if("start"===i){let a,i;if(n.trace)return{ok:!1,error:{code:"INVALID_ARGS",message:"trace already in progress"}};let o=e0(e.positionals?.[1]??(a=n.name.replace(/[^a-zA-Z0-9._-]/g,"_"),i=new Date().toISOString().replace(/[:.]/g,"-"),r.join(eJ,`${a}-${i}.trace.log`)));return d.mkdirSync(r.dirname(o),{recursive:!0}),d.appendFileSync(o,""),n.trace={outPath:o,startedAt:Date.now()},eZ(n,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"start",outPath:o}}),{ok:!0,data:{trace:"started",outPath:o}}}if(!n.trace)return{ok:!1,error:{code:"INVALID_ARGS",message:"no active trace"}};let o=n.trace.outPath;if(e.positionals?.[1]){let t=e0(e.positionals[1]);d.mkdirSync(r.dirname(t),{recursive:!0}),d.existsSync(o)?d.renameSync(o,t):d.appendFileSync(t,""),o=t}return n.trace=void 0,eZ(n,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"stop",outPath:o}}),{ok:!0,data:{trace:"stopped",outPath:o}}}if("settings"===t){let i=e.positionals?.[0],n=e.positionals?.[1];if(!i||!n)return{ok:!1,error:{code:"INVALID_ARGS",message:"settings requires <wifi|airplane|location> <on|off>"}};let r=eV.get(a),o=r?.device??await eE(e.flags??{});r||await e4(o);let s=r?.appBundleId,l=await eR(o,"settings",[i,n,s??""],e.flags?.out,{...ez(e.flags,s,r?.trace?.outPath)});return r&&eZ(r,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:l??{setting:i,state:n}}),{ok:!0,data:l??{setting:i,state:n}}}if("find"===t){let n=e.positionals??[];if(0===n.length)return{ok:!1,error:{code:"INVALID_ARGS",message:"find requires a locator or text"}};let{locator:r,query:o,action:s,value:l,timeoutMs:c}=function(e){let t="any",a=0;["text","label","value","role","id"].includes(e[0])&&(t=e[0],a=1);let i=e[a]??"",n=e.slice(a+1);if(0===n.length)return{locator:t,query:i,action:"click"};let r=n[0].toLowerCase();if("get"===r){let e=n[1]?.toLowerCase();if("text"===e)return{locator:t,query:i,action:"get_text"};if("attrs"===e)return{locator:t,query:i,action:"get_attrs"};throw new f("INVALID_ARGS","find get only supports text or attrs")}if("wait"===r)return{locator:t,query:i,action:"wait",timeoutMs:function(e){if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null}(n[1])??void 0};if("exists"===r)return{locator:t,query:i,action:"exists"};if("click"===r)return{locator:t,query:i,action:"click"};if("focus"===r)return{locator:t,query:i,action:"focus"};if("fill"===r)return{locator:t,query:i,action:"fill",value:n.slice(1).join(" ")};if("type"===r)return{locator:t,query:i,action:"type",value:n.slice(1).join(" ")};throw new f("INVALID_ARGS",`Unsupported find action: ${n[0]}`)}(n);if(!o)return{ok:!1,error:{code:"INVALID_ARGS",message:"find requires a value"}};let u=eV.get(a);if(!u&&"exists"!==s&&"wait"!==s&&"get_text"!==s&&"get_attrs"!==s)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session. Run open first."}};let d=u?.device??await eE(e.flags??{});u||await e4(d);let p=u?.appBundleId,h="role"!==r?o:void 0,m="click"===s||"focus"===s||"fill"===s||"type"===s,w=0,g=null,v=async()=>{let t=Date.now();if(g&&t-w<750)return{nodes:g};let i=await eR(d,"snapshot",[],e.flags?.out,{...ez({...e.flags,snapshotScope:h,snapshotInteractiveOnly:m,snapshotCompact:m},p,u?.trace?.outPath)}),n=i?.nodes??[],r=eC(e.flags?.snapshotRaw?n:e6(n));return w=t,g=r,u&&(u.snapshot={nodes:r,truncated:i?.truncated,createdAt:Date.now(),backend:i?.backend},eV.set(a,u)),{nodes:r,truncated:i?.truncated,backend:i?.backend}};if("wait"===s){let a=c??1e4,i=Date.now();for(;Date.now()-i<a;){let{nodes:a}=await v();if(eB(a,r,o,{requireRect:!1}))return u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{found:!0,waitedMs:Date.now()-i}}),{ok:!0,data:{found:!0,waitedMs:Date.now()-i}};await new Promise(e=>setTimeout(e,300))}return{ok:!1,error:{code:"COMMAND_FAILED",message:"find wait timed out"}}}let{nodes:y}=await v(),I=eB(y,r,o,{requireRect:m});if(!I)return{ok:!1,error:{code:"COMMAND_FAILED",message:"find did not match any element"}};let N="click"===s||"focus"===s||"fill"===s||"type"===s?function(e,t){if(t.hittable)return t;let a=t,i=new Set;for(;void 0!==a.parentIndex&&!i.has(a.ref);){i.add(a.ref);let t=e[a.parentIndex];if(!t)break;if(t.hittable)return t;a=t}return null}(y,I)??I:I,A=`@${N.ref}`,b={...e.flags??{},noRecord:!0};if("exists"===s)return u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{found:!0}}),{ok:!0,data:{found:!0}};if("get_text"===s){var i;let a=[(i=I).label,i.value,i.identifier].map(e=>"string"==typeof e?e.trim():"").filter(e=>e.length>0)[0]??"";return u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:A,action:"get text",text:a}}),{ok:!0,data:{ref:A,text:a,node:I}}}if("get_attrs"===s)return u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:A,action:"get attrs"}}),{ok:!0,data:{ref:A,node:I}};if("click"===s){let i=await eH({token:eX,session:a,command:"click",positionals:[A],flags:b});return i.ok&&u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:A,action:"click"}}),i}if("fill"===s){if(!l)return{ok:!1,error:{code:"INVALID_ARGS",message:"find fill requires text"}};let i=await eH({token:eX,session:a,command:"fill",positionals:[A,l],flags:b});return i.ok&&u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:A,action:"fill"}}),i}if("focus"===s){let a=I.rect?eF(I.rect):null;if(!a)return{ok:!1,error:{code:"COMMAND_FAILED",message:"matched element has no bounds"}};let i=await eR(d,"focus",[String(a.x),String(a.y)],e.flags?.out,{...ez(e.flags,u?.appBundleId,u?.trace?.outPath)});return u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:A,action:"focus"}}),{ok:!0,data:i??{ref:A}}}if("type"===s){if(!l)return{ok:!1,error:{code:"INVALID_ARGS",message:"find type requires text"}};let a=I.rect?eF(I.rect):null;if(!a)return{ok:!1,error:{code:"COMMAND_FAILED",message:"matched element has no bounds"}};await eR(d,"focus",[String(a.x),String(a.y)],e.flags?.out,{...ez(e.flags,u?.appBundleId,u?.trace?.outPath)});let i=await eR(d,"type",[l],e.flags?.out,{...ez(e.flags,u?.appBundleId,u?.trace?.outPath)});return u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:A,action:"type"}}),{ok:!0,data:i??{ref:A}}}}if("click"===t){let i=eV.get(a);if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let n=e.positionals?.[0]??"",r=eM(n);if(!r)return{ok:!1,error:{code:"INVALID_ARGS",message:"click requires a ref like @e2"}};let o=eT(i.snapshot.nodes,r);if(!o?.rect&&e.positionals.length>1){let t=e.positionals.slice(1).join(" ").trim();t.length>0&&(o=e2(i.snapshot.nodes,t))}if(!o?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${n} not found or has no bounds`}};let s=e3(o,i.snapshot.nodes),{x:l,y:c}=eF(o.rect);return await eR(i.device,"press",[String(l),String(c)],e.flags?.out,{...ez(e.flags,i.appBundleId,i.trace?.outPath)}),eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:r,x:l,y:c,refLabel:s}}),{ok:!0,data:{ref:r,x:l,y:c}}}if("fill"===t){let i=eV.get(a);if(e.positionals?.[0]?.startsWith("@")){let a;if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let n=eM(e.positionals[0]);if(!n)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires a ref like @e2"}};let r=e.positionals.length>=3?e.positionals[1]:"",o=e.positionals.length>=3?e.positionals.slice(2).join(" "):e.positionals.slice(1).join(" ");if(!o)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires text after ref"}};let s=eT(i.snapshot.nodes,n);if(!s?.rect&&r&&(s=e2(i.snapshot.nodes,r)),!s?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${e.positionals[0]} not found or has no bounds`}};let l=e3(s,i.snapshot.nodes),c=s.label?.trim();if("ios"===i.device.platform&&"simulator"===i.device.kind&&("textfield"===(a=e7(s.type??""))||"textview"===a||"searchfield"===a||"textarea"===a)){let a=s.rect?eF(s.rect):null;return a?(await eR(i.device,"focus",[String(a.x),String(a.y)],e.flags?.out,{...ez(e.flags,i.appBundleId,i.trace?.outPath)}),await ew(i.device,{command:"type",text:o,appBundleId:i.appBundleId},{verbose:e.flags?.verbose,logPath:eq,traceLogPath:i?.trace?.outPath}),eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:n,refLabel:l??c,action:"fill",text:o}}),{ok:!0,data:{ref:n}}):{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${e.positionals[0]} not found or has no bounds`}}}let{x:u,y:d}=eF(s.rect),p=await eR(i.device,"fill",[String(u),String(d),o],e.flags?.out,{...ez(e.flags,i.appBundleId,i.trace?.outPath)});return eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:p??{ref:n,x:u,y:d,refLabel:l}}),{ok:!0,data:p??{ref:n,x:u,y:d}}}}if("get"===t){let i=e.positionals?.[0],n=e.positionals?.[1];if("text"!==i&&"attrs"!==i)return{ok:!1,error:{code:"INVALID_ARGS",message:"get only supports text or attrs"}};let r=eV.get(a);if(!r?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let o=eM(n??"");if(!o)return{ok:!1,error:{code:"INVALID_ARGS",message:"get text requires a ref like @e2"}};let s=eT(r.snapshot.nodes,o);if(!s&&e.positionals.length>2){let t=e.positionals.slice(2).join(" ").trim();t.length>0&&(s=e2(r.snapshot.nodes,t))}if(!s)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${n} not found`}};if("attrs"===i)return eZ(r,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:o}}),{ok:!0,data:{ref:o,node:s}};let l=[s.label,s.value,s.identifier].map(e=>"string"==typeof e?e.trim():"").filter(e=>e.length>0)[0]??"";return eZ(r,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:o,text:l,refLabel:l||void 0}}),{ok:!0,data:{ref:o,text:l,node:s}}}let n=eV.get(a);if(!n)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session. Run open first."}};let o=await eR(n.device,t,e.positionals??[],e.flags?.out,{...ez(e.flags,n.appBundleId,n.trace?.outPath)});return eZ(n,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:o??{}}),{ok:!0,data:o??{}}}function eZ(e,t){t.flags?.noRecord||e.actions.push({ts:Date.now(),command:t.command,positionals:t.positionals,flags:function(e){if(!e)return{};let{platform:t,device:a,udid:i,serial:n,out:r,verbose:o,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:u,snapshotRaw:d,snapshotBackend:p,appsMetadata:f,noRecord:h,recordJson:m}=e;return{platform:t,device:a,udid:i,serial:n,out:r,verbose:o,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:u,snapshotRaw:d,snapshotBackend:p,appsMetadata:f,noRecord:h,recordJson:m}}(t.flags),result:t.result})}async function eY(e,t,a){let i=eK(await eR(e,"snapshot",[],a?.out,{...ez({...a,snapshotDepth:1,snapshotCompact:!0,snapshotBackend:"ax"},void 0,t)}));if(i?.appName||i?.appBundleId)return{appName:i.appName??i.appBundleId??"unknown",appBundleId:i.appBundleId,source:"snapshot-ax"};let n=eK(await eR(e,"snapshot",[],a?.out,{...ez({...a,snapshotDepth:1,snapshotCompact:!0,snapshotBackend:"xctest"},void 0,t)}));return{appName:n?.appName??n?.appBundleId??"unknown",appBundleId:n?.appBundleId,source:"snapshot-xctest"}}function eK(e){let t=eC(e?.nodes??[]),a=t.find(e=>"application"===e7(e.type??""))??t[0];if(!a)return null;let i=a.label?.trim(),n=a.identifier?.trim();return i||n?{appName:i||void 0,appBundleId:n||void 0}:null}function eQ(e){try{d.existsSync(eJ)||d.mkdirSync(eJ,{recursive:!0});let t=e.name.replace(/[^a-zA-Z0-9._-]/g,"_"),a=new Date(e.createdAt).toISOString().replace(/[:.]/g,"-"),i=r.join(eJ,`${t}-${a}.ad`),n=r.join(eJ,`${t}-${a}.json`),o={name:e.name,device:e.device,createdAt:e.createdAt,appBundleId:e.appBundleId,actions:e.actions,optimizedActions:function(e){let t=[];for(let a of e.actions)if("snapshot"!==a.command){if("click"===a.command||"fill"===a.command||"get"===a.command){let i=a.result?.refLabel;"string"==typeof i&&i.trim().length>0&&t.push({ts:a.ts,command:"snapshot",positionals:[],flags:{platform:e.device.platform,snapshotInteractiveOnly:!0,snapshotCompact:!0,snapshotScope:i.trim()},result:{scope:i.trim()}})}t.push(a)}return t}(e)},s=function(e,t){let a=[],i=e.device.name.replace(/"/g,'\\"'),n=e.device.kind?` kind=${e.device.kind}`:"";for(let r of(a.push(`context platform=${e.device.platform} device="${i}"${n} theme=unknown`),t))r.flags?.noRecord||a.push(function(e){let t=[e.command];if("click"===e.command){let a=e.positionals?.[0];if(a){t.push(e1(a));let i=e.result?.refLabel;return"string"==typeof i&&i.trim().length>0&&t.push(e1(i)),t.join(" ")}}if("fill"===e.command){let a=e.positionals?.[0];if(a&&a.startsWith("@")){t.push(e1(a));let i=e.result?.refLabel,n=e.positionals.slice(1).join(" ");return"string"==typeof i&&i.trim().length>0&&t.push(e1(i)),n&&t.push(e1(n)),t.join(" ")}}if("get"===e.command){let a=e.positionals?.[0],i=e.positionals?.[1];if(a&&i){t.push(e1(a)),t.push(e1(i));let n=e.result?.refLabel;return"string"==typeof n&&n.trim().length>0&&t.push(e1(n)),t.join(" ")}}if("snapshot"===e.command)return e.flags?.snapshotInteractiveOnly&&t.push("-i"),e.flags?.snapshotCompact&&t.push("-c"),"number"==typeof e.flags?.snapshotDepth&&t.push("-d",String(e.flags.snapshotDepth)),e.flags?.snapshotScope&&t.push("-s",e1(e.flags.snapshotScope)),e.flags?.snapshotRaw&&t.push("--raw"),e.flags?.snapshotBackend&&t.push("--backend",e.flags.snapshotBackend),t.join(" ");for(let a of e.positionals??[])t.push(e1(a));return t.join(" ")}(r));return`${a.join("\n")}
6
+ `}(e,o.optimizedActions);d.writeFileSync(i,s),e.actions.some(e=>e.flags?.recordJson)&&d.writeFileSync(n,JSON.stringify(o,null,2))}catch{}}function e0(e){return e.startsWith("~/")?r.join(p.homedir(),e.slice(2)):r.resolve(e)}function e1(e){let t=e.trim();return t.startsWith("@")||/^-?\d+(\.\d+)?$/.test(t)?t:JSON.stringify(t)}function e2(e,t){let a=t.toLowerCase();return e.find(e=>{let t=(e.label??"").toLowerCase(),i=(e.value??"").toLowerCase(),n=(e.identifier??"").toLowerCase();return t.includes(a)||i.includes(a)||n.includes(a)})??null}function e3(e,t){let a=[e.label,e.value,e.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0);return a&&e8(a)?a:function(e,t){if(!e.rect)return;let a=e.rect.y+e.rect.height/2,i=null;for(let e of t){if(!e.rect)continue;let t=[e.label,e.value,e.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0);if(!t||!e8(t))continue;let n=Math.abs(e.rect.y+e.rect.height/2-a);(!i||n<i.distance)&&(i={label:t,distance:n})}return i?.label}(e,t)??(a&&e8(a)?a:void 0)}function e8(e){let t=e.trim();return!(!t||/^(true|false)$/i.test(t)||/^\d+$/.test(t))}async function e4(e){if("ios"===e.platform&&"simulator"===e.kind){let{ensureBootedSimulator:t}=await Promise.resolve().then(()=>({ensureBootedSimulator:ef}));await t(e);return}if("android"===e.platform){let{waitForAndroidBoot:t}=await Promise.resolve().then(()=>({waitForAndroidBoot:I}));await t(e.id)}}function e6(e){let t=[],a=[];for(let i of e){let e=i.depth??0;for(;t.length>0&&e<=t[t.length-1];)t.pop();let n=e7(i.type??""),r=[i.label,i.value,i.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0),o=!!r&&e8(r);if(("group"===n||"ioscontentgroup"===n)&&!o){t.push(e);continue}let s=Math.max(0,e-t.length);a.push({...i,depth:s})}return a}function e7(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase();return t.startsWith("ax")&&(t=t.replace(/^ax/,"")),t}(e=m.createServer(e=>{let t="";e.setEncoding("utf8"),e.on("data",async a=>{let i=(t+=a).indexOf("\n");for(;-1!==i;){let a,n=t.slice(0,i).trim();if(t=t.slice(i+1),0===n.length){i=t.indexOf("\n");continue}try{let e=JSON.parse(n);a=await eH(e)}catch(t){let e=l(t);a={ok:!1,error:{code:e.code,message:e.message,details:e.details}}}e.write(`${JSON.stringify(a)}
7
7
  `),i=t.indexOf("\n")}})})).listen(0,"127.0.0.1",()=>{let t=e.address();if("object"==typeof t&&t?.port){var a;a=t.port,d.existsSync(ej)||d.mkdirSync(ej,{recursive:!0}),d.writeFileSync(eq,""),d.writeFileSync(eG,JSON.stringify({port:a,token:eX,pid:process.pid,version:eW},null,2),{mode:384}),process.stdout.write(`AGENT_DEVICE_DAEMON_PORT=${t.port}
8
8
  `)}}),t=async()=>{for(let e of Array.from(eV.values()))"ios"===e.device.platform&&"simulator"===e.device.kind&&await ev(e.device.id),eQ(e);e.close(()=>{d.existsSync(eG)&&d.unlinkSync(eG),process.exit(0)})},process.on("SIGINT",()=>{t()}),process.on("SIGTERM",()=>{t()}),process.on("SIGHUP",()=>{t()}),process.on("uncaughtException",e=>{let a=e instanceof f?e:l(e);process.stderr.write(`Daemon error: ${a.message}
9
9
  `),t()});
@@ -305,6 +305,12 @@ final class RunnerTests: XCTestCase {
305
305
  }
306
306
  let buttonLabels = alert.buttons.allElementsBoundByIndex.map { $0.label }
307
307
  return Response(ok: true, data: DataPayload(message: alert.label, items: buttonLabels))
308
+ case .pinch:
309
+ guard let scale = command.scale, scale > 0 else {
310
+ return Response(ok: false, error: ErrorPayload(message: "pinch requires scale > 0"))
311
+ }
312
+ pinch(app: activeApp, scale: scale, x: command.x, y: command.y)
313
+ return Response(ok: true, data: DataPayload(message: "pinched"))
308
314
  }
309
315
  }
310
316
 
@@ -372,6 +378,39 @@ final class RunnerTests: XCTestCase {
372
378
  }
373
379
  }
374
380
 
381
+ private func pinch(app: XCUIApplication, scale: Double, x: Double?, y: Double?) {
382
+ let target = app.windows.firstMatch.exists ? app.windows.firstMatch : app
383
+
384
+ // Use double-tap + drag gesture for reliable map zoom
385
+ // Zoom in (scale > 1): tap then drag UP
386
+ // Zoom out (scale < 1): tap then drag DOWN
387
+
388
+ // Determine center point (use provided x/y or screen center)
389
+ let centerX = x.map { $0 / target.frame.width } ?? 0.5
390
+ let centerY = y.map { $0 / target.frame.height } ?? 0.5
391
+ let center = target.coordinate(withNormalizedOffset: CGVector(dx: centerX, dy: centerY))
392
+
393
+ // Calculate drag distance based on scale (clamped to reasonable range)
394
+ // Larger scale = more drag distance
395
+ let dragAmount: CGFloat
396
+ if scale > 1.0 {
397
+ // Zoom in: drag up (negative Y direction in normalized coords)
398
+ dragAmount = min(0.4, CGFloat(scale - 1.0) * 0.2)
399
+ } else {
400
+ // Zoom out: drag down (positive Y direction)
401
+ dragAmount = min(0.4, CGFloat(1.0 - scale) * 0.4)
402
+ }
403
+
404
+ let endY = scale > 1.0 ? (centerY - Double(dragAmount)) : (centerY + Double(dragAmount))
405
+ let endPoint = target.coordinate(withNormalizedOffset: CGVector(dx: centerX, dy: max(0.1, min(0.9, endY))))
406
+
407
+ // Tap first (first tap of double-tap)
408
+ center.tap()
409
+
410
+ // Immediately press and drag (second tap + drag)
411
+ center.press(forDuration: 0.05, thenDragTo: endPoint)
412
+ }
413
+
375
414
  private func aggregatedLabel(for element: XCUIElement, depth: Int = 0) -> String? {
376
415
  if depth > 2 { return nil }
377
416
  let text = element.label.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -718,6 +757,7 @@ enum CommandType: String, Codable {
718
757
  case home
719
758
  case appSwitcher
720
759
  case alert
760
+ case pinch
721
761
  case shutdown
722
762
  }
723
763
 
@@ -736,6 +776,7 @@ struct Command: Codable {
736
776
  let x: Double?
737
777
  let y: Double?
738
778
  let direction: SwipeDirection?
779
+ let scale: Double?
739
780
  let interactiveOnly: Bool?
740
781
  let compact: Bool?
741
782
  let depth: Int?
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-device",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Unified control plane for physical and virtual devices via an agent-driven CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Callstack",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: agent-device
3
- description: Automates mobile and simulator interactions for iOS and Android devices. Use when navigating apps, taking snapshots/screenshots, tapping, typing, scrolling, or extracting UI info on mobile devices or simulators.
3
+ description: Automates mobile and simulator interactions for iOS and Android devices. Use when navigating apps, taking snapshots/screenshots, tapping, typing, scrolling, pinching, or extracting UI info on mobile devices or simulators.
4
4
  ---
5
5
 
6
6
  # Mobile Automation with agent-device
@@ -10,7 +10,6 @@ description: Automates mobile and simulator interactions for iOS and Android dev
10
10
  ```bash
11
11
  agent-device open Settings --platform ios
12
12
  agent-device snapshot -i
13
- agent-device snapshot -s @e3
14
13
  agent-device click @e3
15
14
  agent-device wait text "Camera"
16
15
  agent-device alert wait 10000
@@ -18,11 +17,17 @@ agent-device fill @e5 "test"
18
17
  agent-device close
19
18
  ```
20
19
 
20
+ If not installed, run:
21
+
22
+ ```bash
23
+ npx -y agent-device
24
+ ```
25
+
21
26
  ## Core workflow
22
27
 
23
28
  1. Open app or just boot device: `open [app]`
24
- 2. Snapshot: `snapshot -i` to get compact refs
25
- 3. Interact using refs (`click @eN`, `fill @eN "text"`)
29
+ 2. Snapshot: `snapshot` to get full XCTest accessibility tree snapshot
30
+ 3. Interact using refs (`click @ref`, `fill @ref "text"`)
26
31
  4. Re-snapshot after navigation or UI changes
27
32
  5. Close session when done
28
33
 
@@ -32,6 +37,7 @@ agent-device close
32
37
 
33
38
  ```bash
34
39
  agent-device open [app] # Boot device/simulator; optionally launch app
40
+ agent-device open [app] --activity com.example/.MainActivity # Android: open specific activity
35
41
  agent-device close [app] # Close app or just end session
36
42
  agent-device session list # List active sessions
37
43
  ```
@@ -45,7 +51,7 @@ agent-device snapshot -c # Compact output
45
51
  agent-device snapshot -d 3 # Limit depth
46
52
  agent-device snapshot -s "Camera" # Scope to label/identifier
47
53
  agent-device snapshot --raw # Raw node output
48
- agent-device snapshot --backend xctest # XCTest snapshot (fast, complete, no permissions)
54
+ agent-device snapshot --backend xctest # default: XCTest snapshot (fast, complete, no permissions)
49
55
  agent-device snapshot --backend ax # macOS Accessibility tree (fast, needs permissions, less fidelity, optional)
50
56
  ```
51
57
 
@@ -96,6 +102,8 @@ agent-device type "text" # Type into focused field
96
102
  agent-device press 300 500 # Tap by coordinates
97
103
  agent-device long-press 300 500 800 # Long press (where supported)
98
104
  agent-device scroll down 0.5
105
+ agent-device pinch 2.0 # Zoom in 2x (iOS simulator)
106
+ agent-device pinch 0.5 200 400 # Zoom out at coordinates (iOS simulator)
99
107
  agent-device back
100
108
  agent-device home
101
109
  agent-device app-switcher
@@ -133,6 +141,7 @@ agent-device apps --platform android --user-installed
133
141
 
134
142
  ## Best practices
135
143
 
144
+ - Pinch (`pinch <scale> [x y]`) is supported on iOS simulators only; scale > 1 zooms in, < 1 zooms out.
136
145
  - Always snapshot right before interactions; refs invalidate on UI changes.
137
146
  - Prefer `snapshot -i` to reduce output size.
138
147
  - On iOS, `xctest` is the default and does not require Accessibility permission.
@@ -140,13 +149,14 @@ agent-device apps --platform android --user-installed
140
149
  - `open <app>` can be used within an existing session to switch apps and update the session bundle id.
141
150
  - If AX returns the Simulator window or empty tree, restart Simulator or use `--backend xctest`.
142
151
  - Use `--session <name>` for parallel sessions; avoid device contention.
152
+ - Use `--activity <component>` on Android to launch a specific activity (e.g. TV apps with LEANBACK).
143
153
 
144
154
  ## References
145
155
 
146
156
  - [references/snapshot-refs.md](references/snapshot-refs.md)
147
157
  - [references/session-management.md](references/session-management.md)
148
158
  - [references/permissions.md](references/permissions.md)
149
- - [references/recording.md](references/recording.md)
159
+ - [references/video-recording.md](references/video-recording.md)
150
160
  - [references/coordinate-system.md](references/coordinate-system.md)
151
161
 
152
162
  ## Missing features roadmap (high level)
@@ -2,17 +2,17 @@
2
2
 
3
3
  ## iOS AX snapshot
4
4
 
5
- Hybrid snapshot (default) is recommended for best speed vs correctness; it uses macOS Accessibility APIs and requires permission:
5
+ AX snapshot is an alternative to XCTest for when it fails (which shouldn't happen usually); it uses macOS Accessibility APIs and requires permission:
6
6
 
7
7
  System Settings > Privacy & Security > Accessibility
8
8
 
9
- If permission is missing, use:
9
+ If permission is missing, use XCTest backend:
10
10
 
11
11
  ```bash
12
12
  agent-device snapshot --backend xctest --platform ios
13
13
  ```
14
14
 
15
- Hybrid/AX is fast; XCTest is slower but does not require permissions.
15
+ Hybrid/AX is fast; XCTest is equally fast but does not require permissions.
16
16
 
17
17
  ## Simulator troubleshooting
18
18
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  ```bash
6
6
  agent-device --session auth open Settings --platform ios
7
- agent-device --session auth snapshot -i --platform ios
7
+ agent-device --session auth snapshot -i
8
8
  ```
9
9
 
10
10
  Sessions isolate device context. A device can only be held by one session at a time.
@@ -13,7 +13,7 @@ Sessions isolate device context. A device can only be held by one session at a t
13
13
 
14
14
  - Name sessions semantically.
15
15
  - Close sessions when done.
16
- - Use separate devices for parallel work.
16
+ - Use separate sessions for parallel work.
17
17
 
18
18
  ## Listing sessions
19
19
 
@@ -7,7 +7,7 @@ Refs let agents interact without repeating full UI trees. Snapshot -> refs -> cl
7
7
  ## Snapshot
8
8
 
9
9
  ```bash
10
- agent-device snapshot -i --platform ios
10
+ agent-device snapshot -i
11
11
  ```
12
12
 
13
13
  Output:
@@ -24,8 +24,8 @@ App: com.apple.Preferences
24
24
  ## Using refs
25
25
 
26
26
  ```bash
27
- agent-device click @e2 --platform ios
28
- agent-device fill @e5 "test" --platform ios
27
+ agent-device click @e2
28
+ agent-device fill @e5 "test"
29
29
  ```
30
30
 
31
31
  ## Ref lifecycle
@@ -38,8 +38,8 @@ Always re-snapshot after any transition.
38
38
  Use `-s` to scope to labels/identifiers. This reduces size and speeds up results:
39
39
 
40
40
  ```bash
41
- agent-device snapshot -i -s "Camera" --platform ios
42
- agent-device snapshot -i -s @e3 --platform ios
41
+ agent-device snapshot -i -s "Camera"
42
+ agent-device snapshot -i -s @e3
43
43
  ```
44
44
 
45
45
  ## Troubleshooting
@@ -22,6 +22,7 @@ export type CommandFlags = {
22
22
  udid?: string;
23
23
  serial?: string;
24
24
  out?: string;
25
+ activity?: string;
25
26
  verbose?: boolean;
26
27
  snapshotInteractiveOnly?: boolean;
27
28
  snapshotCompact?: boolean;
@@ -75,6 +76,7 @@ export async function dispatchCommand(
75
76
  outPath?: string,
76
77
  context?: {
77
78
  appBundleId?: string;
79
+ activity?: string;
78
80
  verbose?: boolean;
79
81
  logPath?: string;
80
82
  traceLogPath?: string;
@@ -94,7 +96,7 @@ export async function dispatchCommand(
94
96
  await interactor.openDevice();
95
97
  return { app: null };
96
98
  }
97
- await interactor.open(app);
99
+ await interactor.open(app, { activity: context?.activity });
98
100
  return { app };
99
101
  }
100
102
  case 'close': {
@@ -223,6 +225,24 @@ export async function dispatchCommand(
223
225
  await interactor.scrollIntoView(text);
224
226
  return { text };
225
227
  }
228
+ case 'pinch': {
229
+ const scale = Number(positionals[0]);
230
+ const x = positionals[1] ? Number(positionals[1]) : undefined;
231
+ const y = positionals[2] ? Number(positionals[2]) : undefined;
232
+ if (Number.isNaN(scale) || scale <= 0) {
233
+ throw new AppError('INVALID_ARGS', 'pinch requires scale > 0');
234
+ }
235
+ if (device.platform === 'ios' && device.kind === 'simulator') {
236
+ await runIosRunnerCommand(
237
+ device,
238
+ { command: 'pinch', scale, x, y, appBundleId: context?.appBundleId },
239
+ { verbose: context?.verbose, logPath: context?.logPath, traceLogPath: context?.traceLogPath },
240
+ );
241
+ } else {
242
+ throw new AppError('UNSUPPORTED_OPERATION', 'pinch is only supported on iOS simulators');
243
+ }
244
+ return { scale, x, y };
245
+ }
226
246
  case 'screenshot': {
227
247
  const path = outPath ?? `./screenshot-${Date.now()}.png`;
228
248
  await interactor.screenshot(path);
package/src/daemon.ts CHANGED
@@ -84,6 +84,7 @@ function contextFromFlags(
84
84
  traceLogPath?: string,
85
85
  ): {
86
86
  appBundleId?: string;
87
+ activity?: string;
87
88
  verbose?: boolean;
88
89
  logPath?: string;
89
90
  traceLogPath?: string;
@@ -96,6 +97,7 @@ function contextFromFlags(
96
97
  } {
97
98
  return {
98
99
  appBundleId,
100
+ activity: flags?.activity,
99
101
  verbose: flags?.verbose,
100
102
  logPath,
101
103
  traceLogPath,
@@ -841,7 +843,11 @@ async function handleRequest(req: DaemonRequest): Promise<DaemonResponse> {
841
843
  if (!node) {
842
844
  return { ok: false, error: { code: 'COMMAND_FAILED', message: 'find did not match any element' } };
843
845
  }
844
- const ref = `@${node.ref}`;
846
+ const resolvedNode =
847
+ action === 'click' || action === 'focus' || action === 'fill' || action === 'type'
848
+ ? findNearestHittableAncestor(nodes, node) ?? node
849
+ : node;
850
+ const ref = `@${resolvedNode.ref}`;
845
851
  const actionFlags = { ...(req.flags ?? {}), noRecord: true };
846
852
  if (action === 'exists') {
847
853
  if (session) {
@@ -1684,6 +1690,24 @@ function normalizeType(type: string): string {
1684
1690
  return value;
1685
1691
  }
1686
1692
 
1693
+ function findNearestHittableAncestor(
1694
+ nodes: SnapshotState['nodes'],
1695
+ node: SnapshotState['nodes'][number],
1696
+ ): SnapshotState['nodes'][number] | null {
1697
+ if (node.hittable) return node;
1698
+ let current = node;
1699
+ const visited = new Set<string>();
1700
+ while (current.parentIndex !== undefined) {
1701
+ if (visited.has(current.ref)) break;
1702
+ visited.add(current.ref);
1703
+ const parent = nodes[current.parentIndex];
1704
+ if (!parent) break;
1705
+ if (parent.hittable) return parent;
1706
+ current = parent;
1707
+ }
1708
+ return null;
1709
+ }
1710
+
1687
1711
  function readVersion(): string {
1688
1712
  try {
1689
1713
  const root = findProjectRoot();
@@ -148,25 +148,58 @@ function parseAndroidFocus(text: string): { package?: string; activity?: string
148
148
  return null;
149
149
  }
150
150
 
151
- export async function openAndroidApp(device: DeviceInfo, app: string): Promise<void> {
151
+ export async function openAndroidApp(
152
+ device: DeviceInfo,
153
+ app: string,
154
+ activity?: string,
155
+ ): Promise<void> {
152
156
  if (!device.booted) {
153
157
  await waitForAndroidBoot(device.id);
154
158
  }
155
159
  const resolved = await resolveAndroidApp(device, app);
156
160
  if (resolved.type === 'intent') {
161
+ if (activity) {
162
+ throw new AppError('INVALID_ARGS', 'Activity override requires a package name, not an intent');
163
+ }
157
164
  await runCmd('adb', adbArgs(device, ['shell', 'am', 'start', '-a', resolved.value]));
158
165
  return;
159
166
  }
167
+ if (activity) {
168
+ const component = activity.includes('/')
169
+ ? activity
170
+ : `${resolved.value}/${activity.startsWith('.') ? activity : `.${activity}`}`;
171
+ await runCmd(
172
+ 'adb',
173
+ adbArgs(device, [
174
+ 'shell',
175
+ 'am',
176
+ 'start',
177
+ '-a',
178
+ 'android.intent.action.MAIN',
179
+ '-c',
180
+ 'android.intent.category.DEFAULT',
181
+ '-c',
182
+ 'android.intent.category.LAUNCHER',
183
+ '-n',
184
+ component,
185
+ ]),
186
+ );
187
+ return;
188
+ }
160
189
  await runCmd(
161
190
  'adb',
162
191
  adbArgs(device, [
163
192
  'shell',
164
- 'monkey',
165
- '-p',
166
- resolved.value,
193
+ 'am',
194
+ 'start',
195
+ '-a',
196
+ 'android.intent.action.MAIN',
197
+ '-c',
198
+ 'android.intent.category.DEFAULT',
167
199
  '-c',
168
200
  'android.intent.category.LAUNCHER',
169
- '1',
201
+ '-p',
202
+ resolved.value,
170
203
  ]),
171
204
  );
172
205
  }
@@ -460,7 +493,7 @@ function parseUiHierarchy(
460
493
  const scopedRoot = options.scope ? findScopeNode(tree, options.scope) : null;
461
494
  const roots = scopedRoot ? [scopedRoot] : tree.children;
462
495
 
463
- const walk = (node: AndroidNode, depth: number) => {
496
+ const walk = (node: AndroidNode, depth: number, parentIndex?: number) => {
464
497
  if (nodes.length >= maxNodes) {
465
498
  truncated = true;
466
499
  return;
@@ -468,9 +501,11 @@ function parseUiHierarchy(
468
501
  if (depth > maxDepth) return;
469
502
 
470
503
  const include = options.raw ? true : shouldIncludeAndroidNode(node, options);
504
+ let currentIndex = parentIndex;
471
505
  if (include) {
506
+ currentIndex = nodes.length;
472
507
  nodes.push({
473
- index: nodes.length,
508
+ index: currentIndex,
474
509
  type: node.type ?? undefined,
475
510
  label: node.label ?? undefined,
476
511
  value: node.value ?? undefined,
@@ -479,17 +514,17 @@ function parseUiHierarchy(
479
514
  enabled: node.enabled,
480
515
  hittable: node.hittable,
481
516
  depth,
482
- parentIndex: node.parentIndex,
517
+ parentIndex,
483
518
  });
484
519
  }
485
520
  for (const child of node.children) {
486
- walk(child, depth + 1);
521
+ walk(child, depth + 1, currentIndex);
487
522
  if (truncated) return;
488
523
  }
489
524
  };
490
525
 
491
526
  for (const root of roots) {
492
- walk(root, 0);
527
+ walk(root, 0, undefined);
493
528
  if (truncated) break;
494
529
  }
495
530
 
@@ -20,6 +20,7 @@ export type RunnerCommand = {
20
20
  | 'home'
21
21
  | 'appSwitcher'
22
22
  | 'alert'
23
+ | 'pinch'
23
24
  | 'shutdown';
24
25
  appBundleId?: string;
25
26
  text?: string;
@@ -27,6 +28,7 @@ export type RunnerCommand = {
27
28
  x?: number;
28
29
  y?: number;
29
30
  direction?: 'up' | 'down' | 'left' | 'right';
31
+ scale?: number;
30
32
  interactiveOnly?: boolean;
31
33
  compact?: boolean;
32
34
  depth?: number;
@@ -173,7 +175,7 @@ async function ensureRunnerSession(
173
175
  await ensureBooted(device.id);
174
176
  const xctestrun = await ensureXctestrun(device.id, options);
175
177
  const port = await getFreePort();
176
- const runnerTimeout = process.env.AGENT_DEVICE_RUNNER_TIMEOUT ?? '300';
178
+ const runnerTimeout = process.env.AGENT_DEVICE_RUNNER_TIMEOUT ?? '0';
177
179
  const { xctestrunPath, jsonPath } = await prepareXctestrunWithEnv(
178
180
  xctestrun,
179
181
  { AGENT_DEVICE_RUNNER_PORT: String(port), AGENT_DEVICE_RUNNER_TIMEOUT: runnerTimeout },
package/src/utils/args.ts CHANGED
@@ -20,6 +20,7 @@ export type ParsedArgs = {
20
20
  snapshotBackend?: 'ax' | 'xctest';
21
21
  appsFilter?: 'launchable' | 'user-installed' | 'all';
22
22
  appsMetadata?: boolean;
23
+ activity?: string;
23
24
  noRecord?: boolean;
24
25
  recordJson?: boolean;
25
26
  help: boolean;
@@ -125,6 +126,9 @@ export function parseArgs(argv: string[]): ParsedArgs {
125
126
  case '--session':
126
127
  flags.session = value;
127
128
  break;
129
+ case '--activity':
130
+ flags.activity = value;
131
+ break;
128
132
  default:
129
133
  throw new AppError('INVALID_ARGS', `Unknown flag: ${key}`);
130
134
  }
@@ -208,6 +212,7 @@ Flags:
208
212
  --device <name> Device name to target
209
213
  --udid <udid> iOS device UDID
210
214
  --serial <serial> Android device serial
215
+ --activity <component> Android activity to launch (package/Activity)
211
216
  --out <path> Output path for screenshots
212
217
  --session <name> Named session
213
218
  --verbose Stream daemon/runner logs
@@ -28,7 +28,7 @@ import {
28
28
  } from '../platforms/ios/index.ts';
29
29
 
30
30
  export type Interactor = {
31
- open(app: string): Promise<void>;
31
+ open(app: string, options?: { activity?: string }): Promise<void>;
32
32
  openDevice(): Promise<void>;
33
33
  close(app: string): Promise<void>;
34
34
  tap(x: number, y: number): Promise<void>;
@@ -45,7 +45,7 @@ export function getInteractor(device: DeviceInfo): Interactor {
45
45
  switch (device.platform) {
46
46
  case 'android':
47
47
  return {
48
- open: (app) => openAndroidApp(device, app),
48
+ open: (app, options) => openAndroidApp(device, app, options?.activity),
49
49
  openDevice: () => openAndroidDevice(device),
50
50
  close: (app) => closeAndroidApp(device, app),
51
51
  tap: (x, y) => pressAndroid(device, x, y),