You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

agent-device

Package Overview
Dependencies
Maintainers
1
Versions
69
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

agent-device - npm Package Compare versions

Comparing version
0.1.2
to
0.1.3
dist/bin/axsnapshot

Sorry, the diff of this file is not supported yet

+1
-1

@@ -1,1 +0,1 @@

import{spawn as e}from"node:child_process";function o(e,o,t){return o in e?Object.defineProperty(e,o,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[o]=t,e}class t extends Error{constructor(e,t,r,n){super(t),o(this,"code",void 0),o(this,"details",void 0),o(this,"cause",void 0),this.code=e,this.details=r,this.cause=n}}function r(e){return e instanceof t?e:e instanceof Error?new t("UNKNOWN",e.message,void 0,e):new t("UNKNOWN","Unknown error",{err:e})}async function n(o,r,d={}){return new Promise((n,i)=>{let a=e(o,r,{cwd:d.cwd,env:d.env,stdio:["ignore","pipe","pipe"]}),u="",s=d.binaryStdout?Buffer.alloc(0):void 0,f="";d.binaryStdout||a.stdout.setEncoding("utf8"),a.stderr.setEncoding("utf8"),a.stdout.on("data",e=>{d.binaryStdout?s=Buffer.concat([s??Buffer.alloc(0),Buffer.isBuffer(e)?e:Buffer.from(e)]):u+=e}),a.stderr.on("data",e=>{f+=e}),a.on("error",e=>{"ENOENT"===e.code?i(new t("TOOL_MISSING",`${o} not found in PATH`,{cmd:o},e)):i(new t("COMMAND_FAILED",`Failed to run ${o}`,{cmd:o,args:r},e))}),a.on("close",e=>{let a=e??1;0===a||d.allowFailure?n({stdout:u,stderr:f,exitCode:a,stdoutBuffer:s}):i(new t("COMMAND_FAILED",`${o} exited with code ${a}`,{cmd:o,args:r,stdout:u,stderr:f,exitCode:a}))})})}async function d(e){try{var o;let{shell:t,args:r}=(o=e,"win32"===process.platform?{shell:"cmd.exe",args:["/c","where",o]}:{shell:"bash",args:["-lc",`command -v ${o}`]}),d=await n(t,r,{allowFailure:!0});return 0===d.exitCode&&d.stdout.trim().length>0}catch{return!1}}function i(o,t,r={}){e(o,t,{cwd:r.cwd,env:r.env,stdio:"ignore",detached:!0}).unref()}async function a(o,r,n={}){return new Promise((d,i)=>{let a=e(o,r,{cwd:n.cwd,env:n.env,stdio:["ignore","pipe","pipe"]}),u="",s="",f=n.binaryStdout?Buffer.alloc(0):void 0;n.binaryStdout||a.stdout.setEncoding("utf8"),a.stderr.setEncoding("utf8"),a.stdout.on("data",e=>{if(n.binaryStdout){f=Buffer.concat([f??Buffer.alloc(0),Buffer.isBuffer(e)?e:Buffer.from(e)]);return}let o=String(e);u+=o,n.onStdoutChunk?.(o)}),a.stderr.on("data",e=>{let o=String(e);s+=o,n.onStderrChunk?.(o)}),a.on("error",e=>{"ENOENT"===e.code?i(new t("TOOL_MISSING",`${o} not found in PATH`,{cmd:o},e)):i(new t("COMMAND_FAILED",`Failed to run ${o}`,{cmd:o,args:r},e))}),a.on("close",e=>{let a=e??1;0===a||n.allowFailure?d({stdout:u,stderr:s,exitCode:a,stdoutBuffer:f}):i(new t("COMMAND_FAILED",`${o} exited with code ${a}`,{cmd:o,args:r,stdout:u,stderr:s,exitCode:a}))})})}export{fileURLToPath,pathToFileURL}from"node:url";export{default as node_net}from"node:net";export{default as node_fs,promises}from"node:fs";export{default as node_os}from"node:os";export{default as node_path}from"node:path";export{r as asAppError,t as errors_AppError,n as runCmd,i as runCmdDetached,a as runCmdStreaming,d as whichCmd};
import{spawn as e}from"node:child_process";function t(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}class n extends Error{constructor(e,n,o,r){super(n),t(this,"code",void 0),t(this,"details",void 0),t(this,"cause",void 0),this.code=e,this.details=o,this.cause=r}}function o(e){return e instanceof n?e:e instanceof Error?new n("UNKNOWN",e.message,void 0,e):new n("UNKNOWN","Unknown error",{err:e})}async function r(t,o,d={}){return new Promise((r,i)=>{let s=e(t,o,{cwd:d.cwd,env:d.env,stdio:["pipe","pipe","pipe"]}),u="",a=d.binaryStdout?Buffer.alloc(0):void 0,c="";d.binaryStdout||s.stdout.setEncoding("utf8"),s.stderr.setEncoding("utf8"),void 0!==d.stdin&&s.stdin.write(d.stdin),s.stdin.end(),s.stdout.on("data",e=>{d.binaryStdout?a=Buffer.concat([a??Buffer.alloc(0),Buffer.isBuffer(e)?e:Buffer.from(e)]):u+=e}),s.stderr.on("data",e=>{c+=e}),s.on("error",e=>{"ENOENT"===e.code?i(new n("TOOL_MISSING",`${t} not found in PATH`,{cmd:t},e)):i(new n("COMMAND_FAILED",`Failed to run ${t}`,{cmd:t,args:o},e))}),s.on("close",e=>{let s=e??1;0===s||d.allowFailure?r({stdout:u,stderr:c,exitCode:s,stdoutBuffer:a}):i(new n("COMMAND_FAILED",`${t} exited with code ${s}`,{cmd:t,args:o,stdout:u,stderr:c,exitCode:s}))})})}async function d(e){try{var t;let{shell:n,args:o}=(t=e,"win32"===process.platform?{shell:"cmd.exe",args:["/c","where",t]}:{shell:"bash",args:["-lc",`command -v ${t}`]}),d=await r(n,o,{allowFailure:!0});return 0===d.exitCode&&d.stdout.trim().length>0}catch{return!1}}function i(t,n,o={}){e(t,n,{cwd:o.cwd,env:o.env,stdio:"ignore",detached:!0}).unref()}async function s(t,o,r={}){return new Promise((d,i)=>{let s=e(t,o,{cwd:r.cwd,env:r.env,stdio:["pipe","pipe","pipe"]}),u="",a="",c=r.binaryStdout?Buffer.alloc(0):void 0;r.binaryStdout||s.stdout.setEncoding("utf8"),s.stderr.setEncoding("utf8"),void 0!==r.stdin&&s.stdin.write(r.stdin),s.stdin.end(),s.stdout.on("data",e=>{if(r.binaryStdout){c=Buffer.concat([c??Buffer.alloc(0),Buffer.isBuffer(e)?e:Buffer.from(e)]);return}let t=String(e);u+=t,r.onStdoutChunk?.(t)}),s.stderr.on("data",e=>{let t=String(e);a+=t,r.onStderrChunk?.(t)}),s.on("error",e=>{"ENOENT"===e.code?i(new n("TOOL_MISSING",`${t} not found in PATH`,{cmd:t},e)):i(new n("COMMAND_FAILED",`Failed to run ${t}`,{cmd:t,args:o},e))}),s.on("close",e=>{let s=e??1;0===s||r.allowFailure?d({stdout:u,stderr:a,exitCode:s,stdoutBuffer:c}):i(new n("COMMAND_FAILED",`${t} exited with code ${s}`,{cmd:t,args:o,stdout:u,stderr:a,exitCode:s}))})})}function u(t,o,r={}){let d=e(t,o,{cwd:r.cwd,env:r.env,stdio:["ignore","pipe","pipe"]}),i="",s="";d.stdout.setEncoding("utf8"),d.stderr.setEncoding("utf8"),d.stdout.on("data",e=>{i+=e}),d.stderr.on("data",e=>{s+=e});let a=new Promise((e,u)=>{d.on("error",e=>{"ENOENT"===e.code?u(new n("TOOL_MISSING",`${t} not found in PATH`,{cmd:t},e)):u(new n("COMMAND_FAILED",`Failed to run ${t}`,{cmd:t,args:o},e))}),d.on("close",d=>{let a=d??1;0===a||r.allowFailure?e({stdout:i,stderr:s,exitCode:a}):u(new n("COMMAND_FAILED",`${t} exited with code ${a}`,{cmd:t,args:o,stdout:i,stderr:s,exitCode:a}))})});return{child:d,wait:a}}export{fileURLToPath,pathToFileURL}from"node:url";export{default as node_net}from"node:net";export{default as node_fs,promises}from"node:fs";export{default as node_os}from"node:os";export{default as node_path}from"node:path";export{o as asAppError,n as errors_AppError,r as runCmd,u as runCmdBackground,i as runCmdDetached,s as runCmdStreaming,d as whichCmd};

@@ -1,6 +0,6 @@

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)}
`)}function u(e){let t=e.details?`
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 l}from"./861.js";function c(e){process.stdout.write(`${JSON.stringify(e,null,2)}
`)}function d(e){let t=e.details?`
${JSON.stringify(e.details,null,2)}`:"";process.stderr.write(`Error (${e.code}): ${e.message}${t}
`)}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)}
`)}),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]
`)}let u=e.join(i.homedir(),".agent-device"),p=e.join(u,"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 l("COMMAND_FAILED","Failed to start daemon",{infoPath:p,hint:"Run pnpm build, or delete ~/.agent-device/daemon.json if stale."})}function w(){if(!o.existsSync(p))return null;try{let e=JSON.parse(o.readFileSync(p,"utf8"));if(!e.port||!e.token)return null;return e}catch{return null}}async function y(e){return new Promise(t=>{let r=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 l("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)}
`)}),o=setTimeout(()=>{s.destroy(),n(new l("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("--user-installed"===s){t.appsFilter="user-installed";continue}if("--all"===s){t.appsFilter="all";continue}if(s.startsWith("--backend")){let r=s.includes("=")?s.split("=")[1]:e[n+1];if(s.includes("=")||(n+=1),"ax"!==r&&"xctest"!==r)throw new l("INVALID_ARGS",`Invalid backend: ${r}`);t.snapshotBackend=r;continue}if(s.startsWith("--")){let[r,o]=s.split("="),i=o??e[n+1];switch(!o&&(n+=1),r){case"--platform":if("ios"!==i&&"android"!==i)throw new l("INVALID_ARGS",`Invalid platform: ${i}`);t.platform=i;break;case"--depth":{let e=Number(i);if(!Number.isFinite(e)||e<0)throw new l("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 l("INVALID_ARGS",`Unknown flag: ${r}`)}continue}if("-d"===s){let r=e[n+1];n+=1;let s=Number(r);if(!Number.isFinite(s)||s<0)throw new l("INVALID_ARGS",`Invalid depth: ${r}`);t.snapshotDepth=Math.floor(s);continue}if("-s"===s){let r=e[n+1];n+=1,t.snapshotScope=r;continue}r.push(s)}return{command:r.shift()??null,positionals:r,flags:t}}(t);(n.flags.help||!n.command)&&(process.stdout.write(`agent-device <command> [args] [--json]

@@ -21,4 +21,10 @@ CLI to control iOS and Android devices for AI agents.

xctest: XCTest snapshot (slower, no permissions)
devices List available devices
apps [--user-installed|--all] List installed apps (Android launchable by default, iOS simulator)
back Navigate back (where supported)
home Go to home screen (where supported)
app-switcher Open app switcher (where supported)
wait <ms>|text <text>|@ref [timeoutMs] Wait for duration or text to appear
alert [get|accept|dismiss|wait] [timeout] Inspect or handle alert (iOS simulator)
click <@ref> Click element by snapshot ref
rect <label|@ref> Fetch element frame by label or ref (iOS sim)
get text <@ref> Return element text by ref

@@ -35,2 +41,4 @@ get attrs <@ref> Return element attributes by ref

screenshot [--out path] Capture screenshot
record start [path] Start screen recording
record stop Stop screen recording
session list List active sessions

@@ -49,16 +57,20 @@

