agent-device 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -121,6 +121,13 @@ Find (semantic):
121
121
  - `find text|label|value|role|id <value> <action> [value]` for specific locators.
122
122
  - Actions: `click` (default), `fill`, `type`, `focus`, `get text`, `get attrs`, `wait [timeout]`, `exists`.
123
123
 
124
+ Android fill reliability:
125
+ - `fill` clears the current value, then enters text.
126
+ - `type` enters text into the focused field without clearing.
127
+ - `fill` now verifies the entered value on Android.
128
+ - If value does not match, agent-device clears the field and retries once with slower typing.
129
+ - This reduces IME-related character swaps on long strings (e.g. emails and IDs).
130
+
124
131
  Settings helpers (simulators):
125
132
  - `settings wifi on|off`
126
133
  - `settings airplane on|off`
package/dist/src/bin.js CHANGED
@@ -1,8 +1,8 @@
1
- import{node_path as e,fileURLToPath as t,asAppError as r,pathToFileURL as n,runCmdDetached as a,node_fs as s,node_os as o,node_net as i,errors_AppError as l}from"./861.js";function c(e){process.stdout.write(`${JSON.stringify(e,null,2)}
2
- `)}function u(e){let t=e.details?`
1
+ import{node_path as e,fileURLToPath as t,asAppError as r,pathToFileURL as n,runCmdDetached as a,node_fs as i,node_os as s,node_net as o,errors_AppError as l}from"./861.js";function c(e){process.stdout.write(`${JSON.stringify(e,null,2)}
2
+ `)}function d(e){let t=e.details?`
3
3
  ${JSON.stringify(e.details,null,2)}`:"";process.stderr.write(`Error (${e.code}): ${e.message}${t}
4
- `)}let d=e.join(o.homedir(),".agent-device"),p=e.join(d,"daemon.json"),f=function(){let e=process.env.AGENT_DEVICE_DAEMON_TIMEOUT_MS;if(!e)return 6e4;let t=Number(e);return Number.isFinite(t)?Math.max(1e3,Math.floor(t)):6e4}();async function m(e){let t=await h(),r={...e,token:t.token};return await b(t,r)}async function h(){let t=y(),r=function(){try{let t=v();return JSON.parse(s.readFileSync(e.join(t,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}();if(t&&t.version===r&&await w(t))return t;t&&(t.version!==r||!await w(t))&&s.existsSync(p)&&s.unlinkSync(p),await g();let n=Date.now();for(;Date.now()-n<5e3;){let e=y();if(e&&await w(e))return e;await new Promise(e=>setTimeout(e,100))}throw new l("COMMAND_FAILED","Failed to start daemon",{infoPath:p,hint:"Run pnpm build, or delete ~/.agent-device/daemon.json if stale."})}function y(){if(!s.existsSync(p))return null;try{let e=JSON.parse(s.readFileSync(p,"utf8"));if(!e.port||!e.token)return null;return e}catch{return null}}async function w(e){return new Promise(t=>{let r=i.createConnection({host:"127.0.0.1",port:e.port},()=>{r.destroy(),t(!0)});r.on("error",()=>{t(!1)})})}async function g(){let t=v(),r=e.join(t,"dist","src","daemon.js"),n=e.join(t,"src","daemon.ts"),o=s.existsSync(r);if(!o&&!s.existsSync(n))throw new l("COMMAND_FAILED","Daemon entry not found",{distPath:r,srcPath:n});let i=o?[r]:["--experimental-strip-types",n];a(process.execPath,i)}async function b(e,t){return new Promise((r,n)=>{let a=i.createConnection({host:"127.0.0.1",port:e.port},()=>{a.write(`${JSON.stringify(t)}
5
- `)}),s=setTimeout(()=>{a.destroy(),n(new l("COMMAND_FAILED","Daemon request timed out",{timeoutMs:f}))},f),o="";a.setEncoding("utf8"),a.on("data",e=>{let t=(o+=e).indexOf("\n");if(-1===t)return;let i=o.slice(0,t).trim();if(i)try{let e=JSON.parse(i);a.end(),clearTimeout(s),r(e)}catch(e){clearTimeout(s),n(e)}}),a.on("error",e=>{clearTimeout(s),n(e)})})}function v(){let r=e.dirname(t(import.meta.url)),n=r;for(let t=0;t<6;t+=1){let t=e.join(n,"package.json");if(s.existsSync(t))return n;n=e.dirname(n)}return r}async function $(t){let n=function(e){let t={json:!1,help:!1},r=[];for(let n=0;n<e.length;n+=1){let a=e[n];if("--json"===a){t.json=!0;continue}if("--help"===a||"-h"===a){t.help=!0;continue}if("--verbose"===a||"-v"===a){t.verbose=!0;continue}if("-i"===a){t.snapshotInteractiveOnly=!0;continue}if("-c"===a){t.snapshotCompact=!0;continue}if("--raw"===a){t.snapshotRaw=!0;continue}if("--no-record"===a){t.noRecord=!0;continue}if("--record-json"===a){t.recordJson=!0;continue}if("--user-installed"===a){t.appsFilter="user-installed";continue}if("--all"===a){t.appsFilter="all";continue}if("--metadata"===a){t.appsMetadata=!0;continue}if(a.startsWith("--backend")){let r=a.includes("=")?a.split("=")[1]:e[n+1];if(a.includes("=")||(n+=1),"ax"!==r&&"xctest"!==r)throw new l("INVALID_ARGS",`Invalid backend: ${r}`);t.snapshotBackend=r;continue}if(a.startsWith("--")){let[r,s]=a.split("="),o=s??e[n+1];switch(!s&&(n+=1),r){case"--platform":if("ios"!==o&&"android"!==o)throw new l("INVALID_ARGS",`Invalid platform: ${o}`);t.platform=o;break;case"--depth":{let e=Number(o);if(!Number.isFinite(e)||e<0)throw new l("INVALID_ARGS",`Invalid depth: ${o}`);t.snapshotDepth=Math.floor(e);break}case"--scope":t.snapshotScope=o;break;case"--device":t.device=o;break;case"--udid":t.udid=o;break;case"--serial":t.serial=o;break;case"--out":t.out=o;break;case"--session":t.session=o;break;case"--activity":t.activity=o;break;default:throw new l("INVALID_ARGS",`Unknown flag: ${r}`)}continue}if("-d"===a){let r=e[n+1];n+=1;let a=Number(r);if(!Number.isFinite(a)||a<0)throw new l("INVALID_ARGS",`Invalid depth: ${r}`);t.snapshotDepth=Math.floor(a);continue}if("-s"===a){let r=e[n+1];n+=1,t.snapshotScope=r;continue}r.push(a)}return{command:r.shift()??null,positionals:r,flags:t}}(t);(n.flags.help||!n.command)&&(process.stdout.write(`agent-device <command> [args] [--json]
4
+ `)}function u(e,t,r){let n=p(e.type??"Element"),a=function(e,t){var r,n;let a=e.label?.trim(),i=e.value?.trim();if("text-field"===(r=t)||"text-view"===r||"search"===r){if(i)return i;if(a)return a}else if(a)return a;if(i)return i;let s=e.identifier?.trim();return!s||(n=s,/^[\w.]+:id\/[\w.-]+$/i.test(n)&&("group"===t||"image"===t||"list"===t||"collection"===t))?"":s}(e,n),i=" ".repeat(t),s=e.ref?`@${e.ref}`:"",o=[!1===e.enabled?"disabled":null].filter(Boolean).join(", "),l=o?` [${o}]`:"",c=a?` "${a}"`:"";return r?`${i}${s} [${n}]${l}`.trimEnd():`${i}${s} [${n}]${c}${l}`.trimEnd()}function p(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase(),r=e.includes(".")&&(e.startsWith("android.")||e.startsWith("androidx.")||e.startsWith("com."));switch(t.startsWith("ax")&&(t=t.replace(/^ax/,"")),t.includes(".")&&(t=t.replace(/^android\.widget\./,"").replace(/^android\.view\./,"").replace(/^android\.webkit\./,"").replace(/^androidx\./,"").replace(/^com\.google\.android\./,"").replace(/^com\.android\./,"")),t){case"application":return"application";case"navigationbar":return"navigation-bar";case"tabbar":return"tab-bar";case"button":case"imagebutton":return"button";case"link":return"link";case"cell":return"cell";case"statictext":case"checkedtextview":return"text";case"textfield":case"edittext":return"text-field";case"textview":return r?"text":"text-view";case"textarea":return"text-view";case"switch":return"switch";case"slider":return"slider";case"image":case"imageview":return"image";case"webview":return"webview";case"framelayout":case"linearlayout":case"relativelayout":case"constraintlayout":case"viewgroup":case"view":case"group":return"group";case"listview":case"recyclerview":return"list";case"collectionview":return"collection";case"searchfield":return"search";case"segmentedcontrol":return"segmented-control";case"window":return"window";case"checkbox":return"checkbox";case"radio":return"radio";case"menuitem":return"menu-item";case"toolbar":return"toolbar";case"scrollarea":case"scrollview":case"nestedscrollview":return"scroll-area";case"table":return"table";default:return t||"element"}}let f=e.join(s.homedir(),".agent-device"),m=e.join(f,"daemon.json"),h=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 w(e){let t=await y(),r={...e,token:t.token};return await x(t,r)}async function y(){let t=g(),r=function(){try{let t=$();return JSON.parse(i.readFileSync(e.join(t,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}();if(t&&t.version===r&&await v(t))return t;t&&(t.version!==r||!await v(t))&&i.existsSync(m)&&i.unlinkSync(m),await b();let n=Date.now();for(;Date.now()-n<5e3;){let e=g();if(e&&await v(e))return e;await new Promise(e=>setTimeout(e,100))}throw new l("COMMAND_FAILED","Failed to start daemon",{infoPath:m,hint:"Run pnpm build, or delete ~/.agent-device/daemon.json if stale."})}function g(){if(!i.existsSync(m))return null;try{let e=JSON.parse(i.readFileSync(m,"utf8"));if(!e.port||!e.token)return null;return e}catch{return null}}async function v(e){return new Promise(t=>{let r=o.createConnection({host:"127.0.0.1",port:e.port},()=>{r.destroy(),t(!0)});r.on("error",()=>{t(!1)})})}async function b(){let t=$(),r=e.join(t,"dist","src","daemon.js"),n=e.join(t,"src","daemon.ts"),s=i.existsSync(r);if(!s&&!i.existsSync(n))throw new l("COMMAND_FAILED","Daemon entry not found",{distPath:r,srcPath:n});let o=s?[r]:["--experimental-strip-types",n];a(process.execPath,o)}async function x(e,t){return new Promise((r,n)=>{let a=o.createConnection({host:"127.0.0.1",port:e.port},()=>{a.write(`${JSON.stringify(t)}
5
+ `)}),i=setTimeout(()=>{a.destroy(),n(new l("COMMAND_FAILED","Daemon request timed out",{timeoutMs:h}))},h),s="";a.setEncoding("utf8"),a.on("data",e=>{let t=(s+=e).indexOf("\n");if(-1===t)return;let o=s.slice(0,t).trim();if(o)try{let e=JSON.parse(o);a.end(),clearTimeout(i),r(e)}catch(e){clearTimeout(i),n(e)}}),a.on("error",e=>{clearTimeout(i),n(e)})})}function $(){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(i.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 a=e[n];if("--json"===a){t.json=!0;continue}if("--help"===a||"-h"===a){t.help=!0;continue}if("--verbose"===a||"-v"===a){t.verbose=!0;continue}if("-i"===a){t.snapshotInteractiveOnly=!0;continue}if("-c"===a){t.snapshotCompact=!0;continue}if("--raw"===a){t.snapshotRaw=!0;continue}if("--no-record"===a){t.noRecord=!0;continue}if("--record-json"===a){t.recordJson=!0;continue}if("--user-installed"===a){t.appsFilter="user-installed";continue}if("--all"===a){t.appsFilter="all";continue}if("--metadata"===a){t.appsMetadata=!0;continue}if(a.startsWith("--backend")){let r=a.includes("=")?a.split("=")[1]:e[n+1];if(a.includes("=")||(n+=1),"ax"!==r&&"xctest"!==r)throw new l("INVALID_ARGS",`Invalid backend: ${r}`);t.snapshotBackend=r;continue}if(a.startsWith("--")){let[r,i]=a.split("="),s=i??e[n+1];switch(!i&&(n+=1),r){case"--platform":if("ios"!==s&&"android"!==s)throw new l("INVALID_ARGS",`Invalid platform: ${s}`);t.platform=s;break;case"--depth":{let e=Number(s);if(!Number.isFinite(e)||e<0)throw new l("INVALID_ARGS",`Invalid depth: ${s}`);t.snapshotDepth=Math.floor(e);break}case"--scope":t.snapshotScope=s;break;case"--device":t.device=s;break;case"--udid":t.udid=s;break;case"--serial":t.serial=s;break;case"--out":t.out=s;break;case"--session":t.session=s;break;case"--activity":t.activity=s;break;default:throw new l("INVALID_ARGS",`Unknown flag: ${r}`)}continue}if("-d"===a){let r=e[n+1];n+=1;let a=Number(r);if(!Number.isFinite(a)||a<0)throw new l("INVALID_ARGS",`Invalid depth: ${r}`);t.snapshotDepth=Math.floor(a);continue}if("-s"===a){let r=e[n+1];n+=1,t.snapshotScope=r;continue}r.push(a)}return{command:r.shift()??null,positionals:r,flags:t}}(t);(n.flags.help||!n.command)&&(process.stdout.write(`agent-device <command> [args] [--json]
6
6
 
7
7
  CLI to control iOS and Android devices for AI agents.
8
8
 
@@ -66,27 +66,29 @@ Flags:
66
66
  --user-installed Apps: list user-installed packages (Android only)
67
67
  --all Apps: list all packages (Android only)
68
68
 
69
- `),process.exit(+!n.flags.help));let{command:a,positionals:i,flags:d}=n,p=d.session??process.env.AGENT_DEVICE_SESSION??"default",f=d.verbose&&!d.json?function(){try{let t=e.join(o.homedir(),".agent-device","daemon.log"),r=0,n=!1,a=setInterval(()=>{if(n||!s.existsSync(t))return;let e=s.statSync(t);if(e.size<=r)return;let a=s.openSync(t,"r"),o=Buffer.alloc(e.size-r);s.readSync(a,o,0,o.length,r),s.closeSync(a),r=e.size,o.length>0&&process.stdout.write(o.toString("utf8"))},200);return()=>{n=!0,clearInterval(a)}}catch{return null}}():null;try{if("session"===a){let e=i[0]??"list";if("list"!==e)throw new l("INVALID_ARGS","session only supports list");let t=await m({session:p,command:"session_list",positionals:[],flags:{}});if(!t.ok)throw new l(t.error.code,t.error.message);d.json?c({success:!0,data:t.data??{}}):process.stdout.write(`${JSON.stringify(t.data??{},null,2)}
70
- `),f&&f();return}let e=await m({session:p,command:a,positionals:i,flags:d});if(e.ok){if(d.json){c({success:!0,data:e.data??{}}),f&&f();return}if("snapshot"===a){process.stdout.write(function(e,t={}){let r=e.nodes??[],n=!!e.truncated,a="string"==typeof e.appName?e.appName:void 0,s="string"==typeof e.appBundleId?e.appBundleId:void 0,o=[];a&&o.push(`Page: ${a}`),s&&o.push(`App: ${s}`);let i=`Snapshot: ${r.length} nodes${n?" (truncated)":""}`,l=o.length>0?`${o.join("\n")}
71
- `:"";if(!Array.isArray(r)||0===r.length)return`${l}${i}
72
- `;if(t.raw){let e=r.map(e=>JSON.stringify(e));return`${l}${i}
69
+ `),process.exit(+!n.flags.help));let{command:a,positionals:o,flags:f}=n,m=f.session??process.env.AGENT_DEVICE_SESSION??"default",h=f.verbose&&!f.json?function(){try{let t=e.join(s.homedir(),".agent-device","daemon.log"),r=0,n=!1,a=setInterval(()=>{if(n||!i.existsSync(t))return;let e=i.statSync(t);if(e.size<=r)return;let a=i.openSync(t,"r"),s=Buffer.alloc(e.size-r);i.readSync(a,s,0,s.length,r),i.closeSync(a),r=e.size,s.length>0&&process.stdout.write(s.toString("utf8"))},200);return()=>{n=!0,clearInterval(a)}}catch{return null}}():null;try{if("session"===a){let e=o[0]??"list";if("list"!==e)throw new l("INVALID_ARGS","session only supports list");let t=await w({session:m,command:"session_list",positionals:[],flags:{}});if(!t.ok)throw new l(t.error.code,t.error.message);f.json?c({success:!0,data:t.data??{}}):process.stdout.write(`${JSON.stringify(t.data??{},null,2)}
70
+ `),h&&h();return}let e=await w({session:m,command:a,positionals:o,flags:f});if(e.ok){if(f.json){c({success:!0,data:e.data??{}}),h&&h();return}if("snapshot"===a){process.stdout.write(function(e,t={}){let r=e.nodes,n=Array.isArray(r)?r:[],a=!!e.truncated,i="string"==typeof e.appName?e.appName:void 0,s="string"==typeof e.appBundleId?e.appBundleId:void 0,o=[];i&&o.push(`Page: ${i}`),s&&o.push(`App: ${s}`);let l=`Snapshot: ${n.length} nodes${a?" (truncated)":""}`,c=o.length>0?`${o.join("\n")}
71
+ `:"";if(0===n.length)return`${c}${l}
72
+ `;if(t.raw){let e=n.map(e=>JSON.stringify(e));return`${c}${l}
73
73
  ${e.join("\n")}
74
- `}let c=[],u=[];for(let e of r){let t=e.depth??0;for(;c.length>0&&t<=c[c.length-1];)c.pop();let r=e.label?.trim()||e.value?.trim()||e.identifier?.trim()||"",n=function(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase();switch(t.startsWith("ax")&&(t=t.replace(/^ax/,"")),t){case"application":return"application";case"navigationbar":return"navigation-bar";case"tabbar":return"tab-bar";case"button":return"button";case"link":return"link";case"cell":return"cell";case"statictext":case"statictext":return"text";case"textfield":case"textfield":return"text-field";case"textview":case"textarea":return"text-view";case"switch":return"switch";case"slider":return"slider";case"image":return"image";case"table":return"list";case"collectionview":return"collection";case"searchfield":return"search";case"segmentedcontrol":return"segmented-control";case"group":return"group";case"window":return"window";case"checkbox":return"checkbox";case"radio":return"radio";case"menuitem":return"menu-item";case"toolbar":return"toolbar";case"scrollarea":return"scroll-area";case"table":return"table";default:return t||"element"}}(e.type??"Element"),a="group"===n&&!r;a&&c.push(t);let s=a?t:Math.max(0,t-c.length),o=" ".repeat(s),i=e.ref?`@${e.ref}`:"",l=[!1===e.enabled?"disabled":null].filter(Boolean).join(", "),d=l?` [${l}]`:"",p=r?` "${r}"`:"";if(a){u.push(`${o}${i} [${n}]${d}`.trimEnd());continue}u.push(`${o}${i} [${n}]${p}${d}`.trimEnd())}return`${l}${i}
75
- ${u.join("\n")}
76
- `}(e.data??{},{raw:d.snapshotRaw})),f&&f();return}if("get"===a){let t=i[0];if("text"===t){let t=e.data?.text??"";process.stdout.write(`${t}
77
- `),f&&f();return}if("attrs"===t){let t=e.data?.node??{};process.stdout.write(`${JSON.stringify(t,null,2)}
78
- `),f&&f();return}}if("find"===a){let t=e.data;if("string"==typeof t?.text){process.stdout.write(`${t.text}
79
- `),f&&f();return}if("boolean"==typeof t?.found){process.stdout.write(`Found: ${t.found}
80
- `),f&&f();return}if(t?.node){process.stdout.write(`${JSON.stringify(t.node,null,2)}
81
- `),f&&f();return}}if("click"===a){let t=e.data?.ref??"",r=e.data?.x,n=e.data?.y;t&&"number"==typeof r&&"number"==typeof n&&process.stdout.write(`Clicked @${t} (${r}, ${n})
82
- `),f&&f();return}if(e.data&&"object"==typeof e.data){let t=e.data;if("devices"===a){let e=(Array.isArray(t.devices)?t.devices:[]).map(e=>{let t=e?.name??e?.id??"unknown",r=e?.platform??"unknown",n=e?.kind?` ${e.kind}`:"",a="boolean"==typeof e?.booted?` booted=${e.booted}`:"";return`${t} (${r}${n})${a}`});process.stdout.write(`${e.join("\n")}
83
- `),f&&f();return}if("apps"===a){let e=(Array.isArray(t.apps)?t.apps:[]).map(e=>{if("string"==typeof e)return e;if(e&&"object"==typeof e){let t=e.bundleId??e.package,r=e.name??e.label;return r&&t?`${r} (${t})`:t&&"boolean"==typeof e.launchable?`${t} (launchable=${e.launchable})`:t?String(t):JSON.stringify(e)}return String(e)});process.stdout.write(`${e.join("\n")}
84
- `),f&&f();return}if("appstate"===a){let e=t?.platform,r=t?.appBundleId,n=t?.appName,a=t?.source,s=t?.package,o=t?.activity;if("ios"===e){process.stdout.write(`Foreground app: ${n??r}
74
+ `}if(t.flatten){let e=n.map(e=>u(e,0,!1));return`${c}${l}
75
+ ${e.join("\n")}
76
+ `}let d=[],f=[];for(let e of n){let t=e.depth??0;for(;d.length>0&&t<=d[d.length-1];)d.pop();let r=e.label?.trim()||e.value?.trim()||e.identifier?.trim()||"",n="group"===p(e.type??"Element")&&!r;n&&d.push(t);let a=n?t:Math.max(0,t-d.length);f.push(u(e,a,n))}return`${c}${l}
77
+ ${f.join("\n")}
78
+ `}(e.data??{},{raw:f.snapshotRaw,flatten:f.snapshotInteractiveOnly})),h&&h();return}if("get"===a){let t=o[0];if("text"===t){let t=e.data?.text??"";process.stdout.write(`${t}
79
+ `),h&&h();return}if("attrs"===t){let t=e.data?.node??{};process.stdout.write(`${JSON.stringify(t,null,2)}
80
+ `),h&&h();return}}if("find"===a){let t=e.data;if("string"==typeof t?.text){process.stdout.write(`${t.text}
81
+ `),h&&h();return}if("boolean"==typeof t?.found){process.stdout.write(`Found: ${t.found}
82
+ `),h&&h();return}if(t?.node){process.stdout.write(`${JSON.stringify(t.node,null,2)}
83
+ `),h&&h();return}}if("click"===a){let t=e.data?.ref??"",r=e.data?.x,n=e.data?.y;t&&"number"==typeof r&&"number"==typeof n&&process.stdout.write(`Clicked @${t} (${r}, ${n})
84
+ `),h&&h();return}if(e.data&&"object"==typeof e.data){let t=e.data;if("devices"===a){let e=(Array.isArray(t.devices)?t.devices:[]).map(e=>{let t=e?.name??e?.id??"unknown",r=e?.platform??"unknown",n=e?.kind?` ${e.kind}`:"",a="boolean"==typeof e?.booted?` booted=${e.booted}`:"";return`${t} (${r}${n})${a}`});process.stdout.write(`${e.join("\n")}
85
+ `),h&&h();return}if("apps"===a){let e=(Array.isArray(t.apps)?t.apps:[]).map(e=>{if("string"==typeof e)return e;if(e&&"object"==typeof e){let t=e.bundleId??e.package,r=e.name??e.label;return r&&t?`${r} (${t})`:t&&"boolean"==typeof e.launchable?`${t} (launchable=${e.launchable})`:t?String(t):JSON.stringify(e)}return String(e)});process.stdout.write(`${e.join("\n")}
86
+ `),h&&h();return}if("appstate"===a){let e=t?.platform,r=t?.appBundleId,n=t?.appName,a=t?.source,i=t?.package,s=t?.activity;if("ios"===e){process.stdout.write(`Foreground app: ${n??r}
85
87
  `),r&&process.stdout.write(`Bundle: ${r}
86
88
  `),a&&process.stdout.write(`Source: ${a}
87
- `),f&&f();return}if("android"===e){process.stdout.write(`Foreground app: ${s??"unknown"}
88
- `),o&&process.stdout.write(`Activity: ${o}
89
- `),f&&f();return}}}f&&f();return}throw new l(e.error.code,e.error.message,e.error.details)}catch(t){let e=r(t);if(d.json)c({success:!1,error:{code:e.code,message:e.message,details:e.details}});else if(u(e),d.verbose)try{let e=await import("node:fs"),t=await import("node:os"),r=(await import("node:path")).join(t.homedir(),".agent-device","daemon.log");if(e.existsSync(r)){let t=e.readFileSync(r,"utf8").split("\n"),n=t.slice(Math.max(0,t.length-200)).join("\n");n.trim().length>0&&process.stderr.write(`
89
+ `),h&&h();return}if("android"===e){process.stdout.write(`Foreground app: ${i??"unknown"}
90
+ `),s&&process.stdout.write(`Activity: ${s}
91
+ `),h&&h();return}}}h&&h();return}throw new l(e.error.code,e.error.message,e.error.details)}catch(t){let e=r(t);if(f.json)c({success:!1,error:{code:e.code,message:e.message,details:e.details}});else if(d(e),f.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(`
90
92
  [daemon log]
91
93
  ${n}
92
- `)}}catch{}f&&f(),process.exit(1)}}n(process.argv[1]??"").href===import.meta.url&&$(process.argv.slice(2)).catch(e=>{u(r(e)),process.exit(1)}),$(process.argv.slice(2));
94
+ `)}}catch{}h&&h(),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,9 +1,9 @@
1
- let e,t;import a from"node:crypto";import{isCancel as i,select as n}from"@clack/prompts";import{node_path as r,runCmdStreaming as o,promises as s,asAppError as l,fileURLToPath as c,runCmdBackground as u,node_fs as d,node_os as p,errors_AppError as f,runCmd as h,node_net as m,whichCmd as w}from"./861.js";async function g(e,t){let a=e,r=e=>e.toLowerCase().replace(/_/g," ").replace(/\s+/g," ").trim();if(t.platform&&(a=a.filter(e=>e.platform===t.platform)),t.udid){let e=a.find(e=>e.id===t.udid&&"ios"===e.platform);if(!e)throw new f("DEVICE_NOT_FOUND",`No iOS device with UDID ${t.udid}`);return e}if(t.serial){let e=a.find(e=>e.id===t.serial&&"android"===e.platform);if(!e)throw new f("DEVICE_NOT_FOUND",`No Android device with serial ${t.serial}`);return e}if(t.deviceName){let e=r(t.deviceName),i=a.find(t=>r(t.name)===e);if(!i)throw new f("DEVICE_NOT_FOUND",`No device named ${t.deviceName}`);return i}if(1===a.length)return a[0];if(0===a.length)throw new f("DEVICE_NOT_FOUND","No devices found",{selector:t});let o=a.filter(e=>e.booted);if(1===o.length)return o[0];if(!process.env.CI&&process.stdin.isTTY&&process.stdout.isTTY){let e=await n({message:"Multiple devices available. Choose a device to continue:",options:(o.length>0?o:a).map(e=>({label:`${e.name} (${e.platform}${e.kind?`, ${e.kind}`:""}${e.booted?", booted":""})`,value:e.id}))});if(i(e))throw new f("INVALID_ARGS","Device selection cancelled");if(e){let t=a.find(t=>t.id===e);if(t)return t}}return o[0]??a[0]}async function v(){if(!await w("adb"))throw new f("TOOL_MISSING","adb not found in PATH");let e=(await h("adb",["devices","-l"])).stdout.split("\n").map(e=>e.trim()),t=[];for(let a of e){if(!a||a.startsWith("List of devices"))continue;let e=a.split(/\s+/),i=e[0];if("device"!==e[1])continue;let n=(e.find(e=>e.startsWith("model:"))??"").replace("model:","").replace(/_/g," ").trim()||i;if(i.startsWith("emulator-")){let e=await h("adb",["-s",i,"emu","avd","name"],{allowFailure:!0}),t=e.stdout.trim();0===e.exitCode&&t&&(n=t.replace(/_/g," "))}let r=await y(i);t.push({platform:"android",id:i,name:n,kind:i.startsWith("emulator-")?"emulator":"device",booted:r})}return t}async function y(e){try{let t=await h("adb",["-s",e,"shell","getprop","sys.boot_completed"],{allowFailure:!0});return"1"===t.stdout.trim()}catch{return!1}}async function I(e,t=6e4){let a=Date.now();for(;Date.now()-a<t;){if(await y(e))return;await new Promise(e=>setTimeout(e,1e3))}throw new f("COMMAND_FAILED","Android device did not finish booting in time",{serial:e,timeoutMs:t})}async function N(e,t={}){let a,i=t.attempts??3,n=t.baseDelayMs??200,r=t.maxDelayMs??2e3,o=t.jitter??.2;for(let s=1;s<=i;s+=1)try{return await e()}catch(l){if(a=l,s>=i||t.shouldRetry&&!t.shouldRetry(l,s))break;let e=function(e,t,a,i){let n=Math.min(t,e*2**(i-1));return Math.max(0,n+n*a*(2*Math.random()-1))}(n,r,o,s);await function(e){return new Promise(t=>setTimeout(t,e))}(e)}if(a)throw a;throw new f("COMMAND_FAILED","retry failed")}let A={settings:{type:"intent",value:"android.settings.SETTINGS"}};function b(e,t){return["-s",e.id,...t]}async function S(e,t){let a=t.trim();if(a.includes("."))return{type:"package",value:a};let i=A[a.toLowerCase()];if(i)return i;let n=(await h("adb",b(e,["shell","pm","list","packages"]))).stdout.split("\n").map(e=>e.replace("package:","").trim()).filter(Boolean).filter(e=>e.toLowerCase().includes(a.toLowerCase()));if(1===n.length)return{type:"package",value:n[0]};if(n.length>1)throw new f("INVALID_ARGS",`Multiple packages matched "${t}"`,{matches:n});throw new f("APP_NOT_INSTALLED",`No package found matching "${t}"`)}async function D(e,t="launchable"){if("launchable"===t){let t=await h("adb",b(e,["shell","cmd","package","query-activities","--brief","-a","android.intent.action.MAIN","-c","android.intent.category.LAUNCHER"]),{allowFailure:!0});if(0===t.exitCode&&t.stdout.trim().length>0){let e=new Set;for(let a of t.stdout.split("\n")){let t=a.trim();if(!t)continue;let i=t.split(/\s+/)[0],n=i.includes("/")?i.split("/")[0]:i;n&&e.add(n)}if(e.size>0)return Array.from(e)}}return(await h("adb",b(e,"user-installed"===t?["shell","pm","list","packages","-3"]:["shell","pm","list","packages"]))).stdout.split("\n").map(e=>e.replace("package:","").trim()).filter(Boolean)}async function k(e,t="launchable"){let a=await D(e,t),i=new Set("launchable"===t?a:await D(e,"launchable"));return a.map(e=>({package:e,launchable:i.has(e)}))}async function P(e){let t=await O(e,[["shell","dumpsys","window","windows"],["shell","dumpsys","window"]]);if(t)return t;let a=await O(e,[["shell","dumpsys","activity","activities"],["shell","dumpsys","activity"]]);return a||{}}async function O(e,t){for(let a of t){let t=function(e){for(let t of[/mCurrentFocus=Window\{[^}]*\s([\w.]+)\/([\w.$]+)/,/mFocusedApp=AppWindowToken\{[^}]*\s([\w.]+)\/([\w.$]+)/,/mResumedActivity:.*?\s([\w.]+)\/([\w.$]+)/,/ResumedActivity:.*?\s([\w.]+)\/([\w.$]+)/]){let a=t.exec(e);if(a)return{package:a[1],activity:a[2]}}return null}((await h("adb",b(e,a),{allowFailure:!0})).stdout??"");if(t)return t}return null}async function x(e,t,a){e.booted||await I(e.id);let i=await S(e,t);if("intent"===i.type){if(a)throw new f("INVALID_ARGS","Activity override requires a package name, not an intent");await h("adb",b(e,["shell","am","start","-a",i.value]));return}if(a){let t=a.includes("/")?a:`${i.value}/${a.startsWith(".")?a:`.${a}`}`;await h("adb",b(e,["shell","am","start","-a","android.intent.action.MAIN","-c","android.intent.category.DEFAULT","-c","android.intent.category.LAUNCHER","-n",t]));return}await h("adb",b(e,["shell","am","start","-a","android.intent.action.MAIN","-c","android.intent.category.DEFAULT","-c","android.intent.category.LAUNCHER","-p",i.value]))}async function L(e){e.booted||await I(e.id)}async function _(e,t){if("settings"===t.trim().toLowerCase())return void await h("adb",b(e,["shell","am","force-stop","com.android.settings"]));let a=await S(e,t);if("intent"===a.type)throw new f("INVALID_ARGS","Close requires a package name, not an intent");await h("adb",b(e,["shell","am","force-stop",a.value]))}async function E(e,t,a){await h("adb",b(e,["shell","input","tap",String(t),String(a)]))}async function R(e){await h("adb",b(e,["shell","input","keyevent","4"]))}async function C(e){await h("adb",b(e,["shell","input","keyevent","3"]))}async function M(e){await h("adb",b(e,["shell","input","keyevent","187"]))}async function T(e,t,a,i=800){await h("adb",b(e,["shell","input","swipe",String(t),String(a),String(t),String(a),String(i)]))}async function F(e,t){let a=t.replace(/ /g,"%s");await h("adb",b(e,["shell","input","text",a]))}async function B(e,t,a){await E(e,t,a)}async function $(e,t,a,i){await B(e,t,a),await F(e,i)}async function U(e,t,a=.6){let{width:i,height:n}=await W(e),r=Math.floor(i*a),o=Math.floor(n*a),s=Math.floor(i/2),l=Math.floor(n/2),c=s,u=l,d=s,p=l;switch(t){case"up":u=l-Math.floor(o/2),p=l+Math.floor(o/2);break;case"down":u=l+Math.floor(o/2),p=l-Math.floor(o/2);break;case"left":c=s-Math.floor(r/2),d=s+Math.floor(r/2);break;case"right":c=s+Math.floor(r/2),d=s-Math.floor(r/2);break;default:throw new f("INVALID_ARGS",`Unknown direction: ${t}`)}await h("adb",b(e,["shell","input","swipe",String(c),String(u),String(d),String(p),"300"]))}async function V(e,t){for(let a=0;a<8;a+=1){let a="";try{a=await X(e)}catch(t){let e=t instanceof Error?t.message:String(t);throw new f("UNSUPPORTED_OPERATION",`uiautomator dump failed: ${e}`)}if(function(e,t){let a=t.toLowerCase(),i=/<node[^>]+>/g,n=i.exec(e);for(;n;){let t=n[0],r=/text="([^"]*)"/.exec(t),o=/content-desc="([^"]*)"/.exec(t),s=(r?.[1]??"").toLowerCase(),l=(o?.[1]??"").toLowerCase();if(s.includes(a)||l.includes(a)){let e=/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/.exec(t);if(e){let t=Number(e[1]),a=Number(e[2]);return{x:Math.floor((t+Number(e[3]))/2),y:Math.floor((a+Number(e[4]))/2)}}return{x:0,y:0}}n=i.exec(e)}return null}(a,t))return;await U(e,"down",.5)}throw new f("COMMAND_FAILED",`Could not find element containing "${t}" after scrolling`)}async function j(e,t){let a=await h("adb",b(e,["exec-out","screencap","-p"]),{binaryStdout:!0});if(!a.stdoutBuffer)throw new f("COMMAND_FAILED","Failed to capture screenshot");await s.writeFile(t,a.stdoutBuffer)}async function G(e,t,a){let i=t.toLowerCase(),n=function(e){let t=e.toLowerCase();if("on"===t||"true"===t||"1"===t)return!0;if("off"===t||"false"===t||"0"===t)return!1;throw new f("INVALID_ARGS",`Invalid setting state: ${e}`)}(a);switch(i){case"wifi":return void await h("adb",b(e,["shell","svc","wifi",n?"enable":"disable"]));case"airplane":await h("adb",b(e,["shell","settings","put","global","airplane_mode_on",n?"1":"0"])),await h("adb",b(e,["shell","am","broadcast","-a","android.intent.action.AIRPLANE_MODE","--ez","state",n?"true":"false"]));return;case"location":return void await h("adb",b(e,["shell","settings","put","secure","location_mode",n?"3":"0"]));default:throw new f("INVALID_ARGS",`Unsupported setting: ${t}`)}}async function q(e,t={}){return function(e,t,a){let i=function(e){let t={type:null,label:null,value:null,identifier:null,depth:-1,children:[]},a=[t],i=/<node\b[^>]*>|<\/node>/g,n=i.exec(e);for(;n;){let t=n[0];if(t.startsWith("</node")){a.length>1&&a.pop(),n=i.exec(e);continue}let r=function(e){let t=t=>{let a=RegExp(`${t}="([^"]*)"`).exec(e);return a?a[1]:null},a=e=>{let a=t(e);if(null!==a)return"true"===a};return{text:t("text"),desc:t("content-desc"),resourceId:t("resource-id"),className:t("class"),bounds:t("bounds"),clickable:a("clickable"),enabled:a("enabled"),focusable:a("focusable")}}(t),o=function(e){if(!e)return;let t=/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/.exec(e);if(!t)return;let a=Number(t[1]),i=Number(t[2]);return{x:a,y:i,width:Math.max(0,Number(t[3])-a),height:Math.max(0,Number(t[4])-i)}}(r.bounds),s=a[a.length-1],l={type:r.className,label:r.text||r.desc,value:r.text,identifier:r.resourceId,rect:o,enabled:r.enabled,hittable:r.clickable??r.focusable,depth:s.depth+1,parentIndex:void 0,children:[]};s.children.push(l),t.endsWith("/>")||a.push(l),n=i.exec(e)}return t}(e),n=[],r=!1,o=a.depth??1/0,s=a.scope?function(e,t){let a=t.toLowerCase(),i=[...e.children];for(;i.length>0;){let e=i.shift(),t=e.label?.toLowerCase()??"",n=e.value?.toLowerCase()??"",r=e.identifier?.toLowerCase()??"";if(t.includes(a)||n.includes(a)||r.includes(a))return e;i.push(...e.children)}return null}(i,a.scope):null,l=s?[s]:i.children,c=(e,t,i)=>{if(n.length>=800){r=!0;return}if(t>o)return;let s=!!a.raw||function(e,t){if(t.interactiveOnly)return!!e.hittable;if(t.compact){let t=!!(e.label&&e.label.trim().length>0),a=!!(e.identifier&&e.identifier.trim().length>0);return t||a||!!e.hittable}return!0}(e,a),l=i;for(let a of(s&&(l=n.length,n.push({index:l,type:e.type??void 0,label:e.label??void 0,value:e.value??void 0,identifier:e.identifier??void 0,rect:e.rect,enabled:e.enabled,hittable:e.hittable,depth:t,parentIndex:i})),e.children))if(c(a,t+1,l),r)return};for(let e of l)if(c(e,0,void 0),r)break;return r?{nodes:n,truncated:r}:{nodes:n}}(await X(e),800,t)}async function J(){if(!await w("adb"))throw new f("TOOL_MISSING","adb not found in PATH")}async function W(e){let t=(await h("adb",b(e,["shell","wm","size"]))).stdout.match(/Physical size:\s*(\d+)x(\d+)/);if(!t)throw new f("COMMAND_FAILED","Unable to read screen size");return{width:Number(t[1]),height:Number(t[2])}}async function X(e){return N(()=>z(e),{shouldRetry:H})}async function z(e){return await h("adb",b(e,["shell","uiautomator","dump","/sdcard/window_dump.xml"])),(await h("adb",b(e,["shell","cat","/sdcard/window_dump.xml"]))).stdout}function H(e){if(!(e instanceof f)||"COMMAND_FAILED"!==e.code)return!1;let t=`${e.details?.stderr??""}`.toLowerCase();return!!(t.includes("device offline")||t.includes("device not found")||t.includes("transport error")||t.includes("connection reset")||t.includes("broken pipe")||t.includes("timed out"))}async function Z(){if("darwin"!==process.platform)throw new f("UNSUPPORTED_PLATFORM","iOS tools are only available on macOS");if(!await w("xcrun"))throw new f("TOOL_MISSING","xcrun not found in PATH");let e=[],t=await h("xcrun",["simctl","list","devices","-j"]);try{let a=JSON.parse(t.stdout);for(let t of Object.values(a.devices))for(let a of t)a.isAvailable&&e.push({platform:"ios",id:a.udid,name:a.name,kind:"simulator",booted:"Booted"===a.state})}catch(e){throw new f("COMMAND_FAILED","Failed to parse simctl devices JSON",void 0,e)}if(await w("xcrun"))try{let t=await h("xcrun",["devicectl","list","devices","--json"]);for(let a of JSON.parse(t.stdout).devices??[])a.platform?.toLowerCase().includes("ios")&&e.push({platform:"ios",id:a.identifier,name:a.name,kind:"device",booted:!0})}catch{}return e}let Y={settings:"com.apple.Preferences"};async function K(e,t){let a=t.trim();if(a.includes("."))return a;let i=Y[a.toLowerCase()];if(i)return i;if("simulator"===e.kind){let i=(await ep(e)).filter(e=>e.name.toLowerCase()===a.toLowerCase());if(1===i.length)return i[0].bundleId;if(i.length>1)throw new f("INVALID_ARGS",`Multiple apps matched "${t}"`,{matches:i})}throw new f("APP_NOT_INSTALLED",`No app found matching "${t}"`)}async function Q(e,t){let a=await K(e,t);if("simulator"===e.kind){await ef(e),await h("open",["-a","Simulator"],{allowFailure:!0}),await h("xcrun",["simctl","launch",e.id,a]);return}await h("xcrun",["devicectl","device","process","launch","--device",e.id,a])}async function ee(e){"simulator"!==e.kind||"Booted"!==await eh(e.id)&&(await ef(e),await h("open",["-a","Simulator"],{allowFailure:!0}))}async function et(e,t){let a=await K(e,t);if("simulator"===e.kind){await ef(e);let t=await h("xcrun",["simctl","terminate",e.id,a],{allowFailure:!0});if(0!==t.exitCode){if(t.stderr.toLowerCase().includes("found nothing to terminate"))return;throw new f("COMMAND_FAILED",`xcrun exited with code ${t.exitCode}`,{cmd:"xcrun",args:["simctl","terminate",e.id,a],stdout:t.stdout,stderr:t.stderr,exitCode:t.exitCode})}return}await h("xcrun",["devicectl","device","process","terminate","--device",e.id,a])}async function ea(e,t,a){throw ed(e,"press"),new f("UNSUPPORTED_OPERATION","simctl io tap is not available; use the XCTest runner for input")}async function ei(e,t,a,i=800){throw ed(e,"long-press"),new f("UNSUPPORTED_OPERATION","long-press is not supported on iOS simulators without XCTest runner support")}async function en(e,t,a){await ea(e,t,a)}async function er(e,t){throw ed(e,"type"),new f("UNSUPPORTED_OPERATION","simctl io keyboard is not available; use the XCTest runner for input")}async function eo(e,t,a,i){await en(e,t,a),await er(e,i)}async function es(e,t,a=.6){throw ed(e,"scroll"),new f("UNSUPPORTED_OPERATION","simctl io swipe is not available; use the XCTest runner for input")}async function el(e){throw new f("UNSUPPORTED_OPERATION",`scrollintoview is not supported on iOS without UI automation (${e})`)}async function ec(e,t){if("simulator"===e.kind){await ef(e),await h("xcrun",["simctl","io",e.id,"screenshot",t]);return}await h("xcrun",["devicectl","device","screenshot","--device",e.id,t])}async function eu(e,t,a,i){ed(e,"settings"),await ef(e);let n=t.toLowerCase(),r=function(e){let t=e.toLowerCase();if("on"===t||"true"===t||"1"===t)return!0;if("off"===t||"false"===t||"0"===t)return!1;throw new f("INVALID_ARGS",`Invalid setting state: ${e}`)}(a);switch(n){case"wifi":return void await h("xcrun",["simctl","status_bar",e.id,"override","--wifiMode",r?"active":"failed"]);case"airplane":r?await h("xcrun",["simctl","status_bar",e.id,"override","--dataNetwork","hide","--wifiMode","failed","--wifiBars","0","--cellularMode","failed","--cellularBars","0","--operatorName",""]):await h("xcrun",["simctl","status_bar",e.id,"clear"]);return;case"location":if(!i)throw new f("INVALID_ARGS","location setting requires an active app in session");await h("xcrun",["simctl","privacy",e.id,r?"grant":"revoke","location",i]);return;default:throw new f("INVALID_ARGS",`Unsupported setting: ${t}`)}}function ed(e,t){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION",`${t} is only supported on iOS simulators in v1`)}async function ep(e){let t=(await h("xcrun",["simctl","listapps",e.id],{allowFailure:!0})).stdout.trim();if(!t)return[];let a=null;if(t.startsWith("{"))try{a=JSON.parse(t)}catch{a=null}if(!a&&t.startsWith("{"))try{let e=await h("plutil",["-convert","json","-o","-","-"],{allowFailure:!0,stdin:t});0===e.exitCode&&e.stdout.trim().startsWith("{")&&(a=JSON.parse(e.stdout))}catch{a=null}return a?Object.entries(a).map(([e,t])=>({bundleId:e,name:t.CFBundleDisplayName??t.CFBundleName??e})):[]}async function ef(e){"simulator"!==e.kind||"Booted"!==await eh(e.id)&&(await h("xcrun",["simctl","boot",e.id],{allowFailure:!0}),await h("xcrun",["simctl","bootstatus",e.id,"-b"],{allowFailure:!0}))}async function eh(e){let t=await h("xcrun",["simctl","list","devices","-j"],{allowFailure:!0});if(0!==t.exitCode)return null;try{let a=JSON.parse(t.stdout);for(let t of Object.values(a.devices??{})){let a=t.find(t=>t.udid===e);if(a)return a.state}}catch{}return null}let em=new Map;async function ew(e,t,a={}){var i;return"snapshot"===(i=t.command)||"findText"===i||"listTappables"===i||"alert"===i?N(()=>eg(e,t,a),{shouldRetry:eS}):eg(e,t,a)}async function eg(e,t,a={}){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","iOS runner only supports simulators in v1");try{let i=await eI(e,a),n=await eD(e,i.port,t,a.logPath),r=await n.text(),o={};try{o=JSON.parse(r)}catch{throw new f("COMMAND_FAILED","Invalid runner response",{text:r})}if(!o.ok)throw new f("COMMAND_FAILED",o.error?.message??"Runner error",{runner:o,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:a.logPath});return o.data??{}}catch(n){let i=n instanceof f?n:new f("COMMAND_FAILED",String(n));if("COMMAND_FAILED"===i.code&&"string"==typeof i.message&&i.message.includes("Runner did not accept connection")){await ev(e.id);let i=await eI(e,a),n=await eD(e,i.port,t,a.logPath),r=await n.text(),o={};try{o=JSON.parse(r)}catch{throw new f("COMMAND_FAILED","Invalid runner response",{text:r})}if(!o.ok)throw new f("COMMAND_FAILED",o.error?.message??"Runner error",{runner:o,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:a.logPath});return o.data??{}}throw n}}async function ev(e){let t=em.get(e);if(t){try{await eD(t.device,t.port,{command:"shutdown"})}catch{}try{await t.testPromise}catch{}ex(t.xctestrunPath),ex(t.jsonPath),em.delete(e)}}async function ey(e){await h("xcrun",["simctl","bootstatus",e,"-b"],{allowFailure:!0})}async function eI(e,t){let a=em.get(e.id);if(a)return a;await ey(e.id);let i=await eN(e.id,t),n=await eP(),r=process.env.AGENT_DEVICE_RUNNER_TIMEOUT??"0",{xctestrunPath:s,jsonPath:l}=await eO(i,{AGENT_DEVICE_RUNNER_PORT:String(n),AGENT_DEVICE_RUNNER_TIMEOUT:r},`session-${e.id}-${n}`),c=o("xcodebuild",["test-without-building","-only-testing","AgentDeviceRunnerUITests/RunnerTests/testCommand","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-xctestrun",s,"-destination",`platform=iOS Simulator,id=${e.id}`],{onStdoutChunk:e=>{eb(e,t.logPath,t.traceLogPath,t.verbose)},onStderrChunk:e=>{eb(e,t.logPath,t.traceLogPath,t.verbose)},allowFailure:!0,env:{...process.env,AGENT_DEVICE_RUNNER_PORT:String(n),AGENT_DEVICE_RUNNER_TIMEOUT:r}}),u={device:e,deviceId:e.id,port:n,xctestrunPath:s,jsonPath:l,testPromise:c};return em.set(e.id,u),u}async function eN(e,t){let a,i=r.join(p.homedir(),".agent-device","ios-runner"),n=r.join(i,"derived");if((a=process.env.AGENT_DEVICE_IOS_CLEAN_DERIVED)&&["1","true","yes","on"].includes(a.toLowerCase()))try{d.rmSync(n,{recursive:!0,force:!0})}catch{}let s=eA(n);if(s)return s;let l=function(){let e=r.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=r.join(t,"package.json");if(d.existsSync(e))return t;t=r.dirname(t)}return e}(),u=r.join(l,"ios-runner","AgentDeviceRunner","AgentDeviceRunner.xcodeproj");if(!d.existsSync(u))throw new f("COMMAND_FAILED","iOS runner project not found",{projectPath:u});try{await o("xcodebuild",["build-for-testing","-project",u,"-scheme","AgentDeviceRunner","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-destination",`platform=iOS Simulator,id=${e}`,"-derivedDataPath",n],{onStdoutChunk:e=>{eb(e,t.logPath,t.traceLogPath,t.verbose)},onStderrChunk:e=>{eb(e,t.logPath,t.traceLogPath,t.verbose)}})}catch(a){let e=a instanceof f?a:new f("COMMAND_FAILED",String(a));throw new f("COMMAND_FAILED","xcodebuild build-for-testing failed",{error:e.message,details:e.details,logPath:t.logPath})}let h=eA(n);if(!h)throw new f("COMMAND_FAILED","Failed to locate .xctestrun after build");return h}function eA(e){if(!d.existsSync(e))return null;let t=[],a=[e];for(;a.length>0;){let e=a.pop();for(let i of d.readdirSync(e,{withFileTypes:!0})){let n=r.join(e,i.name);if(i.isDirectory()){a.push(n);continue}if(i.isFile()&&i.name.endsWith(".xctestrun"))try{let e=d.statSync(n);t.push({path:n,mtimeMs:e.mtimeMs})}catch{}}}return 0===t.length?null:(t.sort((e,t)=>t.mtimeMs-e.mtimeMs),t[0]?.path??null)}function eb(e,t,a,i){t&&d.appendFileSync(t,e),a&&d.appendFileSync(a,e),i&&process.stderr.write(e)}function eS(e){if(!(e instanceof f)||"COMMAND_FAILED"!==e.code)return!1;let t=`${e.message??""}`.toLowerCase();return!!(t.includes("runner did not accept connection")||t.includes("fetch failed")||t.includes("econnrefused")||t.includes("socket hang up"))}async function eD(e,t,a,i){let n=Date.now(),r=null;for(;Date.now()-n<15e3;)try{return await fetch(`http://127.0.0.1:${t}/command`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})}catch(e){r=e,await new Promise(e=>setTimeout(e,100))}if("simulator"===e.kind){let i=await ek(e.id,t,a);return new Response(i.body,{status:i.status})}throw new f("COMMAND_FAILED","Runner did not accept connection",{port:t,logPath:i,lastError:r?String(r):void 0})}async function ek(e,t,a){let i=JSON.stringify(a),n=await h("xcrun",["simctl","spawn",e,"/usr/bin/curl","-s","-X","POST","-H","Content-Type: application/json","--data",i,`http://127.0.0.1:${t}/command`],{allowFailure:!0}),r=n.stdout;if(0!==n.exitCode)throw new f("COMMAND_FAILED","Runner did not accept connection (simctl spawn)",{port:t,stdout:n.stdout,stderr:n.stderr,exitCode:n.exitCode});return{status:200,body:r}}async function eP(){return await new Promise((e,t)=>{let a=m.createServer();a.listen(0,"127.0.0.1",()=>{let i=a.address();a.close(),"object"==typeof i&&i?.port?e(i.port):t(new f("COMMAND_FAILED","Failed to allocate port"))}),a.on("error",t)})}async function eO(e,t,a){let i,n=r.dirname(e),o=a.replace(/[^a-zA-Z0-9._-]/g,"_"),s=r.join(n,`AgentDeviceRunner.env.${o}.json`),l=r.join(n,`AgentDeviceRunner.env.${o}.xctestrun`),c=await h("plutil",["-convert","json","-o","-",e],{allowFailure:!0});if(0!==c.exitCode||!c.stdout.trim())throw new f("COMMAND_FAILED","Failed to read xctestrun plist",{xctestrunPath:e,stderr:c.stderr});try{i=JSON.parse(c.stdout)}catch(t){throw new f("COMMAND_FAILED","Failed to parse xctestrun JSON",{xctestrunPath:e,error:String(t)})}let u=e=>{e.EnvironmentVariables={...e.EnvironmentVariables??{},...t},e.UITestEnvironmentVariables={...e.UITestEnvironmentVariables??{},...t},e.UITargetAppEnvironmentVariables={...e.UITargetAppEnvironmentVariables??{},...t},e.TestingEnvironmentVariables={...e.TestingEnvironmentVariables??{},...t}},p=i.TestConfigurations;if(Array.isArray(p))for(let e of p){if(!e||"object"!=typeof e)continue;let t=e.TestTargets;if(Array.isArray(t))for(let e of t)e&&"object"==typeof e&&u(e)}for(let[e,t]of Object.entries(i))t&&"object"==typeof t&&t.TestBundlePath&&(u(t),i[e]=t);d.writeFileSync(s,JSON.stringify(i,null,2));let m=await h("plutil",["-convert","xml1","-o",l,s],{allowFailure:!0});if(0!==m.exitCode)throw new f("COMMAND_FAILED","Failed to write xctestrun plist",{tmpXctestrunPath:l,stderr:m.stderr});return{xctestrunPath:l,jsonPath:s}}function ex(e){try{d.existsSync(e)&&d.unlinkSync(e)}catch{}}async function eL(e,t={}){let a,i;if("ios"!==e.platform||"simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","AX snapshot is only supported on iOS simulators");let n=await e_(),r=await N(async()=>{var e,a;let i,r,o,s=await h(n,[],{allowFailure:!0});if(t.traceLogPath&&(e=t.traceLogPath,i=((a=s).stdout??"").toString(),r=(a.stderr??"").toString(),o=`
1
+ let e,t;import a from"node:crypto";import{isCancel as i,select as n}from"@clack/prompts";import{node_path as r,runCmdStreaming as o,promises as s,asAppError as l,fileURLToPath as c,runCmdBackground as u,node_fs as d,node_os as p,errors_AppError as f,runCmd as h,node_net as m,whichCmd as w}from"./861.js";async function g(e,t){let a=e,r=e=>e.toLowerCase().replace(/_/g," ").replace(/\s+/g," ").trim();if(t.platform&&(a=a.filter(e=>e.platform===t.platform)),t.udid){let e=a.find(e=>e.id===t.udid&&"ios"===e.platform);if(!e)throw new f("DEVICE_NOT_FOUND",`No iOS device with UDID ${t.udid}`);return e}if(t.serial){let e=a.find(e=>e.id===t.serial&&"android"===e.platform);if(!e)throw new f("DEVICE_NOT_FOUND",`No Android device with serial ${t.serial}`);return e}if(t.deviceName){let e=r(t.deviceName),i=a.find(t=>r(t.name)===e);if(!i)throw new f("DEVICE_NOT_FOUND",`No device named ${t.deviceName}`);return i}if(1===a.length)return a[0];if(0===a.length)throw new f("DEVICE_NOT_FOUND","No devices found",{selector:t});let o=a.filter(e=>e.booted);if(1===o.length)return o[0];if(!process.env.CI&&process.stdin.isTTY&&process.stdout.isTTY){let e=await n({message:"Multiple devices available. Choose a device to continue:",options:(o.length>0?o:a).map(e=>({label:`${e.name} (${e.platform}${e.kind?`, ${e.kind}`:""}${e.booted?", booted":""})`,value:e.id}))});if(i(e))throw new f("INVALID_ARGS","Device selection cancelled");if(e){let t=a.find(t=>t.id===e);if(t)return t}}return o[0]??a[0]}async function y(){if(!await w("adb"))throw new f("TOOL_MISSING","adb not found in PATH");let e=(await h("adb",["devices","-l"])).stdout.split("\n").map(e=>e.trim()),t=[];for(let a of e){if(!a||a.startsWith("List of devices"))continue;let e=a.split(/\s+/),i=e[0];if("device"!==e[1])continue;let n=(e.find(e=>e.startsWith("model:"))??"").replace("model:","").replace(/_/g," ").trim()||i;if(i.startsWith("emulator-")){let e=await h("adb",["-s",i,"emu","avd","name"],{allowFailure:!0}),t=e.stdout.trim();0===e.exitCode&&t&&(n=t.replace(/_/g," "))}let r=await v(i);t.push({platform:"android",id:i,name:n,kind:i.startsWith("emulator-")?"emulator":"device",booted:r})}return t}async function v(e){try{let t=await h("adb",["-s",e,"shell","getprop","sys.boot_completed"],{allowFailure:!0});return"1"===t.stdout.trim()}catch{return!1}}async function I(e,t=6e4){let a=Date.now();for(;Date.now()-a<t;){if(await 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})}async function N(e,t={}){let a,i=t.attempts??3,n=t.baseDelayMs??200,r=t.maxDelayMs??2e3,o=t.jitter??.2;for(let s=1;s<=i;s+=1)try{return await e()}catch(l){if(a=l,s>=i||t.shouldRetry&&!t.shouldRetry(l,s))break;let e=function(e,t,a,i){let n=Math.min(t,e*2**(i-1));return Math.max(0,n+n*a*(2*Math.random()-1))}(n,r,o,s);await function(e){return new Promise(t=>setTimeout(t,e))}(e)}if(a)throw a;throw new f("COMMAND_FAILED","retry failed")}let A={settings:{type:"intent",value:"android.settings.SETTINGS"}};function b(e,t){return["-s",e.id,...t]}async function S(e,t){let a=t.trim();if(a.includes("."))return{type:"package",value:a};let i=A[a.toLowerCase()];if(i)return i;let n=(await h("adb",b(e,["shell","pm","list","packages"]))).stdout.split("\n").map(e=>e.replace("package:","").trim()).filter(Boolean).filter(e=>e.toLowerCase().includes(a.toLowerCase()));if(1===n.length)return{type:"package",value:n[0]};if(n.length>1)throw new f("INVALID_ARGS",`Multiple packages matched "${t}"`,{matches:n});throw new f("APP_NOT_INSTALLED",`No package found matching "${t}"`)}async function D(e,t="launchable"){if("launchable"===t){let t=await h("adb",b(e,["shell","cmd","package","query-activities","--brief","-a","android.intent.action.MAIN","-c","android.intent.category.LAUNCHER"]),{allowFailure:!0});if(0===t.exitCode&&t.stdout.trim().length>0){let e=new Set;for(let a of t.stdout.split("\n")){let t=a.trim();if(!t)continue;let i=t.split(/\s+/)[0],n=i.includes("/")?i.split("/")[0]:i;n&&e.add(n)}if(e.size>0)return Array.from(e)}}return(await h("adb",b(e,"user-installed"===t?["shell","pm","list","packages","-3"]:["shell","pm","list","packages"]))).stdout.split("\n").map(e=>e.replace("package:","").trim()).filter(Boolean)}async function k(e,t="launchable"){let a=await D(e,t),i=new Set("launchable"===t?a:await D(e,"launchable"));return a.map(e=>({package:e,launchable:i.has(e)}))}async function P(e){let t=await x(e,[["shell","dumpsys","window","windows"],["shell","dumpsys","window"]]);if(t)return t;let a=await x(e,[["shell","dumpsys","activity","activities"],["shell","dumpsys","activity"]]);return a||{}}async function x(e,t){for(let a of t){let t=function(e){for(let t of[/mCurrentFocus=Window\{[^}]*\s([\w.]+)\/([\w.$]+)/,/mFocusedApp=AppWindowToken\{[^}]*\s([\w.]+)\/([\w.$]+)/,/mResumedActivity:.*?\s([\w.]+)\/([\w.$]+)/,/ResumedActivity:.*?\s([\w.]+)\/([\w.$]+)/]){let a=t.exec(e);if(a)return{package:a[1],activity:a[2]}}return null}((await h("adb",b(e,a),{allowFailure:!0})).stdout??"");if(t)return t}return null}async function O(e,t,a){e.booted||await I(e.id);let i=await S(e,t);if("intent"===i.type){if(a)throw new f("INVALID_ARGS","Activity override requires a package name, not an intent");await h("adb",b(e,["shell","am","start","-a",i.value]));return}if(a){let t=a.includes("/")?a:`${i.value}/${a.startsWith(".")?a:`.${a}`}`;await h("adb",b(e,["shell","am","start","-a","android.intent.action.MAIN","-c","android.intent.category.DEFAULT","-c","android.intent.category.LAUNCHER","-n",t]));return}await h("adb",b(e,["shell","am","start","-a","android.intent.action.MAIN","-c","android.intent.category.DEFAULT","-c","android.intent.category.LAUNCHER","-p",i.value]))}async function L(e){e.booted||await I(e.id)}async function _(e,t){if("settings"===t.trim().toLowerCase())return void await h("adb",b(e,["shell","am","force-stop","com.android.settings"]));let a=await S(e,t);if("intent"===a.type)throw new f("INVALID_ARGS","Close requires a package name, not an intent");await h("adb",b(e,["shell","am","force-stop",a.value]))}async function M(e,t,a){await h("adb",b(e,["shell","input","tap",String(t),String(a)]))}async function C(e){await h("adb",b(e,["shell","input","keyevent","4"]))}async function R(e){await h("adb",b(e,["shell","input","keyevent","3"]))}async function E(e){await h("adb",b(e,["shell","input","keyevent","187"]))}async function T(e,t,a,i=800){await h("adb",b(e,["shell","input","swipe",String(t),String(a),String(t),String(a),String(i)]))}async function F(e,t){let a=t.replace(/ /g,"%s");await h("adb",b(e,["shell","input","text",a]))}async function B(e,t,a){await M(e,t,a)}async function $(e,t,a,i){await B(e,t,a);let n=null;for(let s of[{clearPadding:12,minClear:8,maxClear:48,chunkSize:4,delayMs:0},{clearPadding:24,minClear:16,maxClear:96,chunkSize:1,delayMs:15}]){var r,o;let l=(r=i.length+s.clearPadding,o=s.minClear,Math.max(o,Math.min(s.maxClear,r)));if(await K(e,l),await Y(e,i,s.chunkSize,s.delayMs),(n=await Z(e,t,a))===i)return}throw new f("COMMAND_FAILED","Android fill verification failed",{expected:i,actual:n??null})}async function U(e,t,a=.6){let{width:i,height:n}=await W(e),r=Math.floor(i*a),o=Math.floor(n*a),s=Math.floor(i/2),l=Math.floor(n/2),c=s,u=l,d=s,p=l;switch(t){case"up":u=l-Math.floor(o/2),p=l+Math.floor(o/2);break;case"down":u=l+Math.floor(o/2),p=l-Math.floor(o/2);break;case"left":c=s-Math.floor(r/2),d=s+Math.floor(r/2);break;case"right":c=s+Math.floor(r/2),d=s-Math.floor(r/2);break;default:throw new f("INVALID_ARGS",`Unknown direction: ${t}`)}await h("adb",b(e,["shell","input","swipe",String(c),String(u),String(d),String(p),"300"]))}async function V(e,t){for(let a=0;a<8;a+=1){let a="";try{a=await X(e)}catch(t){let e=t instanceof Error?t.message:String(t);throw new f("UNSUPPORTED_OPERATION",`uiautomator dump failed: ${e}`)}if(function(e,t){let a=t.toLowerCase(),i=/<node[^>]+>/g,n=i.exec(e);for(;n;){let t=n[0],r=/text="([^"]*)"/.exec(t),o=/content-desc="([^"]*)"/.exec(t),s=(r?.[1]??"").toLowerCase(),l=(o?.[1]??"").toLowerCase();if(s.includes(a)||l.includes(a)){let e=/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/.exec(t);if(e){let t=Number(e[1]),a=Number(e[2]);return{x:Math.floor((t+Number(e[3]))/2),y:Math.floor((a+Number(e[4]))/2)}}return{x:0,y:0}}n=i.exec(e)}return null}(a,t))return;await U(e,"down",.5)}throw new f("COMMAND_FAILED",`Could not find element containing "${t}" after scrolling`)}async function j(e,t){let a=await h("adb",b(e,["exec-out","screencap","-p"]),{binaryStdout:!0});if(!a.stdoutBuffer)throw new f("COMMAND_FAILED","Failed to capture screenshot");await s.writeFile(t,a.stdoutBuffer)}async function G(e,t,a){let i=t.toLowerCase(),n=function(e){let t=e.toLowerCase();if("on"===t||"true"===t||"1"===t)return!0;if("off"===t||"false"===t||"0"===t)return!1;throw new f("INVALID_ARGS",`Invalid setting state: ${e}`)}(a);switch(i){case"wifi":return void await h("adb",b(e,["shell","svc","wifi",n?"enable":"disable"]));case"airplane":await h("adb",b(e,["shell","settings","put","global","airplane_mode_on",n?"1":"0"])),await h("adb",b(e,["shell","am","broadcast","-a","android.intent.action.AIRPLANE_MODE","--ez","state",n?"true":"false"]));return;case"location":return void await h("adb",b(e,["shell","settings","put","secure","location_mode",n?"3":"0"]));default:throw new f("INVALID_ARGS",`Unsupported setting: ${t}`)}}async function q(e,t={}){return function(e,t,a){let i=function(e){let t={type:null,label:null,value:null,identifier:null,depth:-1,children:[]},a=[t],i=/<node\b[^>]*>|<\/node>/g,n=i.exec(e);for(;n;){let t=n[0];if(t.startsWith("</node")){a.length>1&&a.pop(),n=i.exec(e);continue}let r=et(t),o=ea(r.bounds),s=a[a.length-1],l={type:r.className,label:r.text||r.desc,value:r.text,identifier:r.resourceId,rect:o,enabled:r.enabled,hittable:r.clickable??r.focusable,depth:s.depth+1,parentIndex:void 0,children:[]};s.children.push(l),t.endsWith("/>")||a.push(l),n=i.exec(e)}return t}(e),n=[],r=!1,o=a.depth??1/0,s=a.scope?function(e,t){let a=t.toLowerCase(),i=[...e.children];for(;i.length>0;){let e=i.shift(),t=e.label?.toLowerCase()??"",n=e.value?.toLowerCase()??"",r=e.identifier?.toLowerCase()??"";if(t.includes(a)||n.includes(a)||r.includes(a))return e;i.push(...e.children)}return null}(i,a.scope):null,l=s?[s]:i.children,c=new Map,u=e=>{let t=c.get(e);if(void 0!==t)return t;for(let t of e.children)if(t.hittable||u(t))return c.set(e,!0),!0;return c.set(e,!1),!1},d=(e,t,i,s=!1,l=!1)=>{var c,p,f,h,m,w;let g,y,v,I,N,A,b,S;if(n.length>=800){r=!0;return}if(t>o)return;let D=!!a.raw||(c=e,p=a,f=s,h=u(e),m=l,y=ei(c.type),v=!!(c.label&&c.label.trim().length>0),I=!!(c.identifier&&c.identifier.trim().length>0),N=v&&!en(c.label??""),A=I&&!en(c.identifier??""),b=(g=(w=y).split(".").pop()??w).includes("layout")||"viewgroup"===g||"view"===g,S="imageview"===y||"imagebutton"===y,p.interactiveOnly?!!c.hittable||!!(N||A)&&!S&&(!b||!!m)&&(f||h||m):p.compact?N||A||!!c.hittable:!b&&!S||!!c.hittable||!!N||!!A&&!!h||h),k=i;D&&(k=n.length,n.push({index:k,type:e.type??void 0,label:e.label??void 0,value:e.value??void 0,identifier:e.identifier??void 0,rect:e.rect,enabled:e.enabled,hittable:e.hittable,depth:t,parentIndex:i}));let P=s||!!e.hittable,x=l||function(e){if(!e)return!1;let t=ei(e);return t.includes("recyclerview")||t.includes("listview")||t.includes("gridview")}(e.type);for(let a of e.children)if(d(a,t+1,k,P,x),r)return};for(let e of l)if(d(e,0,void 0,!1,!1),r)break;return r?{nodes:n,truncated:r}:{nodes:n}}(await X(e),800,t)}async function J(){if(!await w("adb"))throw new f("TOOL_MISSING","adb not found in PATH")}async function W(e){let t=(await h("adb",b(e,["shell","wm","size"]))).stdout.match(/Physical size:\s*(\d+)x(\d+)/);if(!t)throw new f("COMMAND_FAILED","Unable to read screen size");return{width:Number(t[1]),height:Number(t[2])}}async function X(e){return N(()=>z(e),{shouldRetry:H})}async function z(e){return await h("adb",b(e,["shell","uiautomator","dump","/sdcard/window_dump.xml"])),(await h("adb",b(e,["shell","cat","/sdcard/window_dump.xml"]))).stdout}function H(e){if(!(e instanceof f)||"COMMAND_FAILED"!==e.code)return!1;let t=`${e.details?.stderr??""}`.toLowerCase();return!!(t.includes("device offline")||t.includes("device not found")||t.includes("transport error")||t.includes("connection reset")||t.includes("broken pipe")||t.includes("timed out"))}async function Y(e,t,a,i){let n=Math.max(1,Math.floor(a));for(let a=0;a<t.length;a+=n){let r=t.slice(a,a+n);await F(e,r),i>0&&a+n<t.length&&await ee(i)}}async function K(e,t){let a=Math.max(0,t);await h("adb",b(e,["shell","input","keyevent","KEYCODE_MOVE_END"]),{allowFailure:!0});for(let t=0;t<a;t+=24){let i=Math.min(24,a-t);await h("adb",b(e,["shell","input","keyevent",...Array(i).fill("KEYCODE_DEL")]),{allowFailure:!0})}}async function Z(e,t,a){let i,n=await X(e),r=/<node\b[^>]*>/g,o=null,s=null,l=null;for(;null!==(i=r.exec(n));){let e=et(i[0]),n=ea(e.bounds);if(!n)continue;let r=e.className??"",c=(e.text??"").replace(/&quot;/g,'"').replace(/&apos;/g,"'").replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&amp;/g,"&"),u=e.focused??!1;if(!c)continue;let d=Math.max(1,n.width*n.height),p=t>=n.x&&t<=n.x+n.width&&a>=n.y&&a<=n.y+n.height;if(u&&Q(r)){(!o||d<=o.area)&&(o={text:c,area:d});continue}if(p&&Q(r)){(!s||d<=s.area)&&(s={text:c,area:d});continue}p&&(!l||d<=l.area)&&(l={text:c,area:d})}return o?.text??s?.text??l?.text??null}function Q(e){let t=e.toLowerCase();return t.includes("edittext")||t.includes("textfield")}async function ee(e){await new Promise(t=>setTimeout(t,e))}function et(e){let t=t=>{let a=RegExp(`${t}="([^"]*)"`).exec(e);return a?a[1]:null},a=e=>{let a=t(e);if(null!==a)return"true"===a};return{text:t("text"),desc:t("content-desc"),resourceId:t("resource-id"),className:t("class"),bounds:t("bounds"),clickable:a("clickable"),enabled:a("enabled"),focusable:a("focusable"),focused:a("focused")}}function ea(e){if(!e)return;let t=/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/.exec(e);if(!t)return;let a=Number(t[1]),i=Number(t[2]);return{x:a,y:i,width:Math.max(0,Number(t[3])-a),height:Math.max(0,Number(t[4])-i)}}function ei(e){return e?e.toLowerCase():""}function en(e){let t=e.trim();return!!t&&/^[\w.]+:id\/[\w.-]+$/i.test(t)}async function er(){if("darwin"!==process.platform)throw new f("UNSUPPORTED_PLATFORM","iOS tools are only available on macOS");if(!await w("xcrun"))throw new f("TOOL_MISSING","xcrun not found in PATH");let e=[],t=await h("xcrun",["simctl","list","devices","-j"]);try{let a=JSON.parse(t.stdout);for(let t of Object.values(a.devices))for(let a of t)a.isAvailable&&e.push({platform:"ios",id:a.udid,name:a.name,kind:"simulator",booted:"Booted"===a.state})}catch(e){throw new f("COMMAND_FAILED","Failed to parse simctl devices JSON",void 0,e)}if(await w("xcrun"))try{let t=await h("xcrun",["devicectl","list","devices","--json"]);for(let a of JSON.parse(t.stdout).devices??[])a.platform?.toLowerCase().includes("ios")&&e.push({platform:"ios",id:a.identifier,name:a.name,kind:"device",booted:!0})}catch{}return e}let eo={settings:"com.apple.Preferences"};async function es(e,t){let a=t.trim();if(a.includes("."))return a;let i=eo[a.toLowerCase()];if(i)return i;if("simulator"===e.kind){let i=(await eN(e)).filter(e=>e.name.toLowerCase()===a.toLowerCase());if(1===i.length)return i[0].bundleId;if(i.length>1)throw new f("INVALID_ARGS",`Multiple apps matched "${t}"`,{matches:i})}throw new f("APP_NOT_INSTALLED",`No app found matching "${t}"`)}async function el(e,t){let a=await es(e,t);if("simulator"===e.kind){await eA(e),await h("open",["-a","Simulator"],{allowFailure:!0}),await h("xcrun",["simctl","launch",e.id,a]);return}await h("xcrun",["devicectl","device","process","launch","--device",e.id,a])}async function ec(e){"simulator"!==e.kind||"Booted"!==await eb(e.id)&&(await eA(e),await h("open",["-a","Simulator"],{allowFailure:!0}))}async function eu(e,t){let a=await es(e,t);if("simulator"===e.kind){await eA(e);let t=await h("xcrun",["simctl","terminate",e.id,a],{allowFailure:!0});if(0!==t.exitCode){if(t.stderr.toLowerCase().includes("found nothing to terminate"))return;throw new f("COMMAND_FAILED",`xcrun exited with code ${t.exitCode}`,{cmd:"xcrun",args:["simctl","terminate",e.id,a],stdout:t.stdout,stderr:t.stderr,exitCode:t.exitCode})}return}await h("xcrun",["devicectl","device","process","terminate","--device",e.id,a])}async function ed(e,t,a){throw eI(e,"press"),new f("UNSUPPORTED_OPERATION","simctl io tap is not available; use the XCTest runner for input")}async function ep(e,t,a,i=800){throw eI(e,"long-press"),new f("UNSUPPORTED_OPERATION","long-press is not supported on iOS simulators without XCTest runner support")}async function ef(e,t,a){await ed(e,t,a)}async function eh(e,t){throw eI(e,"type"),new f("UNSUPPORTED_OPERATION","simctl io keyboard is not available; use the XCTest runner for input")}async function em(e,t,a,i){await ef(e,t,a),await eh(e,i)}async function ew(e,t,a=.6){throw eI(e,"scroll"),new f("UNSUPPORTED_OPERATION","simctl io swipe is not available; use the XCTest runner for input")}async function eg(e){throw new f("UNSUPPORTED_OPERATION",`scrollintoview is not supported on iOS without UI automation (${e})`)}async function ey(e,t){if("simulator"===e.kind){await eA(e),await h("xcrun",["simctl","io",e.id,"screenshot",t]);return}await h("xcrun",["devicectl","device","screenshot","--device",e.id,t])}async function ev(e,t,a,i){eI(e,"settings"),await eA(e);let n=t.toLowerCase(),r=function(e){let t=e.toLowerCase();if("on"===t||"true"===t||"1"===t)return!0;if("off"===t||"false"===t||"0"===t)return!1;throw new f("INVALID_ARGS",`Invalid setting state: ${e}`)}(a);switch(n){case"wifi":return void await h("xcrun",["simctl","status_bar",e.id,"override","--wifiMode",r?"active":"failed"]);case"airplane":r?await h("xcrun",["simctl","status_bar",e.id,"override","--dataNetwork","hide","--wifiMode","failed","--wifiBars","0","--cellularMode","failed","--cellularBars","0","--operatorName",""]):await h("xcrun",["simctl","status_bar",e.id,"clear"]);return;case"location":if(!i)throw new f("INVALID_ARGS","location setting requires an active app in session");await h("xcrun",["simctl","privacy",e.id,r?"grant":"revoke","location",i]);return;default:throw new f("INVALID_ARGS",`Unsupported setting: ${t}`)}}function eI(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 h("xcrun",["simctl","listapps",e.id],{allowFailure:!0})).stdout.trim();if(!t)return[];let a=null;if(t.startsWith("{"))try{a=JSON.parse(t)}catch{a=null}if(!a&&t.startsWith("{"))try{let e=await h("plutil",["-convert","json","-o","-","-"],{allowFailure:!0,stdin:t});0===e.exitCode&&e.stdout.trim().startsWith("{")&&(a=JSON.parse(e.stdout))}catch{a=null}return a?Object.entries(a).map(([e,t])=>({bundleId:e,name:t.CFBundleDisplayName??t.CFBundleName??e})):[]}async function eA(e){"simulator"!==e.kind||"Booted"!==await eb(e.id)&&(await h("xcrun",["simctl","boot",e.id],{allowFailure:!0}),await h("xcrun",["simctl","bootstatus",e.id,"-b"],{allowFailure:!0}))}async function eb(e){let t=await h("xcrun",["simctl","list","devices","-j"],{allowFailure:!0});if(0!==t.exitCode)return null;try{let a=JSON.parse(t.stdout);for(let t of Object.values(a.devices??{})){let a=t.find(t=>t.udid===e);if(a)return a.state}}catch{}return null}let eS=new Map;async function eD(e,t,a={}){var i;return"snapshot"===(i=t.command)||"findText"===i||"listTappables"===i||"alert"===i?N(()=>ek(e,t,a),{shouldRetry:eC}):ek(e,t,a)}async function ek(e,t,a={}){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","iOS runner only supports simulators in v1");try{let i=await eO(e,a),n=await eR(e,i.port,t,a.logPath),r=await n.text(),o={};try{o=JSON.parse(r)}catch{throw new f("COMMAND_FAILED","Invalid runner response",{text:r})}if(!o.ok)throw new f("COMMAND_FAILED",o.error?.message??"Runner error",{runner:o,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:a.logPath});return o.data??{}}catch(n){let i=n instanceof f?n:new f("COMMAND_FAILED",String(n));if("COMMAND_FAILED"===i.code&&"string"==typeof i.message&&i.message.includes("Runner did not accept connection")){await eP(e.id);let i=await eO(e,a),n=await eR(e,i.port,t,a.logPath),r=await n.text(),o={};try{o=JSON.parse(r)}catch{throw new f("COMMAND_FAILED","Invalid runner response",{text:r})}if(!o.ok)throw new f("COMMAND_FAILED",o.error?.message??"Runner error",{runner:o,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:a.logPath});return o.data??{}}throw n}}async function eP(e){let t=eS.get(e);if(t){try{await eR(t.device,t.port,{command:"shutdown"})}catch{}try{await t.testPromise}catch{}eB(t.xctestrunPath),eB(t.jsonPath),eS.delete(e)}}async function ex(e){await h("xcrun",["simctl","bootstatus",e,"-b"],{allowFailure:!0})}async function eO(e,t){let a=eS.get(e.id);if(a)return a;await ex(e.id);let i=await eL(e.id,t),n=await eT(),{xctestrunPath:r,jsonPath:s}=await eF(i,{AGENT_DEVICE_RUNNER_PORT:String(n)},`session-${e.id}-${n}`),l=o("xcodebuild",["test-without-building","-only-testing","AgentDeviceRunnerUITests/RunnerTests/testCommand","-parallel-testing-enabled","NO","-test-timeouts-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-xctestrun",r,"-destination",`platform=iOS Simulator,id=${e.id}`],{onStdoutChunk:e=>{eM(e,t.logPath,t.traceLogPath,t.verbose)},onStderrChunk:e=>{eM(e,t.logPath,t.traceLogPath,t.verbose)},allowFailure:!0,env:{...process.env,AGENT_DEVICE_RUNNER_PORT:String(n)}}),c={device:e,deviceId:e.id,port:n,xctestrunPath:r,jsonPath:s,testPromise:l};return eS.set(e.id,c),c}async function eL(e,t){let a,i=r.join(p.homedir(),".agent-device","ios-runner"),n=r.join(i,"derived");if((a=process.env.AGENT_DEVICE_IOS_CLEAN_DERIVED)&&["1","true","yes","on"].includes(a.toLowerCase()))try{d.rmSync(n,{recursive:!0,force:!0})}catch{}let s=e_(n);if(s)return s;let l=function(){let e=r.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=r.join(t,"package.json");if(d.existsSync(e))return t;t=r.dirname(t)}return e}(),u=r.join(l,"ios-runner","AgentDeviceRunner","AgentDeviceRunner.xcodeproj");if(!d.existsSync(u))throw new f("COMMAND_FAILED","iOS runner project not found",{projectPath:u});try{await o("xcodebuild",["build-for-testing","-project",u,"-scheme","AgentDeviceRunner","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-destination",`platform=iOS Simulator,id=${e}`,"-derivedDataPath",n],{onStdoutChunk:e=>{eM(e,t.logPath,t.traceLogPath,t.verbose)},onStderrChunk:e=>{eM(e,t.logPath,t.traceLogPath,t.verbose)}})}catch(a){let e=a instanceof f?a:new f("COMMAND_FAILED",String(a));throw new f("COMMAND_FAILED","xcodebuild build-for-testing failed",{error:e.message,details:e.details,logPath:t.logPath})}let h=e_(n);if(!h)throw new f("COMMAND_FAILED","Failed to locate .xctestrun after build");return h}function e_(e){if(!d.existsSync(e))return null;let t=[],a=[e];for(;a.length>0;){let e=a.pop();for(let i of d.readdirSync(e,{withFileTypes:!0})){let n=r.join(e,i.name);if(i.isDirectory()){a.push(n);continue}if(i.isFile()&&i.name.endsWith(".xctestrun"))try{let e=d.statSync(n);t.push({path:n,mtimeMs:e.mtimeMs})}catch{}}}return 0===t.length?null:(t.sort((e,t)=>t.mtimeMs-e.mtimeMs),t[0]?.path??null)}function eM(e,t,a,i){t&&d.appendFileSync(t,e),a&&d.appendFileSync(a,e),i&&process.stderr.write(e)}function eC(e){if(!(e instanceof f)||"COMMAND_FAILED"!==e.code)return!1;let t=`${e.message??""}`.toLowerCase();return!!(t.includes("runner did not accept connection")||t.includes("fetch failed")||t.includes("econnrefused")||t.includes("socket hang up"))}async function eR(e,t,a,i){let n=Date.now(),r=null;for(;Date.now()-n<15e3;)try{return await fetch(`http://127.0.0.1:${t}/command`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})}catch(e){r=e,await new Promise(e=>setTimeout(e,100))}if("simulator"===e.kind){let i=await eE(e.id,t,a);return new Response(i.body,{status:i.status})}throw new f("COMMAND_FAILED","Runner did not accept connection",{port:t,logPath:i,lastError:r?String(r):void 0})}async function eE(e,t,a){let i=JSON.stringify(a),n=await h("xcrun",["simctl","spawn",e,"/usr/bin/curl","-s","-X","POST","-H","Content-Type: application/json","--data",i,`http://127.0.0.1:${t}/command`],{allowFailure:!0}),r=n.stdout;if(0!==n.exitCode)throw new f("COMMAND_FAILED","Runner did not accept connection (simctl spawn)",{port:t,stdout:n.stdout,stderr:n.stderr,exitCode:n.exitCode});return{status:200,body:r}}async function eT(){return await new Promise((e,t)=>{let a=m.createServer();a.listen(0,"127.0.0.1",()=>{let i=a.address();a.close(),"object"==typeof i&&i?.port?e(i.port):t(new f("COMMAND_FAILED","Failed to allocate port"))}),a.on("error",t)})}async function eF(e,t,a){let i,n=r.dirname(e),o=a.replace(/[^a-zA-Z0-9._-]/g,"_"),s=r.join(n,`AgentDeviceRunner.env.${o}.json`),l=r.join(n,`AgentDeviceRunner.env.${o}.xctestrun`),c=await h("plutil",["-convert","json","-o","-",e],{allowFailure:!0});if(0!==c.exitCode||!c.stdout.trim())throw new f("COMMAND_FAILED","Failed to read xctestrun plist",{xctestrunPath:e,stderr:c.stderr});try{i=JSON.parse(c.stdout)}catch(t){throw new f("COMMAND_FAILED","Failed to parse xctestrun JSON",{xctestrunPath:e,error:String(t)})}let u=e=>{e.EnvironmentVariables={...e.EnvironmentVariables??{},...t},e.UITestEnvironmentVariables={...e.UITestEnvironmentVariables??{},...t},e.UITargetAppEnvironmentVariables={...e.UITargetAppEnvironmentVariables??{},...t},e.TestingEnvironmentVariables={...e.TestingEnvironmentVariables??{},...t}},p=i.TestConfigurations;if(Array.isArray(p))for(let e of p){if(!e||"object"!=typeof e)continue;let t=e.TestTargets;if(Array.isArray(t))for(let e of t)e&&"object"==typeof e&&u(e)}for(let[e,t]of Object.entries(i))t&&"object"==typeof t&&t.TestBundlePath&&(u(t),i[e]=t);d.writeFileSync(s,JSON.stringify(i,null,2));let m=await h("plutil",["-convert","xml1","-o",l,s],{allowFailure:!0});if(0!==m.exitCode)throw new f("COMMAND_FAILED","Failed to write xctestrun plist",{tmpXctestrunPath:l,stderr:m.stderr});return{xctestrunPath:l,jsonPath:s}}function eB(e){try{d.existsSync(e)&&d.unlinkSync(e)}catch{}}async function e$(e,t={}){let a,i;if("ios"!==e.platform||"simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","AX snapshot is only supported on iOS simulators");let n=await eU(),r=await N(async()=>{var e,a;let i,r,o,s=await h(n,[],{allowFailure:!0});if(t.traceLogPath&&(e=t.traceLogPath,i=((a=s).stdout??"").toString(),r=(a.stderr??"").toString(),o=`
2
2
  [axsnapshot] exit=${a.exitCode} stdoutBytes=${i.length} stderrBytes=${r.length}
3
3
  `,d.appendFileSync(e,o),(0!==a.exitCode||r.length>0)&&(r.length>0&&d.appendFileSync(e,`${r}
4
4
  `),0!==a.exitCode&&i.length>0&&d.appendFileSync(e,`${i}
5
- `))),0!==s.exitCode){let e,t,a=(s.stderr??"").toString(),i=(e=a.toLowerCase()).includes("accessibility permission")?" Enable Accessibility for your terminal in System Settings > Privacy & Security > Accessibility, or use --backend xctest (slower snapshots via XCTest).":e.includes("could not find ios app content")?" AX snapshot sometimes caches empty content. Try restarting the Simulator app.":"",n=!!((t=a.toLowerCase()).includes("could not find ios app content")||t.includes("timeout"));throw new f("COMMAND_FAILED","AX snapshot failed",{stderr:`${a}${i}`,stdout:s.stdout,retryable:n})}return s},{shouldRetry:e=>{var t;return(t=e)instanceof f&&"COMMAND_FAILED"===t.code&&t.details?.retryable===!0}});try{let e=JSON.parse(r.stdout);if(e&&"object"==typeof e&&"root"in e){if(!e.root)throw Error("AX snapshot missing root");a=e.root,i=e.windowFrame??void 0}else a=e}catch(e){throw new f("COMMAND_FAILED","Invalid AX snapshot JSON",{error:String(e)})}let o=a.frame??i,s=[],l=[],c=(e,t)=>{e.frame&&s.push(e.frame);let a=e.frame&&o?{x:e.frame.x-o.x,y:e.frame.y-o.y,width:e.frame.width,height:e.frame.height}:e.frame;for(let i of(l.push({...e,frame:a,children:void 0,depth:t}),e.children??[]))c(i,t+1)};return c(a,0),{nodes:(function(e,t,a){if(!t||0===a.length)return e;let i=1/0,n=1/0;for(let e of a)e.x<i&&(i=e.x),e.y<n&&(n=e.y);return i<=5&&n<=5?e.map(e=>({...e,frame:e.frame?{x:e.frame.x+t.x,y:e.frame.y+t.y,width:e.frame.width,height:e.frame.height}:void 0})):e})(l,o,s).map((e,t)=>({index:t,type:e.subrole??e.role,label:e.label,value:e.value,identifier:e.identifier,rect:e.frame?{x:e.frame.x,y:e.frame.y,width:e.frame.width,height:e.frame.height}:void 0,depth:e.depth}))}}async function e_(){let e=function(){let e=r.dirname(c(import.meta.url));for(let t=0;t<6;t+=1){let t=r.join(e,"package.json");if(d.existsSync(t))return e;e=r.dirname(e)}return process.cwd()}(),t=r.join(e,"ios-runner","AXSnapshot"),a=process.env.AGENT_DEVICE_AX_BINARY;if(a&&d.existsSync(a))return a;let i=r.join(e,"dist","bin","axsnapshot");if(d.existsSync(i))return i;let n=r.join(t,".build","release","axsnapshot");if(d.existsSync(n))return n;let o=await h("swift",["build","-c","release"],{cwd:t,allowFailure:!0});if(0!==o.exitCode||!d.existsSync(n))throw new f("COMMAND_FAILED","Failed to build AX snapshot tool",{stderr:o.stderr,stdout:o.stdout});return n}async function eE(e){let t={platform:e.platform,deviceName:e.device,udid:e.udid,serial:e.serial};if("android"===t.platform){await J();let e=await v();return await g(e,t)}if("ios"===t.platform){let e=await Z();return await g(e,t)}let a=[];try{a.push(...await v())}catch{}try{a.push(...await Z())}catch{}return await g(a,t)}async function eR(e,t,a,i,n){let r=function(e){switch(e.platform){case"android":return{open:(t,a)=>x(e,t,a?.activity),openDevice:()=>L(e),close:t=>_(e,t),tap:(t,a)=>E(e,t,a),longPress:(t,a,i)=>T(e,t,a,i),focus:(t,a)=>B(e,t,a),type:t=>F(e,t),fill:(t,a,i)=>$(e,t,a,i),scroll:(t,a)=>U(e,t,a),scrollIntoView:t=>V(e,t),screenshot:t=>j(e,t)};case"ios":return{open:t=>Q(e,t),openDevice:()=>ee(e),close:t=>et(e,t),tap:(t,a)=>ea(e,t,a),longPress:(t,a,i)=>ei(e,t,a,i),focus:(t,a)=>en(e,t,a),type:t=>er(e,t),fill:(t,a,i)=>eo(e,t,a,i),scroll:(t,a)=>es(e,t,a),scrollIntoView:e=>el(e),screenshot:t=>ec(e,t)};default:throw new f("UNSUPPORTED_PLATFORM",`Unsupported platform: ${e.platform}`)}}(e);switch(t){case"open":{let e=a[0];if(!e)return await r.openDevice(),{app:null};return await r.open(e,{activity:n?.activity}),{app:e}}case"close":{let e=a[0];if(!e)return{closed:"session"};return await r.close(e),{app:e}}case"press":{let[t,i]=a.map(Number);if(Number.isNaN(t)||Number.isNaN(i))throw new f("INVALID_ARGS","press requires x y");return"ios"===e.platform&&"simulator"===e.kind?await ew(e,{command:"tap",x:t,y:i,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}):await r.tap(t,i),{x:t,y:i}}case"long-press":{let e=Number(a[0]),t=Number(a[1]),i=a[2]?Number(a[2]):void 0;if(Number.isNaN(e)||Number.isNaN(t))throw new f("INVALID_ARGS","long-press requires x y [durationMs]");return await r.longPress(e,t,i),{x:e,y:t,durationMs:i}}case"focus":{let[t,i]=a.map(Number);if(Number.isNaN(t)||Number.isNaN(i))throw new f("INVALID_ARGS","focus requires x y");return"ios"===e.platform&&"simulator"===e.kind?await ew(e,{command:"tap",x:t,y:i,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}):await r.focus(t,i),{x:t,y:i}}case"type":{let t=a.join(" ");if(!t)throw new f("INVALID_ARGS","type requires text");return"ios"===e.platform&&"simulator"===e.kind?await ew(e,{command:"type",text:t,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}):await r.type(t),{text:t}}case"fill":{let t=Number(a[0]),i=Number(a[1]),o=a.slice(2).join(" ");if(Number.isNaN(t)||Number.isNaN(i)||!o)throw new f("INVALID_ARGS","fill requires x y text");return"ios"===e.platform&&"simulator"===e.kind?(await ew(e,{command:"tap",x:t,y:i,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),await ew(e,{command:"type",text:o,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath})):await r.fill(t,i,o),{x:t,y:i,text:o}}case"scroll":{let t=a[0],i=a[1]?Number(a[1]):void 0;if(!t)throw new f("INVALID_ARGS","scroll requires direction");if("ios"===e.platform&&"simulator"===e.kind){if(!["up","down","left","right"].includes(t))throw new f("INVALID_ARGS",`Unknown direction: ${t}`);let a=function(e){switch(e){case"up":return"down";case"down":return"up";case"left":return"right";case"right":return"left"}}(t);await ew(e,{command:"swipe",direction:a,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath})}else await r.scroll(t,i);return{direction:t,amount:i}}case"scrollintoview":{let t=a.join(" ").trim();if(!t)throw new f("INVALID_ARGS","scrollintoview requires text");if("ios"===e.platform&&"simulator"===e.kind){for(let a=0;a<8;a+=1){let i=await ew(e,{command:"findText",text:t,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath});if(i?.found)return{text:t,attempts:a+1};await ew(e,{command:"swipe",direction:"up",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),await new Promise(e=>setTimeout(e,300))}throw new f("COMMAND_FAILED",`scrollintoview could not find text: ${t}`)}return await r.scrollIntoView(t),{text:t}}case"pinch":{let t=Number(a[0]),i=a[1]?Number(a[1]):void 0,r=a[2]?Number(a[2]):void 0;if(Number.isNaN(t)||t<=0)throw new f("INVALID_ARGS","pinch requires scale > 0");if("ios"===e.platform&&"simulator"===e.kind)await ew(e,{command:"pinch",scale:t,x:i,y:r,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath});else throw new f("UNSUPPORTED_OPERATION","pinch is only supported on iOS simulators");return{scale:t,x:i,y:r}}case"screenshot":{let e=i??`./screenshot-${Date.now()}.png`;return await r.screenshot(e),{path:e}}case"back":if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","back is only supported on iOS simulators in v1");return await ew(e,{command:"back",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),{action:"back"}}return await R(e),{action:"back"};case"home":if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","home is only supported on iOS simulators in v1");return await ew(e,{command:"home",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),{action:"home"}}return await C(e),{action:"home"};case"app-switcher":if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","app-switcher is only supported on iOS simulators in v1");return await ew(e,{command:"appSwitcher",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),{action:"app-switcher"}}return await M(e),{action:"app-switcher"};case"settings":{let[t,i,r]=a;if("ios"===e.platform)return await eu(e,t,i,r??n?.appBundleId),{setting:t,state:i};return await G(e,t,i),{setting:t,state:i}}case"snapshot":{let t=n?.snapshotBackend??"xctest";if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","snapshot is only supported on iOS simulators in v1");if("ax"===t)return{nodes:(await eL(e,{traceLogPath:n?.traceLogPath})).nodes??[],truncated:!1,backend:"ax"};let a=await ew(e,{command:"snapshot",appBundleId:n?.appBundleId,interactiveOnly:n?.snapshotInteractiveOnly,compact:n?.snapshotCompact,depth:n?.snapshotDepth,scope:n?.snapshotScope,raw:n?.snapshotRaw},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),i=a.nodes??[];if(0===i.length)try{return{nodes:(await eL(e,{traceLogPath:n?.traceLogPath})).nodes??[],truncated:!1,backend:"ax"}}catch{}return{nodes:i,truncated:a.truncated??!1,backend:"xctest"}}let a=await q(e,{interactiveOnly:n?.snapshotInteractiveOnly,compact:n?.snapshotCompact,depth:n?.snapshotDepth,scope:n?.snapshotScope,raw:n?.snapshotRaw});return{nodes:a.nodes??[],truncated:a.truncated??!1,backend:"android"}}default:throw new f("INVALID_ARGS",`Unknown command: ${t}`)}}function eC(e){return e.map((e,t)=>({...e,ref:`e${t+1}`}))}function eM(e){let t=e.trim();return t.startsWith("@")?t.slice(1)||null:t.startsWith("e")?t:null}function eT(e,t){return e.find(e=>e.ref===t)??null}function eF(e){return{x:Math.round(e.x+e.width/2),y:Math.round(e.y+e.height/2)}}function eB(e,t,a,i={}){let n=eU(a);if(!n)return null;let r=null;for(let a of e){if(i.requireRect&&!a.rect)continue;let e=function(e,t,a){switch(t){case"role":return function(e,t){let a=function(e){let t=e.trim();return t?((t=(t.split(".").pop()??t).replace(/XCUIElementType/gi,"").toLowerCase()).startsWith("ax")&&(t=t.replace(/^ax/,"")),t):""}(e??"");return a?a===t?2:+!!a.includes(t):0}(e.type,a);case"label":return e$(e.label,a);case"value":return e$(e.value,a);case"id":return e$(e.identifier,a);default:return Math.max(e$(e.label,a),e$(e.value,a),e$(e.identifier,a))}}(a,t,n);if(!(e<=0)&&(!r||e>r.score)&&(r={node:a,score:e},e>=2))break}return r?.node??null}function e$(e,t){let a=eU(e??"");return a?a===t?2:+!!a.includes(t):0}function eU(e){return e.trim().toLowerCase().replace(/\s+/g," ")}let eV=new Map,ej=r.join(p.homedir(),".agent-device"),eG=r.join(ej,"daemon.json"),eq=r.join(ej,"daemon.log"),eJ=r.join(ej,"sessions"),eW=function(){try{let e=function(){let e=r.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=r.join(t,"package.json");if(d.existsSync(e))return t;t=r.dirname(t)}return e}();return JSON.parse(d.readFileSync(r.join(e,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}(),eX=a.randomBytes(24).toString("hex");function ez(e,t,a){return{appBundleId:t,activity:e?.activity,verbose:e?.verbose,logPath:eq,traceLogPath:a,snapshotInteractiveOnly:e?.snapshotInteractiveOnly,snapshotCompact:e?.snapshotCompact,snapshotDepth:e?.snapshotDepth,snapshotScope:e?.snapshotScope,snapshotRaw:e?.snapshotRaw,snapshotBackend:e?.snapshotBackend}}async function eH(e){if(e.token!==eX)return{ok:!1,error:{code:"UNAUTHORIZED",message:"Invalid token"}};let t=e.command,a=e.session||"default";if("session_list"===t)return{ok:!0,data:{sessions:Array.from(eV.values()).map(e=>({name:e.name,platform:e.device.platform,device:e.device.name,id:e.device.id,createdAt:e.createdAt}))}};if("devices"===t)try{let t=[];if(e.flags?.platform==="android"){let{listAndroidDevices:e}=await Promise.resolve().then(()=>({listAndroidDevices:v}));t.push(...await e())}else if(e.flags?.platform==="ios"){let{listIosDevices:e}=await Promise.resolve().then(()=>({listIosDevices:Z}));t.push(...await e())}else{let{listAndroidDevices:e}=await Promise.resolve().then(()=>({listAndroidDevices:v})),{listIosDevices:a}=await Promise.resolve().then(()=>({listIosDevices:Z}));try{t.push(...await e())}catch{}try{t.push(...await a())}catch{}}return{ok:!0,data:{devices:t}}}catch(t){let e=l(t);return{ok:!1,error:{code:e.code,message:e.message,details:e.details}}}if("apps"===t){let t=eV.get(a),i=e.flags??{};if(!t&&!i.platform&&!i.device&&!i.udid&&!i.serial)return{ok:!1,error:{code:"INVALID_ARGS",message:"apps requires an active session or an explicit device selector (e.g. --platform ios)."}};let n=t?.device??await eE(i);if(await e4(n),"ios"===n.platform){if("simulator"!==n.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"apps list is only supported on iOS simulators"}};let{listSimulatorApps:t}=await Promise.resolve().then(()=>({listSimulatorApps:ep})),a=await t(n);return e.flags?.appsMetadata?{ok:!0,data:{apps:a}}:{ok:!0,data:{apps:a.map(e=>e.name&&e.name!==e.bundleId?`${e.name} (${e.bundleId})`:e.bundleId)}}}let{listAndroidApps:r,listAndroidAppsMetadata:o}=await Promise.resolve().then(()=>({listAndroidApps:D,listAndroidAppsMetadata:k}));return e.flags?.appsMetadata?{ok:!0,data:{apps:await o(n,e.flags?.appsFilter)}}:{ok:!0,data:{apps:await r(n,e.flags?.appsFilter)}}}if("appstate"===t){let t=eV.get(a),i=e.flags??{},n=t?.device??await eE(i);if(await e4(n),"ios"===n.platform){if(t?.appBundleId)return{ok:!0,data:{platform:"ios",appBundleId:t.appBundleId,appName:t.appName??t.appBundleId,source:"session"}};let a=await eY(n,t?.trace?.outPath,e.flags);return{ok:!0,data:{platform:"ios",appName:a.appName,appBundleId:a.appBundleId,source:a.source}}}let{getAndroidAppState:r}=await Promise.resolve().then(()=>({getAndroidAppState:P})),o=await r(n);return{ok:!0,data:{platform:"android",package:o.package,activity:o.activity}}}if("open"===t){let i;if(eV.has(a)){let i,n=eV.get(a),r=e.positionals?.[0];if(!n||!r)return{ok:!1,error:{code:"INVALID_ARGS",message:"Session already active. Close it first or pass a new --session name."}};if("ios"===n.device.platform)try{let{resolveIosApp:e}=await Promise.resolve().then(()=>({resolveIosApp:K}));i=await e(n.device,r)}catch{i=void 0}await eR(n.device,"open",e.positionals??[],e.flags?.out,{...ez(e.flags,i)});let o={...n,appBundleId:i,appName:r,snapshot:void 0};return eZ(o,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:a,appName:r,appBundleId:i}}),eV.set(a,o),{ok:!0,data:{session:a,appName:r,appBundleId:i}}}let n=await eE(e.flags??{});await e4(n);let r=Array.from(eV.values()).find(e=>e.device.id===n.id);if(r)return{ok:!1,error:{code:"DEVICE_IN_USE",message:`Device is already in use by session "${r.name}".`,details:{session:r.name,deviceId:n.id,deviceName:n.name}}};let o=e.positionals?.[0];if("ios"===n.platform)try{let{resolveIosApp:t}=await Promise.resolve().then(()=>({resolveIosApp:K}));i=await t(n,e.positionals?.[0]??"")}catch{i=void 0}await eR(n,"open",e.positionals??[],e.flags?.out,{...ez(e.flags,i)});let s={name:a,device:n,createdAt:Date.now(),appBundleId:i,appName:o,actions:[]};return eZ(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:a}}),eV.set(a,s),{ok:!0,data:{session:a}}}if("replay"===t){let t=e.positionals?.[0];if(!t)return{ok:!1,error:{code:"INVALID_ARGS",message:"replay requires a path"}};try{let e=e0(t),i=JSON.parse(d.readFileSync(e,"utf8")),n=i.optimizedActions??i.actions??[];for(let e of n)e&&"replay"!==e.command&&await eH({token:eX,session:a,command:e.command,positionals:e.positionals??[],flags:e.flags??{}});return{ok:!0,data:{replayed:n.length,session:a}}}catch(t){let e=l(t);return{ok:!1,error:{code:e.code,message:e.message}}}}if("close"===t){let i=eV.get(a);return i?(e.positionals&&e.positionals.length>0&&await eR(i.device,"close",e.positionals??[],e.flags?.out,{...ez(e.flags,i.appBundleId,i.trace?.outPath)}),"ios"===i.device.platform&&"simulator"===i.device.kind&&await ev(i.device.id),eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:a}}),eQ(i),eV.delete(a),{ok:!0,data:{session:a}}):{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session"}}}if("snapshot"===t){let i=eV.get(a),n=i?.device??await eE(e.flags??{});i||await e4(n);let r=i?.appBundleId,o=e.flags?.snapshotScope;if(o&&o.trim().startsWith("@")){if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"Ref scope requires an existing snapshot in session."}};let e=eM(o.trim());if(!e)return{ok:!1,error:{code:"INVALID_ARGS",message:`Invalid ref scope: ${o}`}};let t=eT(i.snapshot.nodes,e),a=t?e3(t,i.snapshot.nodes):void 0;if(!a)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${o} not found or has no label`}};o=a}let s=await eR(n,"snapshot",[],e.flags?.out,{...ez({...e.flags,snapshotScope:o},r,i?.trace?.outPath)}),l=s?.nodes??[],c=eC(e.flags?.snapshotRaw?l:e6(l)),u={nodes:c,truncated:s?.truncated,createdAt:Date.now(),backend:s?.backend},d={name:a,device:n,createdAt:i?.createdAt??Date.now(),appBundleId:i?.appBundleId??r,snapshot:u,actions:i?.actions??[],appName:i?.appName};return eZ(d,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{nodes:c.length,truncated:s?.truncated??!1}}),eV.set(a,d),{ok:!0,data:{nodes:c,truncated:s?.truncated??!1,appName:d.appBundleId?d.appName??d.appBundleId:void 0,appBundleId:d.appBundleId}}}if("wait"===t){let i=eV.get(a),n=i?.device??await eE(e.flags??{});i||await e4(n);let r=e.positionals??[];if(0===r.length)return{ok:!1,error:{code:"INVALID_ARGS",message:"wait requires a duration or text"}};let o=e=>{if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null},s=o(r[0]);if(null!==s)return await new Promise(e=>setTimeout(e,s)),i&&eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{waitedMs:s}}),{ok:!0,data:{waitedMs:s}};let l="",c=null;if("text"===r[0])l=null!==(c=o(r[r.length-1]))?r.slice(1,-1).join(" "):r.slice(1).join(" ");else if(r[0].startsWith("@")){if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"Ref wait requires an existing snapshot in session."}};let e=eM(r[0]);if(!e)return{ok:!1,error:{code:"INVALID_ARGS",message:`Invalid ref: ${r[0]}`}};let t=eT(i.snapshot.nodes,e),a=t?e3(t,i.snapshot.nodes):void 0;if(!a)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${r[0]} not found or has no label`}};c=o(r[r.length-1]),l=a}else l=null!==(c=o(r[r.length-1]))?r.slice(0,-1).join(" "):r.join(" ");if(!(l=l.trim()))return{ok:!1,error:{code:"INVALID_ARGS",message:"wait requires text"}};let u=c??1e4,d=Date.now();for(;Date.now()-d<u;){if("ios"===n.platform&&"simulator"===n.kind){let a=await ew(n,{command:"findText",text:l,appBundleId:i?.appBundleId},{verbose:e.flags?.verbose,logPath:eq,traceLogPath:i?.trace?.outPath});if(a?.found)return i&&eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{text:l,waitedMs:Date.now()-d}}),{ok:!0,data:{text:l,waitedMs:Date.now()-d}}}else if("android"!==n.platform)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"wait is not supported on this device"}};else if(e2(eC((await q(n,{scope:l})).nodes??[]),l))return i&&eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{text:l,waitedMs:Date.now()-d}}),{ok:!0,data:{text:l,waitedMs:Date.now()-d}};await new Promise(e=>setTimeout(e,300))}return{ok:!1,error:{code:"COMMAND_FAILED",message:`wait timed out for text: ${l}`}}}if("alert"===t){let i=eV.get(a),n=i?.device??await eE(e.flags??{});i||await e4(n);let r=(e.positionals?.[0]??"get").toLowerCase();if("ios"!==n.platform||"simulator"!==n.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"alert is only supported on iOS simulators in v1"}};if("wait"===r){let a=(e=>{if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null})(e.positionals?.[1])??1e4,r=Date.now();for(;Date.now()-r<a;){try{let a=await ew(n,{command:"alert",action:"get",appBundleId:i?.appBundleId},{verbose:e.flags?.verbose,logPath:eq,traceLogPath:i?.trace?.outPath});return i&&eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:a}),{ok:!0,data:a}}catch{}await new Promise(e=>setTimeout(e,300))}return{ok:!1,error:{code:"COMMAND_FAILED",message:"alert wait timed out"}}}let o=await ew(n,{command:"alert",action:"accept"===r||"dismiss"===r?r:"get",appBundleId:i?.appBundleId},{verbose:e.flags?.verbose,logPath:eq,traceLogPath:i?.trace?.outPath});return i&&eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:o}),{ok:!0,data:o}}if("record"===t){let i=(e.positionals?.[0]??"").toLowerCase();if(!["start","stop"].includes(i))return{ok:!1,error:{code:"INVALID_ARGS",message:"record requires start|stop"}};let n=eV.get(a),o=n?.device??await eE(e.flags??{});n||await e4(o);let s=n??{name:a,device:o,createdAt:Date.now(),actions:[]};if("start"===i){if(s.recording)return{ok:!1,error:{code:"INVALID_ARGS",message:"recording already in progress"}};let i=e.positionals?.[1]??`./recording-${Date.now()}.mp4`,n=r.resolve(i),l=r.dirname(n);if(d.existsSync(l)||d.mkdirSync(l,{recursive:!0}),"ios"===o.platform){if("simulator"!==o.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"record is only supported on iOS simulators in v1"}};let{child:e,wait:t}=u("xcrun",["simctl","io",o.id,"recordVideo",n],{allowFailure:!0});s.recording={platform:"ios",outPath:n,child:e,wait:t}}else{let e=`/sdcard/agent-device-recording-${Date.now()}.mp4`,{child:t,wait:a}=u("adb",["-s",o.id,"shell","screenrecord",e],{allowFailure:!0});s.recording={platform:"android",outPath:n,remotePath:e,child:t,wait:a}}return eV.set(a,s),eZ(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"start"}}),{ok:!0,data:{recording:"started",outPath:i}}}if(!s.recording)return{ok:!1,error:{code:"INVALID_ARGS",message:"no active recording"}};let l=s.recording;l.child.kill("SIGINT");try{await l.wait}catch{}if("android"===l.platform&&l.remotePath)try{await h("adb",["-s",o.id,"pull",l.remotePath,l.outPath],{allowFailure:!0}),await h("adb",["-s",o.id,"shell","rm","-f",l.remotePath],{allowFailure:!0})}catch{}return s.recording=void 0,eZ(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"stop",outPath:l.outPath}}),{ok:!0,data:{recording:"stopped",outPath:l.outPath}}}if("trace"===t){let i=(e.positionals?.[0]??"").toLowerCase();if(!["start","stop"].includes(i))return{ok:!1,error:{code:"INVALID_ARGS",message:"trace requires start|stop"}};let n=eV.get(a);if(!n)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session"}};if("start"===i){let a,i;if(n.trace)return{ok:!1,error:{code:"INVALID_ARGS",message:"trace already in progress"}};let o=e0(e.positionals?.[1]??(a=n.name.replace(/[^a-zA-Z0-9._-]/g,"_"),i=new Date().toISOString().replace(/[:.]/g,"-"),r.join(eJ,`${a}-${i}.trace.log`)));return d.mkdirSync(r.dirname(o),{recursive:!0}),d.appendFileSync(o,""),n.trace={outPath:o,startedAt:Date.now()},eZ(n,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"start",outPath:o}}),{ok:!0,data:{trace:"started",outPath:o}}}if(!n.trace)return{ok:!1,error:{code:"INVALID_ARGS",message:"no active trace"}};let o=n.trace.outPath;if(e.positionals?.[1]){let t=e0(e.positionals[1]);d.mkdirSync(r.dirname(t),{recursive:!0}),d.existsSync(o)?d.renameSync(o,t):d.appendFileSync(t,""),o=t}return n.trace=void 0,eZ(n,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"stop",outPath:o}}),{ok:!0,data:{trace:"stopped",outPath:o}}}if("settings"===t){let i=e.positionals?.[0],n=e.positionals?.[1];if(!i||!n)return{ok:!1,error:{code:"INVALID_ARGS",message:"settings requires <wifi|airplane|location> <on|off>"}};let r=eV.get(a),o=r?.device??await eE(e.flags??{});r||await e4(o);let s=r?.appBundleId,l=await eR(o,"settings",[i,n,s??""],e.flags?.out,{...ez(e.flags,s,r?.trace?.outPath)});return r&&eZ(r,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:l??{setting:i,state:n}}),{ok:!0,data:l??{setting:i,state:n}}}if("find"===t){let n=e.positionals??[];if(0===n.length)return{ok:!1,error:{code:"INVALID_ARGS",message:"find requires a locator or text"}};let{locator:r,query:o,action:s,value:l,timeoutMs:c}=function(e){let t="any",a=0;["text","label","value","role","id"].includes(e[0])&&(t=e[0],a=1);let i=e[a]??"",n=e.slice(a+1);if(0===n.length)return{locator:t,query:i,action:"click"};let r=n[0].toLowerCase();if("get"===r){let e=n[1]?.toLowerCase();if("text"===e)return{locator:t,query:i,action:"get_text"};if("attrs"===e)return{locator:t,query:i,action:"get_attrs"};throw new f("INVALID_ARGS","find get only supports text or attrs")}if("wait"===r)return{locator:t,query:i,action:"wait",timeoutMs:function(e){if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null}(n[1])??void 0};if("exists"===r)return{locator:t,query:i,action:"exists"};if("click"===r)return{locator:t,query:i,action:"click"};if("focus"===r)return{locator:t,query:i,action:"focus"};if("fill"===r)return{locator:t,query:i,action:"fill",value:n.slice(1).join(" ")};if("type"===r)return{locator:t,query:i,action:"type",value:n.slice(1).join(" ")};throw new f("INVALID_ARGS",`Unsupported find action: ${n[0]}`)}(n);if(!o)return{ok:!1,error:{code:"INVALID_ARGS",message:"find requires a value"}};let u=eV.get(a);if(!u&&"exists"!==s&&"wait"!==s&&"get_text"!==s&&"get_attrs"!==s)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session. Run open first."}};let d=u?.device??await eE(e.flags??{});u||await e4(d);let p=u?.appBundleId,h="role"!==r?o:void 0,m="click"===s||"focus"===s||"fill"===s||"type"===s,w=0,g=null,v=async()=>{let t=Date.now();if(g&&t-w<750)return{nodes:g};let i=await eR(d,"snapshot",[],e.flags?.out,{...ez({...e.flags,snapshotScope:h,snapshotInteractiveOnly:m,snapshotCompact:m},p,u?.trace?.outPath)}),n=i?.nodes??[],r=eC(e.flags?.snapshotRaw?n:e6(n));return w=t,g=r,u&&(u.snapshot={nodes:r,truncated:i?.truncated,createdAt:Date.now(),backend:i?.backend},eV.set(a,u)),{nodes:r,truncated:i?.truncated,backend:i?.backend}};if("wait"===s){let a=c??1e4,i=Date.now();for(;Date.now()-i<a;){let{nodes:a}=await v();if(eB(a,r,o,{requireRect:!1}))return u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{found:!0,waitedMs:Date.now()-i}}),{ok:!0,data:{found:!0,waitedMs:Date.now()-i}};await new Promise(e=>setTimeout(e,300))}return{ok:!1,error:{code:"COMMAND_FAILED",message:"find wait timed out"}}}let{nodes:y}=await v(),I=eB(y,r,o,{requireRect:m});if(!I)return{ok:!1,error:{code:"COMMAND_FAILED",message:"find did not match any element"}};let N="click"===s||"focus"===s||"fill"===s||"type"===s?function(e,t){if(t.hittable)return t;let a=t,i=new Set;for(;void 0!==a.parentIndex&&!i.has(a.ref);){i.add(a.ref);let t=e[a.parentIndex];if(!t)break;if(t.hittable)return t;a=t}return null}(y,I)??I:I,A=`@${N.ref}`,b={...e.flags??{},noRecord:!0};if("exists"===s)return u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{found:!0}}),{ok:!0,data:{found:!0}};if("get_text"===s){var i;let a=[(i=I).label,i.value,i.identifier].map(e=>"string"==typeof e?e.trim():"").filter(e=>e.length>0)[0]??"";return u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:A,action:"get text",text:a}}),{ok:!0,data:{ref:A,text:a,node:I}}}if("get_attrs"===s)return u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:A,action:"get attrs"}}),{ok:!0,data:{ref:A,node:I}};if("click"===s){let i=await eH({token:eX,session:a,command:"click",positionals:[A],flags:b});return i.ok&&u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:A,action:"click"}}),i}if("fill"===s){if(!l)return{ok:!1,error:{code:"INVALID_ARGS",message:"find fill requires text"}};let i=await eH({token:eX,session:a,command:"fill",positionals:[A,l],flags:b});return i.ok&&u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:A,action:"fill"}}),i}if("focus"===s){let a=I.rect?eF(I.rect):null;if(!a)return{ok:!1,error:{code:"COMMAND_FAILED",message:"matched element has no bounds"}};let i=await eR(d,"focus",[String(a.x),String(a.y)],e.flags?.out,{...ez(e.flags,u?.appBundleId,u?.trace?.outPath)});return u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:A,action:"focus"}}),{ok:!0,data:i??{ref:A}}}if("type"===s){if(!l)return{ok:!1,error:{code:"INVALID_ARGS",message:"find type requires text"}};let a=I.rect?eF(I.rect):null;if(!a)return{ok:!1,error:{code:"COMMAND_FAILED",message:"matched element has no bounds"}};await eR(d,"focus",[String(a.x),String(a.y)],e.flags?.out,{...ez(e.flags,u?.appBundleId,u?.trace?.outPath)});let i=await eR(d,"type",[l],e.flags?.out,{...ez(e.flags,u?.appBundleId,u?.trace?.outPath)});return u&&eZ(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:A,action:"type"}}),{ok:!0,data:i??{ref:A}}}}if("click"===t){let i=eV.get(a);if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let n=e.positionals?.[0]??"",r=eM(n);if(!r)return{ok:!1,error:{code:"INVALID_ARGS",message:"click requires a ref like @e2"}};let o=eT(i.snapshot.nodes,r);if(!o?.rect&&e.positionals.length>1){let t=e.positionals.slice(1).join(" ").trim();t.length>0&&(o=e2(i.snapshot.nodes,t))}if(!o?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${n} not found or has no bounds`}};let s=e3(o,i.snapshot.nodes),{x:l,y:c}=eF(o.rect);return await eR(i.device,"press",[String(l),String(c)],e.flags?.out,{...ez(e.flags,i.appBundleId,i.trace?.outPath)}),eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:r,x:l,y:c,refLabel:s}}),{ok:!0,data:{ref:r,x:l,y:c}}}if("fill"===t){let i=eV.get(a);if(e.positionals?.[0]?.startsWith("@")){let a;if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let n=eM(e.positionals[0]);if(!n)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires a ref like @e2"}};let r=e.positionals.length>=3?e.positionals[1]:"",o=e.positionals.length>=3?e.positionals.slice(2).join(" "):e.positionals.slice(1).join(" ");if(!o)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires text after ref"}};let s=eT(i.snapshot.nodes,n);if(!s?.rect&&r&&(s=e2(i.snapshot.nodes,r)),!s?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${e.positionals[0]} not found or has no bounds`}};let l=e3(s,i.snapshot.nodes),c=s.label?.trim();if("ios"===i.device.platform&&"simulator"===i.device.kind&&("textfield"===(a=e7(s.type??""))||"textview"===a||"searchfield"===a||"textarea"===a)){let a=s.rect?eF(s.rect):null;return a?(await eR(i.device,"focus",[String(a.x),String(a.y)],e.flags?.out,{...ez(e.flags,i.appBundleId,i.trace?.outPath)}),await ew(i.device,{command:"type",text:o,appBundleId:i.appBundleId},{verbose:e.flags?.verbose,logPath:eq,traceLogPath:i?.trace?.outPath}),eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:n,refLabel:l??c,action:"fill",text:o}}),{ok:!0,data:{ref:n}}):{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${e.positionals[0]} not found or has no bounds`}}}let{x:u,y:d}=eF(s.rect),p=await eR(i.device,"fill",[String(u),String(d),o],e.flags?.out,{...ez(e.flags,i.appBundleId,i.trace?.outPath)});return eZ(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:p??{ref:n,x:u,y:d,refLabel:l}}),{ok:!0,data:p??{ref:n,x:u,y:d}}}}if("get"===t){let i=e.positionals?.[0],n=e.positionals?.[1];if("text"!==i&&"attrs"!==i)return{ok:!1,error:{code:"INVALID_ARGS",message:"get only supports text or attrs"}};let r=eV.get(a);if(!r?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let o=eM(n??"");if(!o)return{ok:!1,error:{code:"INVALID_ARGS",message:"get text requires a ref like @e2"}};let s=eT(r.snapshot.nodes,o);if(!s&&e.positionals.length>2){let t=e.positionals.slice(2).join(" ").trim();t.length>0&&(s=e2(r.snapshot.nodes,t))}if(!s)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${n} not found`}};if("attrs"===i)return eZ(r,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:o}}),{ok:!0,data:{ref:o,node:s}};let l=[s.label,s.value,s.identifier].map(e=>"string"==typeof e?e.trim():"").filter(e=>e.length>0)[0]??"";return eZ(r,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:o,text:l,refLabel:l||void 0}}),{ok:!0,data:{ref:o,text:l,node:s}}}let n=eV.get(a);if(!n)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session. Run open first."}};let o=await eR(n.device,t,e.positionals??[],e.flags?.out,{...ez(e.flags,n.appBundleId,n.trace?.outPath)});return eZ(n,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:o??{}}),{ok:!0,data:o??{}}}function eZ(e,t){t.flags?.noRecord||e.actions.push({ts:Date.now(),command:t.command,positionals:t.positionals,flags:function(e){if(!e)return{};let{platform:t,device:a,udid:i,serial:n,out:r,verbose:o,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:u,snapshotRaw:d,snapshotBackend:p,appsMetadata:f,noRecord:h,recordJson:m}=e;return{platform:t,device:a,udid:i,serial:n,out:r,verbose:o,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:u,snapshotRaw:d,snapshotBackend:p,appsMetadata:f,noRecord:h,recordJson:m}}(t.flags),result:t.result})}async function eY(e,t,a){let i=eK(await eR(e,"snapshot",[],a?.out,{...ez({...a,snapshotDepth:1,snapshotCompact:!0,snapshotBackend:"ax"},void 0,t)}));if(i?.appName||i?.appBundleId)return{appName:i.appName??i.appBundleId??"unknown",appBundleId:i.appBundleId,source:"snapshot-ax"};let n=eK(await eR(e,"snapshot",[],a?.out,{...ez({...a,snapshotDepth:1,snapshotCompact:!0,snapshotBackend:"xctest"},void 0,t)}));return{appName:n?.appName??n?.appBundleId??"unknown",appBundleId:n?.appBundleId,source:"snapshot-xctest"}}function eK(e){let t=eC(e?.nodes??[]),a=t.find(e=>"application"===e7(e.type??""))??t[0];if(!a)return null;let i=a.label?.trim(),n=a.identifier?.trim();return i||n?{appName:i||void 0,appBundleId:n||void 0}:null}function eQ(e){try{d.existsSync(eJ)||d.mkdirSync(eJ,{recursive:!0});let t=e.name.replace(/[^a-zA-Z0-9._-]/g,"_"),a=new Date(e.createdAt).toISOString().replace(/[:.]/g,"-"),i=r.join(eJ,`${t}-${a}.ad`),n=r.join(eJ,`${t}-${a}.json`),o={name:e.name,device:e.device,createdAt:e.createdAt,appBundleId:e.appBundleId,actions:e.actions,optimizedActions:function(e){let t=[];for(let a of e.actions)if("snapshot"!==a.command){if("click"===a.command||"fill"===a.command||"get"===a.command){let i=a.result?.refLabel;"string"==typeof i&&i.trim().length>0&&t.push({ts:a.ts,command:"snapshot",positionals:[],flags:{platform:e.device.platform,snapshotInteractiveOnly:!0,snapshotCompact:!0,snapshotScope:i.trim()},result:{scope:i.trim()}})}t.push(a)}return t}(e)},s=function(e,t){let a=[],i=e.device.name.replace(/"/g,'\\"'),n=e.device.kind?` kind=${e.device.kind}`:"";for(let r of(a.push(`context platform=${e.device.platform} device="${i}"${n} theme=unknown`),t))r.flags?.noRecord||a.push(function(e){let t=[e.command];if("click"===e.command){let a=e.positionals?.[0];if(a){t.push(e1(a));let i=e.result?.refLabel;return"string"==typeof i&&i.trim().length>0&&t.push(e1(i)),t.join(" ")}}if("fill"===e.command){let a=e.positionals?.[0];if(a&&a.startsWith("@")){t.push(e1(a));let i=e.result?.refLabel,n=e.positionals.slice(1).join(" ");return"string"==typeof i&&i.trim().length>0&&t.push(e1(i)),n&&t.push(e1(n)),t.join(" ")}}if("get"===e.command){let a=e.positionals?.[0],i=e.positionals?.[1];if(a&&i){t.push(e1(a)),t.push(e1(i));let n=e.result?.refLabel;return"string"==typeof n&&n.trim().length>0&&t.push(e1(n)),t.join(" ")}}if("snapshot"===e.command)return e.flags?.snapshotInteractiveOnly&&t.push("-i"),e.flags?.snapshotCompact&&t.push("-c"),"number"==typeof e.flags?.snapshotDepth&&t.push("-d",String(e.flags.snapshotDepth)),e.flags?.snapshotScope&&t.push("-s",e1(e.flags.snapshotScope)),e.flags?.snapshotRaw&&t.push("--raw"),e.flags?.snapshotBackend&&t.push("--backend",e.flags.snapshotBackend),t.join(" ");for(let a of e.positionals??[])t.push(e1(a));return t.join(" ")}(r));return`${a.join("\n")}
6
- `}(e,o.optimizedActions);d.writeFileSync(i,s),e.actions.some(e=>e.flags?.recordJson)&&d.writeFileSync(n,JSON.stringify(o,null,2))}catch{}}function e0(e){return e.startsWith("~/")?r.join(p.homedir(),e.slice(2)):r.resolve(e)}function e1(e){let t=e.trim();return t.startsWith("@")||/^-?\d+(\.\d+)?$/.test(t)?t:JSON.stringify(t)}function e2(e,t){let a=t.toLowerCase();return e.find(e=>{let t=(e.label??"").toLowerCase(),i=(e.value??"").toLowerCase(),n=(e.identifier??"").toLowerCase();return t.includes(a)||i.includes(a)||n.includes(a)})??null}function e3(e,t){let a=[e.label,e.value,e.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0);return a&&e8(a)?a:function(e,t){if(!e.rect)return;let a=e.rect.y+e.rect.height/2,i=null;for(let e of t){if(!e.rect)continue;let t=[e.label,e.value,e.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0);if(!t||!e8(t))continue;let n=Math.abs(e.rect.y+e.rect.height/2-a);(!i||n<i.distance)&&(i={label:t,distance:n})}return i?.label}(e,t)??(a&&e8(a)?a:void 0)}function e8(e){let t=e.trim();return!(!t||/^(true|false)$/i.test(t)||/^\d+$/.test(t))}async function e4(e){if("ios"===e.platform&&"simulator"===e.kind){let{ensureBootedSimulator:t}=await Promise.resolve().then(()=>({ensureBootedSimulator:ef}));await t(e);return}if("android"===e.platform){let{waitForAndroidBoot:t}=await Promise.resolve().then(()=>({waitForAndroidBoot:I}));await t(e.id)}}function e6(e){let t=[],a=[];for(let i of e){let e=i.depth??0;for(;t.length>0&&e<=t[t.length-1];)t.pop();let n=e7(i.type??""),r=[i.label,i.value,i.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0),o=!!r&&e8(r);if(("group"===n||"ioscontentgroup"===n)&&!o){t.push(e);continue}let s=Math.max(0,e-t.length);a.push({...i,depth:s})}return a}function e7(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase();return t.startsWith("ax")&&(t=t.replace(/^ax/,"")),t}(e=m.createServer(e=>{let t="";e.setEncoding("utf8"),e.on("data",async a=>{let i=(t+=a).indexOf("\n");for(;-1!==i;){let a,n=t.slice(0,i).trim();if(t=t.slice(i+1),0===n.length){i=t.indexOf("\n");continue}try{let e=JSON.parse(n);a=await eH(e)}catch(t){let e=l(t);a={ok:!1,error:{code:e.code,message:e.message,details:e.details}}}e.write(`${JSON.stringify(a)}
7
- `),i=t.indexOf("\n")}})})).listen(0,"127.0.0.1",()=>{let t=e.address();if("object"==typeof t&&t?.port){var a;a=t.port,d.existsSync(ej)||d.mkdirSync(ej,{recursive:!0}),d.writeFileSync(eq,""),d.writeFileSync(eG,JSON.stringify({port:a,token:eX,pid:process.pid,version:eW},null,2),{mode:384}),process.stdout.write(`AGENT_DEVICE_DAEMON_PORT=${t.port}
8
- `)}}),t=async()=>{for(let e of Array.from(eV.values()))"ios"===e.device.platform&&"simulator"===e.device.kind&&await ev(e.device.id),eQ(e);e.close(()=>{d.existsSync(eG)&&d.unlinkSync(eG),process.exit(0)})},process.on("SIGINT",()=>{t()}),process.on("SIGTERM",()=>{t()}),process.on("SIGHUP",()=>{t()}),process.on("uncaughtException",e=>{let a=e instanceof f?e:l(e);process.stderr.write(`Daemon error: ${a.message}
5
+ `))),0!==s.exitCode){let e,t,a=(s.stderr??"").toString(),i=(e=a.toLowerCase()).includes("accessibility permission")?" Enable Accessibility for your terminal in System Settings > Privacy & Security > Accessibility, or use --backend xctest (slower snapshots via XCTest).":e.includes("could not find ios app content")?" AX snapshot sometimes caches empty content. Try restarting the Simulator app.":"",n=!!((t=a.toLowerCase()).includes("could not find ios app content")||t.includes("timeout"));throw new f("COMMAND_FAILED","AX snapshot failed",{stderr:`${a}${i}`,stdout:s.stdout,retryable:n})}return s},{shouldRetry:e=>{var t;return(t=e)instanceof f&&"COMMAND_FAILED"===t.code&&t.details?.retryable===!0}});try{let e=JSON.parse(r.stdout);if(e&&"object"==typeof e&&"root"in e){if(!e.root)throw Error("AX snapshot missing root");a=e.root,i=e.windowFrame??void 0}else a=e}catch(e){throw new f("COMMAND_FAILED","Invalid AX snapshot JSON",{error:String(e)})}let o=a.frame??i,s=[],l=[],c=(e,t)=>{e.frame&&s.push(e.frame);let a=e.frame&&o?{x:e.frame.x-o.x,y:e.frame.y-o.y,width:e.frame.width,height:e.frame.height}:e.frame;for(let i of(l.push({...e,frame:a,children:void 0,depth:t}),e.children??[]))c(i,t+1)};return c(a,0),{nodes:(function(e,t,a){if(!t||0===a.length)return e;let i=1/0,n=1/0;for(let e of a)e.x<i&&(i=e.x),e.y<n&&(n=e.y);return i<=5&&n<=5?e.map(e=>({...e,frame:e.frame?{x:e.frame.x+t.x,y:e.frame.y+t.y,width:e.frame.width,height:e.frame.height}:void 0})):e})(l,o,s).map((e,t)=>({index:t,type:e.subrole??e.role,label:e.label,value:e.value,identifier:e.identifier,rect:e.frame?{x:e.frame.x,y:e.frame.y,width:e.frame.width,height:e.frame.height}:void 0,depth:e.depth}))}}async function eU(){let e=function(){let e=r.dirname(c(import.meta.url));for(let t=0;t<6;t+=1){let t=r.join(e,"package.json");if(d.existsSync(t))return e;e=r.dirname(e)}return process.cwd()}(),t=r.join(e,"ios-runner","AXSnapshot"),a=process.env.AGENT_DEVICE_AX_BINARY;if(a&&d.existsSync(a))return a;let i=r.join(e,"dist","bin","axsnapshot");if(d.existsSync(i))return i;let n=r.join(t,".build","release","axsnapshot");if(d.existsSync(n))return n;let o=await h("swift",["build","-c","release"],{cwd:t,allowFailure:!0});if(0!==o.exitCode||!d.existsSync(n))throw new f("COMMAND_FAILED","Failed to build AX snapshot tool",{stderr:o.stderr,stdout:o.stdout});return n}async function eV(e){let t={platform:e.platform,deviceName:e.device,udid:e.udid,serial:e.serial};if("android"===t.platform){await J();let e=await y();return await g(e,t)}if("ios"===t.platform){let e=await er();return await g(e,t)}let a=[];try{a.push(...await y())}catch{}try{a.push(...await er())}catch{}return await g(a,t)}async function ej(e,t,a,i,n){let r=function(e){switch(e.platform){case"android":return{open:(t,a)=>O(e,t,a?.activity),openDevice:()=>L(e),close:t=>_(e,t),tap:(t,a)=>M(e,t,a),longPress:(t,a,i)=>T(e,t,a,i),focus:(t,a)=>B(e,t,a),type:t=>F(e,t),fill:(t,a,i)=>$(e,t,a,i),scroll:(t,a)=>U(e,t,a),scrollIntoView:t=>V(e,t),screenshot:t=>j(e,t)};case"ios":return{open:t=>el(e,t),openDevice:()=>ec(e),close:t=>eu(e,t),tap:(t,a)=>ed(e,t,a),longPress:(t,a,i)=>ep(e,t,a,i),focus:(t,a)=>ef(e,t,a),type:t=>eh(e,t),fill:(t,a,i)=>em(e,t,a,i),scroll:(t,a)=>ew(e,t,a),scrollIntoView:e=>eg(e),screenshot:t=>ey(e,t)};default:throw new f("UNSUPPORTED_PLATFORM",`Unsupported platform: ${e.platform}`)}}(e);switch(t){case"open":{let e=a[0];if(!e)return await r.openDevice(),{app:null};return await r.open(e,{activity:n?.activity}),{app:e}}case"close":{let e=a[0];if(!e)return{closed:"session"};return await r.close(e),{app:e}}case"press":{let[t,i]=a.map(Number);if(Number.isNaN(t)||Number.isNaN(i))throw new f("INVALID_ARGS","press requires x y");return"ios"===e.platform&&"simulator"===e.kind?await eD(e,{command:"tap",x:t,y:i,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}):await r.tap(t,i),{x:t,y:i}}case"long-press":{let e=Number(a[0]),t=Number(a[1]),i=a[2]?Number(a[2]):void 0;if(Number.isNaN(e)||Number.isNaN(t))throw new f("INVALID_ARGS","long-press requires x y [durationMs]");return await r.longPress(e,t,i),{x:e,y:t,durationMs:i}}case"focus":{let[t,i]=a.map(Number);if(Number.isNaN(t)||Number.isNaN(i))throw new f("INVALID_ARGS","focus requires x y");return"ios"===e.platform&&"simulator"===e.kind?await eD(e,{command:"tap",x:t,y:i,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}):await r.focus(t,i),{x:t,y:i}}case"type":{let t=a.join(" ");if(!t)throw new f("INVALID_ARGS","type requires text");return"ios"===e.platform&&"simulator"===e.kind?await eD(e,{command:"type",text:t,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}):await r.type(t),{text:t}}case"fill":{let t=Number(a[0]),i=Number(a[1]),o=a.slice(2).join(" ");if(Number.isNaN(t)||Number.isNaN(i)||!o)throw new f("INVALID_ARGS","fill requires x y text");return"ios"===e.platform&&"simulator"===e.kind?(await eD(e,{command:"tap",x:t,y:i,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),await eD(e,{command:"type",text:o,clearFirst:!0,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath})):await r.fill(t,i,o),{x:t,y:i,text:o}}case"scroll":{let t=a[0],i=a[1]?Number(a[1]):void 0;if(!t)throw new f("INVALID_ARGS","scroll requires direction");if("ios"===e.platform&&"simulator"===e.kind){if(!["up","down","left","right"].includes(t))throw new f("INVALID_ARGS",`Unknown direction: ${t}`);let a=function(e){switch(e){case"up":return"down";case"down":return"up";case"left":return"right";case"right":return"left"}}(t);await eD(e,{command:"swipe",direction:a,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath})}else await r.scroll(t,i);return{direction:t,amount:i}}case"scrollintoview":{let t=a.join(" ").trim();if(!t)throw new f("INVALID_ARGS","scrollintoview requires text");if("ios"===e.platform&&"simulator"===e.kind){for(let a=0;a<8;a+=1){let i=await eD(e,{command:"findText",text:t,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath});if(i?.found)return{text:t,attempts:a+1};await eD(e,{command:"swipe",direction:"up",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),await new Promise(e=>setTimeout(e,300))}throw new f("COMMAND_FAILED",`scrollintoview could not find text: ${t}`)}return await r.scrollIntoView(t),{text:t}}case"pinch":{let t=Number(a[0]),i=a[1]?Number(a[1]):void 0,r=a[2]?Number(a[2]):void 0;if(Number.isNaN(t)||t<=0)throw new f("INVALID_ARGS","pinch requires scale > 0");if("ios"===e.platform&&"simulator"===e.kind)await eD(e,{command:"pinch",scale:t,x:i,y:r,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath});else throw new f("UNSUPPORTED_OPERATION","pinch is only supported on iOS simulators");return{scale:t,x:i,y:r}}case"screenshot":{let e=i??`./screenshot-${Date.now()}.png`;return await r.screenshot(e),{path:e}}case"back":if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","back is only supported on iOS simulators in v1");return await eD(e,{command:"back",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),{action:"back"}}return await C(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 eD(e,{command:"home",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),{action:"home"}}return await R(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 eD(e,{command:"appSwitcher",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),{action:"app-switcher"}}return await E(e),{action:"app-switcher"};case"settings":{let[t,i,r]=a;if("ios"===e.platform)return await ev(e,t,i,r??n?.appBundleId),{setting:t,state:i};return await G(e,t,i),{setting:t,state:i}}case"snapshot":{let t=n?.snapshotBackend??"xctest";if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","snapshot is only supported on iOS simulators in v1");if("ax"===t)return{nodes:(await e$(e,{traceLogPath:n?.traceLogPath})).nodes??[],truncated:!1,backend:"ax"};let a=await eD(e,{command:"snapshot",appBundleId:n?.appBundleId,interactiveOnly:n?.snapshotInteractiveOnly,compact:n?.snapshotCompact,depth:n?.snapshotDepth,scope:n?.snapshotScope,raw:n?.snapshotRaw},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),i=a.nodes??[];if(0===i.length)try{return{nodes:(await e$(e,{traceLogPath:n?.traceLogPath})).nodes??[],truncated:!1,backend:"ax"}}catch{}return{nodes:i,truncated:a.truncated??!1,backend:"xctest"}}let a=await q(e,{interactiveOnly:n?.snapshotInteractiveOnly,compact:n?.snapshotCompact,depth:n?.snapshotDepth,scope:n?.snapshotScope,raw:n?.snapshotRaw});return{nodes:a.nodes??[],truncated:a.truncated??!1,backend:"android"}}default:throw new f("INVALID_ARGS",`Unknown command: ${t}`)}}function eG(e){return e.map((e,t)=>({...e,ref:`e${t+1}`}))}function eq(e){let t=e.trim();return t.startsWith("@")?t.slice(1)||null:t.startsWith("e")?t:null}function eJ(e,t){return e.find(e=>e.ref===t)??null}function eW(e){return{x:Math.round(e.x+e.width/2),y:Math.round(e.y+e.height/2)}}function eX(e,t,a,i={}){let n=eH(a);if(!n)return null;let r=null;for(let a of e){if(i.requireRect&&!a.rect)continue;let e=function(e,t,a){switch(t){case"role":return function(e,t){let a=function(e){let t=e.trim();return t?((t=(t.split(".").pop()??t).replace(/XCUIElementType/gi,"").toLowerCase()).startsWith("ax")&&(t=t.replace(/^ax/,"")),t):""}(e??"");return a?a===t?2:+!!a.includes(t):0}(e.type,a);case"label":return ez(e.label,a);case"value":return ez(e.value,a);case"id":return ez(e.identifier,a);default:return Math.max(ez(e.label,a),ez(e.value,a),ez(e.identifier,a))}}(a,t,n);if(!(e<=0)&&(!r||e>r.score)&&(r={node:a,score:e},e>=2))break}return r?.node??null}function ez(e,t){let a=eH(e??"");return a?a===t?2:+!!a.includes(t):0}function eH(e){return e.trim().toLowerCase().replace(/\s+/g," ")}let eY=new Map,eK=r.join(p.homedir(),".agent-device"),eZ=r.join(eK,"daemon.json"),eQ=r.join(eK,"daemon.log"),e0=r.join(eK,"sessions"),e1=function(){try{let e=function(){let e=r.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=r.join(t,"package.json");if(d.existsSync(e))return t;t=r.dirname(t)}return e}();return JSON.parse(d.readFileSync(r.join(e,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}(),e2=a.randomBytes(24).toString("hex");function e3(e,t,a){return{appBundleId:t,activity:e?.activity,verbose:e?.verbose,logPath:eQ,traceLogPath:a,snapshotInteractiveOnly:e?.snapshotInteractiveOnly,snapshotCompact:e?.snapshotCompact,snapshotDepth:e?.snapshotDepth,snapshotScope:e?.snapshotScope,snapshotRaw:e?.snapshotRaw,snapshotBackend:e?.snapshotBackend}}async function e4(e){var t,a,i;if(e.token!==e2)return{ok:!1,error:{code:"UNAUTHORIZED",message:"Invalid token"}};let n=e.command,o=e.session||"default";if("session_list"===n)return{ok:!0,data:{sessions:Array.from(eY.values()).map(e=>({name:e.name,platform:e.device.platform,device:e.device.name,id:e.device.id,createdAt:e.createdAt}))}};if("devices"===n)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:er}));t.push(...await e())}else{let{listAndroidDevices:e}=await Promise.resolve().then(()=>({listAndroidDevices:y})),{listIosDevices:a}=await Promise.resolve().then(()=>({listIosDevices:er}));try{t.push(...await e())}catch{}try{t.push(...await a())}catch{}}return{ok:!0,data:{devices:t}}}catch(t){let e=l(t);return{ok:!1,error:{code:e.code,message:e.message,details:e.details}}}if("apps"===n){let t=eY.get(o),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 i=t?.device??await eV(a);if(await tn(i),"ios"===i.platform){if("simulator"!==i.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"apps list is only supported on iOS simulators"}};let{listSimulatorApps:t}=await Promise.resolve().then(()=>({listSimulatorApps:eN})),a=await t(i);return e.flags?.appsMetadata?{ok:!0,data:{apps:a}}:{ok:!0,data:{apps:a.map(e=>e.name&&e.name!==e.bundleId?`${e.name} (${e.bundleId})`:e.bundleId)}}}let{listAndroidApps:n,listAndroidAppsMetadata:r}=await Promise.resolve().then(()=>({listAndroidApps:D,listAndroidAppsMetadata:k}));return e.flags?.appsMetadata?{ok:!0,data:{apps:await r(i,e.flags?.appsFilter)}}:{ok:!0,data:{apps:await n(i,e.flags?.appsFilter)}}}if("appstate"===n){let t=eY.get(o),a=e.flags??{},i=t?.device??await eV(a);if(await tn(i),"ios"===i.platform){if(t?.appBundleId)return{ok:!0,data:{platform:"ios",appBundleId:t.appBundleId,appName:t.appName??t.appBundleId,source:"session"}};let a=await e6(i,t?.trace?.outPath,e.flags);return{ok:!0,data:{platform:"ios",appName:a.appName,appBundleId:a.appBundleId,source:a.source}}}let{getAndroidAppState:n}=await Promise.resolve().then(()=>({getAndroidAppState:P})),r=await n(i);return{ok:!0,data:{platform:"android",package:r.package,activity:r.activity}}}if("open"===n){let t;if(eY.has(o)){let t,a=eY.get(o),i=e.positionals?.[0];if(!a||!i)return{ok:!1,error:{code:"INVALID_ARGS",message:"Session already active. Close it first or pass a new --session name."}};if("ios"===a.device.platform)try{let{resolveIosApp:e}=await Promise.resolve().then(()=>({resolveIosApp:es}));t=await e(a.device,i)}catch{t=void 0}await ej(a.device,"open",e.positionals??[],e.flags?.out,{...e3(e.flags,t)});let r={...a,appBundleId:t,appName:i,snapshot:void 0};return e8(r,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:{session:o,appName:i,appBundleId:t}}),eY.set(o,r),{ok:!0,data:{session:o,appName:i,appBundleId:t}}}let a=await eV(e.flags??{});await tn(a);let i=Array.from(eY.values()).find(e=>e.device.id===a.id);if(i)return{ok:!1,error:{code:"DEVICE_IN_USE",message:`Device is already in use by session "${i.name}".`,details:{session:i.name,deviceId:a.id,deviceName:a.name}}};let r=e.positionals?.[0];if("ios"===a.platform)try{let{resolveIosApp:i}=await Promise.resolve().then(()=>({resolveIosApp:es}));t=await i(a,e.positionals?.[0]??"")}catch{t=void 0}await ej(a,"open",e.positionals??[],e.flags?.out,{...e3(e.flags,t)});let s={name:o,device:a,createdAt:Date.now(),appBundleId:t,appName:r,actions:[]};return e8(s,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:{session:o}}),eY.set(o,s),{ok:!0,data:{session:o}}}if("replay"===n){let t=e.positionals?.[0];if(!t)return{ok:!1,error:{code:"INVALID_ARGS",message:"replay requires a path"}};try{let e=e9(t),a=JSON.parse(d.readFileSync(e,"utf8")),i=a.optimizedActions??a.actions??[];for(let e of i)e&&"replay"!==e.command&&await e4({token:e2,session:o,command:e.command,positionals:e.positionals??[],flags:e.flags??{}});return{ok:!0,data:{replayed:i.length,session:o}}}catch(t){let e=l(t);return{ok:!1,error:{code:e.code,message:e.message}}}}if("close"===n){let t=eY.get(o);return t?(e.positionals&&e.positionals.length>0&&await ej(t.device,"close",e.positionals??[],e.flags?.out,{...e3(e.flags,t.appBundleId,t.trace?.outPath)}),"ios"===t.device.platform&&"simulator"===t.device.kind&&await eP(t.device.id),e8(t,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:{session:o}}),e7(t),eY.delete(o),{ok:!0,data:{session:o}}):{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session"}}}if("snapshot"===n){let t=eY.get(o),a=t?.device??await eV(e.flags??{});t||await tn(a);let i=t?.appBundleId,r=e.flags?.snapshotScope;if(r&&r.trim().startsWith("@")){if(!t?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"Ref scope requires an existing snapshot in session."}};let e=eq(r.trim());if(!e)return{ok:!1,error:{code:"INVALID_ARGS",message:`Invalid ref scope: ${r}`}};let a=eJ(t.snapshot.nodes,e),i=a?ta(a,t.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 ej(a,"snapshot",[],e.flags?.out,{...e3({...e.flags,snapshotScope:r},i,t?.trace?.outPath)}),l=s?.nodes??[],c=eG(e.flags?.snapshotRaw?l:tr(l)),u={nodes:c,truncated:s?.truncated,createdAt:Date.now(),backend:s?.backend},d={name:o,device:a,createdAt:t?.createdAt??Date.now(),appBundleId:t?.appBundleId??i,snapshot:u,actions:t?.actions??[],appName:t?.appName};return e8(d,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:{nodes:c.length,truncated:s?.truncated??!1}}),eY.set(o,d),{ok:!0,data:{nodes:c,truncated:s?.truncated??!1,appName:d.appBundleId?d.appName??d.appBundleId:void 0,appBundleId:d.appBundleId}}}if("wait"===n){let t=eY.get(o),a=t?.device??await eV(e.flags??{});t||await tn(a);let i=e.positionals??[];if(0===i.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(i[0]);if(null!==s)return await new Promise(e=>setTimeout(e,s)),t&&e8(t,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:{waitedMs:s}}),{ok:!0,data:{waitedMs:s}};let l="",c=null;if("text"===i[0])l=null!==(c=r(i[i.length-1]))?i.slice(1,-1).join(" "):i.slice(1).join(" ");else if(i[0].startsWith("@")){if(!t?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"Ref wait requires an existing snapshot in session."}};let e=eq(i[0]);if(!e)return{ok:!1,error:{code:"INVALID_ARGS",message:`Invalid ref: ${i[0]}`}};let a=eJ(t.snapshot.nodes,e),n=a?ta(a,t.snapshot.nodes):void 0;if(!n)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${i[0]} not found or has no label`}};c=r(i[i.length-1]),l=n}else l=null!==(c=r(i[i.length-1]))?i.slice(0,-1).join(" "):i.join(" ");if(!(l=l.trim()))return{ok:!1,error:{code:"INVALID_ARGS",message:"wait requires text"}};let u=c??1e4,d=Date.now();for(;Date.now()-d<u;){if("ios"===a.platform&&"simulator"===a.kind){let i=await eD(a,{command:"findText",text:l,appBundleId:t?.appBundleId},{verbose:e.flags?.verbose,logPath:eQ,traceLogPath:t?.trace?.outPath});if(i?.found)return t&&e8(t,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:{text:l,waitedMs:Date.now()-d}}),{ok:!0,data:{text:l,waitedMs:Date.now()-d}}}else if("android"!==a.platform)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"wait is not supported on this device"}};else if(tt(eG((await q(a,{scope:l})).nodes??[]),l))return t&&e8(t,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:{text:l,waitedMs:Date.now()-d}}),{ok:!0,data:{text:l,waitedMs:Date.now()-d}};await new Promise(e=>setTimeout(e,300))}return{ok:!1,error:{code:"COMMAND_FAILED",message:`wait timed out for text: ${l}`}}}if("alert"===n){let t=eY.get(o),a=t?.device??await eV(e.flags??{});t||await tn(a);let i=(e.positionals?.[0]??"get").toLowerCase();if("ios"!==a.platform||"simulator"!==a.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"alert is only supported on iOS simulators in v1"}};if("wait"===i){let i=(e=>{if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null})(e.positionals?.[1])??1e4,r=Date.now();for(;Date.now()-r<i;){try{let i=await eD(a,{command:"alert",action:"get",appBundleId:t?.appBundleId},{verbose:e.flags?.verbose,logPath:eQ,traceLogPath:t?.trace?.outPath});return t&&e8(t,{command:n,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 eD(a,{command:"alert",action:"accept"===i||"dismiss"===i?i:"get",appBundleId:t?.appBundleId},{verbose:e.flags?.verbose,logPath:eQ,traceLogPath:t?.trace?.outPath});return t&&e8(t,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:r}),{ok:!0,data:r}}if("record"===n){let t=(e.positionals?.[0]??"").toLowerCase();if(!["start","stop"].includes(t))return{ok:!1,error:{code:"INVALID_ARGS",message:"record requires start|stop"}};let a=eY.get(o),i=a?.device??await eV(e.flags??{});a||await tn(i);let s=a??{name:o,device:i,createdAt:Date.now(),actions:[]};if("start"===t){if(s.recording)return{ok:!1,error:{code:"INVALID_ARGS",message:"recording already in progress"}};let t=e.positionals?.[1]??`./recording-${Date.now()}.mp4`,a=r.resolve(t),l=r.dirname(a);if(d.existsSync(l)||d.mkdirSync(l,{recursive:!0}),"ios"===i.platform){if("simulator"!==i.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"record is only supported on iOS simulators in v1"}};let{child:e,wait:t}=u("xcrun",["simctl","io",i.id,"recordVideo",a],{allowFailure:!0});s.recording={platform:"ios",outPath:a,child:e,wait:t}}else{let e=`/sdcard/agent-device-recording-${Date.now()}.mp4`,{child:t,wait:n}=u("adb",["-s",i.id,"shell","screenrecord",e],{allowFailure:!0});s.recording={platform:"android",outPath:a,remotePath:e,child:t,wait:n}}return eY.set(o,s),e8(s,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:{action:"start"}}),{ok:!0,data:{recording:"started",outPath:t}}}if(!s.recording)return{ok:!1,error:{code:"INVALID_ARGS",message:"no active recording"}};let l=s.recording;l.child.kill("SIGINT");try{await l.wait}catch{}if("android"===l.platform&&l.remotePath)try{await h("adb",["-s",i.id,"pull",l.remotePath,l.outPath],{allowFailure:!0}),await h("adb",["-s",i.id,"shell","rm","-f",l.remotePath],{allowFailure:!0})}catch{}return s.recording=void 0,e8(s,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:{action:"stop",outPath:l.outPath}}),{ok:!0,data:{recording:"stopped",outPath:l.outPath}}}if("trace"===n){let t=(e.positionals?.[0]??"").toLowerCase();if(!["start","stop"].includes(t))return{ok:!1,error:{code:"INVALID_ARGS",message:"trace requires start|stop"}};let a=eY.get(o);if(!a)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session"}};if("start"===t){let t,i;if(a.trace)return{ok:!1,error:{code:"INVALID_ARGS",message:"trace already in progress"}};let o=e9(e.positionals?.[1]??(t=a.name.replace(/[^a-zA-Z0-9._-]/g,"_"),i=new Date().toISOString().replace(/[:.]/g,"-"),r.join(e0,`${t}-${i}.trace.log`)));return d.mkdirSync(r.dirname(o),{recursive:!0}),d.appendFileSync(o,""),a.trace={outPath:o,startedAt:Date.now()},e8(a,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:{action:"start",outPath:o}}),{ok:!0,data:{trace:"started",outPath:o}}}if(!a.trace)return{ok:!1,error:{code:"INVALID_ARGS",message:"no active trace"}};let i=a.trace.outPath;if(e.positionals?.[1]){let t=e9(e.positionals[1]);d.mkdirSync(r.dirname(t),{recursive:!0}),d.existsSync(i)?d.renameSync(i,t):d.appendFileSync(t,""),i=t}return a.trace=void 0,e8(a,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:{action:"stop",outPath:i}}),{ok:!0,data:{trace:"stopped",outPath:i}}}if("settings"===n){let t=e.positionals?.[0],a=e.positionals?.[1];if(!t||!a)return{ok:!1,error:{code:"INVALID_ARGS",message:"settings requires <wifi|airplane|location> <on|off>"}};let i=eY.get(o),r=i?.device??await eV(e.flags??{});i||await tn(r);let s=i?.appBundleId,l=await ej(r,"settings",[t,a,s??""],e.flags?.out,{...e3(e.flags,s,i?.trace?.outPath)});return i&&e8(i,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:l??{setting:t,state:a}}),{ok:!0,data:l??{setting:t,state:a}}}if("find"===n){let a=e.positionals??[];if(0===a.length)return{ok:!1,error:{code:"INVALID_ARGS",message:"find requires a locator or text"}};let{locator:i,query:r,action:s,value:l,timeoutMs:c}=function(e){let t="any",a=0;["text","label","value","role","id"].includes(e[0])&&(t=e[0],a=1);let i=e[a]??"",n=e.slice(a+1);if(0===n.length)return{locator:t,query:i,action:"click"};let r=n[0].toLowerCase();if("get"===r){let e=n[1]?.toLowerCase();if("text"===e)return{locator:t,query:i,action:"get_text"};if("attrs"===e)return{locator:t,query:i,action:"get_attrs"};throw new f("INVALID_ARGS","find get only supports text or attrs")}if("wait"===r)return{locator:t,query:i,action:"wait",timeoutMs:function(e){if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null}(n[1])??void 0};if("exists"===r)return{locator:t,query:i,action:"exists"};if("click"===r)return{locator:t,query:i,action:"click"};if("focus"===r)return{locator:t,query:i,action:"focus"};if("fill"===r)return{locator:t,query:i,action:"fill",value:n.slice(1).join(" ")};if("type"===r)return{locator:t,query:i,action:"type",value:n.slice(1).join(" ")};throw new f("INVALID_ARGS",`Unsupported find action: ${n[0]}`)}(a);if(!r)return{ok:!1,error:{code:"INVALID_ARGS",message:"find requires a value"}};let u=eY.get(o);if(!u&&"exists"!==s&&"wait"!==s&&"get_text"!==s&&"get_attrs"!==s)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session. Run open first."}};let d=u?.device??await eV(e.flags??{});u||await tn(d);let p=u?.appBundleId,h="role"!==i?r:void 0,m="click"===s||"focus"===s||"fill"===s||"type"===s,w=0,g=null,y=async()=>{let t=Date.now();if(g&&t-w<750)return{nodes:g};let a=await ej(d,"snapshot",[],e.flags?.out,{...e3({...e.flags,snapshotScope:h,snapshotInteractiveOnly:m,snapshotCompact:m},p,u?.trace?.outPath)}),i=a?.nodes??[],n=eG(e.flags?.snapshotRaw?i:tr(i));return w=t,g=n,u&&(u.snapshot={nodes:n,truncated:a?.truncated,createdAt:Date.now(),backend:a?.backend},eY.set(o,u)),{nodes:n,truncated:a?.truncated,backend:a?.backend}};if("wait"===s){let t=c??1e4,a=Date.now();for(;Date.now()-a<t;){let{nodes:t}=await y();if(eX(t,i,r,{requireRect:!1}))return u&&e8(u,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:{found:!0,waitedMs:Date.now()-a}}),{ok:!0,data:{found:!0,waitedMs:Date.now()-a}};await new Promise(e=>setTimeout(e,300))}return{ok:!1,error:{code:"COMMAND_FAILED",message:"find wait timed out"}}}let{nodes:v}=await y(),I=eX(v,i,r,{requireRect:m});if(!I)return{ok:!1,error:{code:"COMMAND_FAILED",message:"find did not match any element"}};let N="click"===s||"focus"===s||"fill"===s||"type"===s?function(e,t){if(t.hittable)return t;let a=t,i=new Set;for(;void 0!==a.parentIndex&&!i.has(a.ref);){i.add(a.ref);let t=e[a.parentIndex];if(!t)break;if(t.hittable)return t;a=t}return null}(v,I)??I:I,A=`@${N.ref}`,b={...e.flags??{},noRecord:!0};if("exists"===s)return u&&e8(u,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:{found:!0}}),{ok:!0,data:{found:!0}};if("get_text"===s){let a=[(t=I).label,t.value,t.identifier].map(e=>"string"==typeof e?e.trim():"").filter(e=>e.length>0)[0]??"";return u&&e8(u,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:{ref:A,action:"get text",text:a}}),{ok:!0,data:{ref:A,text:a,node:I}}}if("get_attrs"===s)return u&&e8(u,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:{ref:A,action:"get attrs"}}),{ok:!0,data:{ref:A,node:I}};if("click"===s){let t=await e4({token:e2,session:o,command:"click",positionals:[A],flags:b});return t.ok&&u&&e8(u,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:{ref:A,action:"click"}}),t}if("fill"===s){if(!l)return{ok:!1,error:{code:"INVALID_ARGS",message:"find fill requires text"}};let t=await e4({token:e2,session:o,command:"fill",positionals:[A,l],flags:b});return t.ok&&u&&e8(u,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:{ref:A,action:"fill"}}),t}if("focus"===s){let t=I.rect?eW(I.rect):null;if(!t)return{ok:!1,error:{code:"COMMAND_FAILED",message:"matched element has no bounds"}};let a=await ej(d,"focus",[String(t.x),String(t.y)],e.flags?.out,{...e3(e.flags,u?.appBundleId,u?.trace?.outPath)});return u&&e8(u,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:{ref:A,action:"focus"}}),{ok:!0,data:a??{ref:A}}}if("type"===s){if(!l)return{ok:!1,error:{code:"INVALID_ARGS",message:"find type requires text"}};let t=I.rect?eW(I.rect):null;if(!t)return{ok:!1,error:{code:"COMMAND_FAILED",message:"matched element has no bounds"}};await ej(d,"focus",[String(t.x),String(t.y)],e.flags?.out,{...e3(e.flags,u?.appBundleId,u?.trace?.outPath)});let a=await ej(d,"type",[l],e.flags?.out,{...e3(e.flags,u?.appBundleId,u?.trace?.outPath)});return u&&e8(u,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:{ref:A,action:"type"}}),{ok:!0,data:a??{ref:A}}}}if("click"===n){let t=eY.get(o);if(!t?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let a=e.positionals?.[0]??"",i=eq(a);if(!i)return{ok:!1,error:{code:"INVALID_ARGS",message:"click requires a ref like @e2"}};let r=eJ(t.snapshot.nodes,i);if(!r?.rect&&e.positionals.length>1){let a=e.positionals.slice(1).join(" ").trim();a.length>0&&(r=tt(t.snapshot.nodes,a))}if(!r?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${a} not found or has no bounds`}};let s=ta(r,t.snapshot.nodes),{x:l,y:c}=eW(r.rect);return await ej(t.device,"press",[String(l),String(c)],e.flags?.out,{...e3(e.flags,t.appBundleId,t.trace?.outPath)}),e8(t,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:{ref:i,x:l,y:c,refLabel:s}}),{ok:!0,data:{ref:i,x:l,y:c}}}if("fill"===n){let t=eY.get(o);if(e.positionals?.[0]?.startsWith("@")){let r;if(!t?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let o=eq(e.positionals[0]);if(!o)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires a ref like @e2"}};let s=e.positionals.length>=3?e.positionals[1]:"",l=e.positionals.length>=3?e.positionals.slice(2).join(" "):e.positionals.slice(1).join(" ");if(!l)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires text after ref"}};let c=eJ(t.snapshot.nodes,o);if(!c?.rect&&s&&(c=tt(t.snapshot.nodes,s)),!c?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${e.positionals[0]} not found or has no bounds`}};let u=c.type??"";if(u&&(a=u,i=t.device.platform,!(!(r=to(a))||("android"===i?r.includes("edittext")||r.includes("autocompletetextview"):r.includes("textfield")||r.includes("securetextfield")||r.includes("searchfield")||r.includes("textview")||r.includes("textarea")||"search"===r))))return{ok:!1,error:{code:"INVALID_ARGS",message:`fill requires a text input element, got "${u}" for ${e.positionals[0]}. Select a text input ref or use click/focus + type.`}};let d=ta(c,t.snapshot.nodes),{x:p,y:f}=eW(c.rect),h=await ej(t.device,"fill",[String(p),String(f),l],e.flags?.out,{...e3(e.flags,t.appBundleId,t.trace?.outPath)});return e8(t,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:h??{ref:o,x:p,y:f,refLabel:d}}),{ok:!0,data:h??{ref:o,x:p,y:f}}}}if("get"===n){let t=e.positionals?.[0],a=e.positionals?.[1];if("text"!==t&&"attrs"!==t)return{ok:!1,error:{code:"INVALID_ARGS",message:"get only supports text or attrs"}};let i=eY.get(o);if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let r=eq(a??"");if(!r)return{ok:!1,error:{code:"INVALID_ARGS",message:"get text requires a ref like @e2"}};let s=eJ(i.snapshot.nodes,r);if(!s&&e.positionals.length>2){let t=e.positionals.slice(2).join(" ").trim();t.length>0&&(s=tt(i.snapshot.nodes,t))}if(!s)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${a} not found`}};if("attrs"===t)return e8(i,{command:n,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 e8(i,{command:n,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 s=eY.get(o);if(!s)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session. Run open first."}};let c=await ej(s.device,n,e.positionals??[],e.flags?.out,{...e3(e.flags,s.appBundleId,s.trace?.outPath)});return e8(s,{command:n,positionals:e.positionals??[],flags:e.flags??{},result:c??{}}),{ok:!0,data:c??{}}}function e8(e,t){t.flags?.noRecord||e.actions.push({ts:Date.now(),command:t.command,positionals:t.positionals,flags:function(e){if(!e)return{};let{platform:t,device:a,udid:i,serial:n,out:r,verbose:o,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:u,snapshotRaw:d,snapshotBackend:p,appsMetadata:f,noRecord:h,recordJson:m}=e;return{platform:t,device:a,udid:i,serial:n,out:r,verbose:o,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:u,snapshotRaw:d,snapshotBackend:p,appsMetadata:f,noRecord:h,recordJson:m}}(t.flags),result:t.result})}async function e6(e,t,a){let i=e5(await ej(e,"snapshot",[],a?.out,{...e3({...a,snapshotDepth:1,snapshotCompact:!0,snapshotBackend:"ax"},void 0,t)}));if(i?.appName||i?.appBundleId)return{appName:i.appName??i.appBundleId??"unknown",appBundleId:i.appBundleId,source:"snapshot-ax"};let n=e5(await ej(e,"snapshot",[],a?.out,{...e3({...a,snapshotDepth:1,snapshotCompact:!0,snapshotBackend:"xctest"},void 0,t)}));return{appName:n?.appName??n?.appBundleId??"unknown",appBundleId:n?.appBundleId,source:"snapshot-xctest"}}function e5(e){let t=eG(e?.nodes??[]),a=t.find(e=>"application"===to(e.type??""))??t[0];if(!a)return null;let i=a.label?.trim(),n=a.identifier?.trim();return i||n?{appName:i||void 0,appBundleId:n||void 0}:null}function e7(e){try{d.existsSync(e0)||d.mkdirSync(e0,{recursive:!0});let t=e.name.replace(/[^a-zA-Z0-9._-]/g,"_"),a=new Date(e.createdAt).toISOString().replace(/[:.]/g,"-"),i=r.join(e0,`${t}-${a}.ad`),n=r.join(e0,`${t}-${a}.json`),o={name:e.name,device:e.device,createdAt:e.createdAt,appBundleId:e.appBundleId,actions:e.actions,optimizedActions:function(e){let t=[];for(let a of e.actions)if("snapshot"!==a.command){if("click"===a.command||"fill"===a.command||"get"===a.command){let i=a.result?.refLabel;"string"==typeof i&&i.trim().length>0&&t.push({ts:a.ts,command:"snapshot",positionals:[],flags:{platform:e.device.platform,snapshotInteractiveOnly:!0,snapshotCompact:!0,snapshotScope:i.trim()},result:{scope:i.trim()}})}t.push(a)}return t}(e)},s=function(e,t){let a=[],i=e.device.name.replace(/"/g,'\\"'),n=e.device.kind?` kind=${e.device.kind}`:"";for(let r of(a.push(`context platform=${e.device.platform} device="${i}"${n} theme=unknown`),t))r.flags?.noRecord||a.push(function(e){let t=[e.command];if("click"===e.command){let a=e.positionals?.[0];if(a){t.push(te(a));let i=e.result?.refLabel;return"string"==typeof i&&i.trim().length>0&&t.push(te(i)),t.join(" ")}}if("fill"===e.command){let a=e.positionals?.[0];if(a&&a.startsWith("@")){t.push(te(a));let i=e.result?.refLabel,n=e.positionals.slice(1).join(" ");return"string"==typeof i&&i.trim().length>0&&t.push(te(i)),n&&t.push(te(n)),t.join(" ")}}if("get"===e.command){let a=e.positionals?.[0],i=e.positionals?.[1];if(a&&i){t.push(te(a)),t.push(te(i));let n=e.result?.refLabel;return"string"==typeof n&&n.trim().length>0&&t.push(te(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",te(e.flags.snapshotScope)),e.flags?.snapshotRaw&&t.push("--raw"),e.flags?.snapshotBackend&&t.push("--backend",e.flags.snapshotBackend),t.join(" ");for(let a of e.positionals??[])t.push(te(a));return t.join(" ")}(r));return`${a.join("\n")}
6
+ `}(e,o.optimizedActions);d.writeFileSync(i,s),e.actions.some(e=>e.flags?.recordJson)&&d.writeFileSync(n,JSON.stringify(o,null,2))}catch{}}function e9(e){return e.startsWith("~/")?r.join(p.homedir(),e.slice(2)):r.resolve(e)}function te(e){let t=e.trim();return t.startsWith("@")||/^-?\d+(\.\d+)?$/.test(t)?t:JSON.stringify(t)}function tt(e,t){let a=t.toLowerCase();return e.find(e=>{let t=(e.label??"").toLowerCase(),i=(e.value??"").toLowerCase(),n=(e.identifier??"").toLowerCase();return t.includes(a)||i.includes(a)||n.includes(a)})??null}function ta(e,t){let a=[e.label,e.value,e.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0);return a&&ti(a)?a:function(e,t){if(!e.rect)return;let a=e.rect.y+e.rect.height/2,i=null;for(let e of t){if(!e.rect)continue;let t=[e.label,e.value,e.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0);if(!t||!ti(t))continue;let n=Math.abs(e.rect.y+e.rect.height/2-a);(!i||n<i.distance)&&(i={label:t,distance:n})}return i?.label}(e,t)??(a&&ti(a)?a:void 0)}function ti(e){let t=e.trim();return!(!t||/^(true|false)$/i.test(t)||/^\d+$/.test(t))}async function tn(e){if("ios"===e.platform&&"simulator"===e.kind){let{ensureBootedSimulator:t}=await Promise.resolve().then(()=>({ensureBootedSimulator:eA}));await t(e);return}if("android"===e.platform){let{waitForAndroidBoot:t}=await Promise.resolve().then(()=>({waitForAndroidBoot:I}));await t(e.id)}}function tr(e){let t=[],a=[];for(let i of e){let e=i.depth??0;for(;t.length>0&&e<=t[t.length-1];)t.pop();let n=to(i.type??""),r=[i.label,i.value,i.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0),o=!!r&&ti(r);if(("group"===n||"ioscontentgroup"===n)&&!o){t.push(e);continue}let s=Math.max(0,e-t.length);a.push({...i,depth:s})}return a}function to(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase();return t.startsWith("ax")&&(t=t.replace(/^ax/,"")),t}(e=m.createServer(e=>{let t="";e.setEncoding("utf8"),e.on("data",async a=>{let i=(t+=a).indexOf("\n");for(;-1!==i;){let a,n=t.slice(0,i).trim();if(t=t.slice(i+1),0===n.length){i=t.indexOf("\n");continue}try{let e=JSON.parse(n);a=await e4(e)}catch(t){let e=l(t);a={ok:!1,error:{code:e.code,message:e.message,details:e.details}}}e.write(`${JSON.stringify(a)}
7
+ `),i=t.indexOf("\n")}})})).listen(0,"127.0.0.1",()=>{let t=e.address();if("object"==typeof t&&t?.port){var a;a=t.port,d.existsSync(eK)||d.mkdirSync(eK,{recursive:!0}),d.writeFileSync(eQ,""),d.writeFileSync(eZ,JSON.stringify({port:a,token:e2,pid:process.pid,version:e1},null,2),{mode:384}),process.stdout.write(`AGENT_DEVICE_DAEMON_PORT=${t.port}
8
+ `)}}),t=async()=>{for(let e of Array.from(eY.values()))"ios"===e.device.platform&&"simulator"===e.device.kind&&await eP(e.device.id),e7(e);e.close(()=>{d.existsSync(eZ)&&d.unlinkSync(eZ),process.exit(0)})},process.on("SIGINT",()=>{t()}),process.on("SIGTERM",()=>{t()}),process.on("SIGHUP",()=>{t()}),process.on("uncaughtException",e=>{let a=e instanceof f?e:l(e);process.stderr.write(`Daemon error: ${a.message}
9
9
  `),t()});
@@ -81,9 +81,7 @@ final class RunnerTests: XCTestCase {
81
81
  return
82
82
  }
83
83
  NSLog("AGENT_DEVICE_RUNNER_WAITING")
84
- let timeout = resolveRunnerTimeout()
85
- let effectiveTimeout = timeout > 0 ? timeout : 24 * 60 * 60
86
- let result = XCTWaiter.wait(for: [expectation], timeout: effectiveTimeout)
84
+ let result = XCTWaiter.wait(for: [expectation], timeout: 24 * 60 * 60)
87
85
  NSLog("AGENT_DEVICE_RUNNER_WAIT_RESULT=%@", String(describing: result))
88
86
  if result != .completed {
89
87
  XCTFail("runner wait ended with \(result)")
@@ -238,7 +236,19 @@ final class RunnerTests: XCTestCase {
238
236
  guard let text = command.text else {
239
237
  return Response(ok: false, error: ErrorPayload(message: "type requires text"))
240
238
  }
241
- activeApp.typeText(text)
239
+ if command.clearFirst == true {
240
+ guard let focused = focusedTextInput(app: activeApp) else {
241
+ return Response(ok: false, error: ErrorPayload(message: "no focused text input to clear"))
242
+ }
243
+ clearTextInput(focused)
244
+ focused.typeText(text)
245
+ return Response(ok: true, data: DataPayload(message: "typed"))
246
+ }
247
+ if let focused = focusedTextInput(app: activeApp) {
248
+ focused.typeText(text)
249
+ } else {
250
+ activeApp.typeText(text)
251
+ }
242
252
  return Response(ok: true, data: DataPayload(message: "typed"))
243
253
  case .swipe:
244
254
  guard let direction = command.direction else {
@@ -343,6 +353,48 @@ final class RunnerTests: XCTestCase {
343
353
  return element.exists ? element : nil
344
354
  }
345
355
 
356
+ private func clearTextInput(_ element: XCUIElement) {
357
+ moveCaretToEnd(element: element)
358
+ let count = estimatedDeleteCount(for: element)
359
+ let deletes = String(repeating: XCUIKeyboardKey.delete.rawValue, count: count)
360
+ element.typeText(deletes)
361
+ }
362
+
363
+ private func focusedTextInput(app: XCUIApplication) -> XCUIElement? {
364
+ let focused = app
365
+ .descendants(matching: .any)
366
+ .matching(NSPredicate(format: "hasKeyboardFocus == 1"))
367
+ .firstMatch
368
+ guard focused.exists else { return nil }
369
+
370
+ switch focused.elementType {
371
+ case .textField, .secureTextField, .searchField, .textView:
372
+ return focused
373
+ default:
374
+ return nil
375
+ }
376
+ }
377
+
378
+ private func moveCaretToEnd(element: XCUIElement) {
379
+ let frame = element.frame
380
+ guard !frame.isEmpty else {
381
+ element.tap()
382
+ return
383
+ }
384
+ let origin = element.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0))
385
+ let target = origin.withOffset(
386
+ CGVector(dx: max(2, frame.width - 4), dy: max(2, frame.height / 2))
387
+ )
388
+ target.tap()
389
+ }
390
+
391
+ private func estimatedDeleteCount(for element: XCUIElement) -> Int {
392
+ let valueText = String(describing: element.value ?? "")
393
+ .trimmingCharacters(in: .whitespacesAndNewlines)
394
+ let base = valueText.isEmpty ? 24 : (valueText.count + 8)
395
+ return max(24, min(120, base))
396
+ }
397
+
346
398
  private func findScopeElement(app: XCUIApplication, scope: String) -> XCUIElement? {
347
399
  let predicate = NSPredicate(
348
400
  format: "label CONTAINS[c] %@ OR identifier CONTAINS[c] %@",
@@ -738,14 +790,6 @@ private func resolveRunnerPort() -> UInt16 {
738
790
  return 0
739
791
  }
740
792
 
741
- private func resolveRunnerTimeout() -> TimeInterval {
742
- if let env = ProcessInfo.processInfo.environment["AGENT_DEVICE_RUNNER_TIMEOUT"],
743
- let parsed = Double(env) {
744
- return parsed
745
- }
746
- return 0
747
- }
748
-
749
793
  enum CommandType: String, Codable {
750
794
  case tap
751
795
  case type
@@ -772,6 +816,7 @@ struct Command: Codable {
772
816
  let command: CommandType
773
817
  let appBundleId: String?
774
818
  let text: String?
819
+ let clearFirst: Bool?
775
820
  let action: String?
776
821
  let x: Double?
777
822
  let y: Double?
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-device",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Unified control plane for physical and virtual devices via an agent-driven CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Callstack",
@@ -97,8 +97,8 @@ agent-device apps --metadata --platform android
97
97
  ```bash
98
98
  agent-device click @e1
99
99
  agent-device focus @e2
100
- agent-device fill @e2 "text" # Tap then type
101
- agent-device type "text" # Type into focused field
100
+ agent-device fill @e2 "text" # Clear then type (Android: verifies value and retries once on mismatch)
101
+ agent-device type "text" # Type into focused field without clearing
102
102
  agent-device press 300 500 # Tap by coordinates
103
103
  agent-device long-press 300 500 800 # Long press (where supported)
104
104
  agent-device scroll down 0.5
@@ -150,6 +150,9 @@ agent-device apps --platform android --user-installed
150
150
  - If AX returns the Simulator window or empty tree, restart Simulator or use `--backend xctest`.
151
151
  - Use `--session <name>` for parallel sessions; avoid device contention.
152
152
  - Use `--activity <component>` on Android to launch a specific activity (e.g. TV apps with LEANBACK).
153
+ - Use `fill` when you want clear-then-type semantics.
154
+ - Use `type` when you want to append/enter text without clearing.
155
+ - On Android, prefer `fill` for important fields; it verifies entered text and retries once when IME reorders characters.
153
156
 
154
157
  ## References
155
158
 
@@ -158,7 +161,3 @@ agent-device apps --platform android --user-installed
158
161
  - [references/permissions.md](references/permissions.md)
159
162
  - [references/video-recording.md](references/video-recording.md)
160
163
  - [references/coordinate-system.md](references/coordinate-system.md)
161
-
162
- ## Missing features roadmap (high level)
163
-
164
- See [references/missing-features.md](references/missing-features.md) for planned parity with agent-browser.
package/src/cli.ts CHANGED
@@ -54,6 +54,7 @@ export async function runCli(argv: string[]): Promise<void> {
54
54
  process.stdout.write(
55
55
  formatSnapshotText((response.data ?? {}) as Record<string, unknown>, {
56
56
  raw: flags.snapshotRaw,
57
+ flatten: flags.snapshotInteractiveOnly,
57
58
  }),
58
59
  );
59
60
  if (logTailStopper) logTailStopper();
@@ -174,7 +174,7 @@ export async function dispatchCommand(
174
174
  );
175
175
  await runIosRunnerCommand(
176
176
  device,
177
- { command: 'type', text, appBundleId: context?.appBundleId },
177
+ { command: 'type', text, clearFirst: true, appBundleId: context?.appBundleId },
178
178
  { verbose: context?.verbose, logPath: context?.logPath, traceLogPath: context?.traceLogPath },
179
179
  );
180
180
  } else {
package/src/daemon.ts CHANGED
@@ -1036,32 +1036,17 @@ async function handleRequest(req: DaemonRequest): Promise<DaemonResponse> {
1036
1036
  if (!node?.rect) {
1037
1037
  return { ok: false, error: { code: 'COMMAND_FAILED', message: `Ref ${req.positionals[0]} not found or has no bounds` } };
1038
1038
  }
1039
- const refLabel = resolveRefLabel(node, session.snapshot.nodes);
1040
- const label = node.label?.trim();
1041
- if (session.device.platform === 'ios' && session.device.kind === 'simulator' && isTextInputType(node.type)) {
1042
- const coords = node.rect ? centerOfRect(node.rect) : null;
1043
- if (!coords) {
1044
- return {
1045
- ok: false,
1046
- error: { code: 'COMMAND_FAILED', message: `Ref ${req.positionals[0]} not found or has no bounds` },
1047
- };
1048
- }
1049
- await dispatchCommand(session.device, 'focus', [String(coords.x), String(coords.y)], req.flags?.out, {
1050
- ...contextFromFlags(req.flags, session.appBundleId, session.trace?.outPath),
1051
- });
1052
- await runIosRunnerCommand(
1053
- session.device,
1054
- { command: 'type', text, appBundleId: session.appBundleId },
1055
- { verbose: req.flags?.verbose, logPath, traceLogPath: session?.trace?.outPath },
1056
- );
1057
- recordAction(session, {
1058
- command,
1059
- positionals: req.positionals ?? [],
1060
- flags: req.flags ?? {},
1061
- result: { ref, refLabel: refLabel ?? label, action: 'fill', text },
1062
- });
1063
- return { ok: true, data: { ref } };
1039
+ const nodeType = node.type ?? '';
1040
+ if (nodeType && !isFillableType(nodeType, session.device.platform)) {
1041
+ return {
1042
+ ok: false,
1043
+ error: {
1044
+ code: 'INVALID_ARGS',
1045
+ message: `fill requires a text input element, got "${nodeType}" for ${req.positionals[0]}. Select a text input ref or use click/focus + type.`,
1046
+ },
1047
+ };
1064
1048
  }
1049
+ const refLabel = resolveRefLabel(node, session.snapshot.nodes);
1065
1050
  const { x, y } = centerOfRect(node.rect);
1066
1051
  const data = await dispatchCommand(
1067
1052
  session.device,
@@ -1649,16 +1634,6 @@ function isLabelUnique(nodes: SnapshotState['nodes'], label: string): boolean {
1649
1634
  return count === 1;
1650
1635
  }
1651
1636
 
1652
- function isTextInputType(type: string | undefined): boolean {
1653
- const normalized = normalizeType(type ?? '');
1654
- return (
1655
- normalized === 'textfield' ||
1656
- normalized === 'textview' ||
1657
- normalized === 'searchfield' ||
1658
- normalized === 'textarea'
1659
- );
1660
- }
1661
-
1662
1637
  function pruneGroupNodes(nodes: RawSnapshotNode[]): RawSnapshotNode[] {
1663
1638
  const skippedDepths: number[] = [];
1664
1639
  const result: RawSnapshotNode[] = [];
@@ -1690,6 +1665,25 @@ function normalizeType(type: string): string {
1690
1665
  return value;
1691
1666
  }
1692
1667
 
1668
+ function isFillableType(type: string, platform: 'ios' | 'android'): boolean {
1669
+ const normalized = normalizeType(type);
1670
+ if (!normalized) return true;
1671
+ if (platform === 'android') {
1672
+ return (
1673
+ normalized.includes('edittext') ||
1674
+ normalized.includes('autocompletetextview')
1675
+ );
1676
+ }
1677
+ return (
1678
+ normalized.includes('textfield') ||
1679
+ normalized.includes('securetextfield') ||
1680
+ normalized.includes('searchfield') ||
1681
+ normalized.includes('textview') ||
1682
+ normalized.includes('textarea') ||
1683
+ normalized === 'search'
1684
+ );
1685
+ }
1686
+
1693
1687
  function findNearestHittableAncestor(
1694
1688
  nodes: SnapshotState['nodes'],
1695
1689
  node: SnapshotState['nodes'][number],
@@ -275,8 +275,30 @@ export async function fillAndroid(
275
275
  y: number,
276
276
  text: string,
277
277
  ): Promise<void> {
278
+ const attempts = [
279
+ { clearPadding: 12, minClear: 8, maxClear: 48, chunkSize: 4, delayMs: 0 },
280
+ { clearPadding: 24, minClear: 16, maxClear: 96, chunkSize: 1, delayMs: 15 },
281
+ ] as const;
282
+
278
283
  await focusAndroid(device, x, y);
279
- await typeAndroid(device, text);
284
+ let lastActual: string | null = null;
285
+
286
+ for (const attempt of attempts) {
287
+ const clearCount = clampCount(
288
+ text.length + attempt.clearPadding,
289
+ attempt.minClear,
290
+ attempt.maxClear,
291
+ );
292
+ await clearFocusedText(device, clearCount);
293
+ await typeAndroidChunked(device, text, attempt.chunkSize, attempt.delayMs);
294
+ lastActual = await readInputValueAtPoint(device, x, y);
295
+ if (lastActual === text) return;
296
+ }
297
+
298
+ throw new AppError('COMMAND_FAILED', 'Android fill verification failed', {
299
+ expected: text,
300
+ actual: lastActual ?? null,
301
+ });
280
302
  }
281
303
 
282
304
  export async function scrollAndroid(
@@ -455,6 +477,112 @@ function parseSettingState(state: string): boolean {
455
477
  throw new AppError('INVALID_ARGS', `Invalid setting state: ${state}`);
456
478
  }
457
479
 
480
+ async function typeAndroidChunked(
481
+ device: DeviceInfo,
482
+ text: string,
483
+ chunkSize: number,
484
+ delayMs: number,
485
+ ): Promise<void> {
486
+ const size = Math.max(1, Math.floor(chunkSize));
487
+ for (let i = 0; i < text.length; i += size) {
488
+ const chunk = text.slice(i, i + size);
489
+ await typeAndroid(device, chunk);
490
+ if (delayMs > 0 && i + size < text.length) {
491
+ await sleep(delayMs);
492
+ }
493
+ }
494
+ }
495
+
496
+ async function clearFocusedText(device: DeviceInfo, count: number): Promise<void> {
497
+ const deletes = Math.max(0, count);
498
+ await runCmd('adb', adbArgs(device, ['shell', 'input', 'keyevent', 'KEYCODE_MOVE_END']), {
499
+ allowFailure: true,
500
+ });
501
+ const batchSize = 24;
502
+ for (let i = 0; i < deletes; i += batchSize) {
503
+ const size = Math.min(batchSize, deletes - i);
504
+ await runCmd(
505
+ 'adb',
506
+ adbArgs(device, ['shell', 'input', 'keyevent', ...Array(size).fill('KEYCODE_DEL')]),
507
+ {
508
+ allowFailure: true,
509
+ },
510
+ );
511
+ }
512
+ }
513
+
514
+ async function readInputValueAtPoint(
515
+ device: DeviceInfo,
516
+ x: number,
517
+ y: number,
518
+ ): Promise<string | null> {
519
+ const xml = await dumpUiHierarchy(device);
520
+ const nodeRegex = /<node\b[^>]*>/g;
521
+ let match: RegExpExecArray | null;
522
+ let focusedEdit: { text: string; area: number } | null = null;
523
+ let editAtPoint: { text: string; area: number } | null = null;
524
+ let anyAtPoint: { text: string; area: number } | null = null;
525
+
526
+ while ((match = nodeRegex.exec(xml)) !== null) {
527
+ const node = match[0];
528
+ const attrs = readNodeAttributes(node);
529
+ const rect = parseBounds(attrs.bounds);
530
+ if (!rect) continue;
531
+ const className = attrs.className ?? '';
532
+ const text = decodeXmlEntities(attrs.text ?? '');
533
+ const focused = attrs.focused ?? false;
534
+ if (!text) continue;
535
+ const area = Math.max(1, rect.width * rect.height);
536
+ const containsPoint =
537
+ x >= rect.x &&
538
+ x <= rect.x + rect.width &&
539
+ y >= rect.y &&
540
+ y <= rect.y + rect.height;
541
+
542
+ if (focused && isEditTextClass(className)) {
543
+ if (!focusedEdit || area <= focusedEdit.area) {
544
+ focusedEdit = { text, area };
545
+ }
546
+ continue;
547
+ }
548
+ if (containsPoint && isEditTextClass(className)) {
549
+ if (!editAtPoint || area <= editAtPoint.area) {
550
+ editAtPoint = { text, area };
551
+ }
552
+ continue;
553
+ }
554
+ if (containsPoint) {
555
+ if (!anyAtPoint || area <= anyAtPoint.area) {
556
+ anyAtPoint = { text, area };
557
+ }
558
+ }
559
+ }
560
+
561
+ return focusedEdit?.text ?? editAtPoint?.text ?? anyAtPoint?.text ?? null;
562
+ }
563
+
564
+ function isEditTextClass(className: string): boolean {
565
+ const lower = className.toLowerCase();
566
+ return lower.includes('edittext') || lower.includes('textfield');
567
+ }
568
+
569
+ function decodeXmlEntities(value: string): string {
570
+ return value
571
+ .replace(/&quot;/g, '"')
572
+ .replace(/&apos;/g, "'")
573
+ .replace(/&lt;/g, '<')
574
+ .replace(/&gt;/g, '>')
575
+ .replace(/&amp;/g, '&');
576
+ }
577
+
578
+ async function sleep(ms: number): Promise<void> {
579
+ await new Promise((resolve) => setTimeout(resolve, ms));
580
+ }
581
+
582
+ function clampCount(value: number, min: number, max: number): number {
583
+ return Math.max(min, Math.min(max, value));
584
+ }
585
+
458
586
  function findBounds(xml: string, query: string): { x: number; y: number } | null {
459
587
  const q = query.toLowerCase();
460
588
  const nodeRegex = /<node[^>]+>/g;
@@ -493,14 +621,42 @@ function parseUiHierarchy(
493
621
  const scopedRoot = options.scope ? findScopeNode(tree, options.scope) : null;
494
622
  const roots = scopedRoot ? [scopedRoot] : tree.children;
495
623
 
496
- const walk = (node: AndroidNode, depth: number, parentIndex?: number) => {
624
+ const interactiveDescendantMemo = new Map<AndroidNode, boolean>();
625
+ const hasInteractiveDescendant = (node: AndroidNode): boolean => {
626
+ const cached = interactiveDescendantMemo.get(node);
627
+ if (cached !== undefined) return cached;
628
+ for (const child of node.children) {
629
+ if (child.hittable || hasInteractiveDescendant(child)) {
630
+ interactiveDescendantMemo.set(node, true);
631
+ return true;
632
+ }
633
+ }
634
+ interactiveDescendantMemo.set(node, false);
635
+ return false;
636
+ };
637
+
638
+ const walk = (
639
+ node: AndroidNode,
640
+ depth: number,
641
+ parentIndex?: number,
642
+ ancestorHittable: boolean = false,
643
+ ancestorCollection: boolean = false,
644
+ ) => {
497
645
  if (nodes.length >= maxNodes) {
498
646
  truncated = true;
499
647
  return;
500
648
  }
501
649
  if (depth > maxDepth) return;
502
650
 
503
- const include = options.raw ? true : shouldIncludeAndroidNode(node, options);
651
+ const include = options.raw
652
+ ? true
653
+ : shouldIncludeAndroidNode(
654
+ node,
655
+ options,
656
+ ancestorHittable,
657
+ hasInteractiveDescendant(node),
658
+ ancestorCollection,
659
+ );
504
660
  let currentIndex = parentIndex;
505
661
  if (include) {
506
662
  currentIndex = nodes.length;
@@ -517,14 +673,16 @@ function parseUiHierarchy(
517
673
  parentIndex,
518
674
  });
519
675
  }
676
+ const nextAncestorHittable = ancestorHittable || Boolean(node.hittable);
677
+ const nextAncestorCollection = ancestorCollection || isCollectionContainerType(node.type);
520
678
  for (const child of node.children) {
521
- walk(child, depth + 1, currentIndex);
679
+ walk(child, depth + 1, currentIndex, nextAncestorHittable, nextAncestorCollection);
522
680
  if (truncated) return;
523
681
  }
524
682
  };
525
683
 
526
684
  for (const root of roots) {
527
- walk(root, 0, undefined);
685
+ walk(root, 0, undefined, false, false);
528
686
  if (truncated) break;
529
687
  }
530
688
 
@@ -540,6 +698,7 @@ function readNodeAttributes(node: string): {
540
698
  clickable?: boolean;
541
699
  enabled?: boolean;
542
700
  focusable?: boolean;
701
+ focused?: boolean;
543
702
  } {
544
703
  const getAttr = (name: string): string | null => {
545
704
  const regex = new RegExp(`${name}="([^"]*)"`);
@@ -560,6 +719,7 @@ function readNodeAttributes(node: string): {
560
719
  clickable: boolAttr('clickable'),
561
720
  enabled: boolAttr('enabled'),
562
721
  focusable: boolAttr('focusable'),
722
+ focused: boolAttr('focused'),
563
723
  };
564
724
  }
565
725
 
@@ -630,18 +790,71 @@ function parseUiHierarchyTree(xml: string): AndroidNode {
630
790
  return root;
631
791
  }
632
792
 
633
- function shouldIncludeAndroidNode(node: AndroidNode, options: SnapshotOptions): boolean {
793
+ function shouldIncludeAndroidNode(
794
+ node: AndroidNode,
795
+ options: SnapshotOptions,
796
+ ancestorHittable: boolean,
797
+ descendantHittable: boolean,
798
+ ancestorCollection: boolean,
799
+ ): boolean {
800
+ const type = normalizeAndroidType(node.type);
801
+ const hasText = Boolean(node.label && node.label.trim().length > 0);
802
+ const hasId = Boolean(node.identifier && node.identifier.trim().length > 0);
803
+ const hasMeaningfulText = hasText && !isGenericAndroidId(node.label ?? '');
804
+ const hasMeaningfulId = hasId && !isGenericAndroidId(node.identifier ?? '');
805
+ const isStructural = isStructuralAndroidType(type);
806
+ const isVisual = type === 'imageview' || type === 'imagebutton';
634
807
  if (options.interactiveOnly) {
635
- return Boolean(node.hittable);
808
+ if (node.hittable) return true;
809
+ // Keep text proxies for tappable rows while dropping structural noise.
810
+ const proxyCandidate = hasMeaningfulText || hasMeaningfulId;
811
+ if (!proxyCandidate) return false;
812
+ if (isVisual) return false;
813
+ if (isStructural && !ancestorCollection) return false;
814
+ return ancestorHittable || descendantHittable || ancestorCollection;
636
815
  }
637
816
  if (options.compact) {
638
- const hasText = Boolean(node.label && node.label.trim().length > 0);
639
- const hasId = Boolean(node.identifier && node.identifier.trim().length > 0);
640
- return hasText || hasId || Boolean(node.hittable);
817
+ return hasMeaningfulText || hasMeaningfulId || Boolean(node.hittable);
818
+ }
819
+ if (isStructural || isVisual) {
820
+ if (node.hittable) return true;
821
+ if (hasMeaningfulText) return true;
822
+ if (hasMeaningfulId && descendantHittable) return true;
823
+ return descendantHittable;
641
824
  }
642
825
  return true;
643
826
  }
644
827
 
828
+ function isCollectionContainerType(type: string | null): boolean {
829
+ if (!type) return false;
830
+ const normalized = normalizeAndroidType(type);
831
+ return (
832
+ normalized.includes('recyclerview') ||
833
+ normalized.includes('listview') ||
834
+ normalized.includes('gridview')
835
+ );
836
+ }
837
+
838
+ function normalizeAndroidType(type: string | null): string {
839
+ if (!type) return '';
840
+ return type.toLowerCase();
841
+ }
842
+
843
+ function isStructuralAndroidType(type: string): boolean {
844
+ const short = type.split('.').pop() ?? type;
845
+ return (
846
+ short.includes('layout') ||
847
+ short === 'viewgroup' ||
848
+ short === 'view'
849
+ );
850
+ }
851
+
852
+ function isGenericAndroidId(value: string): boolean {
853
+ const trimmed = value.trim();
854
+ if (!trimmed) return false;
855
+ return /^[\w.]+:id\/[\w.-]+$/i.test(trimmed);
856
+ }
857
+
645
858
  function findScopeNode(root: AndroidNode, scope: string): AndroidNode | null {
646
859
  const query = scope.toLowerCase();
647
860
  const stack: AndroidNode[] = [...root.children];
@@ -34,6 +34,7 @@ export type RunnerCommand = {
34
34
  depth?: number;
35
35
  scope?: string;
36
36
  raw?: boolean;
37
+ clearFirst?: boolean;
37
38
  };
38
39
 
39
40
  export type RunnerSession = {
@@ -175,10 +176,9 @@ async function ensureRunnerSession(
175
176
  await ensureBooted(device.id);
176
177
  const xctestrun = await ensureXctestrun(device.id, options);
177
178
  const port = await getFreePort();
178
- const runnerTimeout = process.env.AGENT_DEVICE_RUNNER_TIMEOUT ?? '0';
179
179
  const { xctestrunPath, jsonPath } = await prepareXctestrunWithEnv(
180
180
  xctestrun,
181
- { AGENT_DEVICE_RUNNER_PORT: String(port), AGENT_DEVICE_RUNNER_TIMEOUT: runnerTimeout },
181
+ { AGENT_DEVICE_RUNNER_PORT: String(port) },
182
182
  `session-${device.id}-${port}`,
183
183
  );
184
184
  const testPromise = runCmdStreaming(
@@ -189,6 +189,8 @@ async function ensureRunnerSession(
189
189
  'AgentDeviceRunnerUITests/RunnerTests/testCommand',
190
190
  '-parallel-testing-enabled',
191
191
  'NO',
192
+ '-test-timeouts-enabled',
193
+ 'NO',
192
194
  '-maximum-concurrent-test-simulator-destinations',
193
195
  '1',
194
196
  '-xctestrun',
@@ -204,7 +206,7 @@ async function ensureRunnerSession(
204
206
  logChunk(chunk, options.logPath, options.traceLogPath, options.verbose);
205
207
  },
206
208
  allowFailure: true,
207
- env: { ...process.env, AGENT_DEVICE_RUNNER_PORT: String(port), AGENT_DEVICE_RUNNER_TIMEOUT: runnerTimeout },
209
+ env: { ...process.env, AGENT_DEVICE_RUNNER_PORT: String(port) },
208
210
  },
209
211
  );
210
212
 
@@ -28,9 +28,10 @@ type SnapshotNode = {
28
28
 
29
29
  export function formatSnapshotText(
30
30
  data: Record<string, unknown>,
31
- options: { raw?: boolean } = {},
31
+ options: { raw?: boolean; flatten?: boolean } = {},
32
32
  ): string {
33
- const nodes = (data.nodes ?? []) as SnapshotNode[];
33
+ const rawNodes = data.nodes;
34
+ const nodes = Array.isArray(rawNodes) ? (rawNodes as SnapshotNode[]) : [];
34
35
  const truncated = Boolean(data.truncated);
35
36
  const appName = typeof data.appName === 'string' ? data.appName : undefined;
36
37
  const appBundleId = typeof data.appBundleId === 'string' ? data.appBundleId : undefined;
@@ -39,13 +40,17 @@ export function formatSnapshotText(
39
40
  if (appBundleId) meta.push(`App: ${appBundleId}`);
40
41
  const header = `Snapshot: ${nodes.length} nodes${truncated ? ' (truncated)' : ''}`;
41
42
  const prefix = meta.length > 0 ? `${meta.join('\n')}\n` : '';
42
- if (!Array.isArray(nodes) || nodes.length === 0) {
43
+ if (nodes.length === 0) {
43
44
  return `${prefix}${header}\n`;
44
45
  }
45
46
  if (options.raw) {
46
47
  const rawLines = nodes.map((node) => JSON.stringify(node));
47
48
  return `${prefix}${header}\n${rawLines.join('\n')}\n`;
48
49
  }
50
+ if (options.flatten) {
51
+ const flatLines = nodes.map((node) => formatSnapshotLine(node, 0, false));
52
+ return `${prefix}${header}\n${flatLines.join('\n')}\n`;
53
+ }
49
54
  const hiddenGroupDepths: number[] = [];
50
55
  const lines: string[] = [];
51
56
  for (const node of nodes) {
@@ -62,28 +67,69 @@ export function formatSnapshotText(
62
67
  const adjustedDepth = isHiddenGroup
63
68
  ? depth
64
69
  : Math.max(0, depth - hiddenGroupDepths.length);
65
- const indent = ' '.repeat(adjustedDepth);
66
- const ref = node.ref ? `@${node.ref}` : '';
67
- const flags = [
68
- node.enabled === false ? 'disabled' : null,
69
- ]
70
- .filter(Boolean)
71
- .join(', ');
72
- const flagText = flags ? ` [${flags}]` : '';
73
- const textPart = label ? ` "${label}"` : '';
74
- if (isHiddenGroup) {
75
- lines.push(`${indent}${ref} [${type}]${flagText}`.trimEnd());
76
- continue;
77
- }
78
- lines.push(`${indent}${ref} [${type}]${textPart}${flagText}`.trimEnd());
70
+ lines.push(formatSnapshotLine(node, adjustedDepth, isHiddenGroup));
79
71
  }
80
72
  return `${prefix}${header}\n${lines.join('\n')}\n`;
81
73
  }
82
74
 
75
+ function formatSnapshotLine(node: SnapshotNode, depth: number, hiddenGroup: boolean): string {
76
+ const type = formatRole(node.type ?? 'Element');
77
+ const label = displayLabel(node, type);
78
+ const indent = ' '.repeat(depth);
79
+ const ref = node.ref ? `@${node.ref}` : '';
80
+ const flags = [node.enabled === false ? 'disabled' : null].filter(Boolean).join(', ');
81
+ const flagText = flags ? ` [${flags}]` : '';
82
+ const textPart = label ? ` "${label}"` : '';
83
+ if (hiddenGroup) {
84
+ return `${indent}${ref} [${type}]${flagText}`.trimEnd();
85
+ }
86
+ return `${indent}${ref} [${type}]${textPart}${flagText}`.trimEnd();
87
+ }
88
+
89
+ function displayLabel(node: SnapshotNode, type: string): string {
90
+ const label = node.label?.trim();
91
+ const value = node.value?.trim();
92
+ if (isEditableRole(type)) {
93
+ // For inputs, prefer current value over placeholder/accessibility label.
94
+ if (value) return value;
95
+ if (label) return label;
96
+ } else if (label) {
97
+ return label;
98
+ }
99
+ if (value) return value;
100
+ const identifier = node.identifier?.trim();
101
+ if (!identifier) return '';
102
+ if (isGenericResourceId(identifier) && (type === 'group' || type === 'image' || type === 'list' || type === 'collection')) {
103
+ return '';
104
+ }
105
+ return identifier;
106
+ }
107
+
108
+ function isEditableRole(type: string): boolean {
109
+ return type === 'text-field' || type === 'text-view' || type === 'search';
110
+ }
111
+
112
+ function isGenericResourceId(value: string): boolean {
113
+ return /^[\w.]+:id\/[\w.-]+$/i.test(value);
114
+ }
115
+
83
116
  function formatRole(type: string): string {
117
+ const raw = type;
84
118
  let normalized = type.replace(/XCUIElementType/gi, '').toLowerCase();
85
- if (normalized.startsWith("ax")) {
86
- normalized = normalized.replace(/^ax/, "");
119
+ const isAndroidClass =
120
+ raw.includes('.') &&
121
+ (raw.startsWith('android.') || raw.startsWith('androidx.') || raw.startsWith('com.'));
122
+ if (normalized.startsWith('ax')) {
123
+ normalized = normalized.replace(/^ax/, '');
124
+ }
125
+ if (normalized.includes('.')) {
126
+ normalized = normalized
127
+ .replace(/^android\.widget\./, '')
128
+ .replace(/^android\.view\./, '')
129
+ .replace(/^android\.webkit\./, '')
130
+ .replace(/^androidx\./, '')
131
+ .replace(/^com\.google\.android\./, '')
132
+ .replace(/^com\.android\./, '');
87
133
  }
88
134
  switch (normalized) {
89
135
  case 'application':
@@ -93,24 +139,40 @@ function formatRole(type: string): string {
93
139
  case 'tabbar':
94
140
  return 'tab-bar';
95
141
  case 'button':
142
+ case 'imagebutton':
96
143
  return 'button';
97
144
  case 'link':
98
145
  return 'link';
99
146
  case 'cell':
100
147
  return 'cell';
101
148
  case 'statictext':
149
+ case 'checkedtextview':
102
150
  return 'text';
103
151
  case 'textfield':
152
+ case 'edittext':
104
153
  return 'text-field';
105
154
  case 'textview':
155
+ return isAndroidClass ? 'text' : 'text-view';
156
+ case 'textarea':
106
157
  return 'text-view';
107
158
  case 'switch':
108
159
  return 'switch';
109
160
  case 'slider':
110
161
  return 'slider';
111
162
  case 'image':
163
+ case 'imageview':
112
164
  return 'image';
113
- case 'table':
165
+ case 'webview':
166
+ return 'webview';
167
+ case 'framelayout':
168
+ case 'linearlayout':
169
+ case 'relativelayout':
170
+ case 'constraintlayout':
171
+ case 'viewgroup':
172
+ case 'view':
173
+ return 'group';
174
+ case 'listview':
175
+ case 'recyclerview':
114
176
  return 'list';
115
177
  case 'collectionview':
116
178
  return 'collection';
@@ -122,12 +184,6 @@ function formatRole(type: string): string {
122
184
  return 'group';
123
185
  case 'window':
124
186
  return 'window';
125
- case 'statictext':
126
- return 'text';
127
- case 'textfield':
128
- return 'text-field';
129
- case 'textarea':
130
- return 'text-view';
131
187
  case 'checkbox':
132
188
  return 'checkbox';
133
189
  case 'radio':
@@ -137,6 +193,8 @@ function formatRole(type: string): string {
137
193
  case 'toolbar':
138
194
  return 'toolbar';
139
195
  case 'scrollarea':
196
+ case 'scrollview':
197
+ case 'nestedscrollview':
140
198
  return 'scroll-area';
141
199
  case 'table':
142
200
  return 'table';