agent-device 0.1.0 → 0.1.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,6 @@
1
1
  # agent-device
2
2
 
3
- Unified control CLI for physical and virtual devices (iOS + Android) for AI agents.
3
+ CLI to control iOS and Android devices for AI agents.
4
4
 
5
5
  This project mirrors the spirit of `agent-browser`, but targets iOS simulators/devices and Android emulators/devices.
6
6
 
@@ -20,7 +20,7 @@ npm install -g agent-device
20
20
  Or use it without installing:
21
21
 
22
22
  ```bash
23
- npx agent-device open Settings
23
+ npx agent-device open SampleApp
24
24
  ```
25
25
 
26
26
  ## Usage
@@ -32,15 +32,20 @@ agent-device <command> [args] [--json]
32
32
  Examples:
33
33
 
34
34
  ```bash
35
- agent-device open Settings
36
- agent-device press 120 320
35
+ agent-device open SampleApp
36
+ agent-device snapshot
37
+ agent-device click @e7
37
38
  agent-device type "hello"
38
39
  agent-device screenshot --out ./screenshot.png
39
- agent-device snapshot -i -c -d 6
40
- agent-device close Settings
40
+ agent-device close SampleApp
41
41
  ```
42
42
 
43
43
  Best practice: run `snapshot` immediately before interactions to avoid stale coordinates if the Simulator window moves or UI changes.
44
+ When interacting with UI elements from a snapshot, prefer refs (e.g. `click @e7`) over raw coordinates. Refs are stable across runs and avoid coordinate drift.
45
+
46
+ iOS snapshots:
47
+ - Default backend is `ax` (fast). It requires enabling Accessibility for the terminal app in System Settings.
48
+ - If AX is not available, use `--backend xctest` explicitly.
44
49
 
45
50
  Flags:
46
51
  - `--platform ios|android`
@@ -52,6 +57,7 @@ Flags:
52
57
  - `--verbose` for daemon and runner logs
53
58
  - `--json` for structured output
54
59
  - `--backend ax|xctest` (snapshot only; defaults to `ax` on iOS)
60
+ - `open` without args boots/activates the target device/simulator without launching an app.
55
61
 
56
62
  Sessions:
57
63
  - `open` starts a session.