--record-json Record JSON session log
--user-installed Apps: list user-installed packages (Android only)
--all Apps: list all packages (Android only)
`),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)}
`),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")}
`:"";if(!Array.isArray(r)||0===r.length)return`${c}${a}
`;if(t.raw){let e=r.map(e=>JSON.stringify(e));return`${c}${a}
`),process.exit(+!n.flags.help));let{command:s,positionals:a,flags:u}=n,p=u.session??process.env.AGENT_DEVICE_SESSION??"default",f=u.verbose&&!u.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 l("INVALID_ARGS","session only supports list");let t=await m({session:p,command:"session_list",positionals:[],flags:{}});if(!t.ok)throw new l(t.error.code,t.error.message);u.json?c({success:!0,data:t.data??{}}):process.stdout.write(`${JSON.stringify(t.data??{},null,2)}
`),f&&f();return}let e=await m({session:p,command:s,positionals:a,flags:u});if(e.ok){if(u.json){c({success:!0,data:e.data??{}}),f&&f();return}if("snapshot"===s){process.stdout.write(function(e,t={}){let r=e.nodes??[],n=!!e.truncated,s="string"==typeof e.appName?e.appName:void 0,o="string"==typeof e.appBundleId?e.appBundleId:void 0,i=[];s&&i.push(`Page: ${s}`),o&&i.push(`App: ${o}`);let a=`Snapshot: ${r.length} nodes${n?" (truncated)":""}`,l=i.length>0?`${i.join("\n")}
`:"";if(!Array.isArray(r)||0===r.length)return`${l}${a}
`;if(t.raw){let e=r.map(e=>JSON.stringify(e));return`${l}${a}
${e.join("\n")}
`}let l=[],u=[];for(let e of r){let t=e.depth??0;for(;l.length>0&&t<=l[l.length-1];)l.pop();let r=e.label?.trim()||e.value?.trim()||e.identifier?.trim()||"",n=function(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase();switch(t.startsWith("ax")&&(t=t.replace(/^ax/,"")),t){case"application":return"application";case"navigationbar":return"navigation-bar";case"tabbar":return"tab-bar";case"button":return"button";case"link":return"link";case"cell":return"cell";case"statictext":case"statictext":return"text";case"textfield":case"textfield":return"text-field";case"textview":case"textarea":return"text-view";case"switch":return"switch";case"slider":return"slider";case"image":return"image";case"table":return"list";case"collectionview":return"collection";case"searchfield":return"search";case"segmentedcontrol":return"segmented-control";case"group":return"group";case"window":return"window";case"checkbox":return"checkbox";case"radio":return"radio";case"menuitem":return"menu-item";case"toolbar":return"toolbar";case"scrollarea":return"scroll-area";case"table":return"table";default:return t||"element"}}(e.type??"Element"),s="group"===n&&!r;s&&l.push(t);let o=s?t:Math.max(0,t-l.length),i=" ".repeat(o),a=e.ref?`@${e.ref}`:"",c=[!1===e.enabled?"disabled":null].filter(Boolean).join(", "),d=c?` [${c}]`:"",p=r?` "${r}"`:"";if(s){u.push(`${i}${a} [${n}]${d}`.trimEnd());continue}u.push(`${i}${a} [${n}]${p}${d}`.trimEnd())}return`${c}${a}
${u.join("\n")}
`}(e.data??{},{raw:d.snapshotRaw})),f&&f();return}if("get"===s){let t=a[0];if("text"===t){let t=e.data?.text??"";process.stdout.write(`${t}
`}let c=[],d=[];for(let e of r){let t=e.depth??0;for(;c.length>0&&t<=c[c.length-1];)c.pop();let r=e.label?.trim()||e.value?.trim()||e.identifier?.trim()||"",n=function(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase();switch(t.startsWith("ax")&&(t=t.replace(/^ax/,"")),t){case"application":return"application";case"navigationbar":return"navigation-bar";case"tabbar":return"tab-bar";case"button":return"button";case"link":return"link";case"cell":return"cell";case"statictext":case"statictext":return"text";case"textfield":case"textfield":return"text-field";case"textview":case"textarea":return"text-view";case"switch":return"switch";case"slider":return"slider";case"image":return"image";case"table":return"list";case"collectionview":return"collection";case"searchfield":return"search";case"segmentedcontrol":return"segmented-control";case"group":return"group";case"window":return"window";case"checkbox":return"checkbox";case"radio":return"radio";case"menuitem":return"menu-item";case"toolbar":return"toolbar";case"scrollarea":return"scroll-area";case"table":return"table";default:return t||"element"}}(e.type??"Element"),s="group"===n&&!r;s&&c.push(t);let o=s?t:Math.max(0,t-c.length),i=" ".repeat(o),a=e.ref?`@${e.ref}`:"",l=[!1===e.enabled?"disabled":null].filter(Boolean).join(", "),u=l?` [${l}]`:"",p=r?` "${r}"`:"";if(s){d.push(`${i}${a} [${n}]${u}`.trimEnd());continue}d.push(`${i}${a} [${n}]${p}${u}`.trimEnd())}return`${l}${a}
${d.join("\n")}
`}(e.data??{},{raw:u.snapshotRaw})),f&&f();return}if("get"===s){let t=a[0];if("text"===t){let t=e.data?.text??"";process.stdout.write(`${t}
`),f&&f();return}if("attrs"===t){let t=e.data?.node??{};process.stdout.write(`${JSON.stringify(t,null,2)}
`),f&&f();return}}if("click"===s){let t=e.data?.ref??"",r=e.data?.x,n=e.data?.y;t&&"number"==typeof r&&"number"==typeof n&&process.stdout.write(`Clicked @${t} (${r}, ${n})
`),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(`
`),f&&f();return}if(e.data&&"object"==typeof e.data){let t=e.data;if("devices"===s){let e=(Array.isArray(t.devices)?t.devices:[]).map(e=>{let t=e?.name??e?.id??"unknown",r=e?.platform??"unknown",n=e?.kind?` ${e.kind}`:"",s="boolean"==typeof e?.booted?` booted=${e.booted}`:"";return`${t} (${r}${n})${s}`});process.stdout.write(`${e.join("\n")}
`),f&&f();return}if("apps"===s){let e=Array.isArray(t.apps)?t.apps:[];process.stdout.write(`${e.join("\n")}
`),f&&f();return}}f&&f();return}throw new l(e.error.code,e.error.message,e.error.details)}catch(t){let e=r(t);if(u.json)c({success:!1,error:{code:e.code,message:e.message,details:e.details}});else if(d(e),u.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(`
[daemon log]
${n}
`)}}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));
`)}}catch{}f&&f(),process.exit(1)}}n(process.argv[1]??"").href===import.meta.url&&S(process.argv.slice(2)).catch(e=>{d(r(e)),process.exit(1)}),S(process.argv.slice(2));

@@ -1,5 +0,5 @@

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")}
`}(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)}
`),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}
`)}}),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}
let e,t;import i from"node:crypto";import{isCancel as a,select as n}from"@clack/prompts";import{node_path as o,runCmdStreaming as r,promises as s,asAppError as l,fileURLToPath as c,runCmdBackground as d,node_fs as u,node_os as p,errors_AppError as f,runCmd as m,node_net as h,whichCmd as w}from"./861.js";async function g(e,t){let i=e,o=e=>e.toLowerCase().replace(/_/g," ").replace(/\s+/g," ").trim();if(t.platform&&(i=i.filter(e=>e.platform===t.platform)),t.udid){let e=i.find(e=>e.id===t.udid&&"ios"===e.platform);if(!e)throw new f("DEVICE_NOT_FOUND",`No iOS device with UDID ${t.udid}`);return e}if(t.serial){let e=i.find(e=>e.id===t.serial&&"android"===e.platform);if(!e)throw new f("DEVICE_NOT_FOUND",`No Android device with serial ${t.serial}`);return e}if(t.deviceName){let e=o(t.deviceName),a=i.find(t=>o(t.name)===e);if(!a)throw new f("DEVICE_NOT_FOUND",`No device named ${t.deviceName}`);return a}if(1===i.length)return i[0];if(0===i.length)throw new f("DEVICE_NOT_FOUND","No devices found",{selector:t});let r=i.filter(e=>e.booted);if(1===r.length)return r[0];if(!process.env.CI&&process.stdin.isTTY&&process.stdout.isTTY){let e=await n({message:"Multiple devices available. Choose a device to continue:",options:(r.length>0?r:i).map(e=>({label:`${e.name} (${e.platform}${e.kind?`, ${e.kind}`:""}${e.booted?", booted":""})`,value:e.id}))});if(a(e))throw new f("INVALID_ARGS","Device selection cancelled");if(e){let t=i.find(t=>t.id===e);if(t)return t}}return r[0]??i[0]}async function y(){if(!await w("adb"))throw new f("TOOL_MISSING","adb not found in PATH");let e=(await m("adb",["devices","-l"])).stdout.split("\n").map(e=>e.trim()),t=[];for(let i of e){if(!i||i.startsWith("List of devices"))continue;let e=i.split(/\s+/),a=e[0];if("device"!==e[1])continue;let n=(e.find(e=>e.startsWith("model:"))??"").replace("model:","").replace(/_/g," ").trim()||a;if(a.startsWith("emulator-")){let e=await m("adb",["-s",a,"emu","avd","name"],{allowFailure:!0}),t=e.stdout.trim();0===e.exitCode&&t&&(n=t.replace(/_/g," "))}let o=await v(a);t.push({platform:"android",id:a,name:n,kind:a.startsWith("emulator-")?"emulator":"device",booted:o})}return t}async function v(e){try{let t=await m("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 i=Date.now();for(;Date.now()-i<t;){if(await v(e))return;await new Promise(e=>setTimeout(e,1e3))}throw new f("COMMAND_FAILED","Android device did not finish booting in time",{serial:e,timeoutMs:t})}let I={settings:{type:"intent",value:"android.settings.SETTINGS"}};function b(e,t){return["-s",e.id,...t]}async function A(e,t){let i=t.trim();if(i.includes("."))return{type:"package",value:i};let a=I[i.toLowerCase()];if(a)return a;let n=(await m("adb",b(e,["shell","pm","list","packages"]))).stdout.split("\n").map(e=>e.replace("package:","").trim()).filter(Boolean).filter(e=>e.toLowerCase().includes(i.toLowerCase()));if(1===n.length)return{type:"package",value:n[0]};if(n.length>1)throw new f("INVALID_ARGS",`Multiple packages matched "${t}"`,{matches:n});throw new f("APP_NOT_INSTALLED",`No package found matching "${t}"`)}async function S(e,t="launchable"){if("launchable"===t){let t=await m("adb",b(e,["shell","cmd","package","query-activities","--brief","-a","android.intent.action.MAIN","-c","android.intent.category.LAUNCHER"]),{allowFailure:!0});if(0===t.exitCode&&t.stdout.trim().length>0){let e=new Set;for(let i of t.stdout.split("\n")){let t=i.trim();if(!t)continue;let a=t.split(/\s+/)[0],n=a.includes("/")?a.split("/")[0]:a;n&&e.add(n)}if(e.size>0)return Array.from(e)}}return(await m("adb",b(e,"user-installed"===t?["shell","pm","list","packages","-3"]:["shell","pm","list","packages"]))).stdout.split("\n").map(e=>e.replace("package:","").trim()).filter(Boolean)}async function D(e,t){e.booted||await N(e.id);let i=await A(e,t);"intent"===i.type?await m("adb",b(e,["shell","am","start","-a",i.value])):await m("adb",b(e,["shell","monkey","-p",i.value,"-c","android.intent.category.LAUNCHER","1"]))}async function O(e){e.booted||await N(e.id)}async function k(e,t){if("settings"===t.trim().toLowerCase())return void await m("adb",b(e,["shell","am","force-stop","com.android.settings"]));let i=await A(e,t);if("intent"===i.type)throw new f("INVALID_ARGS","Close requires a package name, not an intent");await m("adb",b(e,["shell","am","force-stop",i.value]))}async function x(e,t,i){await m("adb",b(e,["shell","input","tap",String(t),String(i)]))}async function _(e){await m("adb",b(e,["shell","input","keyevent","4"]))}async function E(e){await m("adb",b(e,["shell","input","keyevent","3"]))}async function P(e){await m("adb",b(e,["shell","input","keyevent","187"]))}async function R(e,t,i,a=800){await m("adb",b(e,["shell","input","swipe",String(t),String(i),String(t),String(i),String(a)]))}async function C(e,t){let i=t.replace(/ /g,"%s");await m("adb",b(e,["shell","input","text",i]))}async function T(e,t,i){await x(e,t,i)}async function L(e,t,i,a){await T(e,t,i),await C(e,a)}async function M(e,t,i=.6){let{width:a,height:n}=await V(e),o=Math.floor(a*i),r=Math.floor(n*i),s=Math.floor(a/2),l=Math.floor(n/2),c=s,d=l,u=s,p=l;switch(t){case"up":d=l-Math.floor(r/2),p=l+Math.floor(r/2);break;case"down":d=l+Math.floor(r/2),p=l-Math.floor(r/2);break;case"left":c=s-Math.floor(o/2),u=s+Math.floor(o/2);break;case"right":c=s+Math.floor(o/2),u=s-Math.floor(o/2);break;default:throw new f("INVALID_ARGS",`Unknown direction: ${t}`)}await m("adb",b(e,["shell","input","swipe",String(c),String(d),String(u),String(p),"300"]))}async function F(e,t){for(let i=0;i<8;i+=1){let i="";try{i=await B(e)}catch(t){let e=t instanceof Error?t.message:String(t);throw new f("UNSUPPORTED_OPERATION",`uiautomator dump failed: ${e}`)}if(function(e,t){let i=t.toLowerCase(),a=/<node[^>]+>/g,n=a.exec(e);for(;n;){let t=n[0],o=/text="([^"]*)"/.exec(t),r=/content-desc="([^"]*)"/.exec(t),s=(o?.[1]??"").toLowerCase(),l=(r?.[1]??"").toLowerCase();if(s.includes(i)||l.includes(i)){let e=/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/.exec(t);if(e){let t=Number(e[1]),i=Number(e[2]);return{x:Math.floor((t+Number(e[3]))/2),y:Math.floor((i+Number(e[4]))/2)}}return{x:0,y:0}}n=a.exec(e)}return null}(i,t))return;await M(e,"down",.5)}throw new f("COMMAND_FAILED",`Could not find element containing "${t}" after scrolling`)}async function U(e,t){let i=await m("adb",b(e,["exec-out","screencap","-p"]),{binaryStdout:!0});if(!i.stdoutBuffer)throw new f("COMMAND_FAILED","Failed to capture screenshot");await s.writeFile(t,i.stdoutBuffer)}async function j(e,t={}){return function(e,t,i){let a=function(e){let t={type:null,label:null,value:null,identifier:null,depth:-1,children:[]},i=[t],a=/<node\b[^>]*>|<\/node>/g,n=a.exec(e);for(;n;){let t=n[0];if(t.startsWith("</node")){i.length>1&&i.pop(),n=a.exec(e);continue}let o=function(e){let t=t=>{let i=RegExp(`${t}="([^"]*)"`).exec(e);return i?i[1]:null},i=e=>{let i=t(e);if(null!==i)return"true"===i};return{text:t("text"),desc:t("content-desc"),resourceId:t("resource-id"),className:t("class"),bounds:t("bounds"),clickable:i("clickable"),enabled:i("enabled"),focusable:i("focusable")}}(t),r=function(e){if(!e)return;let t=/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/.exec(e);if(!t)return;let i=Number(t[1]),a=Number(t[2]);return{x:i,y:a,width:Math.max(0,Number(t[3])-i),height:Math.max(0,Number(t[4])-a)}}(o.bounds),s=i[i.length-1],l={type:o.className,label:o.text||o.desc,value:o.text,identifier:o.resourceId,rect:r,enabled:o.enabled,hittable:o.clickable??o.focusable,depth:s.depth+1,parentIndex:void 0,children:[]};s.children.push(l),t.endsWith("/>")||i.push(l),n=a.exec(e)}return t}(e),n=[],o=!1,r=i.depth??1/0,s=i.scope?function(e,t){let i=t.toLowerCase(),a=[...e.children];for(;a.length>0;){let e=a.shift(),t=e.label?.toLowerCase()??"",n=e.value?.toLowerCase()??"",o=e.identifier?.toLowerCase()??"";if(t.includes(i)||n.includes(i)||o.includes(i))return e;a.push(...e.children)}return null}(a,i.scope):null,l=s?[s]:a.children,c=(e,t)=>{if(n.length>=800){o=!0;return}if(!(t>r)){for(let a of((i.raw||function(e,t){if(t.interactiveOnly)return!!e.hittable;if(t.compact){let t=!!(e.label&&e.label.trim().length>0),i=!!(e.identifier&&e.identifier.trim().length>0);return t||i||!!e.hittable}return!0}(e,i))&&n.push({index:n.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(a,t+1),o)return}};for(let e of l)if(c(e,0),o)break;return o?{nodes:n,truncated:o}:{nodes:n}}(await B(e),800,t)}async function $(){if(!await w("adb"))throw new f("TOOL_MISSING","adb not found in PATH")}async function V(e){let t=(await m("adb",b(e,["shell","wm","size"]))).stdout.match(/Physical size:\s*(\d+)x(\d+)/);if(!t)throw new f("COMMAND_FAILED","Unable to read screen size");return{width:Number(t[1]),height:Number(t[2])}}async function B(e){return await m("adb",b(e,["shell","uiautomator","dump","/sdcard/window_dump.xml"])),(await m("adb",b(e,["shell","cat","/sdcard/window_dump.xml"]))).stdout}async function G(){if("darwin"!==process.platform)throw new f("UNSUPPORTED_PLATFORM","iOS tools are only available on macOS");if(!await w("xcrun"))throw new f("TOOL_MISSING","xcrun not found in PATH");let e=[],t=await m("xcrun",["simctl","list","devices","-j"]);try{let i=JSON.parse(t.stdout);for(let t of Object.values(i.devices))for(let i of t)i.isAvailable&&e.push({platform:"ios",id:i.udid,name:i.name,kind:"simulator",booted:"Booted"===i.state})}catch(e){throw new f("COMMAND_FAILED","Failed to parse simctl devices JSON",void 0,e)}if(await w("xcrun"))try{let t=await m("xcrun",["devicectl","list","devices","--json"]);for(let i of JSON.parse(t.stdout).devices??[])i.platform?.toLowerCase().includes("ios")&&e.push({platform:"ios",id:i.identifier,name:i.name,kind:"device",booted:!0})}catch{}return e}let J={settings:"com.apple.Preferences"};async function q(e,t){let i=t.trim();if(i.includes("."))return i;let a=J[i.toLowerCase()];if(a)return a;if("simulator"===e.kind){let a=(await en(e)).filter(e=>e.name.toLowerCase()===i.toLowerCase());if(1===a.length)return a[0].bundleId;if(a.length>1)throw new f("INVALID_ARGS",`Multiple apps matched "${t}"`,{matches:a})}throw new f("APP_NOT_INSTALLED",`No app found matching "${t}"`)}async function W(e,t){let i=await q(e,t);if("simulator"===e.kind){await eo(e),await m("open",["-a","Simulator"],{allowFailure:!0}),await m("xcrun",["simctl","launch",e.id,i]);return}await m("xcrun",["devicectl","device","process","launch","--device",e.id,i])}async function X(e){"simulator"!==e.kind||"Booted"!==await er(e.id)&&(await eo(e),await m("open",["-a","Simulator"],{allowFailure:!0}))}async function z(e,t){let i=await q(e,t);if("simulator"===e.kind){await eo(e);let t=await m("xcrun",["simctl","terminate",e.id,i],{allowFailure:!0});if(0!==t.exitCode){if(t.stderr.toLowerCase().includes("found nothing to terminate"))return;throw new f("COMMAND_FAILED",`xcrun exited with code ${t.exitCode}`,{cmd:"xcrun",args:["simctl","terminate",e.id,i],stdout:t.stdout,stderr:t.stderr,exitCode:t.exitCode})}return}await m("xcrun",["devicectl","device","process","terminate","--device",e.id,i])}async function H(e,t,i){throw ea(e,"press"),new f("UNSUPPORTED_OPERATION","simctl io tap is not available; use the XCTest runner for input")}async function Y(e,t,i,a=800){throw ea(e,"long-press"),new f("UNSUPPORTED_OPERATION","long-press is not supported on iOS simulators without XCTest runner support")}async function Z(e,t,i){await H(e,t,i)}async function K(e,t){throw ea(e,"type"),new f("UNSUPPORTED_OPERATION","simctl io keyboard is not available; use the XCTest runner for input")}async function Q(e,t,i,a){await Z(e,t,i),await K(e,a)}async function ee(e,t,i=.6){throw ea(e,"scroll"),new f("UNSUPPORTED_OPERATION","simctl io swipe is not available; use the XCTest runner for input")}async function et(e){throw new f("UNSUPPORTED_OPERATION",`scrollintoview is not supported on iOS without UI automation (${e})`)}async function ei(e,t){if("simulator"===e.kind){await eo(e),await m("xcrun",["simctl","io",e.id,"screenshot",t]);return}await m("xcrun",["devicectl","device","screenshot","--device",e.id,t])}function ea(e,t){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION",`${t} is only supported on iOS simulators in v1`)}async function en(e){let t=(await m("xcrun",["simctl","listapps",e.id],{allowFailure:!0})).stdout.trim();if(!t)return[];let i=null;if(t.startsWith("{"))try{i=JSON.parse(t)}catch{i=null}if(!i&&t.startsWith("{"))try{let e=await m("plutil",["-convert","json","-o","-","-"],{allowFailure:!0,stdin:t});0===e.exitCode&&e.stdout.trim().startsWith("{")&&(i=JSON.parse(e.stdout))}catch{i=null}return i?Object.entries(i).map(([e,t])=>({bundleId:e,name:t.CFBundleDisplayName??t.CFBundleName??e})):[]}async function eo(e){"simulator"!==e.kind||"Booted"!==await er(e.id)&&(await m("xcrun",["simctl","boot",e.id],{allowFailure:!0}),await m("xcrun",["simctl","bootstatus",e.id,"-b"],{allowFailure:!0}))}async function er(e){let t=await m("xcrun",["simctl","list","devices","-j"],{allowFailure:!0});if(0!==t.exitCode)return null;try{let i=JSON.parse(t.stdout);for(let t of Object.values(i.devices??{})){let i=t.find(t=>t.udid===e);if(i)return i.state}}catch{}return null}let es=new Map;async function el(e,t,i={}){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","iOS runner only supports simulators in v1");try{let a=await eu(e,i),n=await eh(e,a.port,t,i.logPath),o=await n.text(),r={};try{r=JSON.parse(o)}catch{throw new f("COMMAND_FAILED","Invalid runner response",{text:o})}if(!r.ok)throw new f("COMMAND_FAILED",r.error?.message??"Runner error",{runner:r,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:i.logPath});return r.data??{}}catch(n){let a=n instanceof f?n:new f("COMMAND_FAILED",String(n));if("COMMAND_FAILED"===a.code&&"string"==typeof a.message&&a.message.includes("Runner did not accept connection")){await ec(e.id);let a=await eu(e,i),n=await eh(e,a.port,t,i.logPath),o=await n.text(),r={};try{r=JSON.parse(o)}catch{throw new f("COMMAND_FAILED","Invalid runner response",{text:o})}if(!r.ok)throw new f("COMMAND_FAILED",r.error?.message??"Runner error",{runner:r,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:i.logPath});return r.data??{}}throw n}}async function ec(e){let t=es.get(e);if(t){try{await eh(t.device,t.port,{command:"shutdown"})}catch{}try{await t.testPromise}catch{}eN(t.xctestrunPath),eN(t.jsonPath),es.delete(e)}}async function ed(e){await m("xcrun",["simctl","bootstatus",e,"-b"],{allowFailure:!0})}async function eu(e,t){let i=es.get(e.id);if(i)return i;await ed(e.id);let a=await ep(e.id,t),n=await eg(),o=process.env.AGENT_DEVICE_RUNNER_TIMEOUT??"300",{xctestrunPath:s,jsonPath:l}=await ev(a,{AGENT_DEVICE_RUNNER_PORT:String(n),AGENT_DEVICE_RUNNER_TIMEOUT:o},`session-${e.id}-${n}`),c=r("xcodebuild",["test-without-building","-only-testing","AgentDeviceRunnerUITests/RunnerTests/testCommand","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-xctestrun",s,"-destination",`platform=iOS Simulator,id=${e.id}`],{onStdoutChunk:e=>{em(e,t.logPath,t.verbose)},onStderrChunk:e=>{em(e,t.logPath,t.verbose)},allowFailure:!0,env:{...process.env,AGENT_DEVICE_RUNNER_PORT:String(n),AGENT_DEVICE_RUNNER_TIMEOUT:o}}),d={device:e,deviceId:e.id,port:n,xctestrunPath:s,jsonPath:l,testPromise:c};return es.set(e.id,d),d}async function ep(e,t){let i,a=o.join(p.homedir(),".agent-device","ios-runner"),n=o.join(a,"derived");if((i=process.env.AGENT_DEVICE_IOS_CLEAN_DERIVED)&&["1","true","yes","on"].includes(i.toLowerCase()))try{u.rmSync(n,{recursive:!0,force:!0})}catch{}let s=ef(n);if(s)return s;let l=function(){let e=o.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=o.join(t,"package.json");if(u.existsSync(e))return t;t=o.dirname(t)}return e}(),d=o.join(l,"ios-runner","AgentDeviceRunner","AgentDeviceRunner.xcodeproj");if(!u.existsSync(d))throw new f("COMMAND_FAILED","iOS runner project not found",{projectPath:d});try{await r("xcodebuild",["build-for-testing","-project",d,"-scheme","AgentDeviceRunner","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-destination",`platform=iOS Simulator,id=${e}`,"-derivedDataPath",n],{onStdoutChunk:e=>{em(e,t.logPath,t.verbose)},onStderrChunk:e=>{em(e,t.logPath,t.verbose)}})}catch(i){let e=i instanceof f?i:new f("COMMAND_FAILED",String(i));throw new f("COMMAND_FAILED","xcodebuild build-for-testing failed",{error:e.message,details:e.details,logPath:t.logPath})}let m=ef(n);if(!m)throw new f("COMMAND_FAILED","Failed to locate .xctestrun after build");return m}function ef(e){if(!u.existsSync(e))return null;let t=[],i=[e];for(;i.length>0;){let e=i.pop();for(let a of u.readdirSync(e,{withFileTypes:!0})){let n=o.join(e,a.name);if(a.isDirectory()){i.push(n);continue}if(a.isFile()&&a.name.endsWith(".xctestrun"))try{let e=u.statSync(n);t.push({path:n,mtimeMs:e.mtimeMs})}catch{}}}return 0===t.length?null:(t.sort((e,t)=>t.mtimeMs-e.mtimeMs),t[0]?.path??null)}function em(e,t,i){t&&u.appendFileSync(t,e),i&&process.stderr.write(e)}async function eh(e,t,i,a){a&&await ey(a,4e3);let n=Date.now(),o=null;for(;Date.now()-n<8e3;)try{return await fetch(`http://127.0.0.1:${t}/command`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)})}catch(e){o=e,await new Promise(e=>setTimeout(e,100))}if("simulator"===e.kind){let a=await ew(e.id,t,i);return new Response(a.body,{status:a.status})}let r=a?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}(a):null;if(r&&r!==t)try{return await fetch(`http://127.0.0.1:${r}/command`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)})}catch(e){o=e}throw new f("COMMAND_FAILED","Runner did not accept connection",{port:t,fallbackPort:r,logPath:a,lastError:o?String(o):void 0})}async function ew(e,t,i){let a=JSON.stringify(i),n=await m("xcrun",["simctl","spawn",e,"/usr/bin/curl","-s","-X","POST","-H","Content-Type: application/json","--data",a,`http://127.0.0.1:${t}/command`],{allowFailure:!0}),o=n.stdout;if(0!==n.exitCode)throw new f("COMMAND_FAILED","Runner did not accept connection (simctl spawn)",{port:t,stdout:n.stdout,stderr:n.stderr,exitCode:n.exitCode});return{status:200,body:o}}async function eg(){return await new Promise((e,t)=>{let i=h.createServer();i.listen(0,"127.0.0.1",()=>{let a=i.address();i.close(),"object"==typeof a&&a?.port?e(a.port):t(new f("COMMAND_FAILED","Failed to allocate port"))}),i.on("error",t)})}async function ey(e,t){if(!u.existsSync(e))return;let i=Date.now(),a=0;for(;Date.now()-i<t;){if(!u.existsSync(e))return;let t=u.statSync(e);if(t.size>a){let i=u.openSync(e,"r"),n=Buffer.alloc(t.size-a);u.readSync(i,n,0,n.length,a),u.closeSync(i),a=t.size;let o=n.toString("utf8");if(o.includes("AGENT_DEVICE_RUNNER_LISTENER_READY")||o.includes("AGENT_DEVICE_RUNNER_PORT="))return}await new Promise(e=>setTimeout(e,100))}}async function ev(e,t,i){let a,n=o.dirname(e),r=i.replace(/[^a-zA-Z0-9._-]/g,"_"),s=o.join(n,`AgentDeviceRunner.env.${r}.json`),l=o.join(n,`AgentDeviceRunner.env.${r}.xctestrun`),c=await m("plutil",["-convert","json","-o","-",e],{allowFailure:!0});if(0!==c.exitCode||!c.stdout.trim())throw new f("COMMAND_FAILED","Failed to read xctestrun plist",{xctestrunPath:e,stderr:c.stderr});try{a=JSON.parse(c.stdout)}catch(t){throw new f("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}},p=a.TestConfigurations;if(Array.isArray(p))for(let e of p){if(!e||"object"!=typeof e)continue;let t=e.TestTargets;if(Array.isArray(t))for(let e of t)e&&"object"==typeof e&&d(e)}for(let[e,t]of Object.entries(a))t&&"object"==typeof t&&t.TestBundlePath&&(d(t),a[e]=t);u.writeFileSync(s,JSON.stringify(a,null,2));let h=await m("plutil",["-convert","xml1","-o",l,s],{allowFailure:!0});if(0!==h.exitCode)throw new f("COMMAND_FAILED","Failed to write xctestrun plist",{tmpXctestrunPath:l,stderr:h.stderr});return{xctestrunPath:l,jsonPath:s}}function eN(e){try{u.existsSync(e)&&u.unlinkSync(e)}catch{}}async function eI(e){let t,i;if("ios"!==e.platform||"simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","AX snapshot is only supported on iOS simulators");let a=await eb(),n=await m(a,[],{allowFailure:!0});if(0!==n.exitCode){let e=(n.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)."),e.toLowerCase().includes("could not find ios app content")&&(t=" AX snapshot sometimes caches empty content. Try restarting the Simulator app."),new f("COMMAND_FAILED","AX snapshot failed",{stderr:`${e}${t}`,stdout:n.stdout})}try{let e=JSON.parse(n.stdout);if(e&&"object"==typeof e&&"root"in e){if(!e.root)throw Error("AX snapshot missing root");t=e.root,i=e.windowFrame??void 0}else t=e}catch(e){throw new f("COMMAND_FAILED","Invalid AX snapshot JSON",{error:String(e)})}let o=t.frame??i,r=[],s=[],l=(e,t)=>{e.frame&&r.push(e.frame);let i=e.frame&&o?{x:e.frame.x-o.x,y:e.frame.y-o.y,width:e.frame.width,height:e.frame.height}:e.frame;for(let a of(s.push({...e,frame:i,children:void 0,depth:t}),e.children??[]))l(a,t+1)};return l(t,0),{nodes:(function(e,t,i){if(!t||0===i.length)return e;let a=1/0,n=1/0;for(let e of i)e.x<a&&(a=e.x),e.y<n&&(n=e.y);return a<=5&&n<=5?e.map(e=>({...e,frame:e.frame?{x:e.frame.x+t.x,y:e.frame.y+t.y,width:e.frame.width,height:e.frame.height}:void 0})):e})(s,o,r).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 eb(){let e=function(){let e=process.cwd();for(let t=0;t<6;t+=1){let t=o.join(e,"package.json");if(u.existsSync(t))return e;e=o.dirname(e)}return process.cwd()}(),t=o.join(e,"ios-runner","AXSnapshot"),i=process.env.AGENT_DEVICE_AX_BINARY;if(i&&u.existsSync(i))return i;let a=o.join(e,"dist","bin","axsnapshot");if(u.existsSync(a))return a;let n=o.join(t,".build","release","axsnapshot");if(u.existsSync(n))return n;let r=await m("swift",["build","-c","release"],{cwd:t,allowFailure:!0});if(0!==r.exitCode||!u.existsSync(n))throw new f("COMMAND_FAILED","Failed to build AX snapshot tool",{stderr:r.stderr,stdout:r.stdout});return n}async function eA(e){let t={platform:e.platform,deviceName:e.device,udid:e.udid,serial:e.serial};if("android"===t.platform){await $();let e=await y();return await g(e,t)}if("ios"===t.platform){let e=await G();return await g(e,t)}let i=[];try{i.push(...await y())}catch{}try{i.push(...await G())}catch{}return await g(i,t)}async function eS(e,t,i,a,n){let o=function(e){switch(e.platform){case"android":return{open:t=>D(e,t),openDevice:()=>O(e),close:t=>k(e,t),tap:(t,i)=>x(e,t,i),longPress:(t,i,a)=>R(e,t,i,a),focus:(t,i)=>T(e,t,i),type:t=>C(e,t),fill:(t,i,a)=>L(e,t,i,a),scroll:(t,i)=>M(e,t,i),scrollIntoView:t=>F(e,t),screenshot:t=>U(e,t)};case"ios":return{open:t=>W(e,t),openDevice:()=>X(e),close:t=>z(e,t),tap:(t,i)=>H(e,t,i),longPress:(t,i,a)=>Y(e,t,i,a),focus:(t,i)=>Z(e,t,i),type:t=>K(e,t),fill:(t,i,a)=>Q(e,t,i,a),scroll:(t,i)=>ee(e,t,i),scrollIntoView:e=>et(e),screenshot:t=>ei(e,t)};default:throw new f("UNSUPPORTED_PLATFORM",`Unsupported platform: ${e.platform}`)}}(e);switch(t){case"open":{let e=i[0];if(!e)return await o.openDevice(),{app:null};return await o.open(e),{app:e}}case"close":{let e=i[0];if(!e)return{closed:"session"};return await o.close(e),{app:e}}case"press":{let[t,a]=i.map(Number);if(Number.isNaN(t)||Number.isNaN(a))throw new f("INVALID_ARGS","press requires x y");return"ios"===e.platform&&"simulator"===e.kind?await el(e,{command:"tap",x:t,y:a,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath}):await o.tap(t,a),{x:t,y:a}}case"long-press":{let e=Number(i[0]),t=Number(i[1]),a=i[2]?Number(i[2]):void 0;if(Number.isNaN(e)||Number.isNaN(t))throw new f("INVALID_ARGS","long-press requires x y [durationMs]");return await o.longPress(e,t,a),{x:e,y:t,durationMs:a}}case"focus":{let[t,a]=i.map(Number);if(Number.isNaN(t)||Number.isNaN(a))throw new f("INVALID_ARGS","focus requires x y");return"ios"===e.platform&&"simulator"===e.kind?await el(e,{command:"tap",x:t,y:a,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath}):await o.focus(t,a),{x:t,y:a}}case"type":{let t=i.join(" ");if(!t)throw new f("INVALID_ARGS","type requires text");return"ios"===e.platform&&"simulator"===e.kind?await el(e,{command:"type",text:t,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath}):await o.type(t),{text:t}}case"fill":{let t=Number(i[0]),a=Number(i[1]),r=i.slice(2).join(" ");if(Number.isNaN(t)||Number.isNaN(a)||!r)throw new f("INVALID_ARGS","fill requires x y text");return"ios"===e.platform&&"simulator"===e.kind?(await el(e,{command:"tap",x:t,y:a,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath}),await el(e,{command:"type",text:r,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath})):await o.fill(t,a,r),{x:t,y:a,text:r}}case"scroll":{let t=i[0],a=i[1]?Number(i[1]):void 0;if(!t)throw new f("INVALID_ARGS","scroll requires direction");if("ios"===e.platform&&"simulator"===e.kind){if(!["up","down","left","right"].includes(t))throw new f("INVALID_ARGS",`Unknown direction: ${t}`);let i=function(e){switch(e){case"up":return"down";case"down":return"up";case"left":return"right";case"right":return"left"}}(t);await el(e,{command:"swipe",direction:i,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath})}else await o.scroll(t,a);return{direction:t,amount:a}}case"scrollintoview":{let t=i.join(" ").trim();if(!t)throw new f("INVALID_ARGS","scrollintoview requires text");if("ios"===e.platform&&"simulator"===e.kind){for(let i=0;i<8;i+=1){let a=await el(e,{command:"findText",text:t,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath});if(a?.found)return{text:t,attempts:i+1};await el(e,{command:"swipe",direction:"up",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath}),await new Promise(e=>setTimeout(e,300))}throw new f("COMMAND_FAILED",`scrollintoview could not find text: ${t}`)}return await o.scrollIntoView(t),{text:t}}case"screenshot":{let e=a??`./screenshot-${Date.now()}.png`;return await o.screenshot(e),{path:e}}case"back":if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","back is only supported on iOS simulators in v1");return await el(e,{command:"back",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath}),{action:"back"}}return await _(e),{action:"back"};case"home":if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","home is only supported on iOS simulators in v1");return await el(e,{command:"home",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath}),{action:"home"}}return await E(e),{action:"home"};case"app-switcher":if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","app-switcher is only supported on iOS simulators in v1");return await el(e,{command:"appSwitcher",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath}),{action:"app-switcher"}}return await P(e),{action:"app-switcher"};case"snapshot":{let t=n?.snapshotBackend??"ax";if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","snapshot is only supported on iOS simulators in v1");if("ax"===t)return{nodes:(await eI(e)).nodes??[],truncated:!1,backend:"ax"};let i=await el(e,{command:"snapshot",appBundleId:n?.appBundleId,interactiveOnly:n?.snapshotInteractiveOnly,compact:n?.snapshotCompact,depth:n?.snapshotDepth,scope:n?.snapshotScope,raw:n?.snapshotRaw},{verbose:n?.verbose,logPath:n?.logPath});return{nodes:i.nodes??[],truncated:i.truncated??!1,backend:"xctest"}}let i=await j(e,{interactiveOnly:n?.snapshotInteractiveOnly,compact:n?.snapshotCompact,depth:n?.snapshotDepth,scope:n?.snapshotScope,raw:n?.snapshotRaw});return{nodes:i.nodes??[],truncated:i.truncated??!1,backend:"android"}}default:throw new f("INVALID_ARGS",`Unknown command: ${t}`)}}function eD(e){return e.map((e,t)=>({...e,ref:`e${t+1}`}))}function eO(e){let t=e.trim();return t.startsWith("@")?t.slice(1)||null:t.startsWith("e")?t:null}function ek(e,t){return e.find(e=>e.ref===t)??null}function ex(e){return{x:Math.round(e.x+e.width/2),y:Math.round(e.y+e.height/2)}}let e_=new Map,eE=o.join(p.homedir(),".agent-device"),eP=o.join(eE,"daemon.json"),eR=o.join(eE,"daemon.log"),eC=o.join(eE,"sessions"),eT=function(){try{let e=function(){let e=o.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=o.join(t,"package.json");if(u.existsSync(e))return t;t=o.dirname(t)}return e}();return JSON.parse(u.readFileSync(o.join(e,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}(),eL=i.randomBytes(24).toString("hex");function eM(e,t){return{appBundleId:t,verbose:e?.verbose,logPath:eR,snapshotInteractiveOnly:e?.snapshotInteractiveOnly,snapshotCompact:e?.snapshotCompact,snapshotDepth:e?.snapshotDepth,snapshotScope:e?.snapshotScope,snapshotRaw:e?.snapshotRaw,snapshotBackend:e?.snapshotBackend}}async function eF(e){if(e.token!==eL)return{ok:!1,error:{code:"UNAUTHORIZED",message:"Invalid token"}};let t=e.command,i=e.session||"default";if("session_list"===t)return{ok:!0,data:{sessions:Array.from(e_.values()).map(e=>({name:e.name,platform:e.device.platform,device:e.device.name,id:e.device.id,createdAt:e.createdAt}))}};if("devices"===t)try{let t=[];if(e.flags?.platform==="android"){let{listAndroidDevices:e}=await Promise.resolve().then(()=>({listAndroidDevices:y}));t.push(...await e())}else if(e.flags?.platform==="ios"){let{listIosDevices:e}=await Promise.resolve().then(()=>({listIosDevices:G}));t.push(...await e())}else{let{listAndroidDevices:e}=await Promise.resolve().then(()=>({listAndroidDevices:y})),{listIosDevices:i}=await Promise.resolve().then(()=>({listIosDevices:G}));try{t.push(...await e())}catch{}try{t.push(...await i())}catch{}}return{ok:!0,data:{devices:t}}}catch(t){let e=l(t);return{ok:!1,error:{code:e.code,message:e.message,details:e.details}}}if("apps"===t){let t=e_.get(i),a=e.flags??{};if(!t&&!a.platform&&!a.device&&!a.udid&&!a.serial)return{ok:!1,error:{code:"INVALID_ARGS",message:"apps requires an active session or an explicit device selector (e.g. --platform ios)."}};let n=t?.device??await eA(a);if(await eJ(n),"ios"===n.platform){if("simulator"!==n.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"apps list is only supported on iOS simulators"}};let{listSimulatorApps:e}=await Promise.resolve().then(()=>({listSimulatorApps:en}));return{ok:!0,data:{apps:(await e(n)).map(e=>e.name&&e.name!==e.bundleId?`${e.name} (${e.bundleId})`:e.bundleId)}}}let{listAndroidApps:o}=await Promise.resolve().then(()=>({listAndroidApps:S}));return{ok:!0,data:{apps:await o(n,e.flags?.appsFilter)}}}if("open"===t){let a;if(e_.has(i))return{ok:!1,error:{code:"INVALID_ARGS",message:"Session already active. Close it first or pass a new --session name."}};let n=await eA(e.flags??{});await eJ(n);let o=Array.from(e_.values()).find(e=>e.device.id===n.id);if(o)return{ok:!1,error:{code:"DEVICE_IN_USE",message:`Device is already in use by session "${o.name}".`,details:{session:o.name,deviceId:n.id,deviceName:n.name}}};let r=e.positionals?.[0];if("ios"===n.platform)try{let{resolveIosApp:t}=await Promise.resolve().then(()=>({resolveIosApp:q}));a=await t(n,e.positionals?.[0]??"")}catch{a=void 0}await eS(n,"open",e.positionals??[],e.flags?.out,{...eM(e.flags,a)});let s={name:i,device:n,createdAt:Date.now(),appBundleId:a,appName:r,actions:[]};return eU(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:i}}),e_.set(i,s),{ok:!0,data:{session:i}}}if("replay"===t){let t=e.positionals?.[0];if(!t)return{ok:!1,error:{code:"INVALID_ARGS",message:"replay requires a path"}};try{var a;let e=(a=t).startsWith("~/")?o.join(p.homedir(),a.slice(2)):o.resolve(a),n=JSON.parse(u.readFileSync(e,"utf8")),r=n.optimizedActions??n.actions??[];for(let e of r)e&&"replay"!==e.command&&await eF({token:eL,session:i,command:e.command,positionals:e.positionals??[],flags:e.flags??{}});return{ok:!0,data:{replayed:r.length,session:i}}}catch(t){let e=l(t);return{ok:!1,error:{code:e.code,message:e.message}}}}if("close"===t){let a=e_.get(i);return a?(e.positionals&&e.positionals.length>0&&await eS(a.device,"close",e.positionals??[],e.flags?.out,{...eM(e.flags,a.appBundleId)}),"ios"===a.device.platform&&"simulator"===a.device.kind&&await ec(a.device.id),eU(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:i}}),ej(a),e_.delete(i),{ok:!0,data:{session:i}}):{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session"}}}if("snapshot"===t){let a=e_.get(i),n=a?.device??await eA(e.flags??{});a||await eJ(n);let o=a?.appBundleId,r=e.flags?.snapshotScope;if(r&&r.trim().startsWith("@")){if(!a?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"Ref scope requires an existing snapshot in session."}};let e=eO(r.trim());if(!e)return{ok:!1,error:{code:"INVALID_ARGS",message:`Invalid ref scope: ${r}`}};let t=ek(a.snapshot.nodes,e),i=t?eB(t,a.snapshot.nodes):void 0;if(!i)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${r} not found or has no label`}};r=i}let s=await eS(n,"snapshot",[],e.flags?.out,{...eM({...e.flags,snapshotScope:r},o)}),l=s?.nodes??[],c=eD(e.flags?.snapshotRaw?l:function(e){let t=[],i=[];for(let a of e){let e=a.depth??0;for(;t.length>0&&e<=t[t.length-1];)t.pop();let n=function(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase();return t.startsWith("ax")&&(t=t.replace(/^ax/,"")),t}(a.type??""),o=[a.label,a.value,a.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0),r=!!o&&eG(o);if(("group"===n||"ioscontentgroup"===n)&&!r){t.push(e);continue}let s=Math.max(0,e-t.length);i.push({...a,depth:s})}return i}(l)),d={nodes:c,truncated:s?.truncated,createdAt:Date.now(),backend:s?.backend},u={name:i,device:n,createdAt:a?.createdAt??Date.now(),appBundleId:o,snapshot:d,actions:a?.actions??[]};return eU(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{nodes:c.length,truncated:s?.truncated??!1}}),e_.set(i,u),{ok:!0,data:{nodes:c,truncated:s?.truncated??!1,appName:a?.appName??o??n.name,appBundleId:o}}}if("wait"===t){let a=e_.get(i),n=a?.device??await eA(e.flags??{});a||await eJ(n);let o=e.positionals??[];if(0===o.length)return{ok:!1,error:{code:"INVALID_ARGS",message:"wait requires a duration or text"}};let r=e=>{if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null},s=r(o[0]);if(null!==s)return await new Promise(e=>setTimeout(e,s)),a&&eU(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{waitedMs:s}}),{ok:!0,data:{waitedMs:s}};let l="",c=null;if("text"===o[0])l=null!==(c=r(o[o.length-1]))?o.slice(1,-1).join(" "):o.slice(1).join(" ");else if(o[0].startsWith("@")){if(!a?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"Ref wait requires an existing snapshot in session."}};let e=eO(o[0]);if(!e)return{ok:!1,error:{code:"INVALID_ARGS",message:`Invalid ref: ${o[0]}`}};let t=ek(a.snapshot.nodes,e),i=t?eB(t,a.snapshot.nodes):void 0;if(!i)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${o[0]} not found or has no label`}};c=r(o[o.length-1]),l=i}else l=null!==(c=r(o[o.length-1]))?o.slice(0,-1).join(" "):o.join(" ");if(!(l=l.trim()))return{ok:!1,error:{code:"INVALID_ARGS",message:"wait requires text"}};let d=c??1e4,u=Date.now();for(;Date.now()-u<d;){if("ios"===n.platform&&"simulator"===n.kind){let i=await el(n,{command:"findText",text:l,appBundleId:a?.appBundleId},{verbose:e.flags?.verbose,logPath:eR});if(i?.found)return a&&eU(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{text:l,waitedMs:Date.now()-u}}),{ok:!0,data:{text:l,waitedMs:Date.now()-u}}}else if("android"!==n.platform)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"wait is not supported on this device"}};else if(eV(eD((await j(n,{scope:l})).nodes??[]),l))return a&&eU(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{text:l,waitedMs:Date.now()-u}}),{ok:!0,data:{text:l,waitedMs:Date.now()-u}};await new Promise(e=>setTimeout(e,300))}return{ok:!1,error:{code:"COMMAND_FAILED",message:`wait timed out for text: ${l}`}}}if("alert"===t){let a=e_.get(i),n=a?.device??await eA(e.flags??{});a||await eJ(n);let o=(e.positionals?.[0]??"get").toLowerCase();if("ios"!==n.platform||"simulator"!==n.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"alert is only supported on iOS simulators in v1"}};if("wait"===o){let i=(e=>{if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null})(e.positionals?.[1])??1e4,o=Date.now();for(;Date.now()-o<i;){try{let i=await el(n,{command:"alert",action:"get",appBundleId:a?.appBundleId},{verbose:e.flags?.verbose,logPath:eR});return a&&eU(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:i}),{ok:!0,data:i}}catch{}await new Promise(e=>setTimeout(e,300))}return{ok:!1,error:{code:"COMMAND_FAILED",message:"alert wait timed out"}}}let r=await el(n,{command:"alert",action:"accept"===o||"dismiss"===o?o:"get",appBundleId:a?.appBundleId},{verbose:e.flags?.verbose,logPath:eR});return a&&eU(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:r}),{ok:!0,data:r}}if("record"===t){let a=(e.positionals?.[0]??"").toLowerCase();if(!["start","stop"].includes(a))return{ok:!1,error:{code:"INVALID_ARGS",message:"record requires start|stop"}};let n=e_.get(i),r=n?.device??await eA(e.flags??{});n||await eJ(r);let s=n??{name:i,device:r,createdAt:Date.now(),actions:[]};if("start"===a){if(s.recording)return{ok:!1,error:{code:"INVALID_ARGS",message:"recording already in progress"}};let a=e.positionals?.[1]??`./recording-${Date.now()}.mp4`,n=o.resolve(a),l=o.dirname(n);if(u.existsSync(l)||u.mkdirSync(l,{recursive:!0}),"ios"===r.platform){if("simulator"!==r.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"record is only supported on iOS simulators in v1"}};let{child:e,wait:t}=d("xcrun",["simctl","io",r.id,"recordVideo",n],{allowFailure:!0});s.recording={platform:"ios",outPath:n,child:e,wait:t}}else{let e=`/sdcard/agent-device-recording-${Date.now()}.mp4`,{child:t,wait:i}=d("adb",["-s",r.id,"shell","screenrecord",e],{allowFailure:!0});s.recording={platform:"android",outPath:n,remotePath:e,child:t,wait:i}}return e_.set(i,s),eU(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"start"}}),{ok:!0,data:{recording:"started",outPath:a}}}if(!s.recording)return{ok:!1,error:{code:"INVALID_ARGS",message:"no active recording"}};let l=s.recording;l.child.kill("SIGINT");try{await l.wait}catch{}if("android"===l.platform&&l.remotePath)try{await m("adb",["-s",r.id,"pull",l.remotePath,l.outPath],{allowFailure:!0}),await m("adb",["-s",r.id,"shell","rm","-f",l.remotePath],{allowFailure:!0})}catch{}return s.recording=void 0,eU(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"stop",outPath:l.outPath}}),{ok:!0,data:{recording:"stopped",outPath:l.outPath}}}if("click"===t){let a=e_.get(i);if(!a?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let n=e.positionals?.[0]??"",o=eO(n);if(!o)return{ok:!1,error:{code:"INVALID_ARGS",message:"click requires a ref like @e2"}};let r=ek(a.snapshot.nodes,o);if(!r?.rect&&e.positionals.length>1){let t=e.positionals.slice(1).join(" ").trim();t.length>0&&(r=eV(a.snapshot.nodes,t))}if(!r?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${n} not found or has no bounds`}};let s=eB(r,a.snapshot.nodes),l=r.label?.trim();if("ios"===a.device.platform&&"simulator"===a.device.kind&&l&&function(e,t){let i=t.trim().toLowerCase();if(!i)return!1;let a=0;for(let t of e)if((t.label??"").trim().toLowerCase()===i&&(a+=1)>1)return!1;return 1===a}(a.snapshot.nodes,l))return await el(a.device,{command:"tap",text:l,appBundleId:a.appBundleId},{verbose:e.flags?.verbose,logPath:eR}),eU(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:o,refLabel:l,mode:"text"}}),{ok:!0,data:{ref:o,mode:"text"}};let{x:c,y:d}=ex(r.rect);return await eS(a.device,"press",[String(c),String(d)],e.flags?.out,{...eM(e.flags,a.appBundleId)}),eU(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:o,x:c,y:d,refLabel:s}}),{ok:!0,data:{ref:o,x:c,y:d}}}if("fill"===t){let a=e_.get(i);if(e.positionals?.[0]?.startsWith("@")){if(!a?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let i=eO(e.positionals[0]);if(!i)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires a ref like @e2"}};let n=e.positionals.length>=3?e.positionals[1]:"",o=e.positionals.length>=3?e.positionals.slice(2).join(" "):e.positionals.slice(1).join(" ");if(!o)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires text after ref"}};let r=ek(a.snapshot.nodes,i);if(!r?.rect&&n&&(r=eV(a.snapshot.nodes,n)),!r?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${e.positionals[0]} not found or has no bounds`}};let s=eB(r,a.snapshot.nodes),{x:l,y:c}=ex(r.rect),d=await eS(a.device,"fill",[String(l),String(c),o],e.flags?.out,{...eM(e.flags,a.appBundleId)});return eU(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:d??{ref:i,x:l,y:c,refLabel:s}}),{ok:!0,data:d??{ref:i,x:l,y:c}}}}if("get"===t){let a=e.positionals?.[0],n=e.positionals?.[1];if("text"!==a&&"attrs"!==a)return{ok:!1,error:{code:"INVALID_ARGS",message:"get only supports text or attrs"}};let o=e_.get(i);if(!o?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let r=eO(n??"");if(!r)return{ok:!1,error:{code:"INVALID_ARGS",message:"get text requires a ref like @e2"}};let s=ek(o.snapshot.nodes,r);if(!s&&e.positionals.length>2){let t=e.positionals.slice(2).join(" ").trim();t.length>0&&(s=eV(o.snapshot.nodes,t))}if(!s)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${n} not found`}};if("attrs"===a)return eU(o,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:r}}),{ok:!0,data:{ref:r,node:s}};let l=[s.label,s.value,s.identifier].map(e=>"string"==typeof e?e.trim():"").filter(e=>e.length>0)[0]??"";return eU(o,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:r,text:l,refLabel:l||void 0}}),{ok:!0,data:{ref:r,text:l,node:s}}}let n=e_.get(i);if(!n)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session. Run open first."}};let r=await eS(n.device,t,e.positionals??[],e.flags?.out,{...eM(e.flags,n.appBundleId)});return eU(n,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:r??{}}),{ok:!0,data:r??{}}}function eU(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:i,udid:a,serial:n,out:o,verbose:r,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:d,snapshotRaw:u,snapshotBackend:p,noRecord:f,recordJson:m}=e;return{platform:t,device:i,udid:a,serial:n,out:o,verbose:r,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:d,snapshotRaw:u,snapshotBackend:p,noRecord:f,recordJson:m}}(t.flags),result:t.result})}function ej(e){try{u.existsSync(eC)||u.mkdirSync(eC,{recursive:!0});let t=e.name.replace(/[^a-zA-Z0-9._-]/g,"_"),i=new Date(e.createdAt).toISOString().replace(/[:.]/g,"-"),a=o.join(eC,`${t}-${i}.ad`),n=o.join(eC,`${t}-${i}.json`),r={name:e.name,device:e.device,createdAt:e.createdAt,appBundleId:e.appBundleId,actions:e.actions,optimizedActions:function(e){let t=[];for(let i of e.actions)if("snapshot"!==i.command){if("click"===i.command||"fill"===i.command||"get"===i.command){let a=i.result?.refLabel;"string"==typeof a&&a.trim().length>0&&t.push({ts:i.ts,command:"snapshot",positionals:[],flags:{platform:e.device.platform,snapshotInteractiveOnly:!0,snapshotCompact:!0,snapshotScope:a.trim()},result:{scope:a.trim()}})}t.push(i)}return t}(e)},s=function(e,t){let i=[],a=e.device.name.replace(/"/g,'\\"'),n=e.device.kind?` kind=${e.device.kind}`:"";for(let o of(i.push(`context platform=${e.device.platform} device="${a}"${n} theme=unknown`),t))o.flags?.noRecord||i.push(function(e){let t=[e.command];if("click"===e.command){let i=e.positionals?.[0];if(i){t.push(e$(i));let a=e.result?.refLabel;return"string"==typeof a&&a.trim().length>0&&t.push(e$(a)),t.join(" ")}}if("fill"===e.command){let i=e.positionals?.[0];if(i&&i.startsWith("@")){t.push(e$(i));let a=e.result?.refLabel,n=e.positionals.slice(1).join(" ");return"string"==typeof a&&a.trim().length>0&&t.push(e$(a)),n&&t.push(e$(n)),t.join(" ")}}if("get"===e.command){let i=e.positionals?.[0],a=e.positionals?.[1];if(i&&a){t.push(e$(i)),t.push(e$(a));let n=e.result?.refLabel;return"string"==typeof n&&n.trim().length>0&&t.push(e$(n)),t.join(" ")}}if("snapshot"===e.command)return e.flags?.snapshotInteractiveOnly&&t.push("-i"),e.flags?.snapshotCompact&&t.push("-c"),"number"==typeof e.flags?.snapshotDepth&&t.push("-d",String(e.flags.snapshotDepth)),e.flags?.snapshotScope&&t.push("-s",e$(e.flags.snapshotScope)),e.flags?.snapshotRaw&&t.push("--raw"),e.flags?.snapshotBackend&&t.push("--backend",e.flags.snapshotBackend),t.join(" ");for(let i of e.positionals??[])t.push(e$(i));return t.join(" ")}(o));return`${i.join("\n")}
`}(e,r.optimizedActions);u.writeFileSync(a,s),e.actions.some(e=>e.flags?.recordJson)&&u.writeFileSync(n,JSON.stringify(r,null,2))}catch{}}function e$(e){let t=e.trim();return t.startsWith("@")||/^-?\d+(\.\d+)?$/.test(t)?t:JSON.stringify(t)}function eV(e,t){let i=t.toLowerCase();return e.find(e=>{let t=(e.label??"").toLowerCase(),a=(e.value??"").toLowerCase(),n=(e.identifier??"").toLowerCase();return t.includes(i)||a.includes(i)||n.includes(i)})??null}function eB(e,t){let i=[e.label,e.value,e.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0);return i&&eG(i)?i:function(e,t){if(!e.rect)return;let i=e.rect.y+e.rect.height/2,a=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||!eG(t))continue;let n=Math.abs(e.rect.y+e.rect.height/2-i);(!a||n<a.distance)&&(a={label:t,distance:n})}return a?.label}(e,t)??(i&&eG(i)?i:void 0)}function eG(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:eo}));await t(e);return}if("android"===e.platform){let{waitForAndroidBoot:t}=await Promise.resolve().then(()=>({waitForAndroidBoot:N}));await t(e.id)}}(e=h.createServer(e=>{let t="";e.setEncoding("utf8"),e.on("data",async i=>{let a=(t+=i).indexOf("\n");for(;-1!==a;){let i,n=t.slice(0,a).trim();if(t=t.slice(a+1),0===n.length){a=t.indexOf("\n");continue}try{let e=JSON.parse(n);i=await eF(e)}catch(t){let e=l(t);i={ok:!1,error:{code:e.code,message:e.message,details:e.details}}}e.write(`${JSON.stringify(i)}
`),a=t.indexOf("\n")}})})).listen(0,"127.0.0.1",()=>{let t=e.address();if("object"==typeof t&&t?.port){var i;i=t.port,u.existsSync(eE)||u.mkdirSync(eE,{recursive:!0}),u.writeFileSync(eR,""),u.writeFileSync(eP,JSON.stringify({port:i,token:eL,pid:process.pid,version:eT},null,2),{mode:384}),process.stdout.write(`AGENT_DEVICE_DAEMON_PORT=${t.port}
`)}}),t=async()=>{for(let e of Array.from(e_.values()))"ios"===e.device.platform&&"simulator"===e.device.kind&&await ec(e.device.id),ej(e);e.close(()=>{u.existsSync(eP)&&u.unlinkSync(eP),process.exit(0)})},process.on("SIGINT",()=>{t()}),process.on("SIGTERM",()=>{t()}),process.on("SIGHUP",()=>{t()}),process.on("uncaughtException",e=>{let i=e instanceof f?e:l(e);process.stderr.write(`Daemon error: ${i.message}
`),t()});

@@ -252,17 +252,2 @@ //

return Response(ok: true, data: DataPayload(found: found))
case .rect:
guard let text = command.text else {
return Response(ok: false, error: ErrorPayload(message: "rect requires text"))
}
guard let element = findElement(app: activeApp, text: text) else {
return Response(ok: false, error: ErrorPayload(message: "element not found"))
}
let frame = element.frame
let rect = SnapshotRect(
x: Double(frame.origin.x),
y: Double(frame.origin.y),
width: Double(frame.size.width),
height: Double(frame.size.height)
)
return Response(ok: true, data: DataPayload(rect: rect))
case .listTappables:

@@ -291,5 +276,58 @@ let elements = activeApp.descendants(matching: .any).allElementsBoundByIndex

return Response(ok: true, data: snapshotFast(app: activeApp, options: options))
case .back:
if tapNavigationBack(app: activeApp) {
return Response(ok: true, data: DataPayload(message: "back"))
}
performBackGesture(app: activeApp)
return Response(ok: true, data: DataPayload(message: "back"))
case .home:
XCUIDevice.shared.press(.home)
return Response(ok: true, data: DataPayload(message: "home"))
case .appSwitcher:
performAppSwitcherGesture(app: activeApp)
return Response(ok: true, data: DataPayload(message: "appSwitcher"))
case .alert:
let action = (command.action ?? "get").lowercased()
let alert = activeApp.alerts.firstMatch
if !alert.exists {
return Response(ok: false, error: ErrorPayload(message: "alert not found"))
}
if action == "accept" {
let button = alert.buttons.allElementsBoundByIndex.first
button?.tap()
return Response(ok: true, data: DataPayload(message: "accepted"))
}
if action == "dismiss" {
let button = alert.buttons.allElementsBoundByIndex.last
button?.tap()
return Response(ok: true, data: DataPayload(message: "dismissed"))
}
let buttonLabels = alert.buttons.allElementsBoundByIndex.map { $0.label }
return Response(ok: true, data: DataPayload(message: alert.label, items: buttonLabels))
}
}
private func tapNavigationBack(app: XCUIApplication) -> Bool {
let buttons = app.navigationBars.buttons.allElementsBoundByIndex
if let back = buttons.first(where: { $0.isHittable }) {
back.tap()
return true
}
return false
}
private func performBackGesture(app: XCUIApplication) {
let target = app.windows.firstMatch.exists ? app.windows.firstMatch : app
let start = target.coordinate(withNormalizedOffset: CGVector(dx: 0.05, dy: 0.5))
let end = target.coordinate(withNormalizedOffset: CGVector(dx: 0.8, dy: 0.5))
start.press(forDuration: 0.05, thenDragTo: end)
}
private func performAppSwitcherGesture(app: XCUIApplication) {
let target = app.windows.firstMatch.exists ? app.windows.firstMatch : app
let start = target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.99))
let end = target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.7))
start.press(forDuration: 0.6, thenDragTo: end)
}
private func findElement(app: XCUIApplication, text: String) -> XCUIElement? {

@@ -607,3 +645,6 @@ let predicate = NSPredicate(format: "label CONTAINS[c] %@ OR identifier CONTAINS[c] %@ OR value CONTAINS[c] %@", text, text, text)

case snapshot
case rect
case back
case home
case appSwitcher
case alert
case shutdown

@@ -623,2 +664,3 @@ }

let text: String?
let action: String?
let x: Double?

@@ -652,3 +694,2 @@ let y: Double?

let truncated: Bool?
let rect: SnapshotRect?

@@ -660,4 +701,3 @@ init(

nodes: [SnapshotNode]? = nil,
truncated: Bool? = nil,
rect: SnapshotRect? = nil
truncated: Bool? = nil
) {

@@ -669,3 +709,2 @@ self.message = message

self.truncated = truncated
self.rect = rect
}

@@ -672,0 +711,0 @@ }

@@ -22,7 +22,2 @@ import Foundation

struct AXSnapshot: Codable {
let windowFrame: AXNode.Frame?
let root: AXNode
}
struct AXSnapshotError: Error, CustomStringConvertible {

@@ -34,2 +29,3 @@ let message: String

let simulatorBundleId = "com.apple.iphonesimulator"
let defaultMaxDepth = 40

@@ -56,13 +52,17 @@ func hasAccessibilityPermission() -> Bool {

func getChildren(_ element: AXUIElement) -> [AXUIElement] {
getAttribute(element, kAXChildrenAttribute as CFString) ?? []
if let children: [AXUIElement] = getAttribute(element, kAXChildrenAttribute as CFString),
!children.isEmpty {
return children
}
if let children: [AXUIElement] = getAttribute(element, kAXVisibleChildrenAttribute as CFString),
!children.isEmpty {
return children
}
if let children: [AXUIElement] = getAttribute(element, kAXContentsAttribute as CFString),
!children.isEmpty {
return children
}
return []
}
func getRole(_ element: AXUIElement) -> String? {
getAttribute(element, kAXRoleAttribute as CFString)
}
func getSubrole(_ element: AXUIElement) -> String? {
getAttribute(element, kAXSubroleAttribute as CFString)
}
func getLabel(_ element: AXUIElement) -> String? {

@@ -78,2 +78,6 @@ if let label: String = getAttribute(element, "AXLabel" as CFString) {

func getDescription(_ element: AXUIElement) -> String? {
getAttribute(element, kAXDescriptionAttribute as CFString)
}
func getValue(_ element: AXUIElement) -> String? {

@@ -118,7 +122,9 @@ if let value: String = getAttribute(element, kAXValueAttribute as CFString) {

func buildTree(_ element: AXUIElement, depth: Int = 0, maxDepth: Int = 40) -> AXNode {
let children = depth < maxDepth ? getChildren(element).map { buildTree($0, depth: depth + 1, maxDepth: maxDepth) } : []
func buildTree(_ element: AXUIElement, depth: Int = 0, maxDepth: Int = defaultMaxDepth) -> AXNode {
let children = depth < maxDepth
? getChildren(element).map { buildTree($0, depth: depth + 1, maxDepth: maxDepth) }
: []
return AXNode(
role: getRole(element),
subrole: getSubrole(element),
role: getAttribute(element, kAXRoleAttribute as CFString),
subrole: getAttribute(element, kAXSubroleAttribute as CFString),
label: getLabel(element),

@@ -132,15 +138,248 @@ value: getValue(element),

func findIOSAppRoot(in simulator: NSRunningApplication) -> (AXUIElement, AXNode.Frame?)? {
func findIOSAppSnapshot(in simulator: NSRunningApplication) -> (AXUIElement, AXNode.Frame?, AXUIElement, [AXUIElement], [AXUIElement])? {
let appElement = axElement(for: simulator)
let windows = getChildren(appElement).filter { getRole($0) == "AXWindow" }
let windows = getChildren(appElement).filter {
(getAttribute($0, kAXRoleAttribute as CFString) as String?) == (kAXWindowRole as String)
}
if windows.isEmpty { return nil }
if let focused: AXUIElement = getAttribute(appElement, kAXFocusedWindowAttribute as CFString),
let root = chooseRoot(in: focused) {
let extras = dedupeElements(findToolbarExtras(in: focused, root: root) + findTabBarExtras(in: focused, root: root))
let modalRoots = findAdditionalWindowRoots(windows: windows, excluding: focused, windowFrame: getFrame(focused))
return (root, getFrame(focused), focused, extras, modalRoots)
}
let sorted = windows.sorted { lhs, rhs in
let l = getFrame(lhs)
let r = getFrame(rhs)
let la = (l?.width ?? 0) * (l?.height ?? 0)
let ra = (r?.width ?? 0) * (r?.height ?? 0)
return la > ra
}
for window in sorted {
if let root = chooseRoot(in: window) {
let extras = dedupeElements(findToolbarExtras(in: window, root: root) + findTabBarExtras(in: window, root: root))
let modalRoots = findAdditionalWindowRoots(windows: windows, excluding: window, windowFrame: getFrame(window))
return (root, getFrame(window), window, extras, modalRoots)
}
}
return nil
}
private func findAdditionalWindowRoots(
windows: [AXUIElement],
excluding mainWindow: AXUIElement,
windowFrame: AXNode.Frame?
) -> [AXUIElement] {
var roots: [AXUIElement] = []
for window in windows {
for child in getChildren(window) {
if getRole(child) == "AXGroup" {
return (child, getFrame(window))
if CFEqual(window, mainWindow) { continue }
let frame = getFrame(window)
if let windowFrame = windowFrame, !frameIntersects(frame, windowFrame) {
continue
}
if let root = chooseRoot(in: window) {
roots.append(root)
}
}
return dedupeElements(roots)
}
private func dedupeElements(_ elements: [AXUIElement]) -> [AXUIElement] {
var seen: Set<AXWrapper> = []
var result: [AXUIElement] = []
for element in elements {
let wrapper = AXWrapper(element)
if seen.contains(wrapper) { continue }
seen.insert(wrapper)
result.append(element)
}
return result
}
func chooseRoot(in window: AXUIElement) -> AXUIElement? {
let windowFrame = getFrame(window)
let candidates = findGroupCandidates(in: window, windowFrame: windowFrame)
if let best = candidates.first?.element { return best }
return findLargestChildCandidate(in: window, windowFrame: windowFrame)
}
private func findLargestChildCandidate(in window: AXUIElement, windowFrame: AXNode.Frame?) -> AXUIElement? {
var best: (element: AXUIElement, area: Double)? = nil
for child in getChildren(window) {
let children = getChildren(child)
if children.isEmpty { continue }
let area = frameArea(getFrame(child), windowFrame: windowFrame)
if area <= 0 { continue }
if best == nil || area > best!.area {
best = (child, area)
}
}
return best?.element
}
private func frameIntersects(_ frame: AXNode.Frame?, _ target: AXNode.Frame?) -> Bool {
guard let frame = frame, let target = target else { return false }
let fx1 = frame.x
let fy1 = frame.y
let fx2 = frame.x + frame.width
let fy2 = frame.y + frame.height
let tx1 = target.x
let ty1 = target.y
let tx2 = target.x + target.width
let ty2 = target.y + target.height
return fx1 < tx2 && fx2 > tx1 && fy1 < ty2 && fy2 > ty1
}
private func isToolbarLike(_ element: AXUIElement) -> Bool {
let role = (getAttribute(element, kAXRoleAttribute as CFString) as String?) ?? ""
let subrole = (getAttribute(element, kAXSubroleAttribute as CFString) as String?) ?? ""
if role == (kAXToolbarRole as String) ||
role == (kAXTabGroupRole as String) ||
role == "AXTabBar" {
return true
}
if subrole == "AXTabBar" {
return true
}
return false
}
private func isTabBarLike(_ element: AXUIElement) -> Bool {
let role = (getAttribute(element, kAXRoleAttribute as CFString) as String?) ?? ""
let subrole = (getAttribute(element, kAXSubroleAttribute as CFString) as String?) ?? ""
if role == (kAXTabGroupRole as String) || role == "AXTabBar" { return true }
if subrole == "AXTabBar" { return true }
let desc = (getDescription(element) ?? "").lowercased()
if desc.contains("tab bar") { return true }
let label = (getLabel(element) ?? "").lowercased()
if label.contains("tab bar") { return true }
return false
}
private func findToolbarExtras(in window: AXUIElement, root: AXUIElement) -> [AXUIElement] {
let rootFrame = getFrame(root)
let rootIds = collectDescendantWrappers(from: root)
var extras: [AXUIElement] = []
var stack = getChildren(window)
while !stack.isEmpty {
let current = stack.removeLast()
if isToolbarLike(current) && !rootIds.contains(AXWrapper(current)) {
let frame = getFrame(current)
if frameIntersects(frame, rootFrame) {
extras.append(current)
}
}
stack.append(contentsOf: getChildren(current))
}
return nil
return extras
}
private func findTabBarExtras(in window: AXUIElement, root: AXUIElement) -> [AXUIElement] {
let rootFrame = getFrame(root)
let rootIds = collectDescendantWrappers(from: root)
var extras: [AXUIElement] = []
var stack = getChildren(window)
while !stack.isEmpty {
let current = stack.removeLast()
if isTabBarLike(current) && !rootIds.contains(AXWrapper(current)) {
let frame = getFrame(current)
if frameIntersects(frame, rootFrame) {
extras.append(current)
}
}
stack.append(contentsOf: getChildren(current))
}
return extras
}
private struct GroupCandidate {
let element: AXUIElement
let area: Double
let childCount: Int
}
private func findGroupCandidates(in root: AXUIElement, windowFrame: AXNode.Frame?) -> [GroupCandidate] {
var candidates: [GroupCandidate] = []
var visited: Set<AXWrapper> = []
func walk(_ element: AXUIElement) {
let wrapper = AXWrapper(element)
if visited.contains(wrapper) { return }
visited.insert(wrapper)
let children = getChildren(element)
let role = (getAttribute(element, kAXRoleAttribute as CFString) as String?) ?? ""
let isContainer = role == (kAXGroupRole as String) ||
role == (kAXScrollAreaRole as String) ||
role == (kAXUnknownRole as String)
if isContainer {
let hasNonToolbarChild = children.contains {
((getAttribute($0, kAXRoleAttribute as CFString) as String?) ?? "") != (kAXToolbarRole as String)
}
if hasNonToolbarChild {
let frame = getFrame(element)
let area = frameArea(frame, windowFrame: windowFrame)
if area > 0 {
let childCount = children.count
candidates.append(
GroupCandidate(
element: element,
area: area,
childCount: childCount
)
)
}
}
}
for child in children {
walk(child)
}
}
walk(root)
candidates.sort { lhs, rhs in
if lhs.area == rhs.area { return lhs.childCount > rhs.childCount }
return lhs.area > rhs.area
}
return candidates
}
private func frameArea(_ frame: AXNode.Frame?, windowFrame: AXNode.Frame?) -> Double {
guard let frame = frame else { return 0 }
if let windowFrame = windowFrame {
let windowArea = max(1.0, windowFrame.width * windowFrame.height)
let area = frame.width * frame.height
if area > windowArea { return 0 }
return area
}
return frame.width * frame.height
}
private final class AXWrapper: Hashable {
let element: AXUIElement
init(_ element: AXUIElement) { self.element = element }
func hash(into hasher: inout Hasher) { hasher.combine(CFHash(element)) }
static func == (lhs: AXWrapper, rhs: AXWrapper) -> Bool {
return CFEqual(lhs.element, rhs.element)
}
}
private func collectDescendantWrappers(from root: AXUIElement) -> Set<AXWrapper> {
var seen: Set<AXWrapper> = []
var stack = [root]
while !stack.isEmpty {
let current = stack.removeLast()
let wrapper = AXWrapper(current)
if seen.contains(wrapper) { continue }
seen.insert(wrapper)
stack.append(contentsOf: getChildren(current))
}
return seen
}
private struct SnapshotPayload: Codable {
let windowFrame: AXNode.Frame?
let root: AXNode
}
func main() throws {

@@ -153,10 +392,48 @@ guard hasAccessibilityPermission() else {

}
guard let (root, windowFrame) = findIOSAppRoot(in: simulator) else {
let maxAttempts = 5
var snapshot: (AXUIElement, AXNode.Frame?, AXUIElement, [AXUIElement], [AXUIElement])? = nil
for attempt in 0..<maxAttempts {
if let candidate = findIOSAppSnapshot(in: simulator) {
let (root, _, _, _, modalRoots) = candidate
if !getChildren(root).isEmpty || !modalRoots.isEmpty {
snapshot = candidate
break
}
}
if attempt < maxAttempts - 1 {
usleep(300_000)
}
}
guard let (root, windowFrame, _, extras, modalRoots) = snapshot else {
throw AXSnapshotError(message: "Could not find iOS app content in Simulator.")
}
let tree = buildTree(root)
let snapshot = AXSnapshot(windowFrame: windowFrame, root: tree)
var tree = buildTree(root)
if !extras.isEmpty {
let extraNodes = extras.map { buildTree($0) }
tree = AXNode(
role: tree.role,
subrole: tree.subrole,
label: tree.label,
value: tree.value,
identifier: tree.identifier,
frame: tree.frame,
children: tree.children + extraNodes
)
}
if !modalRoots.isEmpty {
let modalNodes = modalRoots.map { buildTree($0) }
tree = AXNode(
role: tree.role,
subrole: tree.subrole,
label: tree.label,
value: tree.value,
identifier: tree.identifier,
frame: tree.frame,
children: tree.children + modalNodes
)
}
let payload = SnapshotPayload(windowFrame: windowFrame, root: tree)
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
let data = try encoder.encode(snapshot)
let data = try encoder.encode(payload)
if let json = String(data: data, encoding: .utf8) {

@@ -163,0 +440,0 @@ print(json)

{
"name": "agent-device",
"version": "0.1.2",
"version": "0.1.3",
"description": "Unified control plane for physical and virtual devices via an agent-driven CLI.",

@@ -19,4 +19,6 @@ "license": "MIT",

"build:swift": "swift build -c release --package-path ios-runner/AXSnapshot",
"build:axsnapshot": "pnpm build:swift && mkdir -p bin && cp -f ios-runner/AXSnapshot/.build/release/axsnapshot bin/axsnapshot && chmod +x bin/axsnapshot",
"build:axsnapshot": "pnpm build:swift && mkdir -p dist/bin && cp -f ios-runner/AXSnapshot/.build/release/axsnapshot dist/bin/axsnapshot && chmod +x dist/bin/axsnapshot",
"build:xcuitest": "AGENT_DEVICE_IOS_CLEAN_DERIVED=1 xcodebuild build-for-testing -project ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj -scheme AgentDeviceRunner -destination \"generic/platform=iOS Simulator\" -derivedDataPath ~/.agent-device/ios-runner/derived",
"build:clis": "pnpm build:node && pnpm build:axsnapshot",
"build:all": "pnpm build:node && pnpm build:axsnapshot && pnpm build:xcuitest",
"format": "prettier --write .",

@@ -33,3 +35,2 @@ "prepublishOnly": "pnpm build:node && pnpm build:axsnapshot",

"dist",
"bin/axsnapshot",
"ios-runner",

@@ -36,0 +37,0 @@ "!ios-runner/**/.build",

@@ -9,3 +9,3 @@ # agent-device

- Platforms: iOS (simulator + limited device support) and Android (emulator + device).
- Core commands: `open`, `press`, `long-press`, `focus`, `type`, `fill`, `scroll`, `scrollintoview`, `screenshot`, `close`.
- Core commands: `open`, `back`, `home`, `app-switcher`, `press`, `long-press`, `focus`, `type`, `fill`, `scroll`, `scrollintoview`, `wait`, `alert`, `screenshot`, `close`.
- Inspection commands: `snapshot` (accessibility tree).

@@ -38,3 +38,7 @@ - Device tooling: `adb` (Android), `simctl`/`devicectl` (iOS via Xcode).

agent-device snapshot
agent-device snapshot -s @e7
agent-device click @e7
agent-device wait text "Camera"
agent-device alert wait 10000
agent-device back
agent-device type "hello"

@@ -48,5 +52,12 @@ agent-device screenshot --out ./screenshot.png

Coordinates:
- All coordinate-based commands (`press`, `long-press`, `focus`, `fill`) use device coordinates with origin at top-left.
- X increases to the right, Y increases downward.
iOS snapshots:
- Default backend is `ax` (fast). It requires enabling Accessibility for the terminal app in System Settings.
- If AX is not available, use `--backend xctest` explicitly.
- If AX shows a container like `group` or `tab bar` without children, re-snapshot with XCTest and scope to the container label:
`snapshot --backend xctest -s "<label>"`
- Use `-s @ref` to scope the snapshot to the label of a prior ref in the current session.

@@ -63,6 +74,5 @@ Flags:

- `--backend ax|xctest` (snapshot only; defaults to `ax` on iOS)
- `open` without args boots/activates the target device/simulator without launching an app.
Sessions:
- `open` starts a session.
- `open` starts a session. Without args boots/activates the target device/simulator without launching an app.
- All interaction commands require an open session.

@@ -73,3 +83,3 @@ - `close` stops the session and releases device resources. Pass an app to close it explicitly, or omit to just close the session.

Snapshot defaults to the AX backend on iOS simulators and falls back to XCTest if AX is unavailable.
Snapshot defaults to the AX backend on iOS simulators. Use `--backend xctest` if AX is unavailable.

@@ -82,4 +92,4 @@ ## App resolution

## iOS notes
- Input commands (`press`, `type`, `scroll`, etc.) are supported only on simulators in v1.
- Support depends on your Xcode version; the CLI reports `UNSUPPORTED_OPERATION` if `simctl io` lacks input operations.
- Input commands (`press`, `type`, `scroll`, etc.) are supported only on simulators in v1 and use the XCTest runner.
- `alert` and `scrollintoview` use the XCTest runner and are simulator-only in v1.

@@ -86,0 +96,0 @@ ## Testing

@@ -87,2 +87,24 @@ import { parseArgs, usage } from './utils/args.ts';

}
if (response.data && typeof response.data === 'object') {
const data = response.data as Record<string, unknown>;
if (command === 'devices') {
const devices = Array.isArray((data as any).devices) ? (data as any).devices : [];
const lines = devices.map((d: any) => {
const name = d?.name ?? d?.id ?? 'unknown';
const platform = d?.platform ?? 'unknown';
const kind = d?.kind ? ` ${d.kind}` : '';
const booted = typeof d?.booted === 'boolean' ? ` booted=${d.booted}` : '';
return `${name} (${platform}${kind})${booted}`;
});
process.stdout.write(`${lines.join('\n')}\n`);
if (logTailStopper) logTailStopper();
return;
}
if (command === 'apps') {
const apps = Array.isArray((data as any).apps) ? (data as any).apps : [];
process.stdout.write(`${apps.join('\n')}\n`);
if (logTailStopper) logTailStopper();
return;
}
}
if (logTailStopper) logTailStopper();

@@ -89,0 +111,0 @@ return;

import { AppError } from '../utils/errors.ts';
import { selectDevice, type DeviceInfo } from '../utils/device.ts';
import { listAndroidDevices } from '../platforms/android/devices.ts';
import { ensureAdb, snapshotAndroid } from '../platforms/android/index.ts';
import { appSwitcherAndroid, backAndroid, ensureAdb, homeAndroid, snapshotAndroid } from '../platforms/android/index.ts';
import { listIosDevices } from '../platforms/ios/devices.ts';

@@ -26,2 +26,3 @@ import { getInteractor } from '../utils/interactors.ts';

recordJson?: boolean;
appsFilter?: 'launchable' | 'user-installed' | 'all';
};

@@ -193,4 +194,22 @@

case 'scrollintoview': {
const text = positionals.join(' ');
const text = positionals.join(' ').trim();
if (!text) throw new AppError('INVALID_ARGS', 'scrollintoview requires text');
if (device.platform === 'ios' && device.kind === 'simulator') {
const maxAttempts = 8;
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
const found = (await runIosRunnerCommand(
device,
{ command: 'findText', text, appBundleId: context?.appBundleId },
{ verbose: context?.verbose, logPath: context?.logPath },
)) as { found?: boolean };
if (found?.found) return { text, attempts: attempt + 1 };
await runIosRunnerCommand(
device,
{ command: 'swipe', direction: 'up', appBundleId: context?.appBundleId },
{ verbose: context?.verbose, logPath: context?.logPath },
);
await new Promise((resolve) => setTimeout(resolve, 300));
}
throw new AppError('COMMAND_FAILED', `scrollintoview could not find text: ${text}`);
}
await interactor.scrollIntoView(text);

@@ -204,2 +223,47 @@ return { text };

}
case 'back': {
if (device.platform === 'ios') {
if (device.kind !== 'simulator') {
throw new AppError('UNSUPPORTED_OPERATION', 'back is only supported on iOS simulators in v1');
}
await runIosRunnerCommand(
device,
{ command: 'back', appBundleId: context?.appBundleId },
{ verbose: context?.verbose, logPath: context?.logPath },
);
return { action: 'back' };
}
await backAndroid(device);
return { action: 'back' };
}
case 'home': {
if (device.platform === 'ios') {
if (device.kind !== 'simulator') {
throw new AppError('UNSUPPORTED_OPERATION', 'home is only supported on iOS simulators in v1');
}
await runIosRunnerCommand(
device,
{ command: 'home', appBundleId: context?.appBundleId },
{ verbose: context?.verbose, logPath: context?.logPath },
);
return { action: 'home' };
}
await homeAndroid(device);
return { action: 'home' };
}
case 'app-switcher': {
if (device.platform === 'ios') {
if (device.kind !== 'simulator') {
throw new AppError('UNSUPPORTED_OPERATION', 'app-switcher is only supported on iOS simulators in v1');
}
await runIosRunnerCommand(
device,
{ command: 'appSwitcher', appBundleId: context?.appBundleId },
{ verbose: context?.verbose, logPath: context?.logPath },
);
return { action: 'app-switcher' };
}
await appSwitcherAndroid(device);
return { action: 'app-switcher' };
}
case 'snapshot': {

@@ -206,0 +270,0 @@ const backend = context?.snapshotBackend ?? 'ax';

@@ -19,2 +19,4 @@ import net from 'node:net';

import { runIosRunnerCommand, stopIosRunnerSession } from './platforms/ios/runner-client.ts';
import { runCmd, runCmdBackground } from './utils/exec.ts';
import { snapshotAndroid } from './platforms/android/index.ts';

@@ -41,2 +43,9 @@ type DaemonRequest = {

actions: SessionAction[];
recording?: {
platform: 'ios' | 'android';
outPath: string;
remotePath?: string;
child: ReturnType<typeof import('node:child_process').spawn>;
wait: Promise<import('./utils/exec.ts').ExecResult>;
};
};

@@ -117,2 +126,67 @@

if (command === 'devices') {
try {
const devices: DeviceInfo[] = [];
if (req.flags?.platform === 'android') {
const { listAndroidDevices } = await import('./platforms/android/devices.ts');
devices.push(...(await listAndroidDevices()));
} else if (req.flags?.platform === 'ios') {
const { listIosDevices } = await import('./platforms/ios/devices.ts');
devices.push(...(await listIosDevices()));
} else {
const { listAndroidDevices } = await import('./platforms/android/devices.ts');
const { listIosDevices } = await import('./platforms/ios/devices.ts');
try {
devices.push(...(await listAndroidDevices()));
} catch {
// ignore
}
try {
devices.push(...(await listIosDevices()));
} catch {
// ignore
}
}
return { ok: true, data: { devices } };
} catch (err) {
const appErr = asAppError(err);
return { ok: false, error: { code: appErr.code, message: appErr.message, details: appErr.details } };
}
}
if (command === 'apps') {
const session = sessions.get(sessionName);
const flags = req.flags ?? {};
if (
!session &&
!flags.platform &&
!flags.device &&
!flags.udid &&
!flags.serial
) {
return {
ok: false,
error: {
code: 'INVALID_ARGS',
message: 'apps requires an active session or an explicit device selector (e.g. --platform ios).',
},
};
}
const device = session?.device ?? (await resolveTargetDevice(flags));
await ensureDeviceReady(device);
if (device.platform === 'ios') {
if (device.kind !== 'simulator') {
return { ok: false, error: { code: 'UNSUPPORTED_OPERATION', message: 'apps list is only supported on iOS simulators' } };
}
const { listSimulatorApps } = await import('./platforms/ios/index.ts');
const apps = (await listSimulatorApps(device)).map((app) =>
app.name && app.name !== app.bundleId ? `${app.name} (${app.bundleId})` : app.bundleId,
);
return { ok: true, data: { apps } };
}
const { listAndroidApps } = await import('./platforms/android/index.ts');
const apps = await listAndroidApps(device, req.flags?.appsFilter);
return { ok: true, data: { apps } };
}
if (command === 'open') {

@@ -232,4 +306,20 @@ if (sessions.has(sessionName)) {

const appBundleId = session?.appBundleId;
let snapshotScope = req.flags?.snapshotScope;
if (snapshotScope && snapshotScope.trim().startsWith('@')) {
if (!session?.snapshot) {
return { ok: false, error: { code: 'INVALID_ARGS', message: 'Ref scope requires an existing snapshot in session.' } };
}
const ref = normalizeRef(snapshotScope.trim());
if (!ref) {
return { ok: false, error: { code: 'INVALID_ARGS', message: `Invalid ref scope: ${snapshotScope}` } };
}
const node = findNodeByRef(session.snapshot.nodes, ref);
const resolved = node ? resolveRefLabel(node, session.snapshot.nodes) : undefined;
if (!resolved) {
return { ok: false, error: { code: 'COMMAND_FAILED', message: `Ref ${snapshotScope} not found or has no label` } };
}
snapshotScope = resolved;
}
const data = (await dispatchCommand(device, 'snapshot', [], req.flags?.out, {
...contextFromFlags(req.flags, appBundleId),
...contextFromFlags({ ...req.flags, snapshotScope }, appBundleId),
})) as {

@@ -240,4 +330,4 @@ nodes?: RawSnapshotNode[];

};
const pruned = pruneGroupNodes(data?.nodes ?? []);
const nodes = attachRefs(pruned);
const rawNodes = data?.nodes ?? [];
const nodes = attachRefs(req.flags?.snapshotRaw ? rawNodes : pruneGroupNodes(rawNodes));
const snapshot: SnapshotState = {

@@ -275,2 +365,225 @@ nodes,

if (command === 'wait') {
const session = sessions.get(sessionName);
const device = session?.device ?? (await resolveTargetDevice(req.flags ?? {}));
if (!session) {
await ensureDeviceReady(device);
}
const args = req.positionals ?? [];
if (args.length === 0) {
return { ok: false, error: { code: 'INVALID_ARGS', message: 'wait requires a duration or text' } };
}
const parseTimeout = (value: string | undefined): number | null => {
if (!value) return null;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
};
const sleepMs = parseTimeout(args[0]);
if (sleepMs !== null) {
await new Promise((resolve) => setTimeout(resolve, sleepMs));
if (session) {
recordAction(session, { command, positionals: req.positionals ?? [], flags: req.flags ?? {}, result: { waitedMs: sleepMs } });
}
return { ok: true, data: { waitedMs: sleepMs } };
}
let text = '';
let timeoutMs: number | null = null;
if (args[0] === 'text') {
timeoutMs = parseTimeout(args[args.length - 1]);
text = timeoutMs !== null ? args.slice(1, -1).join(' ') : args.slice(1).join(' ');
} else if (args[0].startsWith('@')) {
if (!session?.snapshot) {
return { ok: false, error: { code: 'INVALID_ARGS', message: 'Ref wait requires an existing snapshot in session.' } };
}
const ref = normalizeRef(args[0]);
if (!ref) {
return { ok: false, error: { code: 'INVALID_ARGS', message: `Invalid ref: ${args[0]}` } };
}
const node = findNodeByRef(session.snapshot.nodes, ref);
const resolved = node ? resolveRefLabel(node, session.snapshot.nodes) : undefined;
if (!resolved) {
return { ok: false, error: { code: 'COMMAND_FAILED', message: `Ref ${args[0]} not found or has no label` } };
}
timeoutMs = parseTimeout(args[args.length - 1]);
text = resolved;
} else {
timeoutMs = parseTimeout(args[args.length - 1]);
text = timeoutMs !== null ? args.slice(0, -1).join(' ') : args.join(' ');
}
text = text.trim();
if (!text) {
return { ok: false, error: { code: 'INVALID_ARGS', message: 'wait requires text' } };
}
const timeout = timeoutMs ?? 10000;
const start = Date.now();
while (Date.now() - start < timeout) {
if (device.platform === 'ios' && device.kind === 'simulator') {
const result = (await runIosRunnerCommand(
device,
{ command: 'findText', text, appBundleId: session?.appBundleId },
{ verbose: req.flags?.verbose, logPath },
)) as { found?: boolean };
if (result?.found) {
if (session) {
recordAction(session, { command, positionals: req.positionals ?? [], flags: req.flags ?? {}, result: { text, waitedMs: Date.now() - start } });
}
return { ok: true, data: { text, waitedMs: Date.now() - start } };
}
} else if (device.platform === 'android') {
const androidResult = await snapshotAndroid(device, { scope: text });
if (findNodeByLabel(attachRefs(androidResult.nodes ?? []), text)) {
if (session) {
recordAction(session, { command, positionals: req.positionals ?? [], flags: req.flags ?? {}, result: { text, waitedMs: Date.now() - start } });
}
return { ok: true, data: { text, waitedMs: Date.now() - start } };
}
} else {
return { ok: false, error: { code: 'UNSUPPORTED_OPERATION', message: 'wait is not supported on this device' } };
}
await new Promise((resolve) => setTimeout(resolve, 300));
}
return { ok: false, error: { code: 'COMMAND_FAILED', message: `wait timed out for text: ${text}` } };
}
if (command === 'alert') {
const session = sessions.get(sessionName);
const device = session?.device ?? (await resolveTargetDevice(req.flags ?? {}));
if (!session) {
await ensureDeviceReady(device);
}
const action = (req.positionals?.[0] ?? 'get').toLowerCase();
const parseTimeout = (value: string | undefined): number | null => {
if (!value) return null;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
};
if (device.platform !== 'ios' || device.kind !== 'simulator') {
return { ok: false, error: { code: 'UNSUPPORTED_OPERATION', message: 'alert is only supported on iOS simulators in v1' } };
}
if (action === 'wait') {
const timeout = parseTimeout(req.positionals?.[1]) ?? 10000;
const start = Date.now();
while (Date.now() - start < timeout) {
try {
const data = await runIosRunnerCommand(
device,
{ command: 'alert', action: 'get', appBundleId: session?.appBundleId },
{ verbose: req.flags?.verbose, logPath },
);
if (session) {
recordAction(session, {
command,
positionals: req.positionals ?? [],
flags: req.flags ?? {},
result: data,
});
}
return { ok: true, data };
} catch {
// keep waiting
}
await new Promise((resolve) => setTimeout(resolve, 300));
}
return { ok: false, error: { code: 'COMMAND_FAILED', message: 'alert wait timed out' } };
}
const data = await runIosRunnerCommand(
device,
{
command: 'alert',
action: action === 'accept' || action === 'dismiss' ? (action as 'accept' | 'dismiss') : 'get',
appBundleId: session?.appBundleId,
},
{ verbose: req.flags?.verbose, logPath },
);
if (session) {
recordAction(session, {
command,
positionals: req.positionals ?? [],
flags: req.flags ?? {},
result: data,
});
}
return { ok: true, data };
}
if (command === 'record') {
const action = (req.positionals?.[0] ?? '').toLowerCase();
if (!['start', 'stop'].includes(action)) {
return { ok: false, error: { code: 'INVALID_ARGS', message: 'record requires start|stop' } };
}
const session = sessions.get(sessionName);
const device = session?.device ?? (await resolveTargetDevice(req.flags ?? {}));
if (!session) {
await ensureDeviceReady(device);
}
const activeSession = session ?? {
name: sessionName,
device,
createdAt: Date.now(),
actions: [],
};
if (action === 'start') {
if (activeSession.recording) {
return { ok: false, error: { code: 'INVALID_ARGS', message: 'recording already in progress' } };
}
const outPath = req.positionals?.[1] ?? `./recording-${Date.now()}.mp4`;
const resolvedOut = path.resolve(outPath);
const outDir = path.dirname(resolvedOut);
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir, { recursive: true });
}
if (device.platform === 'ios') {
if (device.kind !== 'simulator') {
return { ok: false, error: { code: 'UNSUPPORTED_OPERATION', message: 'record is only supported on iOS simulators in v1' } };
}
const { child, wait } = runCmdBackground('xcrun', ['simctl', 'io', device.id, 'recordVideo', resolvedOut], {
allowFailure: true,
});
activeSession.recording = { platform: 'ios', outPath: resolvedOut, child, wait };
} else {
const remotePath = `/sdcard/agent-device-recording-${Date.now()}.mp4`;
const { child, wait } = runCmdBackground('adb', ['-s', device.id, 'shell', 'screenrecord', remotePath], {
allowFailure: true,
});
activeSession.recording = { platform: 'android', outPath: resolvedOut, remotePath, child, wait };
}
sessions.set(sessionName, activeSession);
recordAction(activeSession, {
command,
positionals: req.positionals ?? [],
flags: req.flags ?? {},
result: { action: 'start' },
});
return { ok: true, data: { recording: 'started', outPath } };
}
if (!activeSession.recording) {
return { ok: false, error: { code: 'INVALID_ARGS', message: 'no active recording' } };
}
const recording = activeSession.recording;
recording.child.kill('SIGINT');
try {
await recording.wait;
} catch {
// ignore
}
if (recording.platform === 'android' && recording.remotePath) {
try {
await runCmd('adb', ['-s', device.id, 'pull', recording.remotePath, recording.outPath], { allowFailure: true });
await runCmd('adb', ['-s', device.id, 'shell', 'rm', '-f', recording.remotePath], { allowFailure: true });
} catch {
// ignore
}
}
activeSession.recording = undefined;
recordAction(activeSession, {
command,
positionals: req.positionals ?? [],
flags: req.flags ?? {},
result: { action: 'stop', outPath: recording.outPath },
});
return { ok: true, data: { recording: 'stopped', outPath: recording.outPath } };
}
if (command === 'click') {

@@ -419,35 +732,2 @@ const session = sessions.get(sessionName);

if (command === 'rect') {
const session = sessions.get(sessionName);
if (!session?.snapshot) {
return { ok: false, error: { code: 'INVALID_ARGS', message: 'No snapshot in session. Run snapshot first.' } };
}
const target = req.positionals?.[0] ?? '';
const ref = normalizeRef(target);
let label = '';
if (ref) {
const node = findNodeByRef(session.snapshot.nodes, ref);
label = node?.label?.trim() ?? '';
} else {
label = req.positionals.join(' ').trim();
}
if (!label) {
return { ok: false, error: { code: 'INVALID_ARGS', message: 'rect requires a label or ref with label' } };
}
if (session.device.platform !== 'ios' || session.device.kind !== 'simulator') {
return { ok: false, error: { code: 'UNSUPPORTED_OPERATION', message: 'rect is only supported on iOS simulators' } };
}
const data = await runIosRunnerCommand(
session.device,
{ command: 'rect', text: label, appBundleId: session.appBundleId },
{ verbose: req.flags?.verbose, logPath },
);
recordAction(session, {
command,
positionals: req.positionals ?? [],
flags: req.flags ?? {},
result: { label, rect: data?.rect },
});
return { ok: true, data: { label, rect: data?.rect } };
}

@@ -846,3 +1126,7 @@ const session = sessions.get(sessionName);

const type = normalizeType(node.type ?? '');
if (type === 'group' || type === 'ioscontentgroup') {
const labelCandidate = [node.label, node.value, node.identifier]
.map((value) => (typeof value === 'string' ? value.trim() : ''))
.find((value) => value && value.length > 0);
const hasMeaningfulLabel = labelCandidate ? isMeaningfulLabel(labelCandidate) : false;
if ((type === 'group' || type === 'ioscontentgroup') && !hasMeaningfulLabel) {
skippedDepths.push(depth);

@@ -849,0 +1133,0 @@ continue;

@@ -46,2 +46,49 @@ import { promises as fs } from 'node:fs';

export async function listAndroidApps(
device: DeviceInfo,
filter: 'launchable' | 'user-installed' | 'all' = 'launchable',
): Promise<string[]> {
if (filter === 'launchable') {
const result = await runCmd(
'adb',
adbArgs(device, [
'shell',
'cmd',
'package',
'query-activities',
'--brief',
'-a',
'android.intent.action.MAIN',
'-c',
'android.intent.category.LAUNCHER',
]),
{ allowFailure: true },
);
if (result.exitCode === 0 && result.stdout.trim().length > 0) {
const packages = new Set<string>();
for (const line of result.stdout.split('\n')) {
const trimmed = line.trim();
if (!trimmed) continue;
const firstToken = trimmed.split(/\s+/)[0];
const pkg = firstToken.includes('/') ? firstToken.split('/')[0] : firstToken;
if (pkg) packages.add(pkg);
}
if (packages.size > 0) {
return Array.from(packages);
}
}
// fallback: list all if query-activities not available
}
const args =
filter === 'user-installed'
? ['shell', 'pm', 'list', 'packages', '-3']
: ['shell', 'pm', 'list', 'packages'];
const result = await runCmd('adb', adbArgs(device, args));
return result.stdout
.split('\n')
.map((line: string) => line.replace('package:', '').trim())
.filter(Boolean);
}
export async function openAndroidApp(device: DeviceInfo, app: string): Promise<void> {

@@ -93,2 +140,14 @@ if (!device.booted) {

export async function backAndroid(device: DeviceInfo): Promise<void> {
await runCmd('adb', adbArgs(device, ['shell', 'input', 'keyevent', '4']));
}
export async function homeAndroid(device: DeviceInfo): Promise<void> {
await runCmd('adb', adbArgs(device, ['shell', 'input', 'keyevent', '3']));
}
export async function appSwitcherAndroid(device: DeviceInfo): Promise<void> {
await runCmd('adb', adbArgs(device, ['shell', 'input', 'keyevent', '187']));
}
export async function longPressAndroid(

@@ -95,0 +154,0 @@ device: DeviceInfo,

@@ -35,2 +35,6 @@ import path from 'node:path';

}
if (stderrText.toLowerCase().includes('could not find ios app content')) {
hint =
' AX snapshot sometimes caches empty content. Try restarting the Simulator app.';
}
throw new AppError('COMMAND_FAILED', 'AX snapshot failed', {

@@ -131,10 +135,4 @@ stderr: `${stderrText}${hint}`,

if (envPath && fs.existsSync(envPath)) return envPath;
const packagedCandidates = [
path.join(projectRoot, 'bin', 'axsnapshot'),
path.join(projectRoot, 'dist', 'bin', 'axsnapshot'),
path.join(projectRoot, 'dist', 'axsnapshot'),
];
for (const candidate of packagedCandidates) {
if (fs.existsSync(candidate)) return candidate;
}
const packagedPath = path.join(projectRoot, 'dist', 'bin', 'axsnapshot');
if (fs.existsSync(packagedPath)) return packagedPath;
const binaryPath = path.join(packageDir, '.build', 'release', 'axsnapshot');

@@ -141,0 +139,0 @@ if (fs.existsSync(binaryPath)) return binaryPath;

@@ -166,3 +166,3 @@ import { runCmd } from '../../utils/exec.ts';

async function listSimulatorApps(
export async function listSimulatorApps(
device: DeviceInfo,

@@ -172,15 +172,36 @@ ): Promise<{ bundleId: string; name: string }[]> {

const stdout = result.stdout as string;
if (!stdout.trim().startsWith('{')) return [];
try {
const payload = JSON.parse(stdout) as Record<
string,
{ CFBundleDisplayName?: string; CFBundleName?: string }
>;
return Object.entries(payload).map(([bundleId, info]) => ({
bundleId,
name: info.CFBundleDisplayName ?? info.CFBundleName ?? bundleId,
}));
} catch {
return [];
const trimmed = stdout.trim();
if (!trimmed) return [];
let parsed: Record<string, { CFBundleDisplayName?: string; CFBundleName?: string }> | null = null;
if (trimmed.startsWith('{')) {
try {
parsed = JSON.parse(trimmed) as Record<
string,
{ CFBundleDisplayName?: string; CFBundleName?: string }
>;
} catch {
parsed = null;
}
}
if (!parsed && trimmed.startsWith('{')) {
try {
const converted = await runCmd('plutil', ['-convert', 'json', '-o', '-', '-'], {
allowFailure: true,
stdin: trimmed,
});
if (converted.exitCode === 0 && converted.stdout.trim().startsWith('{')) {
parsed = JSON.parse(converted.stdout) as Record<
string,
{ CFBundleDisplayName?: string; CFBundleName?: string }
>;
}
} catch {
parsed = null;
}
}
if (!parsed) return [];
return Object.entries(parsed).map(([bundleId, info]) => ({
bundleId,
name: info.CFBundleDisplayName ?? info.CFBundleName ?? bundleId,
}));
}

@@ -187,0 +208,0 @@

@@ -11,5 +11,17 @@ import fs from 'node:fs';

export type RunnerCommand = {
command: 'tap' | 'type' | 'swipe' | 'findText' | 'listTappables' | 'snapshot' | 'rect' | 'shutdown';
command:
| 'tap'
| 'type'
| 'swipe'
| 'findText'
| 'listTappables'
| 'snapshot'
| 'back'
| 'home'
| 'appSwitcher'
| 'alert'
| 'shutdown';
appBundleId?: string;
text?: string;
action?: 'get' | 'accept' | 'dismiss';
x?: number;

@@ -16,0 +28,0 @@ y?: number;

@@ -21,2 +21,3 @@ import { AppError } from './errors.ts';

snapshotBackend?: 'ax' | 'xctest';
appsFilter?: 'launchable' | 'user-installed' | 'all';
noRecord?: boolean;

@@ -66,2 +67,10 @@ recordJson?: boolean;

}
if (arg === '--user-installed') {
flags.appsFilter = 'user-installed';
continue;
}
if (arg === '--all') {
flags.appsFilter = 'all';
continue;
}
if (arg.startsWith('--backend')) {

@@ -161,4 +170,10 @@ const value = arg.includes('=')

xctest: XCTest snapshot (slower, no permissions)
devices List available devices
apps [--user-installed|--all] List installed apps (Android launchable by default, iOS simulator)
back Navigate back (where supported)
home Go to home screen (where supported)
app-switcher Open app switcher (where supported)
wait <ms>|text <text>|@ref [timeoutMs] Wait for duration or text to appear
alert [get|accept|dismiss|wait] [timeout] Inspect or handle alert (iOS simulator)
click <@ref> Click element by snapshot ref
rect <label|@ref> Fetch element frame by label or ref (iOS sim)
get text <@ref> Return element text by ref

@@ -175,2 +190,4 @@ get attrs <@ref> Return element attributes by ref

screenshot [--out path] Capture screenshot
record start [path] Start screen recording
record stop Stop screen recording
session list List active sessions

@@ -189,3 +206,5 @@

--record-json Record JSON session log
--user-installed Apps: list user-installed packages (Android only)
--all Apps: list all packages (Android only)
`;
}

@@ -16,2 +16,3 @@ import { spawn, spawnSync } from 'node:child_process';

binaryStdout?: boolean;
stdin?: string | Buffer;
};