package/dist/src/bin.js CHANGED
@@ -1,38 +1,49 @@
1
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 i,node_net as a,errors_AppError as c}from"./861.js";function l(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(i.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 x(t,r)}async function h(){let t=w(),r=function(){try{let t=b();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 g(t))return t;t&&(t.version!==r||!await g(t))&&o.existsSync(p)&&o.unlinkSync(p),await y();let n=Date.now();for(;Date.now()-n<5e3;){let e=w();if(e&&await g(e))return e;await new Promise(e=>setTimeout(e,100))}throw new c("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 g(e){return new Promise(t=>{let r=a.createConnection({host:"127.0.0.1",port:e.port},()=>{r.destroy(),t(!0)});r.on("error",()=>{t(!1)})})}async function y(){let t=b(),r=e.join(t,"dist","src","daemon.js"),n=e.join(t,"src","daemon.ts"),i=o.existsSync(r);if(!i&&!o.existsSync(n))throw new c("COMMAND_FAILED","Daemon entry not found",{distPath:r,srcPath:n});let a=i?[r]:["--experimental-strip-types",n];s(process.execPath,a)}async function x(e,t){return new Promise((r,n)=>{let s=a.createConnection({host:"127.0.0.1",port:e.port},()=>{s.write(`${JSON.stringify(t)}
5
- `)}),o=setTimeout(()=>{s.destroy(),n(new c("COMMAND_FAILED","Daemon request timed out",{timeoutMs:f}))},f),i="";s.setEncoding("utf8"),s.on("data",e=>{let t=(i+=e).indexOf("\n");if(-1===t)return;let a=i.slice(0,t).trim();if(a)try{let e=JSON.parse(a);s.end(),clearTimeout(o),r(e)}catch(e){clearTimeout(o),n(e)}}),s.on("error",e=>{clearTimeout(o),n(e)})})}function b(){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(s.startsWith("--backend")){let r=s.includes("=")?s.split("=")[1]:e[n+1];if(s.includes("=")||(n+=1),"ax"!==r&&"xctest"!==r)throw new c("INVALID_ARGS",`Invalid backend: ${r}`);t.snapshotBackend=r;continue}if(s.startsWith("--")){let[r,o]=s.split("="),i=o??e[n+1];switch(!o&&(n+=1),r){case"--platform":if("ios"!==i&&"android"!==i)throw new c("INVALID_ARGS",`Invalid platform: ${i}`);t.platform=i;break;case"--depth":{let e=Number(i);if(!Number.isFinite(e)||e<0)throw new c("INVALID_ARGS",`Invalid depth: ${i}`);t.snapshotDepth=Math.floor(e);break}case"--scope":t.snapshotScope=i;break;case"--device":t.device=i;break;case"--udid":t.udid=i;break;case"--serial":t.serial=i;break;case"--out":t.out=i;break;case"--session":t.session=i;break;default:throw new c("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 c("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(i.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=x();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 c("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=a.createConnection({host:"127.0.0.1",port:e.port},()=>{r.destroy(),t(!0)});r.on("error",()=>{t(!1)})})}async function g(){let t=x(),r=e.join(t,"dist","src","daemon.js"),n=e.join(t,"src","daemon.ts"),i=o.existsSync(r);if(!i&&!o.existsSync(n))throw new c("COMMAND_FAILED","Daemon entry not found",{distPath:r,srcPath:n});let a=i?[r]:["--experimental-strip-types",n];s(process.execPath,a)}async function b(e,t){return new Promise((r,n)=>{let s=a.createConnection({host:"127.0.0.1",port:e.port},()=>{s.write(`${JSON.stringify(t)}
5
+ `)}),o=setTimeout(()=>{s.destroy(),n(new c("COMMAND_FAILED","Daemon request timed out",{timeoutMs:f}))},f),i="";s.setEncoding("utf8"),s.on("data",e=>{let t=(i+=e).indexOf("\n");if(-1===t)return;let a=i.slice(0,t).trim();if(a)try{let e=JSON.parse(a);s.end(),clearTimeout(o),r(e)}catch(e){clearTimeout(o),n(e)}}),s.on("error",e=>{clearTimeout(o),n(e)})})}function x(){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 S(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(s.startsWith("--backend")){let r=s.includes("=")?s.split("=")[1]:e[n+1];if(s.includes("=")||(n+=1),"ax"!==r&&"xctest"!==r)throw new c("INVALID_ARGS",`Invalid backend: ${r}`);t.snapshotBackend=r;continue}if(s.startsWith("--")){let[r,o]=s.split("="),i=o??e[n+1];switch(!o&&(n+=1),r){case"--platform":if("ios"!==i&&"android"!==i)throw new c("INVALID_ARGS",`Invalid platform: ${i}`);t.platform=i;break;case"--depth":{let e=Number(i);if(!Number.isFinite(e)||e<0)throw new c("INVALID_ARGS",`Invalid depth: ${i}`);t.snapshotDepth=Math.floor(e);break}case"--scope":t.snapshotScope=i;break;case"--device":t.device=i;break;case"--udid":t.udid=i;break;case"--serial":t.serial=i;break;case"--out":t.out=i;break;case"--session":t.session=i;break;default:throw new c("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 c("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]
6
+
7
+ CLI to control iOS and Android devices for AI agents.
6
8
 
7
9
  Commands:
8
- open <app>
9
- close [app]
10
+ open [app] Boot device/simulator; optionally launch app
11
+ close [app] Close app or just end session
10
12
  snapshot [-i] [-c] [-d <depth>] [-s <scope>] [--raw] [--backend ax|xctest]
11
- click <@ref>
12
- get text <@ref>
13
- get attrs <@ref>
14
- replay <path>
15
- press <x> <y>
16
- long-press <x> <y> [durationMs]
17
- focus <x> <y>
18
- type <text>
19
- fill <x> <y> <text> | fill <@ref> <text>
20
- scroll <direction> [amount]
21
- scrollintoview <text>
22
- screenshot [--out path]
23
- session list
13
+ Capture accessibility tree
14
+ -i Interactive elements only
15
+ -c Compact output (drop empty structure)
16
+ -d <depth> Limit snapshot depth
17
+ -s <scope> Scope snapshot to label/identifier
18
+ --raw Raw node output
19
+ --backend ax|xctest ax: macOS Accessibility tree (fast, recommended, needs permissions)
20
+ xctest: XCTest snapshot (slower, no permissions)
21
+ click <@ref> Click element by snapshot ref
22
+ rect <label|@ref> Fetch element frame by label or ref (iOS sim)
23
+ get text <@ref> Return element text by ref
24
+ get attrs <@ref> Return element attributes by ref
25
+ replay <path> Replay a recorded session
26
+ press <x> <y> Tap at coordinates
27
+ long-press <x> <y> [durationMs] Long press (where supported)
28
+ focus <x> <y> Focus input at coordinates
29
+ type <text> Type text in focused field
30
+ fill <x> <y> <text> | fill <@ref> <text> Tap then type
31
+ scroll <direction> [amount] Scroll in direction (0-1 amount)
32
+ scrollintoview <text> Scroll until text appears (Android only)
33
+ screenshot [--out path] Capture screenshot
34
+ session list List active sessions
24
35
 
25
36
  Flags:
26
- --platform ios|android
27
- --device <name>
28
- --udid <udid>
29
- --serial <serial>
30
- --out <path>
31
- --session <name>
32
- --verbose
33
- --json
34
- --no-record
35
- --record-json
37
+ --platform ios|android Platform to target
38
+ --device <name> Device name to target
39
+ --udid <udid> iOS device UDID
40
+ --serial <serial> Android device serial
41
+ --out <path> Output path for screenshots
42
+ --session <name> Named session
43
+ --verbose Stream daemon/runner logs
44
+ --json JSON output
45
+ --no-record Do not record this action
46
+ --record-json Record JSON session log
36
47
 
37
48
  `),process.exit(+!n.flags.help));let{command:s,positionals:a,flags:d}=n,p=d.session??process.env.AGENT_DEVICE_SESSION??"default",f=d.verbose&&!d.json?function(){try{let t=e.join(i.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"),i=Buffer.alloc(e.size-r);o.readSync(s,i,0,i.length,r),o.closeSync(s),r=e.size,i.length>0&&process.stdout.write(i.toString("utf8"))},200);return()=>{n=!0,clearInterval(s)}}catch{return null}}():null;try{if("session"===s){let e=a[0]??"list";if("list"!==e)throw new c("INVALID_ARGS","session only supports list");let t=await m({session:p,command:"session_list",positionals:[],flags:{}});if(!t.ok)throw new c(t.error.code,t.error.message);d.json?l({success:!0,data:t.data??{}}):process.stdout.write(`${JSON.stringify(t.data??{},null,2)}
38
49
  `),f&&f();return}let e=await m({session:p,command:s,positionals:a,flags:d});if(e.ok){if(d.json){l({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,i=[];s&&i.push(`Page: ${s}`),o&&i.push(`App: ${o}`);let a=`Snapshot: ${r.length} nodes${n?" (truncated)":""}`,c=i.length>0?`${i.join("\n")}
@@ -47,4 +58,4 @@ ${u.join("\n")}
47
58
  `),f&&f();return}f&&f();return}throw new c(e.error.code,e.error.message,e.error.details)}catch(t){let e=r(t);if(d.json)l({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(`
48
59
  [daemon log]
49
60
  ${n}
50
- `)}}catch{}f&&f(),process.exit(1)}}n(process.argv[1]??"").href===import.meta.url&&$(process.argv.slice(2)).catch(e=>{u(r(e)),process.exit(1)}),$(process.argv.slice(2));
61
+ `)}}catch{}f&&f(),process.exit(1)}}n(process.argv[1]??"").href===import.meta.url&&S(process.argv.slice(2)).catch(e=>{u(r(e)),process.exit(1)}),S(process.argv.slice(2));
@@ -1,5 +1,5 @@
1
- let e,t;import n from"node:crypto";import{isCancel as i,select as r}from"@clack/prompts";import{node_path as a,runCmdStreaming as o,promises as s,asAppError as l,fileURLToPath as c,node_fs as u,node_os as d,node_net as f,errors_AppError as p,runCmd as h,whichCmd as m}from"./861.js";async function w(e,t){let n=e,a=e=>e.toLowerCase().replace(/_/g," ").replace(/\s+/g," ").trim();if(t.platform&&(n=n.filter(e=>e.platform===t.platform)),t.udid){let e=n.find(e=>e.id===t.udid&&"ios"===e.platform);if(!e)throw new p("DEVICE_NOT_FOUND",`No iOS device with UDID ${t.udid}`);return e}if(t.serial){let e=n.find(e=>e.id===t.serial&&"android"===e.platform);if(!e)throw new p("DEVICE_NOT_FOUND",`No Android device with serial ${t.serial}`);return e}if(t.deviceName){let e=a(t.deviceName),i=n.find(t=>a(t.name)===e);if(!i)throw new p("DEVICE_NOT_FOUND",`No device named ${t.deviceName}`);return i}if(1===n.length)return n[0];if(0===n.length)throw new p("DEVICE_NOT_FOUND","No devices found",{selector:t});let o=n.filter(e=>e.booted);if(1===o.length)return o[0];if(!process.env.CI&&process.stdin.isTTY&&process.stdout.isTTY){let e=await r({message:"Multiple devices available. Choose a device to continue:",options:(o.length>0?o:n).map(e=>({label:`${e.name} (${e.platform}${e.kind?`, ${e.kind}`:""}${e.booted?", booted":""})`,value:e.id}))});if(i(e))throw new p("INVALID_ARGS","Device selection cancelled");if(e){let t=n.find(t=>t.id===e);if(t)return t}}return o[0]??n[0]}async function g(){if(!await m("adb"))throw new p("TOOL_MISSING","adb not found in PATH");let e=(await h("adb",["devices","-l"])).stdout.split("\n").map(e=>e.trim()),t=[];for(let n of e){if(!n||n.startsWith("List of devices"))continue;let e=n.split(/\s+/),i=e[0];if("device"!==e[1])continue;let r=(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&&(r=t.replace(/_/g," "))}let a=await y(i);t.push({platform:"android",id:i,name:r,kind:i.startsWith("emulator-")?"emulator":"device",booted:a})}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}}let N={settings:{type:"intent",value:"android.settings.SETTINGS"}};function v(e,t){return["-s",e.id,...t]}async function b(e,t){let n=t.trim();if(n.includes("."))return{type:"package",value:n};let i=N[n.toLowerCase()];if(i)return i;let r=(await h("adb",v(e,["shell","pm","list","packages"]))).stdout.split("\n").map(e=>e.replace("package:","").trim()).filter(Boolean).filter(e=>e.toLowerCase().includes(n.toLowerCase()));if(1===r.length)return{type:"package",value:r[0]};if(r.length>1)throw new p("INVALID_ARGS",`Multiple packages matched "${t}"`,{matches:r});throw new p("APP_NOT_INSTALLED",`No package found matching "${t}"`)}async function I(e,t){let n=await b(e,t);"intent"===n.type?await h("adb",v(e,["shell","am","start","-a",n.value])):await h("adb",v(e,["shell","monkey","-p",n.value,"-c","android.intent.category.LAUNCHER","1"]))}async function A(e,t){if("settings"===t.trim().toLowerCase())return void await h("adb",v(e,["shell","am","force-stop","com.android.settings"]));let n=await b(e,t);if("intent"===n.type)throw new p("INVALID_ARGS","Close requires a package name, not an intent");await h("adb",v(e,["shell","am","force-stop",n.value]))}async function S(e,t,n){await h("adb",v(e,["shell","input","tap",String(t),String(n)]))}async function O(e,t,n,i=800){await h("adb",v(e,["shell","input","swipe",String(t),String(n),String(t),String(n),String(i)]))}async function D(e,t){let n=t.replace(/ /g,"%s");await h("adb",v(e,["shell","input","text",n]))}async function x(e,t,n){await S(e,t,n)}async function _(e,t,n,i){await x(e,t,n),await D(e,i)}async function E(e,t,n=.6){let{width:i,height:r}=await T(e),a=Math.floor(i*n),o=Math.floor(r*n),s=Math.floor(i/2),l=Math.floor(r/2),c=s,u=l,d=s,f=l;switch(t){case"up":u=l-Math.floor(o/2),f=l+Math.floor(o/2);break;case"down":u=l+Math.floor(o/2),f=l-Math.floor(o/2);break;case"left":c=s-Math.floor(a/2),d=s+Math.floor(a/2);break;case"right":c=s+Math.floor(a/2),d=s-Math.floor(a/2);break;default:throw new p("INVALID_ARGS",`Unknown direction: ${t}`)}await h("adb",v(e,["shell","input","swipe",String(c),String(u),String(d),String(f),"300"]))}async function k(e,t){for(let n=0;n<8;n+=1){let n="";try{n=await L(e)}catch(t){let e=t instanceof Error?t.message:String(t);throw new p("UNSUPPORTED_OPERATION",`uiautomator dump failed: ${e}`)}if(function(e,t){let n=t.toLowerCase(),i=/<node[^>]+>/g,r=i.exec(e);for(;r;){let t=r[0],a=/text="([^"]*)"/.exec(t),o=/content-desc="([^"]*)"/.exec(t),s=(a?.[1]??"").toLowerCase(),l=(o?.[1]??"").toLowerCase();if(s.includes(n)||l.includes(n)){let e=/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/.exec(t);if(e){let t=Number(e[1]),n=Number(e[2]);return{x:Math.floor((t+Number(e[3]))/2),y:Math.floor((n+Number(e[4]))/2)}}return{x:0,y:0}}r=i.exec(e)}return null}(n,t))return;await E(e,"down",.5)}throw new p("COMMAND_FAILED",`Could not find element containing "${t}" after scrolling`)}async function C(e,t){let n=await h("adb",v(e,["exec-out","screencap","-p"]),{binaryStdout:!0});if(!n.stdoutBuffer)throw new p("COMMAND_FAILED","Failed to capture screenshot");await s.writeFile(t,n.stdoutBuffer)}async function R(e,t={}){return function(e,t,n){let i=function(e){let t={type:null,label:null,value:null,identifier:null,depth:-1,children:[]},n=[t],i=/<node\b[^>]*>|<\/node>/g,r=i.exec(e);for(;r;){let t=r[0];if(t.startsWith("</node")){n.length>1&&n.pop(),r=i.exec(e);continue}let a=function(e){let t=t=>{let n=RegExp(`${t}="([^"]*)"`).exec(e);return n?n[1]:null},n=e=>{let n=t(e);if(null!==n)return"true"===n};return{text:t("text"),desc:t("content-desc"),resourceId:t("resource-id"),className:t("class"),bounds:t("bounds"),clickable:n("clickable"),enabled:n("enabled"),focusable:n("focusable")}}(t),o=function(e){if(!e)return;let t=/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/.exec(e);if(!t)return;let n=Number(t[1]),i=Number(t[2]);return{x:n,y:i,width:Math.max(0,Number(t[3])-n),height:Math.max(0,Number(t[4])-i)}}(a.bounds),s=n[n.length-1],l={type:a.className,label:a.text||a.desc,value:a.text,identifier:a.resourceId,rect:o,enabled:a.enabled,hittable:a.clickable??a.focusable,depth:s.depth+1,parentIndex:void 0,children:[]};s.children.push(l),t.endsWith("/>")||n.push(l),r=i.exec(e)}return t}(e),r=[],a=!1,o=n.depth??1/0,s=n.scope?function(e,t){let n=t.toLowerCase(),i=[...e.children];for(;i.length>0;){let e=i.shift(),t=e.label?.toLowerCase()??"",r=e.value?.toLowerCase()??"",a=e.identifier?.toLowerCase()??"";if(t.includes(n)||r.includes(n)||a.includes(n))return e;i.push(...e.children)}return null}(i,n.scope):null,l=s?[s]:i.children,c=(e,t)=>{if(r.length>=800){a=!0;return}if(!(t>o)){for(let i of((n.raw||function(e,t){if(t.interactiveOnly)return!!e.hittable;if(t.compact){let t=!!(e.label&&e.label.trim().length>0),n=!!(e.identifier&&e.identifier.trim().length>0);return t||n||!!e.hittable}return!0}(e,n))&&r.push({index:r.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),a)return}};for(let e of l)if(c(e,0),a)break;return a?{nodes:r,truncated:a}:{nodes:r}}(await L(e),800,t)}async function P(){if(!await m("adb"))throw new p("TOOL_MISSING","adb not found in PATH")}async function T(e){let t=(await h("adb",v(e,["shell","wm","size"]))).stdout.match(/Physical size:\s*(\d+)x(\d+)/);if(!t)throw new p("COMMAND_FAILED","Unable to read screen size");return{width:Number(t[1]),height:Number(t[2])}}async function L(e){return await h("adb",v(e,["shell","uiautomator","dump","/sdcard/window_dump.xml"])),(await h("adb",v(e,["shell","cat","/sdcard/window_dump.xml"]))).stdout}async function M(){if("darwin"!==process.platform)throw new p("UNSUPPORTED_PLATFORM","iOS tools are only available on macOS");if(!await m("xcrun"))throw new p("TOOL_MISSING","xcrun not found in PATH");let e=[],t=await h("xcrun",["simctl","list","devices","-j"]);try{let n=JSON.parse(t.stdout);for(let t of Object.values(n.devices))for(let n of t)n.isAvailable&&e.push({platform:"ios",id:n.udid,name:n.name,kind:"simulator",booted:"Booted"===n.state})}catch(e){throw new p("COMMAND_FAILED","Failed to parse simctl devices JSON",void 0,e)}if(await m("xcrun"))try{let t=await h("xcrun",["devicectl","list","devices","--json"]);for(let n of JSON.parse(t.stdout).devices??[])n.platform?.toLowerCase().includes("ios")&&e.push({platform:"ios",id:n.identifier,name:n.name,kind:"device",booted:!0})}catch{}return e}let F={settings:"com.apple.Preferences"};async function j(e,t){let n=t.trim();if(n.includes("."))return n;let i=F[n.toLowerCase()];if(i)return i;if("simulator"===e.kind){let i=(await Y(e)).filter(e=>e.name.toLowerCase()===n.toLowerCase());if(1===i.length)return i[0].bundleId;if(i.length>1)throw new p("INVALID_ARGS",`Multiple apps matched "${t}"`,{matches:i})}throw new p("APP_NOT_INSTALLED",`No app found matching "${t}"`)}async function U(e,t){let n=await j(e,t);if("simulator"===e.kind){await Z(e),await h("xcrun",["simctl","launch",e.id,n]);return}await h("xcrun",["devicectl","device","process","launch","--device",e.id,n])}async function V(e,t){let n=await j(e,t);if("simulator"===e.kind){await Z(e);let t=await h("xcrun",["simctl","terminate",e.id,n],{allowFailure:!0});if(0!==t.exitCode){if(t.stderr.toLowerCase().includes("found nothing to terminate"))return;throw new p("COMMAND_FAILED",`xcrun exited with code ${t.exitCode}`,{cmd:"xcrun",args:["simctl","terminate",e.id,n],stdout:t.stdout,stderr:t.stderr,exitCode:t.exitCode})}return}await h("xcrun",["devicectl","device","process","terminate","--device",e.id,n])}async function $(e,t,n){throw H(e,"press"),await Z(e),new p("UNSUPPORTED_OPERATION","simctl io tap is not available; use the XCTest runner for input")}async function G(e,t,n,i=800){throw H(e,"long-press"),await Z(e),new p("UNSUPPORTED_OPERATION","long-press is not supported on iOS simulators without XCTest runner support")}async function B(e,t,n){await $(e,t,n)}async function J(e,t){throw H(e,"type"),await Z(e),new p("UNSUPPORTED_OPERATION","simctl io keyboard is not available; use the XCTest runner for input")}async function q(e,t,n,i){await B(e,t,n),await J(e,i)}async function W(e,t,n=.6){throw H(e,"scroll"),await Z(e),new p("UNSUPPORTED_OPERATION","simctl io swipe is not available; use the XCTest runner for input")}async function X(e){throw new p("UNSUPPORTED_OPERATION",`scrollintoview is not supported on iOS without UI automation (${e})`)}async function z(e,t){if("simulator"===e.kind){await Z(e),await h("xcrun",["simctl","io",e.id,"screenshot",t]);return}await h("xcrun",["devicectl","device","screenshot","--device",e.id,t])}function H(e,t){if("simulator"!==e.kind)throw new p("UNSUPPORTED_OPERATION",`${t} is only supported on iOS simulators in v1`)}async function Y(e){let t=(await h("xcrun",["simctl","listapps",e.id],{allowFailure:!0})).stdout;if(!t.trim().startsWith("{"))return[];try{let e=JSON.parse(t);return Object.entries(e).map(([e,t])=>({bundleId:e,name:t.CFBundleDisplayName??t.CFBundleName??e}))}catch{return[]}}async function Z(e){"simulator"!==e.kind||"Booted"!==await K(e.id)&&(await h("xcrun",["simctl","boot",e.id],{allowFailure:!0}),await h("xcrun",["simctl","bootstatus",e.id,"-b"],{allowFailure:!0}))}async function K(e){let t=await h("xcrun",["simctl","list","devices","-j"],{allowFailure:!0});if(0!==t.exitCode)return null;try{let n=JSON.parse(t.stdout);for(let t of Object.values(n.devices??{})){let n=t.find(t=>t.udid===e);if(n)return n.state}}catch{}return null}let Q=new Map;async function ee(e,t,n={}){if("simulator"!==e.kind)throw new p("UNSUPPORTED_OPERATION","iOS runner only supports simulators in v1");try{let i=await ei(e,n),r=await es(e,i.port,t,n.logPath),a=await r.text(),o={};try{o=JSON.parse(a)}catch{throw new p("COMMAND_FAILED","Invalid runner response",{text:a})}if(!o.ok)throw new p("COMMAND_FAILED",o.error?.message??"Runner error",{runner:o,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:n.logPath});return o.data??{}}catch(r){let i=r instanceof p?r:new p("COMMAND_FAILED",String(r));if("COMMAND_FAILED"===i.code&&"string"==typeof i.message&&i.message.includes("Runner did not accept connection")){await et(e.id);let i=await ei(e,n),r=await es(e,i.port,t,n.logPath),a=await r.text(),o={};try{o=JSON.parse(a)}catch{throw new p("COMMAND_FAILED","Invalid runner response",{text:a})}if(!o.ok)throw new p("COMMAND_FAILED",o.error?.message??"Runner error",{runner:o,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:n.logPath});return o.data??{}}throw r}}async function et(e){let t=Q.get(e);if(t){try{await es(t.device,t.port,{command:"shutdown"})}catch{}try{await t.testPromise}catch{}ef(t.xctestrunPath),ef(t.jsonPath),Q.delete(e)}}async function en(e){await h("xcrun",["simctl","bootstatus",e,"-b"],{allowFailure:!0})}async function ei(e,t){let n=Q.get(e.id);if(n)return n;await en(e.id);let i=await er(e.id,t),r=await ec(),a=process.env.AGENT_DEVICE_RUNNER_TIMEOUT??"300",{xctestrunPath:s,jsonPath:l}=await ed(i,{AGENT_DEVICE_RUNNER_PORT:String(r),AGENT_DEVICE_RUNNER_TIMEOUT:a},`session-${e.id}-${r}`),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=>{eo(e,t.logPath,t.verbose)},onStderrChunk:e=>{eo(e,t.logPath,t.verbose)},allowFailure:!0,env:{...process.env,AGENT_DEVICE_RUNNER_PORT:String(r),AGENT_DEVICE_RUNNER_TIMEOUT:a}}),u={device:e,deviceId:e.id,port:r,xctestrunPath:s,jsonPath:l,testPromise:c};return Q.set(e.id,u),u}async function er(e,t){let n,i=a.join(d.homedir(),".agent-device","ios-runner"),r=a.join(i,"derived");if((n=process.env.AGENT_DEVICE_IOS_CLEAN_DERIVED)&&["1","true","yes","on"].includes(n.toLowerCase()))try{u.rmSync(r,{recursive:!0,force:!0})}catch{}let s=ea(r);if(s)return s;let l=function(){let e=a.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=a.join(t,"package.json");if(u.existsSync(e))return t;t=a.dirname(t)}return e}(),f=a.join(l,"ios-runner","AgentDeviceRunner","AgentDeviceRunner.xcodeproj");if(!u.existsSync(f))throw new p("COMMAND_FAILED","iOS runner project not found",{projectPath:f});try{await o("xcodebuild",["build-for-testing","-project",f,"-scheme","AgentDeviceRunner","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-destination",`platform=iOS Simulator,id=${e}`,"-derivedDataPath",r],{onStdoutChunk:e=>{eo(e,t.logPath,t.verbose)},onStderrChunk:e=>{eo(e,t.logPath,t.verbose)}})}catch(n){let e=n instanceof p?n:new p("COMMAND_FAILED",String(n));throw new p("COMMAND_FAILED","xcodebuild build-for-testing failed",{error:e.message,details:e.details,logPath:t.logPath})}let h=ea(r);if(!h)throw new p("COMMAND_FAILED","Failed to locate .xctestrun after build");return h}function ea(e){if(!u.existsSync(e))return null;let t=[],n=[e];for(;n.length>0;){let e=n.pop();for(let i of u.readdirSync(e,{withFileTypes:!0})){let r=a.join(e,i.name);if(i.isDirectory()){n.push(r);continue}if(i.isFile()&&i.name.endsWith(".xctestrun"))try{let e=u.statSync(r);t.push({path:r,mtimeMs:e.mtimeMs})}catch{}}}return 0===t.length?null:(t.sort((e,t)=>t.mtimeMs-e.mtimeMs),t[0]?.path??null)}function eo(e,t,n){t&&u.appendFileSync(t,e),n&&process.stderr.write(e)}async function es(e,t,n,i){i&&await eu(i,4e3);let r=Date.now(),a=null;for(;Date.now()-r<8e3;)try{return await fetch(`http://127.0.0.1:${t}/command`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})}catch(e){a=e,await new Promise(e=>setTimeout(e,100))}if("simulator"===e.kind){let i=await el(e.id,t,n);return new Response(i.body,{status:i.status})}let o=i?function(e){try{if(!u.existsSync(e))return null;let t=u.readFileSync(e,"utf8").match(/AGENT_DEVICE_RUNNER_PORT=(\d+)/);if(t)return Number(t[1])}catch{}return null}(i):null;if(o&&o!==t)try{return await fetch(`http://127.0.0.1:${o}/command`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})}catch(e){a=e}throw new p("COMMAND_FAILED","Runner did not accept connection",{port:t,fallbackPort:o,logPath:i,lastError:a?String(a):void 0})}async function el(e,t,n){let i=JSON.stringify(n),r=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}),a=r.stdout;if(0!==r.exitCode)throw new p("COMMAND_FAILED","Runner did not accept connection (simctl spawn)",{port:t,stdout:r.stdout,stderr:r.stderr,exitCode:r.exitCode});return{status:200,body:a}}async function ec(){return await new Promise((e,t)=>{let n=f.createServer();n.listen(0,"127.0.0.1",()=>{let i=n.address();n.close(),"object"==typeof i&&i?.port?e(i.port):t(new p("COMMAND_FAILED","Failed to allocate port"))}),n.on("error",t)})}async function eu(e,t){if(!u.existsSync(e))return;let n=Date.now(),i=0;for(;Date.now()-n<t;){if(!u.existsSync(e))return;let t=u.statSync(e);if(t.size>i){let n=u.openSync(e,"r"),r=Buffer.alloc(t.size-i);u.readSync(n,r,0,r.length,i),u.closeSync(n),i=t.size;let a=r.toString("utf8");if(a.includes("AGENT_DEVICE_RUNNER_LISTENER_READY")||a.includes("AGENT_DEVICE_RUNNER_PORT="))return}await new Promise(e=>setTimeout(e,100))}}async function ed(e,t,n){let i,r=a.dirname(e),o=n.replace(/[^a-zA-Z0-9._-]/g,"_"),s=a.join(r,`AgentDeviceRunner.env.${o}.json`),l=a.join(r,`AgentDeviceRunner.env.${o}.xctestrun`),c=await h("plutil",["-convert","json","-o","-",e],{allowFailure:!0});if(0!==c.exitCode||!c.stdout.trim())throw new p("COMMAND_FAILED","Failed to read xctestrun plist",{xctestrunPath:e,stderr:c.stderr});try{i=JSON.parse(c.stdout)}catch(t){throw new p("COMMAND_FAILED","Failed to parse xctestrun JSON",{xctestrunPath:e,error:String(t)})}let d=e=>{e.EnvironmentVariables={...e.EnvironmentVariables??{},...t},e.UITestEnvironmentVariables={...e.UITestEnvironmentVariables??{},...t},e.UITargetAppEnvironmentVariables={...e.UITargetAppEnvironmentVariables??{},...t},e.TestingEnvironmentVariables={...e.TestingEnvironmentVariables??{},...t}},f=i.TestConfigurations;if(Array.isArray(f))for(let e of f){if(!e||"object"!=typeof e)continue;let t=e.TestTargets;if(Array.isArray(t))for(let e of t)e&&"object"==typeof e&&d(e)}for(let[e,t]of Object.entries(i))t&&"object"==typeof t&&t.TestBundlePath&&(d(t),i[e]=t);u.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 p("COMMAND_FAILED","Failed to write xctestrun plist",{tmpXctestrunPath:l,stderr:m.stderr});return{xctestrunPath:l,jsonPath:s}}function ef(e){try{u.existsSync(e)&&u.unlinkSync(e)}catch{}}async function ep(e){let t,n;if("ios"!==e.platform||"simulator"!==e.kind)throw new p("UNSUPPORTED_OPERATION","AX snapshot is only supported on iOS simulators");let i=await eh(),r=await h(i,[],{allowFailure:!0});if(0!==r.exitCode)throw new p("COMMAND_FAILED","AX snapshot failed",{stderr:r.stderr,stdout:r.stdout});try{let e=JSON.parse(r.stdout);if(e&&"object"==typeof e&&"root"in e){if(!e.root)throw Error("AX snapshot missing root");t=e.root,n=e.windowFrame??void 0}else t=e}catch(e){throw new p("COMMAND_FAILED","Invalid AX snapshot JSON",{error:String(e)})}let a=t.frame??n,o=[],s=[],l=(e,t)=>{e.frame&&o.push(e.frame);let n=e.frame&&a?{x:e.frame.x-a.x,y:e.frame.y-a.y,width:e.frame.width,height:e.frame.height}:e.frame;for(let i of(s.push({...e,frame:n,children:void 0,depth:t}),e.children??[]))l(i,t+1)};return l(t,0),{nodes:(function(e,t,n){if(!t||0===n.length)return e;let i=1/0,r=1/0;for(let e of n)e.x<i&&(i=e.x),e.y<r&&(r=e.y);return i<=5&&r<=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})(s,a,o).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})),rootRect:a}}async function eh(){let e=function(){let e=process.cwd();for(let t=0;t<6;t+=1){let t=a.join(e,"package.json");if(u.existsSync(t))return e;e=a.dirname(e)}return process.cwd()}(),t=a.join(e,"ios-runner","AXSnapshot"),n=process.env.AGENT_DEVICE_AX_BINARY;if(n&&u.existsSync(n))return n;for(let t of[a.join(e,"bin","axsnapshot"),a.join(e,"dist","bin","axsnapshot"),a.join(e,"dist","axsnapshot")])if(u.existsSync(t))return t;let i=a.join(t,".build","release","axsnapshot");if(u.existsSync(i))return i;let r=await h("swift",["build","-c","release"],{cwd:t,allowFailure:!0});if(0!==r.exitCode||!u.existsSync(i))throw new p("COMMAND_FAILED","Failed to build AX snapshot tool",{stderr:r.stderr,stdout:r.stdout});return i}async function em(e){let t={platform:e.platform,deviceName:e.device,udid:e.udid,serial:e.serial};if("android"===t.platform){await P();let e=await g();return await w(e,t)}if("ios"===t.platform){let e=await M();return await w(e,t)}let n=[];try{n.push(...await g())}catch{}try{n.push(...await M())}catch{}return await w(n,t)}async function ew(e,t,n,i,r){let a=function(e){switch(e.platform){case"android":return{open:t=>I(e,t),close:t=>A(e,t),tap:(t,n)=>S(e,t,n),longPress:(t,n,i)=>O(e,t,n,i),focus:(t,n)=>x(e,t,n),type:t=>D(e,t),fill:(t,n,i)=>_(e,t,n,i),scroll:(t,n)=>E(e,t,n),scrollIntoView:t=>k(e,t),screenshot:t=>C(e,t)};case"ios":return{open:t=>U(e,t),close:t=>V(e,t),tap:(t,n)=>$(e,t,n),longPress:(t,n,i)=>G(e,t,n,i),focus:(t,n)=>B(e,t,n),type:t=>J(e,t),fill:(t,n,i)=>q(e,t,n,i),scroll:(t,n)=>W(e,t,n),scrollIntoView:e=>X(e),screenshot:t=>z(e,t)};default:throw new p("UNSUPPORTED_PLATFORM",`Unsupported platform: ${e.platform}`)}}(e);switch(t){case"open":{let e=n[0];if(!e)throw new p("INVALID_ARGS","open requires an app name or bundle/package id");return await a.open(e),{app:e}}case"close":{let e=n[0];if(!e)return{closed:"session"};return await a.close(e),{app:e}}case"press":{let[t,i]=n.map(Number);if(Number.isNaN(t)||Number.isNaN(i))throw new p("INVALID_ARGS","press requires x y");return"ios"===e.platform&&"simulator"===e.kind?await ee(e,{command:"tap",x:t,y:i,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath}):await a.tap(t,i),{x:t,y:i}}case"long-press":{let e=Number(n[0]),t=Number(n[1]),i=n[2]?Number(n[2]):void 0;if(Number.isNaN(e)||Number.isNaN(t))throw new p("INVALID_ARGS","long-press requires x y [durationMs]");return await a.longPress(e,t,i),{x:e,y:t,durationMs:i}}case"focus":{let[t,i]=n.map(Number);if(Number.isNaN(t)||Number.isNaN(i))throw new p("INVALID_ARGS","focus requires x y");return"ios"===e.platform&&"simulator"===e.kind?await ee(e,{command:"tap",x:t,y:i,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath}):await a.focus(t,i),{x:t,y:i}}case"type":{let t=n.join(" ");if(!t)throw new p("INVALID_ARGS","type requires text");return"ios"===e.platform&&"simulator"===e.kind?await ee(e,{command:"type",text:t,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath}):await a.type(t),{text:t}}case"fill":{let t=Number(n[0]),i=Number(n[1]),o=n.slice(2).join(" ");if(Number.isNaN(t)||Number.isNaN(i)||!o)throw new p("INVALID_ARGS","fill requires x y text");return"ios"===e.platform&&"simulator"===e.kind?(await ee(e,{command:"tap",x:t,y:i,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath}),await ee(e,{command:"type",text:o,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath})):await a.fill(t,i,o),{x:t,y:i,text:o}}case"scroll":{let t=n[0],i=n[1]?Number(n[1]):void 0;if(!t)throw new p("INVALID_ARGS","scroll requires direction");if("ios"===e.platform&&"simulator"===e.kind){if(!["up","down","left","right"].includes(t))throw new p("INVALID_ARGS",`Unknown direction: ${t}`);let n=function(e){switch(e){case"up":return"down";case"down":return"up";case"left":return"right";case"right":return"left"}}(t);await ee(e,{command:"swipe",direction:n,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath})}else await a.scroll(t,i);return{direction:t,amount:i}}case"scrollintoview":{let e=n.join(" ");if(!e)throw new p("INVALID_ARGS","scrollintoview requires text");return await a.scrollIntoView(e),{text:e}}case"screenshot":{let e=i??`./screenshot-${Date.now()}.png`;return await a.screenshot(e),{path:e}}case"snapshot":{let t=r?.snapshotBackend??"ax";if("ios"===e.platform){if("simulator"!==e.kind)throw new p("UNSUPPORTED_OPERATION","snapshot is only supported on iOS simulators in v1");if("ax"===t)try{let t=await ep(e);return{nodes:t.nodes??[],truncated:!1,backend:"ax",rootRect:t.rootRect}}catch(e){if(r?.snapshotBackend==="ax")throw e}let n=await ee(e,{command:"snapshot",appBundleId:r?.appBundleId,interactiveOnly:r?.snapshotInteractiveOnly,compact:r?.snapshotCompact,depth:r?.snapshotDepth,scope:r?.snapshotScope,raw:r?.snapshotRaw},{verbose:r?.verbose,logPath:r?.logPath});return{nodes:n.nodes??[],truncated:n.truncated??!1,backend:"xctest"}}let n=await R(e,{interactiveOnly:r?.snapshotInteractiveOnly,compact:r?.snapshotCompact,depth:r?.snapshotDepth,scope:r?.snapshotScope,raw:r?.snapshotRaw});return{nodes:n.nodes??[],truncated:n.truncated??!1,backend:"android"}}default:throw new p("INVALID_ARGS",`Unknown command: ${t}`)}}function eg(e){let t=e.trim();return t.startsWith("@")?t.slice(1)||null:t.startsWith("e")?t:null}function ey(e,t){return e.find(e=>e.ref===t)??null}function eN(e){return{x:Math.round(e.x+e.width/2),y:Math.round(e.y+e.height/2)}}let ev=new Map,eb=a.join(d.homedir(),".agent-device"),eI=a.join(eb,"daemon.json"),eA=a.join(eb,"daemon.log"),eS=a.join(eb,"sessions"),eO=function(){try{let e=function(){let e=a.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=a.join(t,"package.json");if(u.existsSync(e))return t;t=a.dirname(t)}return e}();return JSON.parse(u.readFileSync(a.join(e,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}(),eD=n.randomBytes(24).toString("hex");function ex(e,t){return{appBundleId:t,verbose:e?.verbose,logPath:eA,snapshotInteractiveOnly:e?.snapshotInteractiveOnly,snapshotCompact:e?.snapshotCompact,snapshotDepth:e?.snapshotDepth,snapshotScope:e?.snapshotScope,snapshotRaw:e?.snapshotRaw,snapshotBackend:e?.snapshotBackend}}async function e_(e){if(e.token!==eD)return{ok:!1,error:{code:"UNAUTHORIZED",message:"Invalid token"}};let t=e.command,n=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("open"===t){let i,r=await em(e.flags??{}),a=e.positionals?.[0];if("ios"===r.platform)try{let{resolveIosApp:t}=await Promise.resolve().then(()=>({resolveIosApp:j}));i=await t(r,e.positionals?.[0]??"")}catch{i=void 0}await ew(r,"open",e.positionals??[],e.flags?.out,{...ex(e.flags,i)});let o={name:n,device:r,createdAt:Date.now(),appBundleId:i,appName:a,actions:[]};return eE(o,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:n}}),ev.set(n,o),{ok:!0,data:{session:n}}}if("replay"===t){let t=e.positionals?.[0];if(!t)return{ok:!1,error:{code:"INVALID_ARGS",message:"replay requires a path"}};try{var i;let e=(i=t).startsWith("~/")?a.join(d.homedir(),i.slice(2)):a.resolve(i),r=JSON.parse(u.readFileSync(e,"utf8")),o=r.optimizedActions??r.actions??[];for(let e of o)e&&"replay"!==e.command&&await e_({token:eD,session:n,command:e.command,positionals:e.positionals??[],flags:e.flags??{}});return{ok:!0,data:{replayed:o.length,session:n}}}catch(t){let e=l(t);return{ok:!1,error:{code:e.code,message:e.message}}}}if("close"===t){let i=ev.get(n);return i?(e.positionals&&e.positionals.length>0&&await ew(i.device,"close",e.positionals??[],e.flags?.out,{...ex(e.flags,i.appBundleId)}),"ios"===i.device.platform&&"simulator"===i.device.kind&&await et(i.device.id),eE(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:n}}),ek(i),ev.delete(n),{ok:!0,data:{session:n}}):{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session"}}}if("snapshot"===t){let i=ev.get(n),r=i?.device??await em(e.flags??{}),a=i?.appBundleId,o=await ew(r,"snapshot",[],e.flags?.out,{...ex(e.flags,a)}),s=(function(e){let t=[],n=[];for(let i of e){let e=i.depth??0;for(;t.length>0&&e<=t[t.length-1];)t.pop();let r=function(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase();return t.startsWith("ax")&&(t=t.replace(/^ax/,"")),t}(i.type??"");if("group"===r||"ioscontentgroup"===r){t.push(e);continue}let a=Math.max(0,e-t.length);n.push({...i,depth:a})}return n})(o?.nodes??[]).map((e,t)=>({...e,ref:`e${t+1}`})),l={nodes:s,truncated:o?.truncated,createdAt:Date.now(),backend:o?.backend},c={name:n,device:r,createdAt:i?.createdAt??Date.now(),appBundleId:a,snapshot:l,actions:i?.actions??[]};return eE(c,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{nodes:s.length,truncated:o?.truncated??!1}}),ev.set(n,c),{ok:!0,data:{nodes:s,truncated:o?.truncated??!1,appName:i?.appName??a??r.name,appBundleId:a}}}if("click"===t){let i=ev.get(n);if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let r=e.positionals?.[0]??"",a=eg(r);if(!a)return{ok:!1,error:{code:"INVALID_ARGS",message:"click requires a ref like @e2"}};let o=ey(i.snapshot.nodes,a);if(!o?.rect&&e.positionals.length>1){let t=e.positionals.slice(1).join(" ").trim();t.length>0&&(o=eR(i.snapshot.nodes,t))}if(!o?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${r} not found or has no bounds`}};let s=eP(o,i.snapshot.nodes),l=o.label?.trim();if("ios"===i.device.platform&&"simulator"===i.device.kind&&l&&function(e,t){let n=t.trim().toLowerCase();if(!n)return!1;let i=0;for(let t of e)if((t.label??"").trim().toLowerCase()===n&&(i+=1)>1)return!1;return 1===i}(i.snapshot.nodes,l))return await ee(i.device,{command:"tap",text:l,appBundleId:i.appBundleId},{verbose:e.flags?.verbose,logPath:eA}),eE(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:a,refLabel:l,mode:"text"}}),{ok:!0,data:{ref:a,mode:"text"}};let{x:c,y:u}=eN(o.rect);return await ew(i.device,"press",[String(c),String(u)],e.flags?.out,{...ex(e.flags,i.appBundleId)}),eE(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:a,x:c,y:u,refLabel:s}}),{ok:!0,data:{ref:a,x:c,y:u}}}if("fill"===t){let i=ev.get(n);if(e.positionals?.[0]?.startsWith("@")){if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let n=eg(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]:"",a=e.positionals.length>=3?e.positionals.slice(2).join(" "):e.positionals.slice(1).join(" ");if(!a)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires text after ref"}};let o=ey(i.snapshot.nodes,n);if(!o?.rect&&r&&(o=eR(i.snapshot.nodes,r)),!o?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${e.positionals[0]} not found or has no bounds`}};let s=eP(o,i.snapshot.nodes),{x:l,y:c}=eN(o.rect),u=await ew(i.device,"fill",[String(l),String(c),a],e.flags?.out,{...ex(e.flags,i.appBundleId)});return eE(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:u??{ref:n,x:l,y:c,refLabel:s}}),{ok:!0,data:u??{ref:n,x:l,y:c}}}}if("get"===t){let i=e.positionals?.[0],r=e.positionals?.[1];if("text"!==i&&"attrs"!==i)return{ok:!1,error:{code:"INVALID_ARGS",message:"get only supports text or attrs"}};let a=ev.get(n);if(!a?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let o=eg(r??"");if(!o)return{ok:!1,error:{code:"INVALID_ARGS",message:"get text requires a ref like @e2"}};let s=ey(a.snapshot.nodes,o);if(!s&&e.positionals.length>2){let t=e.positionals.slice(2).join(" ").trim();t.length>0&&(s=eR(a.snapshot.nodes,t))}if(!s)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${r} not found`}};if("attrs"===i)return eE(a,{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 eE(a,{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}}}if("rect"===t){let i=ev.get(n);if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let r=eg(e.positionals?.[0]??""),a="";if(r){let e=ey(i.snapshot.nodes,r);a=e?.label?.trim()??""}else a=e.positionals.join(" ").trim();if(!a)return{ok:!1,error:{code:"INVALID_ARGS",message:"rect requires a label or ref with label"}};if("ios"!==i.device.platform||"simulator"!==i.device.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"rect is only supported on iOS simulators"}};let o=await ee(i.device,{command:"rect",text:a,appBundleId:i.appBundleId},{verbose:e.flags?.verbose,logPath:eA});return eE(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{label:a,rect:o?.rect}}),{ok:!0,data:{label:a,rect:o?.rect}}}let r=ev.get(n);if(!r)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session. Run open first."}};let o=await ew(r.device,t,e.positionals??[],e.flags?.out,{...ex(e.flags,r.appBundleId)});return eE(r,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:o??{}}),{ok:!0,data:o??{}}}function eE(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:n,udid:i,serial:r,out:a,verbose:o,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:u,snapshotRaw:d,snapshotBackend:f,noRecord:p,recordJson:h}=e;return{platform:t,device:n,udid:i,serial:r,out:a,verbose:o,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:u,snapshotRaw:d,snapshotBackend:f,noRecord:p,recordJson:h}}(t.flags),result:t.result})}function ek(e){try{u.existsSync(eS)||u.mkdirSync(eS,{recursive:!0});let t=e.name.replace(/[^a-zA-Z0-9._-]/g,"_"),n=new Date(e.createdAt).toISOString().replace(/[:.]/g,"-"),i=a.join(eS,`${t}-${n}.ad`),r=a.join(eS,`${t}-${n}.json`),o={name:e.name,device:e.device,createdAt:e.createdAt,appBundleId:e.appBundleId,actions:e.actions,optimizedActions:function(e){let t=[];for(let n of e.actions)if("snapshot"!==n.command){if("click"===n.command||"fill"===n.command||"get"===n.command){let i=n.result?.refLabel;"string"==typeof i&&i.trim().length>0&&t.push({ts:n.ts,command:"snapshot",positionals:[],flags:{platform:e.device.platform,snapshotInteractiveOnly:!0,snapshotCompact:!0,snapshotScope:i.trim()},result:{scope:i.trim()}})}t.push(n)}return t}(e)},s=function(e,t){let n=[],i=e.device.name.replace(/"/g,'\\"'),r=e.device.kind?` kind=${e.device.kind}`:"";for(let a of(n.push(`context platform=${e.device.platform} device="${i}"${r} theme=unknown`),t))a.flags?.noRecord||n.push(function(e){let t=[e.command];if("click"===e.command){let n=e.positionals?.[0];if(n){t.push(eC(n));let i=e.result?.refLabel;return"string"==typeof i&&i.trim().length>0&&t.push(eC(i)),t.join(" ")}}if("fill"===e.command){let n=e.positionals?.[0];if(n&&n.startsWith("@")){t.push(eC(n));let i=e.result?.refLabel,r=e.positionals.slice(1).join(" ");return"string"==typeof i&&i.trim().length>0&&t.push(eC(i)),r&&t.push(eC(r)),t.join(" ")}}if("get"===e.command){let n=e.positionals?.[0],i=e.positionals?.[1];if(n&&i){t.push(eC(n)),t.push(eC(i));let r=e.result?.refLabel;return"string"==typeof r&&r.trim().length>0&&t.push(eC(r)),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",eC(e.flags.snapshotScope)),e.flags?.snapshotRaw&&t.push("--raw"),e.flags?.snapshotBackend&&t.push("--backend",e.flags.snapshotBackend),t.join(" ");for(let n of e.positionals??[])t.push(eC(n));return t.join(" ")}(a));return`${n.join("\n")}
2
- `}(e,o.optimizedActions);u.writeFileSync(i,s),e.actions.some(e=>e.flags?.recordJson)&&u.writeFileSync(r,JSON.stringify(o,null,2))}catch{}}function eC(e){let t=e.trim();return t.startsWith("@")||/^-?\d+(\.\d+)?$/.test(t)?t:JSON.stringify(t)}function eR(e,t){let n=t.toLowerCase();return e.find(e=>{let t=(e.label??"").toLowerCase(),i=(e.value??"").toLowerCase(),r=(e.identifier??"").toLowerCase();return t.includes(n)||i.includes(n)||r.includes(n)})??null}function eP(e,t){let n=[e.label,e.value,e.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0);return n&&eT(n)?n:function(e,t){if(!e.rect)return;let n=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||!eT(t))continue;let r=Math.abs(e.rect.y+e.rect.height/2-n);(!i||r<i.distance)&&(i={label:t,distance:r})}return i?.label}(e,t)??(n&&eT(n)?n:void 0)}function eT(e){let t=e.trim();return!(!t||/^(true|false)$/i.test(t)||/^\d+$/.test(t))}(e=f.createServer(e=>{let t="";e.setEncoding("utf8"),e.on("data",async n=>{let i=(t+=n).indexOf("\n");for(;-1!==i;){let n,r=t.slice(0,i).trim();if(t=t.slice(i+1),0===r.length){i=t.indexOf("\n");continue}try{let e=JSON.parse(r);n=await e_(e)}catch(t){let e=l(t);n={ok:!1,error:{code:e.code,message:e.message,details:e.details}}}e.write(`${JSON.stringify(n)}
3
- `),i=t.indexOf("\n")}})})).listen(0,"127.0.0.1",()=>{let t=e.address();if("object"==typeof t&&t?.port){var n;n=t.port,u.existsSync(eb)||u.mkdirSync(eb,{recursive:!0}),u.writeFileSync(eA,""),u.writeFileSync(eI,JSON.stringify({port:n,token:eD,pid:process.pid,version:eO},null,2),{mode:384}),process.stdout.write(`AGENT_DEVICE_DAEMON_PORT=${t.port}
4
- `)}}),t=async()=>{for(let e of Array.from(ev.values()))"ios"===e.device.platform&&"simulator"===e.device.kind&&await et(e.device.id),ek(e);e.close(()=>{u.existsSync(eI)&&u.unlinkSync(eI),process.exit(0)})},process.on("SIGINT",()=>{t()}),process.on("SIGTERM",()=>{t()}),process.on("SIGHUP",()=>{t()}),process.on("uncaughtException",e=>{let n=e instanceof p?e:l(e);process.stderr.write(`Daemon error: ${n.message}
1
+ let e,t;import n from"node:crypto";import{isCancel as i,select as r}from"@clack/prompts";import{node_path as a,runCmdStreaming as o,promises as s,asAppError as l,fileURLToPath as c,node_fs as u,node_os as d,node_net as f,errors_AppError as p,runCmd as h,whichCmd as m}from"./861.js";async function w(e,t){let n=e,a=e=>e.toLowerCase().replace(/_/g," ").replace(/\s+/g," ").trim();if(t.platform&&(n=n.filter(e=>e.platform===t.platform)),t.udid){let e=n.find(e=>e.id===t.udid&&"ios"===e.platform);if(!e)throw new p("DEVICE_NOT_FOUND",`No iOS device with UDID ${t.udid}`);return e}if(t.serial){let e=n.find(e=>e.id===t.serial&&"android"===e.platform);if(!e)throw new p("DEVICE_NOT_FOUND",`No Android device with serial ${t.serial}`);return e}if(t.deviceName){let e=a(t.deviceName),i=n.find(t=>a(t.name)===e);if(!i)throw new p("DEVICE_NOT_FOUND",`No device named ${t.deviceName}`);return i}if(1===n.length)return n[0];if(0===n.length)throw new p("DEVICE_NOT_FOUND","No devices found",{selector:t});let o=n.filter(e=>e.booted);if(1===o.length)return o[0];if(!process.env.CI&&process.stdin.isTTY&&process.stdout.isTTY){let e=await r({message:"Multiple devices available. Choose a device to continue:",options:(o.length>0?o:n).map(e=>({label:`${e.name} (${e.platform}${e.kind?`, ${e.kind}`:""}${e.booted?", booted":""})`,value:e.id}))});if(i(e))throw new p("INVALID_ARGS","Device selection cancelled");if(e){let t=n.find(t=>t.id===e);if(t)return t}}return o[0]??n[0]}async function g(){if(!await m("adb"))throw new p("TOOL_MISSING","adb not found in PATH");let e=(await h("adb",["devices","-l"])).stdout.split("\n").map(e=>e.trim()),t=[];for(let n of e){if(!n||n.startsWith("List of devices"))continue;let e=n.split(/\s+/),i=e[0];if("device"!==e[1])continue;let r=(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&&(r=t.replace(/_/g," "))}let a=await y(i);t.push({platform:"android",id:i,name:r,kind:i.startsWith("emulator-")?"emulator":"device",booted:a})}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 N(e,t=6e4){let n=Date.now();for(;Date.now()-n<t;){if(await y(e))return;await new Promise(e=>setTimeout(e,1e3))}throw new p("COMMAND_FAILED","Android device did not finish booting in time",{serial:e,timeoutMs:t})}let v={settings:{type:"intent",value:"android.settings.SETTINGS"}};function b(e,t){return["-s",e.id,...t]}async function S(e,t){let n=t.trim();if(n.includes("."))return{type:"package",value:n};let i=v[n.toLowerCase()];if(i)return i;let r=(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(n.toLowerCase()));if(1===r.length)return{type:"package",value:r[0]};if(r.length>1)throw new p("INVALID_ARGS",`Multiple packages matched "${t}"`,{matches:r});throw new p("APP_NOT_INSTALLED",`No package found matching "${t}"`)}async function A(e,t){e.booted||await N(e.id);let n=await S(e,t);"intent"===n.type?await h("adb",b(e,["shell","am","start","-a",n.value])):await h("adb",b(e,["shell","monkey","-p",n.value,"-c","android.intent.category.LAUNCHER","1"]))}async function I(e){e.booted||await N(e.id)}async function D(e,t){if("settings"===t.trim().toLowerCase())return void await h("adb",b(e,["shell","am","force-stop","com.android.settings"]));let n=await S(e,t);if("intent"===n.type)throw new p("INVALID_ARGS","Close requires a package name, not an intent");await h("adb",b(e,["shell","am","force-stop",n.value]))}async function O(e,t,n){await h("adb",b(e,["shell","input","tap",String(t),String(n)]))}async function x(e,t,n,i=800){await h("adb",b(e,["shell","input","swipe",String(t),String(n),String(t),String(n),String(i)]))}async function E(e,t){let n=t.replace(/ /g,"%s");await h("adb",b(e,["shell","input","text",n]))}async function _(e,t,n){await O(e,t,n)}async function k(e,t,n,i){await _(e,t,n),await E(e,i)}async function C(e,t,n=.6){let{width:i,height:r}=await M(e),a=Math.floor(i*n),o=Math.floor(r*n),s=Math.floor(i/2),l=Math.floor(r/2),c=s,u=l,d=s,f=l;switch(t){case"up":u=l-Math.floor(o/2),f=l+Math.floor(o/2);break;case"down":u=l+Math.floor(o/2),f=l-Math.floor(o/2);break;case"left":c=s-Math.floor(a/2),d=s+Math.floor(a/2);break;case"right":c=s+Math.floor(a/2),d=s-Math.floor(a/2);break;default:throw new p("INVALID_ARGS",`Unknown direction: ${t}`)}await h("adb",b(e,["shell","input","swipe",String(c),String(u),String(d),String(f),"300"]))}async function R(e,t){for(let n=0;n<8;n+=1){let n="";try{n=await F(e)}catch(t){let e=t instanceof Error?t.message:String(t);throw new p("UNSUPPORTED_OPERATION",`uiautomator dump failed: ${e}`)}if(function(e,t){let n=t.toLowerCase(),i=/<node[^>]+>/g,r=i.exec(e);for(;r;){let t=r[0],a=/text="([^"]*)"/.exec(t),o=/content-desc="([^"]*)"/.exec(t),s=(a?.[1]??"").toLowerCase(),l=(o?.[1]??"").toLowerCase();if(s.includes(n)||l.includes(n)){let e=/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/.exec(t);if(e){let t=Number(e[1]),n=Number(e[2]);return{x:Math.floor((t+Number(e[3]))/2),y:Math.floor((n+Number(e[4]))/2)}}return{x:0,y:0}}r=i.exec(e)}return null}(n,t))return;await C(e,"down",.5)}throw new p("COMMAND_FAILED",`Could not find element containing "${t}" after scrolling`)}async function P(e,t){let n=await h("adb",b(e,["exec-out","screencap","-p"]),{binaryStdout:!0});if(!n.stdoutBuffer)throw new p("COMMAND_FAILED","Failed to capture screenshot");await s.writeFile(t,n.stdoutBuffer)}async function T(e,t={}){return function(e,t,n){let i=function(e){let t={type:null,label:null,value:null,identifier:null,depth:-1,children:[]},n=[t],i=/<node\b[^>]*>|<\/node>/g,r=i.exec(e);for(;r;){let t=r[0];if(t.startsWith("</node")){n.length>1&&n.pop(),r=i.exec(e);continue}let a=function(e){let t=t=>{let n=RegExp(`${t}="([^"]*)"`).exec(e);return n?n[1]:null},n=e=>{let n=t(e);if(null!==n)return"true"===n};return{text:t("text"),desc:t("content-desc"),resourceId:t("resource-id"),className:t("class"),bounds:t("bounds"),clickable:n("clickable"),enabled:n("enabled"),focusable:n("focusable")}}(t),o=function(e){if(!e)return;let t=/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/.exec(e);if(!t)return;let n=Number(t[1]),i=Number(t[2]);return{x:n,y:i,width:Math.max(0,Number(t[3])-n),height:Math.max(0,Number(t[4])-i)}}(a.bounds),s=n[n.length-1],l={type:a.className,label:a.text||a.desc,value:a.text,identifier:a.resourceId,rect:o,enabled:a.enabled,hittable:a.clickable??a.focusable,depth:s.depth+1,parentIndex:void 0,children:[]};s.children.push(l),t.endsWith("/>")||n.push(l),r=i.exec(e)}return t}(e),r=[],a=!1,o=n.depth??1/0,s=n.scope?function(e,t){let n=t.toLowerCase(),i=[...e.children];for(;i.length>0;){let e=i.shift(),t=e.label?.toLowerCase()??"",r=e.value?.toLowerCase()??"",a=e.identifier?.toLowerCase()??"";if(t.includes(n)||r.includes(n)||a.includes(n))return e;i.push(...e.children)}return null}(i,n.scope):null,l=s?[s]:i.children,c=(e,t)=>{if(r.length>=800){a=!0;return}if(!(t>o)){for(let i of((n.raw||function(e,t){if(t.interactiveOnly)return!!e.hittable;if(t.compact){let t=!!(e.label&&e.label.trim().length>0),n=!!(e.identifier&&e.identifier.trim().length>0);return t||n||!!e.hittable}return!0}(e,n))&&r.push({index:r.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),a)return}};for(let e of l)if(c(e,0),a)break;return a?{nodes:r,truncated:a}:{nodes:r}}(await F(e),800,t)}async function L(){if(!await m("adb"))throw new p("TOOL_MISSING","adb not found in PATH")}async function M(e){let t=(await h("adb",b(e,["shell","wm","size"]))).stdout.match(/Physical size:\s*(\d+)x(\d+)/);if(!t)throw new p("COMMAND_FAILED","Unable to read screen size");return{width:Number(t[1]),height:Number(t[2])}}async function F(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}async function j(){if("darwin"!==process.platform)throw new p("UNSUPPORTED_PLATFORM","iOS tools are only available on macOS");if(!await m("xcrun"))throw new p("TOOL_MISSING","xcrun not found in PATH");let e=[],t=await h("xcrun",["simctl","list","devices","-j"]);try{let n=JSON.parse(t.stdout);for(let t of Object.values(n.devices))for(let n of t)n.isAvailable&&e.push({platform:"ios",id:n.udid,name:n.name,kind:"simulator",booted:"Booted"===n.state})}catch(e){throw new p("COMMAND_FAILED","Failed to parse simctl devices JSON",void 0,e)}if(await m("xcrun"))try{let t=await h("xcrun",["devicectl","list","devices","--json"]);for(let n of JSON.parse(t.stdout).devices??[])n.platform?.toLowerCase().includes("ios")&&e.push({platform:"ios",id:n.identifier,name:n.name,kind:"device",booted:!0})}catch{}return e}let U={settings:"com.apple.Preferences"};async function V(e,t){let n=t.trim();if(n.includes("."))return n;let i=U[n.toLowerCase()];if(i)return i;if("simulator"===e.kind){let i=(await Q(e)).filter(e=>e.name.toLowerCase()===n.toLowerCase());if(1===i.length)return i[0].bundleId;if(i.length>1)throw new p("INVALID_ARGS",`Multiple apps matched "${t}"`,{matches:i})}throw new p("APP_NOT_INSTALLED",`No app found matching "${t}"`)}async function $(e,t){let n=await V(e,t);if("simulator"===e.kind){await ee(e),await h("open",["-a","Simulator"],{allowFailure:!0}),await h("xcrun",["simctl","launch",e.id,n]);return}await h("xcrun",["devicectl","device","process","launch","--device",e.id,n])}async function B(e){"simulator"!==e.kind||"Booted"!==await et(e.id)&&(await ee(e),await h("open",["-a","Simulator"],{allowFailure:!0}))}async function G(e,t){let n=await V(e,t);if("simulator"===e.kind){await ee(e);let t=await h("xcrun",["simctl","terminate",e.id,n],{allowFailure:!0});if(0!==t.exitCode){if(t.stderr.toLowerCase().includes("found nothing to terminate"))return;throw new p("COMMAND_FAILED",`xcrun exited with code ${t.exitCode}`,{cmd:"xcrun",args:["simctl","terminate",e.id,n],stdout:t.stdout,stderr:t.stderr,exitCode:t.exitCode})}return}await h("xcrun",["devicectl","device","process","terminate","--device",e.id,n])}async function J(e,t,n){throw K(e,"press"),new p("UNSUPPORTED_OPERATION","simctl io tap is not available; use the XCTest runner for input")}async function W(e,t,n,i=800){throw K(e,"long-press"),new p("UNSUPPORTED_OPERATION","long-press is not supported on iOS simulators without XCTest runner support")}async function q(e,t,n){await J(e,t,n)}async function X(e,t){throw K(e,"type"),new p("UNSUPPORTED_OPERATION","simctl io keyboard is not available; use the XCTest runner for input")}async function z(e,t,n,i){await q(e,t,n),await X(e,i)}async function H(e,t,n=.6){throw K(e,"scroll"),new p("UNSUPPORTED_OPERATION","simctl io swipe is not available; use the XCTest runner for input")}async function Y(e){throw new p("UNSUPPORTED_OPERATION",`scrollintoview is not supported on iOS without UI automation (${e})`)}async function Z(e,t){if("simulator"===e.kind){await ee(e),await h("xcrun",["simctl","io",e.id,"screenshot",t]);return}await h("xcrun",["devicectl","device","screenshot","--device",e.id,t])}function K(e,t){if("simulator"!==e.kind)throw new p("UNSUPPORTED_OPERATION",`${t} is only supported on iOS simulators in v1`)}async function Q(e){let t=(await h("xcrun",["simctl","listapps",e.id],{allowFailure:!0})).stdout;if(!t.trim().startsWith("{"))return[];try{let e=JSON.parse(t);return Object.entries(e).map(([e,t])=>({bundleId:e,name:t.CFBundleDisplayName??t.CFBundleName??e}))}catch{return[]}}async function ee(e){"simulator"!==e.kind||"Booted"!==await et(e.id)&&(await h("xcrun",["simctl","boot",e.id],{allowFailure:!0}),await h("xcrun",["simctl","bootstatus",e.id,"-b"],{allowFailure:!0}))}async function et(e){let t=await h("xcrun",["simctl","list","devices","-j"],{allowFailure:!0});if(0!==t.exitCode)return null;try{let n=JSON.parse(t.stdout);for(let t of Object.values(n.devices??{})){let n=t.find(t=>t.udid===e);if(n)return n.state}}catch{}return null}let en=new Map;async function ei(e,t,n={}){if("simulator"!==e.kind)throw new p("UNSUPPORTED_OPERATION","iOS runner only supports simulators in v1");try{let i=await eo(e,n),r=await eu(e,i.port,t,n.logPath),a=await r.text(),o={};try{o=JSON.parse(a)}catch{throw new p("COMMAND_FAILED","Invalid runner response",{text:a})}if(!o.ok)throw new p("COMMAND_FAILED",o.error?.message??"Runner error",{runner:o,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:n.logPath});return o.data??{}}catch(r){let i=r instanceof p?r:new p("COMMAND_FAILED",String(r));if("COMMAND_FAILED"===i.code&&"string"==typeof i.message&&i.message.includes("Runner did not accept connection")){await er(e.id);let i=await eo(e,n),r=await eu(e,i.port,t,n.logPath),a=await r.text(),o={};try{o=JSON.parse(a)}catch{throw new p("COMMAND_FAILED","Invalid runner response",{text:a})}if(!o.ok)throw new p("COMMAND_FAILED",o.error?.message??"Runner error",{runner:o,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:n.logPath});return o.data??{}}throw r}}async function er(e){let t=en.get(e);if(t){try{await eu(t.device,t.port,{command:"shutdown"})}catch{}try{await t.testPromise}catch{}em(t.xctestrunPath),em(t.jsonPath),en.delete(e)}}async function ea(e){await h("xcrun",["simctl","bootstatus",e,"-b"],{allowFailure:!0})}async function eo(e,t){let n=en.get(e.id);if(n)return n;await ea(e.id);let i=await es(e.id,t),r=await ef(),a=process.env.AGENT_DEVICE_RUNNER_TIMEOUT??"300",{xctestrunPath:s,jsonPath:l}=await eh(i,{AGENT_DEVICE_RUNNER_PORT:String(r),AGENT_DEVICE_RUNNER_TIMEOUT:a},`session-${e.id}-${r}`),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=>{ec(e,t.logPath,t.verbose)},onStderrChunk:e=>{ec(e,t.logPath,t.verbose)},allowFailure:!0,env:{...process.env,AGENT_DEVICE_RUNNER_PORT:String(r),AGENT_DEVICE_RUNNER_TIMEOUT:a}}),u={device:e,deviceId:e.id,port:r,xctestrunPath:s,jsonPath:l,testPromise:c};return en.set(e.id,u),u}async function es(e,t){let n,i=a.join(d.homedir(),".agent-device","ios-runner"),r=a.join(i,"derived");if((n=process.env.AGENT_DEVICE_IOS_CLEAN_DERIVED)&&["1","true","yes","on"].includes(n.toLowerCase()))try{u.rmSync(r,{recursive:!0,force:!0})}catch{}let s=el(r);if(s)return s;let l=function(){let e=a.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=a.join(t,"package.json");if(u.existsSync(e))return t;t=a.dirname(t)}return e}(),f=a.join(l,"ios-runner","AgentDeviceRunner","AgentDeviceRunner.xcodeproj");if(!u.existsSync(f))throw new p("COMMAND_FAILED","iOS runner project not found",{projectPath:f});try{await o("xcodebuild",["build-for-testing","-project",f,"-scheme","AgentDeviceRunner","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-destination",`platform=iOS Simulator,id=${e}`,"-derivedDataPath",r],{onStdoutChunk:e=>{ec(e,t.logPath,t.verbose)},onStderrChunk:e=>{ec(e,t.logPath,t.verbose)}})}catch(n){let e=n instanceof p?n:new p("COMMAND_FAILED",String(n));throw new p("COMMAND_FAILED","xcodebuild build-for-testing failed",{error:e.message,details:e.details,logPath:t.logPath})}let h=el(r);if(!h)throw new p("COMMAND_FAILED","Failed to locate .xctestrun after build");return h}function el(e){if(!u.existsSync(e))return null;let t=[],n=[e];for(;n.length>0;){let e=n.pop();for(let i of u.readdirSync(e,{withFileTypes:!0})){let r=a.join(e,i.name);if(i.isDirectory()){n.push(r);continue}if(i.isFile()&&i.name.endsWith(".xctestrun"))try{let e=u.statSync(r);t.push({path:r,mtimeMs:e.mtimeMs})}catch{}}}return 0===t.length?null:(t.sort((e,t)=>t.mtimeMs-e.mtimeMs),t[0]?.path??null)}function ec(e,t,n){t&&u.appendFileSync(t,e),n&&process.stderr.write(e)}async function eu(e,t,n,i){i&&await ep(i,4e3);let r=Date.now(),a=null;for(;Date.now()-r<8e3;)try{return await fetch(`http://127.0.0.1:${t}/command`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})}catch(e){a=e,await new Promise(e=>setTimeout(e,100))}if("simulator"===e.kind){let i=await ed(e.id,t,n);return new Response(i.body,{status:i.status})}let o=i?function(e){try{if(!u.existsSync(e))return null;let t=u.readFileSync(e,"utf8").match(/AGENT_DEVICE_RUNNER_PORT=(\d+)/);if(t)return Number(t[1])}catch{}return null}(i):null;if(o&&o!==t)try{return await fetch(`http://127.0.0.1:${o}/command`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})}catch(e){a=e}throw new p("COMMAND_FAILED","Runner did not accept connection",{port:t,fallbackPort:o,logPath:i,lastError:a?String(a):void 0})}async function ed(e,t,n){let i=JSON.stringify(n),r=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}),a=r.stdout;if(0!==r.exitCode)throw new p("COMMAND_FAILED","Runner did not accept connection (simctl spawn)",{port:t,stdout:r.stdout,stderr:r.stderr,exitCode:r.exitCode});return{status:200,body:a}}async function ef(){return await new Promise((e,t)=>{let n=f.createServer();n.listen(0,"127.0.0.1",()=>{let i=n.address();n.close(),"object"==typeof i&&i?.port?e(i.port):t(new p("COMMAND_FAILED","Failed to allocate port"))}),n.on("error",t)})}async function ep(e,t){if(!u.existsSync(e))return;let n=Date.now(),i=0;for(;Date.now()-n<t;){if(!u.existsSync(e))return;let t=u.statSync(e);if(t.size>i){let n=u.openSync(e,"r"),r=Buffer.alloc(t.size-i);u.readSync(n,r,0,r.length,i),u.closeSync(n),i=t.size;let a=r.toString("utf8");if(a.includes("AGENT_DEVICE_RUNNER_LISTENER_READY")||a.includes("AGENT_DEVICE_RUNNER_PORT="))return}await new Promise(e=>setTimeout(e,100))}}async function eh(e,t,n){let i,r=a.dirname(e),o=n.replace(/[^a-zA-Z0-9._-]/g,"_"),s=a.join(r,`AgentDeviceRunner.env.${o}.json`),l=a.join(r,`AgentDeviceRunner.env.${o}.xctestrun`),c=await h("plutil",["-convert","json","-o","-",e],{allowFailure:!0});if(0!==c.exitCode||!c.stdout.trim())throw new p("COMMAND_FAILED","Failed to read xctestrun plist",{xctestrunPath:e,stderr:c.stderr});try{i=JSON.parse(c.stdout)}catch(t){throw new p("COMMAND_FAILED","Failed to parse xctestrun JSON",{xctestrunPath:e,error:String(t)})}let d=e=>{e.EnvironmentVariables={...e.EnvironmentVariables??{},...t},e.UITestEnvironmentVariables={...e.UITestEnvironmentVariables??{},...t},e.UITargetAppEnvironmentVariables={...e.UITargetAppEnvironmentVariables??{},...t},e.TestingEnvironmentVariables={...e.TestingEnvironmentVariables??{},...t}},f=i.TestConfigurations;if(Array.isArray(f))for(let e of f){if(!e||"object"!=typeof e)continue;let t=e.TestTargets;if(Array.isArray(t))for(let e of t)e&&"object"==typeof e&&d(e)}for(let[e,t]of Object.entries(i))t&&"object"==typeof t&&t.TestBundlePath&&(d(t),i[e]=t);u.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 p("COMMAND_FAILED","Failed to write xctestrun plist",{tmpXctestrunPath:l,stderr:m.stderr});return{xctestrunPath:l,jsonPath:s}}function em(e){try{u.existsSync(e)&&u.unlinkSync(e)}catch{}}async function ew(e){let t,n;if("ios"!==e.platform||"simulator"!==e.kind)throw new p("UNSUPPORTED_OPERATION","AX snapshot is only supported on iOS simulators");let i=await eg(),r=await h(i,[],{allowFailure:!0});if(0!==r.exitCode){let e=(r.stderr??"").toString(),t="";throw e.toLowerCase().includes("accessibility permission")&&(t=" Enable Accessibility for your terminal in System Settings > Privacy & Security > Accessibility, or use --backend xctest (slower snapshots via XCTest)."),new p("COMMAND_FAILED","AX snapshot failed",{stderr:`${e}${t}`,stdout:r.stdout})}try{let e=JSON.parse(r.stdout);if(e&&"object"==typeof e&&"root"in e){if(!e.root)throw Error("AX snapshot missing root");t=e.root,n=e.windowFrame??void 0}else t=e}catch(e){throw new p("COMMAND_FAILED","Invalid AX snapshot JSON",{error:String(e)})}let a=t.frame??n,o=[],s=[],l=(e,t)=>{e.frame&&o.push(e.frame);let n=e.frame&&a?{x:e.frame.x-a.x,y:e.frame.y-a.y,width:e.frame.width,height:e.frame.height}:e.frame;for(let i of(s.push({...e,frame:n,children:void 0,depth:t}),e.children??[]))l(i,t+1)};return l(t,0),{nodes:(function(e,t,n){if(!t||0===n.length)return e;let i=1/0,r=1/0;for(let e of n)e.x<i&&(i=e.x),e.y<r&&(r=e.y);return i<=5&&r<=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})(s,a,o).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 eg(){let e=function(){let e=process.cwd();for(let t=0;t<6;t+=1){let t=a.join(e,"package.json");if(u.existsSync(t))return e;e=a.dirname(e)}return process.cwd()}(),t=a.join(e,"ios-runner","AXSnapshot"),n=process.env.AGENT_DEVICE_AX_BINARY;if(n&&u.existsSync(n))return n;for(let t of[a.join(e,"bin","axsnapshot"),a.join(e,"dist","bin","axsnapshot"),a.join(e,"dist","axsnapshot")])if(u.existsSync(t))return t;let i=a.join(t,".build","release","axsnapshot");if(u.existsSync(i))return i;let r=await h("swift",["build","-c","release"],{cwd:t,allowFailure:!0});if(0!==r.exitCode||!u.existsSync(i))throw new p("COMMAND_FAILED","Failed to build AX snapshot tool",{stderr:r.stderr,stdout:r.stdout});return i}async function ey(e){let t={platform:e.platform,deviceName:e.device,udid:e.udid,serial:e.serial};if("android"===t.platform){await L();let e=await g();return await w(e,t)}if("ios"===t.platform){let e=await j();return await w(e,t)}let n=[];try{n.push(...await g())}catch{}try{n.push(...await j())}catch{}return await w(n,t)}async function eN(e,t,n,i,r){let a=function(e){switch(e.platform){case"android":return{open:t=>A(e,t),openDevice:()=>I(e),close:t=>D(e,t),tap:(t,n)=>O(e,t,n),longPress:(t,n,i)=>x(e,t,n,i),focus:(t,n)=>_(e,t,n),type:t=>E(e,t),fill:(t,n,i)=>k(e,t,n,i),scroll:(t,n)=>C(e,t,n),scrollIntoView:t=>R(e,t),screenshot:t=>P(e,t)};case"ios":return{open:t=>$(e,t),openDevice:()=>B(e),close:t=>G(e,t),tap:(t,n)=>J(e,t,n),longPress:(t,n,i)=>W(e,t,n,i),focus:(t,n)=>q(e,t,n),type:t=>X(e,t),fill:(t,n,i)=>z(e,t,n,i),scroll:(t,n)=>H(e,t,n),scrollIntoView:e=>Y(e),screenshot:t=>Z(e,t)};default:throw new p("UNSUPPORTED_PLATFORM",`Unsupported platform: ${e.platform}`)}}(e);switch(t){case"open":{let e=n[0];if(!e)return await a.openDevice(),{app:null};return await a.open(e),{app:e}}case"close":{let e=n[0];if(!e)return{closed:"session"};return await a.close(e),{app:e}}case"press":{let[t,i]=n.map(Number);if(Number.isNaN(t)||Number.isNaN(i))throw new p("INVALID_ARGS","press requires x y");return"ios"===e.platform&&"simulator"===e.kind?await ei(e,{command:"tap",x:t,y:i,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath}):await a.tap(t,i),{x:t,y:i}}case"long-press":{let e=Number(n[0]),t=Number(n[1]),i=n[2]?Number(n[2]):void 0;if(Number.isNaN(e)||Number.isNaN(t))throw new p("INVALID_ARGS","long-press requires x y [durationMs]");return await a.longPress(e,t,i),{x:e,y:t,durationMs:i}}case"focus":{let[t,i]=n.map(Number);if(Number.isNaN(t)||Number.isNaN(i))throw new p("INVALID_ARGS","focus requires x y");return"ios"===e.platform&&"simulator"===e.kind?await ei(e,{command:"tap",x:t,y:i,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath}):await a.focus(t,i),{x:t,y:i}}case"type":{let t=n.join(" ");if(!t)throw new p("INVALID_ARGS","type requires text");return"ios"===e.platform&&"simulator"===e.kind?await ei(e,{command:"type",text:t,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath}):await a.type(t),{text:t}}case"fill":{let t=Number(n[0]),i=Number(n[1]),o=n.slice(2).join(" ");if(Number.isNaN(t)||Number.isNaN(i)||!o)throw new p("INVALID_ARGS","fill requires x y text");return"ios"===e.platform&&"simulator"===e.kind?(await ei(e,{command:"tap",x:t,y:i,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath}),await ei(e,{command:"type",text:o,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath})):await a.fill(t,i,o),{x:t,y:i,text:o}}case"scroll":{let t=n[0],i=n[1]?Number(n[1]):void 0;if(!t)throw new p("INVALID_ARGS","scroll requires direction");if("ios"===e.platform&&"simulator"===e.kind){if(!["up","down","left","right"].includes(t))throw new p("INVALID_ARGS",`Unknown direction: ${t}`);let n=function(e){switch(e){case"up":return"down";case"down":return"up";case"left":return"right";case"right":return"left"}}(t);await ei(e,{command:"swipe",direction:n,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath})}else await a.scroll(t,i);return{direction:t,amount:i}}case"scrollintoview":{let e=n.join(" ");if(!e)throw new p("INVALID_ARGS","scrollintoview requires text");return await a.scrollIntoView(e),{text:e}}case"screenshot":{let e=i??`./screenshot-${Date.now()}.png`;return await a.screenshot(e),{path:e}}case"snapshot":{let t=r?.snapshotBackend??"ax";if("ios"===e.platform){if("simulator"!==e.kind)throw new p("UNSUPPORTED_OPERATION","snapshot is only supported on iOS simulators in v1");if("ax"===t)return{nodes:(await ew(e)).nodes??[],truncated:!1,backend:"ax"};let n=await ei(e,{command:"snapshot",appBundleId:r?.appBundleId,interactiveOnly:r?.snapshotInteractiveOnly,compact:r?.snapshotCompact,depth:r?.snapshotDepth,scope:r?.snapshotScope,raw:r?.snapshotRaw},{verbose:r?.verbose,logPath:r?.logPath});return{nodes:n.nodes??[],truncated:n.truncated??!1,backend:"xctest"}}let n=await T(e,{interactiveOnly:r?.snapshotInteractiveOnly,compact:r?.snapshotCompact,depth:r?.snapshotDepth,scope:r?.snapshotScope,raw:r?.snapshotRaw});return{nodes:n.nodes??[],truncated:n.truncated??!1,backend:"android"}}default:throw new p("INVALID_ARGS",`Unknown command: ${t}`)}}function ev(e){let t=e.trim();return t.startsWith("@")?t.slice(1)||null:t.startsWith("e")?t:null}function eb(e,t){return e.find(e=>e.ref===t)??null}function eS(e){return{x:Math.round(e.x+e.width/2),y:Math.round(e.y+e.height/2)}}let eA=new Map,eI=a.join(d.homedir(),".agent-device"),eD=a.join(eI,"daemon.json"),eO=a.join(eI,"daemon.log"),ex=a.join(eI,"sessions"),eE=function(){try{let e=function(){let e=a.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=a.join(t,"package.json");if(u.existsSync(e))return t;t=a.dirname(t)}return e}();return JSON.parse(u.readFileSync(a.join(e,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}(),e_=n.randomBytes(24).toString("hex");function ek(e,t){return{appBundleId:t,verbose:e?.verbose,logPath:eO,snapshotInteractiveOnly:e?.snapshotInteractiveOnly,snapshotCompact:e?.snapshotCompact,snapshotDepth:e?.snapshotDepth,snapshotScope:e?.snapshotScope,snapshotRaw:e?.snapshotRaw,snapshotBackend:e?.snapshotBackend}}async function eC(e){if(e.token!==e_)return{ok:!1,error:{code:"UNAUTHORIZED",message:"Invalid token"}};let t=e.command,n=e.session||"default";if("session_list"===t)return{ok:!0,data:{sessions:Array.from(eA.values()).map(e=>({name:e.name,platform:e.device.platform,device:e.device.name,id:e.device.id,createdAt:e.createdAt}))}};if("open"===t){let i;if(eA.has(n))return{ok:!1,error:{code:"INVALID_ARGS",message:"Session already active. Close it first or pass a new --session name."}};let r=await ey(e.flags??{});await ej(r);let a=Array.from(eA.values()).find(e=>e.device.id===r.id);if(a)return{ok:!1,error:{code:"DEVICE_IN_USE",message:`Device is already in use by session "${a.name}".`,details:{session:a.name,deviceId:r.id,deviceName:r.name}}};let o=e.positionals?.[0];if("ios"===r.platform)try{let{resolveIosApp:t}=await Promise.resolve().then(()=>({resolveIosApp:V}));i=await t(r,e.positionals?.[0]??"")}catch{i=void 0}await eN(r,"open",e.positionals??[],e.flags?.out,{...ek(e.flags,i)});let s={name:n,device:r,createdAt:Date.now(),appBundleId:i,appName:o,actions:[]};return eR(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:n}}),eA.set(n,s),{ok:!0,data:{session:n}}}if("replay"===t){let t=e.positionals?.[0];if(!t)return{ok:!1,error:{code:"INVALID_ARGS",message:"replay requires a path"}};try{var i;let e=(i=t).startsWith("~/")?a.join(d.homedir(),i.slice(2)):a.resolve(i),r=JSON.parse(u.readFileSync(e,"utf8")),o=r.optimizedActions??r.actions??[];for(let e of o)e&&"replay"!==e.command&&await eC({token:e_,session:n,command:e.command,positionals:e.positionals??[],flags:e.flags??{}});return{ok:!0,data:{replayed:o.length,session:n}}}catch(t){let e=l(t);return{ok:!1,error:{code:e.code,message:e.message}}}}if("close"===t){let i=eA.get(n);return i?(e.positionals&&e.positionals.length>0&&await eN(i.device,"close",e.positionals??[],e.flags?.out,{...ek(e.flags,i.appBundleId)}),"ios"===i.device.platform&&"simulator"===i.device.kind&&await er(i.device.id),eR(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:n}}),eP(i),eA.delete(n),{ok:!0,data:{session:n}}):{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session"}}}if("snapshot"===t){let i=eA.get(n),r=i?.device??await ey(e.flags??{});i||await ej(r);let a=i?.appBundleId,o=await eN(r,"snapshot",[],e.flags?.out,{...ek(e.flags,a)}),s=(function(e){let t=[],n=[];for(let i of e){let e=i.depth??0;for(;t.length>0&&e<=t[t.length-1];)t.pop();let r=function(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase();return t.startsWith("ax")&&(t=t.replace(/^ax/,"")),t}(i.type??"");if("group"===r||"ioscontentgroup"===r){t.push(e);continue}let a=Math.max(0,e-t.length);n.push({...i,depth:a})}return n})(o?.nodes??[]).map((e,t)=>({...e,ref:`e${t+1}`})),l={nodes:s,truncated:o?.truncated,createdAt:Date.now(),backend:o?.backend},c={name:n,device:r,createdAt:i?.createdAt??Date.now(),appBundleId:a,snapshot:l,actions:i?.actions??[]};return eR(c,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{nodes:s.length,truncated:o?.truncated??!1}}),eA.set(n,c),{ok:!0,data:{nodes:s,truncated:o?.truncated??!1,appName:i?.appName??a??r.name,appBundleId:a}}}if("click"===t){let i=eA.get(n);if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let r=e.positionals?.[0]??"",a=ev(r);if(!a)return{ok:!1,error:{code:"INVALID_ARGS",message:"click requires a ref like @e2"}};let o=eb(i.snapshot.nodes,a);if(!o?.rect&&e.positionals.length>1){let t=e.positionals.slice(1).join(" ").trim();t.length>0&&(o=eL(i.snapshot.nodes,t))}if(!o?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${r} not found or has no bounds`}};let s=eM(o,i.snapshot.nodes),l=o.label?.trim();if("ios"===i.device.platform&&"simulator"===i.device.kind&&l&&function(e,t){let n=t.trim().toLowerCase();if(!n)return!1;let i=0;for(let t of e)if((t.label??"").trim().toLowerCase()===n&&(i+=1)>1)return!1;return 1===i}(i.snapshot.nodes,l))return await ei(i.device,{command:"tap",text:l,appBundleId:i.appBundleId},{verbose:e.flags?.verbose,logPath:eO}),eR(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:a,refLabel:l,mode:"text"}}),{ok:!0,data:{ref:a,mode:"text"}};let{x:c,y:u}=eS(o.rect);return await eN(i.device,"press",[String(c),String(u)],e.flags?.out,{...ek(e.flags,i.appBundleId)}),eR(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:a,x:c,y:u,refLabel:s}}),{ok:!0,data:{ref:a,x:c,y:u}}}if("fill"===t){let i=eA.get(n);if(e.positionals?.[0]?.startsWith("@")){if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let n=ev(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]:"",a=e.positionals.length>=3?e.positionals.slice(2).join(" "):e.positionals.slice(1).join(" ");if(!a)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires text after ref"}};let o=eb(i.snapshot.nodes,n);if(!o?.rect&&r&&(o=eL(i.snapshot.nodes,r)),!o?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${e.positionals[0]} not found or has no bounds`}};let s=eM(o,i.snapshot.nodes),{x:l,y:c}=eS(o.rect),u=await eN(i.device,"fill",[String(l),String(c),a],e.flags?.out,{...ek(e.flags,i.appBundleId)});return eR(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:u??{ref:n,x:l,y:c,refLabel:s}}),{ok:!0,data:u??{ref:n,x:l,y:c}}}}if("get"===t){let i=e.positionals?.[0],r=e.positionals?.[1];if("text"!==i&&"attrs"!==i)return{ok:!1,error:{code:"INVALID_ARGS",message:"get only supports text or attrs"}};let a=eA.get(n);if(!a?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let o=ev(r??"");if(!o)return{ok:!1,error:{code:"INVALID_ARGS",message:"get text requires a ref like @e2"}};let s=eb(a.snapshot.nodes,o);if(!s&&e.positionals.length>2){let t=e.positionals.slice(2).join(" ").trim();t.length>0&&(s=eL(a.snapshot.nodes,t))}if(!s)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${r} not found`}};if("attrs"===i)return eR(a,{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 eR(a,{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}}}if("rect"===t){let i=eA.get(n);if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let r=ev(e.positionals?.[0]??""),a="";if(r){let e=eb(i.snapshot.nodes,r);a=e?.label?.trim()??""}else a=e.positionals.join(" ").trim();if(!a)return{ok:!1,error:{code:"INVALID_ARGS",message:"rect requires a label or ref with label"}};if("ios"!==i.device.platform||"simulator"!==i.device.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"rect is only supported on iOS simulators"}};let o=await ei(i.device,{command:"rect",text:a,appBundleId:i.appBundleId},{verbose:e.flags?.verbose,logPath:eO});return eR(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{label:a,rect:o?.rect}}),{ok:!0,data:{label:a,rect:o?.rect}}}let r=eA.get(n);if(!r)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session. Run open first."}};let o=await eN(r.device,t,e.positionals??[],e.flags?.out,{...ek(e.flags,r.appBundleId)});return eR(r,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:o??{}}),{ok:!0,data:o??{}}}function eR(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:n,udid:i,serial:r,out:a,verbose:o,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:u,snapshotRaw:d,snapshotBackend:f,noRecord:p,recordJson:h}=e;return{platform:t,device:n,udid:i,serial:r,out:a,verbose:o,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:u,snapshotRaw:d,snapshotBackend:f,noRecord:p,recordJson:h}}(t.flags),result:t.result})}function eP(e){try{u.existsSync(ex)||u.mkdirSync(ex,{recursive:!0});let t=e.name.replace(/[^a-zA-Z0-9._-]/g,"_"),n=new Date(e.createdAt).toISOString().replace(/[:.]/g,"-"),i=a.join(ex,`${t}-${n}.ad`),r=a.join(ex,`${t}-${n}.json`),o={name:e.name,device:e.device,createdAt:e.createdAt,appBundleId:e.appBundleId,actions:e.actions,optimizedActions:function(e){let t=[];for(let n of e.actions)if("snapshot"!==n.command){if("click"===n.command||"fill"===n.command||"get"===n.command){let i=n.result?.refLabel;"string"==typeof i&&i.trim().length>0&&t.push({ts:n.ts,command:"snapshot",positionals:[],flags:{platform:e.device.platform,snapshotInteractiveOnly:!0,snapshotCompact:!0,snapshotScope:i.trim()},result:{scope:i.trim()}})}t.push(n)}return t}(e)},s=function(e,t){let n=[],i=e.device.name.replace(/"/g,'\\"'),r=e.device.kind?` kind=${e.device.kind}`:"";for(let a of(n.push(`context platform=${e.device.platform} device="${i}"${r} theme=unknown`),t))a.flags?.noRecord||n.push(function(e){let t=[e.command];if("click"===e.command){let n=e.positionals?.[0];if(n){t.push(eT(n));let i=e.result?.refLabel;return"string"==typeof i&&i.trim().length>0&&t.push(eT(i)),t.join(" ")}}if("fill"===e.command){let n=e.positionals?.[0];if(n&&n.startsWith("@")){t.push(eT(n));let i=e.result?.refLabel,r=e.positionals.slice(1).join(" ");return"string"==typeof i&&i.trim().length>0&&t.push(eT(i)),r&&t.push(eT(r)),t.join(" ")}}if("get"===e.command){let n=e.positionals?.[0],i=e.positionals?.[1];if(n&&i){t.push(eT(n)),t.push(eT(i));let r=e.result?.refLabel;return"string"==typeof r&&r.trim().length>0&&t.push(eT(r)),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",eT(e.flags.snapshotScope)),e.flags?.snapshotRaw&&t.push("--raw"),e.flags?.snapshotBackend&&t.push("--backend",e.flags.snapshotBackend),t.join(" ");for(let n of e.positionals??[])t.push(eT(n));return t.join(" ")}(a));return`${n.join("\n")}
2
+ `}(e,o.optimizedActions);u.writeFileSync(i,s),e.actions.some(e=>e.flags?.recordJson)&&u.writeFileSync(r,JSON.stringify(o,null,2))}catch{}}function eT(e){let t=e.trim();return t.startsWith("@")||/^-?\d+(\.\d+)?$/.test(t)?t:JSON.stringify(t)}function eL(e,t){let n=t.toLowerCase();return e.find(e=>{let t=(e.label??"").toLowerCase(),i=(e.value??"").toLowerCase(),r=(e.identifier??"").toLowerCase();return t.includes(n)||i.includes(n)||r.includes(n)})??null}function eM(e,t){let n=[e.label,e.value,e.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0);return n&&eF(n)?n:function(e,t){if(!e.rect)return;let n=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||!eF(t))continue;let r=Math.abs(e.rect.y+e.rect.height/2-n);(!i||r<i.distance)&&(i={label:t,distance:r})}return i?.label}(e,t)??(n&&eF(n)?n:void 0)}function eF(e){let t=e.trim();return!(!t||/^(true|false)$/i.test(t)||/^\d+$/.test(t))}async function ej(e){if("ios"===e.platform&&"simulator"===e.kind){let{ensureBootedSimulator:t}=await Promise.resolve().then(()=>({ensureBootedSimulator:ee}));await t(e);return}if("android"===e.platform){let{waitForAndroidBoot:t}=await Promise.resolve().then(()=>({waitForAndroidBoot:N}));await t(e.id)}}(e=f.createServer(e=>{let t="";e.setEncoding("utf8"),e.on("data",async n=>{let i=(t+=n).indexOf("\n");for(;-1!==i;){let n,r=t.slice(0,i).trim();if(t=t.slice(i+1),0===r.length){i=t.indexOf("\n");continue}try{let e=JSON.parse(r);n=await eC(e)}catch(t){let e=l(t);n={ok:!1,error:{code:e.code,message:e.message,details:e.details}}}e.write(`${JSON.stringify(n)}
3
+ `),i=t.indexOf("\n")}})})).listen(0,"127.0.0.1",()=>{let t=e.address();if("object"==typeof t&&t?.port){var n;n=t.port,u.existsSync(eI)||u.mkdirSync(eI,{recursive:!0}),u.writeFileSync(eO,""),u.writeFileSync(eD,JSON.stringify({port:n,token:e_,pid:process.pid,version:eE},null,2),{mode:384}),process.stdout.write(`AGENT_DEVICE_DAEMON_PORT=${t.port}
4
+ `)}}),t=async()=>{for(let e of Array.from(eA.values()))"ios"===e.device.platform&&"simulator"===e.device.kind&&await er(e.device.id),eP(e);e.close(()=>{u.existsSync(eD)&&u.unlinkSync(eD),process.exit(0)})},process.on("SIGINT",()=>{t()}),process.on("SIGTERM",()=>{t()}),process.on("SIGHUP",()=>{t()}),process.on("uncaughtException",e=>{let n=e instanceof p?e:l(e);process.stderr.write(`Daemon error: ${n.message}
5
5
  `),t()});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-device",
3
- "version": "0.1.0",
3
+ "version": "0.1.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",
@@ -20,6 +20,7 @@
20
20
  "build:clis": "pnpm build:node && pnpm build:axsnapshot",
21
21
  "format": "prettier --write .",
22
22
  "prepublishOnly": "pnpm build:node && pnpm build:axsnapshot",
23
+ "prepack": "pnpm build:node && pnpm build:axsnapshot",
23
24
  "typecheck": "tsc -p tsconfig.json",
24
25
  "test": "node --test",
25
26
  "test:smoke": "node --test test/smoke/*.test.ts",
@@ -72,13 +72,17 @@ export async function dispatchCommand(
72
72
  snapshotDepth?: number;
73
73
  snapshotScope?: string;
74
74
  snapshotRaw?: boolean;
75
+ snapshotBackend?: 'ax' | 'xctest';
75
76
  },
76
77
  ): Promise<Record<string, unknown> | void> {
77
78
  const interactor = getInteractor(device);
78
79
  switch (command) {
79
80
  case 'open': {
80
81
  const app = positionals[0];
81
- if (!app) throw new AppError('INVALID_ARGS', 'open requires an app name or bundle/package id');
82
+ if (!app) {
83
+ await interactor.openDevice();
84
+ return { app: null };
85
+ }
82
86
  await interactor.open(app);
83
87
  return { app };
84
88
  }
@@ -205,14 +209,8 @@ export async function dispatchCommand(
205
209
  );
206
210
  }
207
211
  if (backend === 'ax') {
208
- try {
209
- const ax = await snapshotAx(device);
210
- return { nodes: ax.nodes ?? [], truncated: false, backend: 'ax', rootRect: ax.rootRect };
211
- } catch (err) {
212
- if (context?.snapshotBackend === 'ax') {
213
- throw err;
214
- }
215
- }
212
+ const ax = await snapshotAx(device);
213
+ return { nodes: ax.nodes ?? [], truncated: false, backend: 'ax' };
216
214
  }
217
215
  const result = (await runIosRunnerCommand(
218
216
  device,
package/src/daemon.ts CHANGED
@@ -76,6 +76,7 @@ function contextFromFlags(
76
76
  snapshotDepth?: number;
77
77
  snapshotScope?: string;
78
78
  snapshotBackend?: 'ax' | 'xctest';
79
+ snapshotRaw?: boolean;
79
80
  } {
80
81
  return {
81
82
  appBundleId,
@@ -112,7 +113,28 @@ async function handleRequest(req: DaemonRequest): Promise<DaemonResponse> {
112
113
  }
113
114
 
114
115
  if (command === 'open') {
116
+ if (sessions.has(sessionName)) {
117
+ return {
118
+ ok: false,
119
+ error: {
120
+ code: 'INVALID_ARGS',
121
+ message: 'Session already active. Close it first or pass a new --session name.',
122
+ },
123
+ };
124
+ }
115
125
  const device = await resolveTargetDevice(req.flags ?? {});
126
+ await ensureDeviceReady(device);
127
+ const inUse = Array.from(sessions.values()).find((s) => s.device.id === device.id);
128
+ if (inUse) {
129
+ return {
130
+ ok: false,
131
+ error: {
132
+ code: 'DEVICE_IN_USE',
133
+ message: `Device is already in use by session "${inUse.name}".`,
134
+ details: { session: inUse.name, deviceId: device.id, deviceName: device.name },
135
+ },
136
+ };
137
+ }
116
138
  let appBundleId: string | undefined;
117
139
  const appName = req.positionals?.[0];
118
140
  if (device.platform === 'ios') {
@@ -200,6 +222,9 @@ async function handleRequest(req: DaemonRequest): Promise<DaemonResponse> {
200
222
  if (command === 'snapshot') {
201
223
  const session = sessions.get(sessionName);
202
224
  const device = session?.device ?? (await resolveTargetDevice(req.flags ?? {}));
225
+ if (!session) {
226
+ await ensureDeviceReady(device);
227
+ }
203
228
  const appBundleId = session?.appBundleId;
204
229
  const data = (await dispatchCommand(device, 'snapshot', [], req.flags?.out, {
205
230
  ...contextFromFlags(req.flags, appBundleId),
@@ -207,7 +232,6 @@ async function handleRequest(req: DaemonRequest): Promise<DaemonResponse> {
207
232
  nodes?: RawSnapshotNode[];
208
233
  truncated?: boolean;
209
234
  backend?: 'ax' | 'xctest' | 'android';
210
- rootRect?: { width: number; height: number };
211
235
  };
212
236
  const pruned = pruneGroupNodes(data?.nodes ?? []);
213
237
  const nodes = attachRefs(pruned);
@@ -778,6 +802,18 @@ function findNearestMeaningfulLabel(
778
802
  return best?.label;
779
803
  }
780
804
 
805
+ async function ensureDeviceReady(device: DeviceInfo): Promise<void> {
806
+ if (device.platform === 'ios' && device.kind === 'simulator') {
807
+ const { ensureBootedSimulator } = await import('./platforms/ios/index.ts');
808
+ await ensureBootedSimulator(device);
809
+ return;
810
+ }
811
+ if (device.platform === 'android') {
812
+ const { waitForAndroidBoot } = await import('./platforms/android/devices.ts');
813
+ await waitForAndroidBoot(device.id);
814
+ }
815
+ }
816
+
781
817
  function isLabelUnique(nodes: SnapshotState['nodes'], label: string): boolean {
782
818
  const target = label.trim().toLowerCase();
783
819
  if (!target) return false;
@@ -47,7 +47,7 @@ export async function listAndroidDevices(): Promise<DeviceInfo[]> {
47
47
  return devices;
48
48
  }
49
49
 
50
- async function isAndroidBooted(serial: string): Promise<boolean> {
50
+ export async function isAndroidBooted(serial: string): Promise<boolean> {
51
51
  try {
52
52
  const result = await runCmd('adb', ['-s', serial, 'shell', 'getprop', 'sys.boot_completed'], {
53
53
  allowFailure: true,
@@ -57,3 +57,15 @@ async function isAndroidBooted(serial: string): Promise<boolean> {
57
57
  return false;
58
58
  }
59
59
  }
60
+
61
+ export async function waitForAndroidBoot(serial: string, timeoutMs = 60000): Promise<void> {
62
+ const start = Date.now();
63
+ while (Date.now() - start < timeoutMs) {
64
+ if (await isAndroidBooted(serial)) return;
65
+ await new Promise((resolve) => setTimeout(resolve, 1000));
66
+ }
67
+ throw new AppError('COMMAND_FAILED', 'Android device did not finish booting in time', {
68
+ serial,
69
+ timeoutMs,
70
+ });
71
+ }
@@ -3,6 +3,7 @@ import { runCmd, whichCmd } from '../../utils/exec.ts';
3
3
  import { AppError } from '../../utils/errors.ts';
4
4
  import type { DeviceInfo } from '../../utils/device.ts';
5
5
  import type { RawSnapshotNode, Rect, SnapshotOptions } from '../../utils/snapshot.ts';
6
+ import { waitForAndroidBoot } from './devices.ts';
6
7
 
7
8
  const ALIASES: Record<string, { type: 'intent' | 'package'; value: string }> = {
8
9
  settings: { type: 'intent', value: 'android.settings.SETTINGS' },
@@ -43,6 +44,9 @@ export async function resolveAndroidApp(
43
44
  }
44
45
 
45
46
  export async function openAndroidApp(device: DeviceInfo, app: string): Promise<void> {
47
+ if (!device.booted) {
48
+ await waitForAndroidBoot(device.id);
49
+ }
46
50
  const resolved = await resolveAndroidApp(device, app);
47
51
  if (resolved.type === 'intent') {
48
52
  await runCmd('adb', adbArgs(device, ['shell', 'am', 'start', '-a', resolved.value]));
@@ -62,6 +66,12 @@ export async function openAndroidApp(device: DeviceInfo, app: string): Promise<v
62
66
  );
63
67
  }
64
68
 
69
+ export async function openAndroidDevice(device: DeviceInfo): Promise<void> {
70
+ if (!device.booted) {
71
+ await waitForAndroidBoot(device.id);
72
+ }
73
+ }
74
+
65
75
  export async function closeAndroidApp(device: DeviceInfo, app: string): Promise<void> {
66
76
  const trimmed = app.trim();
67
77
  if (trimmed.toLowerCase() === 'settings') {
@@ -18,15 +18,22 @@ type AXNode = {
18
18
 
19
19
  export async function snapshotAx(
20
20
  device: DeviceInfo,
21
- ): Promise<{ nodes: RawSnapshotNode[]; rootRect?: AXFrame }> {
21
+ ): Promise<{ nodes: RawSnapshotNode[] }> {
22
22
  if (device.platform !== 'ios' || device.kind !== 'simulator') {
23
23
  throw new AppError('UNSUPPORTED_OPERATION', 'AX snapshot is only supported on iOS simulators');
24
24
  }
25
25
  const binary = await ensureAxSnapshotBinary();
26
26
  const result = await runCmd(binary, [], { allowFailure: true });
27
27
  if (result.exitCode !== 0) {
28
+ const stderrText = (result.stderr ?? '').toString();
29
+ let hint = '';
30
+ if (stderrText.toLowerCase().includes('accessibility permission')) {
31
+ hint =
32
+ ' Enable Accessibility for your terminal in System Settings > Privacy & Security > Accessibility, ' +
33
+ 'or use --backend xctest (slower snapshots via XCTest).';
34
+ }
28
35
  throw new AppError('COMMAND_FAILED', 'AX snapshot failed', {
29
- stderr: result.stderr,
36
+ stderr: `${stderrText}${hint}`,
30
37
  stdout: result.stdout,
31
38
  });
32
39
  }
@@ -83,7 +90,7 @@ export async function snapshotAx(
83
90
  : undefined,
84
91
  depth: node.depth,
85
92
  }));
86
- return { nodes: mapped, rootRect: rootFrame };
93
+ return { nodes: mapped };
87
94
  }
88
95
 
89
96
  function normalizeFrames(
@@ -29,6 +29,7 @@ export async function openIosApp(device: DeviceInfo, app: string): Promise<void>
29
29
  const bundleId = await resolveIosApp(device, app);
30
30
  if (device.kind === 'simulator') {
31
31
  await ensureBootedSimulator(device);
32
+ await runCmd('open', ['-a', 'Simulator'], { allowFailure: true });
32
33
  await runCmd('xcrun', ['simctl', 'launch', device.id, bundleId]);
33
34
  return;
34
35
  }
@@ -43,6 +44,14 @@ export async function openIosApp(device: DeviceInfo, app: string): Promise<void>
43
44
  ]);
44
45
  }
45
46
 
47
+ export async function openIosDevice(device: DeviceInfo): Promise<void> {
48
+ if (device.kind !== 'simulator') return;
49
+ const state = await getSimulatorState(device.id);
50
+ if (state === 'Booted') return;
51
+ await ensureBootedSimulator(device);
52
+ await runCmd('open', ['-a', 'Simulator'], { allowFailure: true });
53
+ }
54
+
46
55
  export async function closeIosApp(device: DeviceInfo, app: string): Promise<void> {
47
56
  const bundleId = await resolveIosApp(device, app);
48
57
  if (device.kind === 'simulator') {
@@ -76,7 +85,6 @@ export async function closeIosApp(device: DeviceInfo, app: string): Promise<void
76
85
 
77
86
  export async function pressIos(device: DeviceInfo, x: number, y: number): Promise<void> {
78
87
  ensureSimulator(device, 'press');
79
- await ensureBootedSimulator(device);
80
88
  throw new AppError(
81
89
  'UNSUPPORTED_OPERATION',
82
90
  'simctl io tap is not available; use the XCTest runner for input',
@@ -90,7 +98,6 @@ export async function longPressIos(
90
98
  durationMs = 800,
91
99
  ): Promise<void> {
92
100
  ensureSimulator(device, 'long-press');
93
- await ensureBootedSimulator(device);
94
101
  throw new AppError(
95
102
  'UNSUPPORTED_OPERATION',
96
103
  'long-press is not supported on iOS simulators without XCTest runner support',
@@ -103,7 +110,6 @@ export async function focusIos(device: DeviceInfo, x: number, y: number): Promis
103
110
 
104
111
  export async function typeIos(device: DeviceInfo, text: string): Promise<void> {
105
112
  ensureSimulator(device, 'type');
106
- await ensureBootedSimulator(device);
107
113
  throw new AppError(
108
114
  'UNSUPPORTED_OPERATION',
109
115
  'simctl io keyboard is not available; use the XCTest runner for input',
@@ -126,7 +132,6 @@ export async function scrollIos(
126
132
  amount = 0.6,
127
133
  ): Promise<void> {
128
134
  ensureSimulator(device, 'scroll');
129
- await ensureBootedSimulator(device);
130
135
  throw new AppError(
131
136
  'UNSUPPORTED_OPERATION',
132
137
  'simctl io swipe is not available; use the XCTest runner for input',
@@ -178,19 +183,7 @@ async function listSimulatorApps(
178
183
  }
179
184
  }
180
185
 
181
- export async function getSimulatorScreenSize(
182
- device: DeviceInfo,
183
- ): Promise<{ width: number; height: number }> {
184
- await ensureBootedSimulator(device);
185
- const result = await runCmd('xcrun', ['simctl', 'io', device.id, 'status-bar', '--list'], {
186
- allowFailure: true,
187
- });
188
- const match = (result.stdout as string).match(/(\d+)x(\d+)/);
189
- if (match) return { width: Number(match[1]), height: Number(match[2]) };
190
- return { width: 1170, height: 2532 };
191
- }
192
-
193
- async function ensureBootedSimulator(device: DeviceInfo): Promise<void> {
186
+ export async function ensureBootedSimulator(device: DeviceInfo): Promise<void> {
194
187
  if (device.kind !== 'simulator') return;
195
188
  const state = await getSimulatorState(device.id);
196
189
  if (state === 'Booted') return;
package/src/utils/args.ts CHANGED
@@ -142,34 +142,45 @@ export function parseArgs(argv: string[]): ParsedArgs {
142
142
  export function usage(): string {
143
143
  return `agent-device <command> [args] [--json]
144
144
 
145
+ CLI to control iOS and Android devices for AI agents.
146
+
145
147
  Commands:
146
- open <app>
147
- close [app]
148
+ open [app] Boot device/simulator; optionally launch app
149
+ close [app] Close app or just end session
148
150
  snapshot [-i] [-c] [-d <depth>] [-s <scope>] [--raw] [--backend ax|xctest]
149
- click <@ref>
150
- get text <@ref>
151
- get attrs <@ref>
152
- replay <path>
153
- press <x> <y>
154
- long-press <x> <y> [durationMs]
155
- focus <x> <y>
156
- type <text>
157
- fill <x> <y> <text> | fill <@ref> <text>
158
- scroll <direction> [amount]
159
- scrollintoview <text>
160
- screenshot [--out path]
161
- session list
151
+ Capture accessibility tree
152
+ -i Interactive elements only
153
+ -c Compact output (drop empty structure)
154
+ -d <depth> Limit snapshot depth
155
+ -s <scope> Scope snapshot to label/identifier
156
+ --raw Raw node output
157
+ --backend ax|xctest ax: macOS Accessibility tree (fast, recommended, needs permissions)
158
+ xctest: XCTest snapshot (slower, no permissions)
159
+ click <@ref> Click element by snapshot ref
160
+ rect <label|@ref> Fetch element frame by label or ref (iOS sim)
161
+ get text <@ref> Return element text by ref
162
+ get attrs <@ref> Return element attributes by ref
163
+ replay <path> Replay a recorded session
164
+ press <x> <y> Tap at coordinates
165
+ long-press <x> <y> [durationMs] Long press (where supported)
166
+ focus <x> <y> Focus input at coordinates
167
+ type <text> Type text in focused field
168
+ fill <x> <y> <text> | fill <@ref> <text> Tap then type
169
+ scroll <direction> [amount] Scroll in direction (0-1 amount)
170
+ scrollintoview <text> Scroll until text appears (Android only)
171
+ screenshot [--out path] Capture screenshot
172
+ session list List active sessions
162
173
 
163
174
  Flags:
164
- --platform ios|android
165
- --device <name>
166
- --udid <udid>
167
- --serial <serial>
168
- --out <path>
169
- --session <name>
170
- --verbose
171
- --json
172
- --no-record
173
- --record-json
175
+ --platform ios|android Platform to target
176
+ --device <name> Device name to target
177
+ --udid <udid> iOS device UDID
178
+ --serial <serial> Android device serial
179
+ --out <path> Output path for screenshots
180
+ --session <name> Named session
181
+ --verbose Stream daemon/runner logs
182
+ --json JSON output
183
+ --no-record Do not record this action
184
+ --record-json Record JSON session log
174
185
  `;
175
186
  }
@@ -6,6 +6,7 @@ import {
6
6
  focusAndroid,
7
7
  longPressAndroid,
8
8
  openAndroidApp,
9
+ openAndroidDevice,
9
10
  pressAndroid,
10
11
  scrollAndroid,
11
12
  scrollIntoViewAndroid,
@@ -18,6 +19,7 @@ import {
18
19
  focusIos,
19
20
  longPressIos,
20
21
  openIosApp,
22
+ openIosDevice,
21
23
  pressIos,
22
24
  scrollIos,
23
25
  scrollIntoViewIos,
@@ -27,6 +29,7 @@ import {
27
29
 
28
30
  export type Interactor = {
29
31
  open(app: string): Promise<void>;
32
+ openDevice(): Promise<void>;
30
33
  close(app: string): Promise<void>;
31
34
  tap(x: number, y: number): Promise<void>;
32
35
  longPress(x: number, y: number, durationMs?: number): Promise<void>;
@@ -43,6 +46,7 @@ export function getInteractor(device: DeviceInfo): Interactor {
43
46
  case 'android':
44
47
  return {
45
48
  open: (app) => openAndroidApp(device, app),
49
+ openDevice: () => openAndroidDevice(device),
46
50
  close: (app) => closeAndroidApp(device, app),
47
51
  tap: (x, y) => pressAndroid(device, x, y),
48
52
  longPress: (x, y, durationMs) => longPressAndroid(device, x, y, durationMs),
@@ -56,6 +60,7 @@ export function getInteractor(device: DeviceInfo): Interactor {
56
60
  case 'ios':
57
61
  return {
58
62
  open: (app) => openIosApp(device, app),
63
+ openDevice: () => openIosDevice(device),
59
64
  close: (app) => closeIosApp(device, app),
60
65
  tap: (x, y) => pressIos(device, x, y),
61
66
  longPress: (x, y, durationMs) => longPressIos(device, x, y, durationMs),