@@ -24,2 +25,7 @@

export type ExecBackgroundResult = {
child: ReturnType<typeof spawn>;
wait: Promise<ExecResult>;
};
export async function runCmd(

@@ -34,3 +40,3 @@ cmd: string,

env: options.env,
stdio: ['ignore', 'pipe', 'pipe'],
stdio: ['pipe', 'pipe', 'pipe'],
});

@@ -45,2 +51,7 @@

if (options.stdin !== undefined) {
child.stdin.write(options.stdin);
}
child.stdin.end();
child.stdout.on('data', (chunk) => {

@@ -103,4 +114,5 @@ if (options.binaryStdout) {

env: options.env,
stdio: ['ignore', 'pipe', 'pipe'],
stdio: ['pipe', 'pipe', 'pipe'],
encoding: options.binaryStdout ? undefined : 'utf8',
input: options.stdin,
});

@@ -162,3 +174,3 @@

env: options.env,
stdio: ['ignore', 'pipe', 'pipe'],
stdio: ['pipe', 'pipe', 'pipe'],
});

@@ -173,2 +185,7 @@

if (options.stdin !== undefined) {
child.stdin.write(options.stdin);
}
child.stdin.end();
child.stdout.on('data', (chunk) => {

@@ -221,2 +238,56 @@ if (options.binaryStdout) {

export function runCmdBackground(
cmd: string,
args: string[],
options: ExecOptions = {},
): ExecBackgroundResult {
const child = spawn(cmd, args, {
cwd: options.cwd,
env: options.env,
stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (chunk) => {
stdout += chunk;
});
child.stderr.on('data', (chunk) => {
stderr += chunk;
});
const wait = new Promise<ExecResult>((resolve, reject) => {
child.on('error', (err) => {
const code = (err as NodeJS.ErrnoException).code;
if (code === 'ENOENT') {
reject(new AppError('TOOL_MISSING', `${cmd} not found in PATH`, { cmd }, err));
return;
}
reject(new AppError('COMMAND_FAILED', `Failed to run ${cmd}`, { cmd, args }, err));
});
child.on('close', (code) => {
const exitCode = code ?? 1;
if (exitCode !== 0 && !options.allowFailure) {
reject(
new AppError('COMMAND_FAILED', `${cmd} exited with code ${exitCode}`, {
cmd,
args,
stdout,
stderr,
exitCode,
}),
);
return;
}
resolve({ stdout, stderr, exitCode });
});
});
return { child, wait };
}
export function whichCmdSync(cmd: string): boolean {

@@ -223,0 +294,0 @@ try {

Sorry, the diff of this file is not supported yet