agent-device 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,7 +20,7 @@ npm install -g agent-device
20
20
  Or use it without installing:
21
21
 
22
22
  ```bash
23
- npx agent-device open Settings
23
+ npx agent-device open SampleApp
24
24
  ```
25
25
 
26
26
  ## Usage
@@ -32,15 +32,20 @@ agent-device <command> [args] [--json]
32
32
  Examples:
33
33
 
34
34
  ```bash
35
- agent-device open Settings
36
- agent-device press 120 320
35
+ agent-device open SampleApp
36
+ agent-device snapshot
37
+ agent-device click @e7
37
38
  agent-device type "hello"
38
39
  agent-device screenshot --out ./screenshot.png
39
- agent-device snapshot -i -c -d 6
40
- agent-device close Settings
40
+ agent-device close SampleApp
41
41
  ```
42
42
 
43
43
  Best practice: run `snapshot` immediately before interactions to avoid stale coordinates if the Simulator window moves or UI changes.
44
+ When interacting with UI elements from a snapshot, prefer refs (e.g. `click @e7`) over raw coordinates. Refs are stable across runs and avoid coordinate drift.
45
+
46
+ iOS snapshots:
47
+ - Default backend is `ax` (fast). It requires enabling Accessibility for the terminal app in System Settings.
48
+ - If AX is not available, use `--backend xctest` explicitly.
44
49
 
45
50
  Flags:
46
51
  - `--platform ios|android`
@@ -1,5 +1,5 @@
1
- let e,t;import n from"node:crypto";import{isCancel as i,select as r}from"@clack/prompts";import{node_path as a,runCmdStreaming as o,promises as s,asAppError as l,fileURLToPath as c,node_fs as u,node_os as d,node_net as f,errors_AppError as p,runCmd as h,whichCmd as m}from"./861.js";async function w(e,t){let n=e,a=e=>e.toLowerCase().replace(/_/g," ").replace(/\s+/g," ").trim();if(t.platform&&(n=n.filter(e=>e.platform===t.platform)),t.udid){let e=n.find(e=>e.id===t.udid&&"ios"===e.platform);if(!e)throw new p("DEVICE_NOT_FOUND",`No iOS device with UDID ${t.udid}`);return e}if(t.serial){let e=n.find(e=>e.id===t.serial&&"android"===e.platform);if(!e)throw new p("DEVICE_NOT_FOUND",`No Android device with serial ${t.serial}`);return e}if(t.deviceName){let e=a(t.deviceName),i=n.find(t=>a(t.name)===e);if(!i)throw new p("DEVICE_NOT_FOUND",`No device named ${t.deviceName}`);return i}if(1===n.length)return n[0];if(0===n.length)throw new p("DEVICE_NOT_FOUND","No devices found",{selector:t});let o=n.filter(e=>e.booted);if(1===o.length)return o[0];if(!process.env.CI&&process.stdin.isTTY&&process.stdout.isTTY){let e=await r({message:"Multiple devices available. Choose a device to continue:",options:(o.length>0?o:n).map(e=>({label:`${e.name} (${e.platform}${e.kind?`, ${e.kind}`:""}${e.booted?", booted":""})`,value:e.id}))});if(i(e))throw new p("INVALID_ARGS","Device selection cancelled");if(e){let t=n.find(t=>t.id===e);if(t)return t}}return o[0]??n[0]}async function g(){if(!await m("adb"))throw new p("TOOL_MISSING","adb not found in PATH");let e=(await h("adb",["devices","-l"])).stdout.split("\n").map(e=>e.trim()),t=[];for(let n of e){if(!n||n.startsWith("List of devices"))continue;let e=n.split(/\s+/),i=e[0];if("device"!==e[1])continue;let r=(e.find(e=>e.startsWith("model:"))??"").replace("model:","").replace(/_/g," ").trim()||i;if(i.startsWith("emulator-")){let e=await h("adb",["-s",i,"emu","avd","name"],{allowFailure:!0}),t=e.stdout.trim();0===e.exitCode&&t&&(r=t.replace(/_/g," "))}let a=await y(i);t.push({platform:"android",id:i,name:r,kind:i.startsWith("emulator-")?"emulator":"device",booted:a})}return t}async function y(e){try{let t=await h("adb",["-s",e,"shell","getprop","sys.boot_completed"],{allowFailure:!0});return"1"===t.stdout.trim()}catch{return!1}}let N={settings:{type:"intent",value:"android.settings.SETTINGS"}};function v(e,t){return["-s",e.id,...t]}async function b(e,t){let n=t.trim();if(n.includes("."))return{type:"package",value:n};let i=N[n.toLowerCase()];if(i)return i;let r=(await h("adb",v(e,["shell","pm","list","packages"]))).stdout.split("\n").map(e=>e.replace("package:","").trim()).filter(Boolean).filter(e=>e.toLowerCase().includes(n.toLowerCase()));if(1===r.length)return{type:"package",value:r[0]};if(r.length>1)throw new p("INVALID_ARGS",`Multiple packages matched "${t}"`,{matches:r});throw new p("APP_NOT_INSTALLED",`No package found matching "${t}"`)}async function I(e,t){let n=await b(e,t);"intent"===n.type?await h("adb",v(e,["shell","am","start","-a",n.value])):await h("adb",v(e,["shell","monkey","-p",n.value,"-c","android.intent.category.LAUNCHER","1"]))}async function A(e,t){if("settings"===t.trim().toLowerCase())return void await h("adb",v(e,["shell","am","force-stop","com.android.settings"]));let n=await b(e,t);if("intent"===n.type)throw new p("INVALID_ARGS","Close requires a package name, not an intent");await h("adb",v(e,["shell","am","force-stop",n.value]))}async function S(e,t,n){await h("adb",v(e,["shell","input","tap",String(t),String(n)]))}async function O(e,t,n,i=800){await h("adb",v(e,["shell","input","swipe",String(t),String(n),String(t),String(n),String(i)]))}async function D(e,t){let n=t.replace(/ /g,"%s");await h("adb",v(e,["shell","input","text",n]))}async function x(e,t,n){await S(e,t,n)}async function _(e,t,n,i){await x(e,t,n),await D(e,i)}async function E(e,t,n=.6){let{width:i,height:r}=await T(e),a=Math.floor(i*n),o=Math.floor(r*n),s=Math.floor(i/2),l=Math.floor(r/2),c=s,u=l,d=s,f=l;switch(t){case"up":u=l-Math.floor(o/2),f=l+Math.floor(o/2);break;case"down":u=l+Math.floor(o/2),f=l-Math.floor(o/2);break;case"left":c=s-Math.floor(a/2),d=s+Math.floor(a/2);break;case"right":c=s+Math.floor(a/2),d=s-Math.floor(a/2);break;default:throw new p("INVALID_ARGS",`Unknown direction: ${t}`)}await h("adb",v(e,["shell","input","swipe",String(c),String(u),String(d),String(f),"300"]))}async function k(e,t){for(let n=0;n<8;n+=1){let n="";try{n=await L(e)}catch(t){let e=t instanceof Error?t.message:String(t);throw new p("UNSUPPORTED_OPERATION",`uiautomator dump failed: ${e}`)}if(function(e,t){let n=t.toLowerCase(),i=/<node[^>]+>/g,r=i.exec(e);for(;r;){let t=r[0],a=/text="([^"]*)"/.exec(t),o=/content-desc="([^"]*)"/.exec(t),s=(a?.[1]??"").toLowerCase(),l=(o?.[1]??"").toLowerCase();if(s.includes(n)||l.includes(n)){let e=/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/.exec(t);if(e){let t=Number(e[1]),n=Number(e[2]);return{x:Math.floor((t+Number(e[3]))/2),y:Math.floor((n+Number(e[4]))/2)}}return{x:0,y:0}}r=i.exec(e)}return null}(n,t))return;await E(e,"down",.5)}throw new p("COMMAND_FAILED",`Could not find element containing "${t}" after scrolling`)}async function C(e,t){let n=await h("adb",v(e,["exec-out","screencap","-p"]),{binaryStdout:!0});if(!n.stdoutBuffer)throw new p("COMMAND_FAILED","Failed to capture screenshot");await s.writeFile(t,n.stdoutBuffer)}async function R(e,t={}){return function(e,t,n){let i=function(e){let t={type:null,label:null,value:null,identifier:null,depth:-1,children:[]},n=[t],i=/<node\b[^>]*>|<\/node>/g,r=i.exec(e);for(;r;){let t=r[0];if(t.startsWith("</node")){n.length>1&&n.pop(),r=i.exec(e);continue}let a=function(e){let t=t=>{let n=RegExp(`${t}="([^"]*)"`).exec(e);return n?n[1]:null},n=e=>{let n=t(e);if(null!==n)return"true"===n};return{text:t("text"),desc:t("content-desc"),resourceId:t("resource-id"),className:t("class"),bounds:t("bounds"),clickable:n("clickable"),enabled:n("enabled"),focusable:n("focusable")}}(t),o=function(e){if(!e)return;let t=/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/.exec(e);if(!t)return;let n=Number(t[1]),i=Number(t[2]);return{x:n,y:i,width:Math.max(0,Number(t[3])-n),height:Math.max(0,Number(t[4])-i)}}(a.bounds),s=n[n.length-1],l={type:a.className,label:a.text||a.desc,value:a.text,identifier:a.resourceId,rect:o,enabled:a.enabled,hittable:a.clickable??a.focusable,depth:s.depth+1,parentIndex:void 0,children:[]};s.children.push(l),t.endsWith("/>")||n.push(l),r=i.exec(e)}return t}(e),r=[],a=!1,o=n.depth??1/0,s=n.scope?function(e,t){let n=t.toLowerCase(),i=[...e.children];for(;i.length>0;){let e=i.shift(),t=e.label?.toLowerCase()??"",r=e.value?.toLowerCase()??"",a=e.identifier?.toLowerCase()??"";if(t.includes(n)||r.includes(n)||a.includes(n))return e;i.push(...e.children)}return null}(i,n.scope):null,l=s?[s]:i.children,c=(e,t)=>{if(r.length>=800){a=!0;return}if(!(t>o)){for(let i of((n.raw||function(e,t){if(t.interactiveOnly)return!!e.hittable;if(t.compact){let t=!!(e.label&&e.label.trim().length>0),n=!!(e.identifier&&e.identifier.trim().length>0);return t||n||!!e.hittable}return!0}(e,n))&&r.push({index:r.length,type:e.type??void 0,label:e.label??void 0,value:e.value??void 0,identifier:e.identifier??void 0,rect:e.rect,enabled:e.enabled,hittable:e.hittable,depth:t,parentIndex:e.parentIndex}),e.children))if(c(i,t+1),a)return}};for(let e of l)if(c(e,0),a)break;return a?{nodes:r,truncated:a}:{nodes:r}}(await L(e),800,t)}async function P(){if(!await m("adb"))throw new p("TOOL_MISSING","adb not found in PATH")}async function T(e){let t=(await h("adb",v(e,["shell","wm","size"]))).stdout.match(/Physical size:\s*(\d+)x(\d+)/);if(!t)throw new p("COMMAND_FAILED","Unable to read screen size");return{width:Number(t[1]),height:Number(t[2])}}async function L(e){return await h("adb",v(e,["shell","uiautomator","dump","/sdcard/window_dump.xml"])),(await h("adb",v(e,["shell","cat","/sdcard/window_dump.xml"]))).stdout}async function M(){if("darwin"!==process.platform)throw new p("UNSUPPORTED_PLATFORM","iOS tools are only available on macOS");if(!await m("xcrun"))throw new p("TOOL_MISSING","xcrun not found in PATH");let e=[],t=await h("xcrun",["simctl","list","devices","-j"]);try{let n=JSON.parse(t.stdout);for(let t of Object.values(n.devices))for(let n of t)n.isAvailable&&e.push({platform:"ios",id:n.udid,name:n.name,kind:"simulator",booted:"Booted"===n.state})}catch(e){throw new p("COMMAND_FAILED","Failed to parse simctl devices JSON",void 0,e)}if(await m("xcrun"))try{let t=await h("xcrun",["devicectl","list","devices","--json"]);for(let n of JSON.parse(t.stdout).devices??[])n.platform?.toLowerCase().includes("ios")&&e.push({platform:"ios",id:n.identifier,name:n.name,kind:"device",booted:!0})}catch{}return e}let F={settings:"com.apple.Preferences"};async function j(e,t){let n=t.trim();if(n.includes("."))return n;let i=F[n.toLowerCase()];if(i)return i;if("simulator"===e.kind){let i=(await Y(e)).filter(e=>e.name.toLowerCase()===n.toLowerCase());if(1===i.length)return i[0].bundleId;if(i.length>1)throw new p("INVALID_ARGS",`Multiple apps matched "${t}"`,{matches:i})}throw new p("APP_NOT_INSTALLED",`No app found matching "${t}"`)}async function U(e,t){let n=await j(e,t);if("simulator"===e.kind){await Z(e),await h("xcrun",["simctl","launch",e.id,n]);return}await h("xcrun",["devicectl","device","process","launch","--device",e.id,n])}async function V(e,t){let n=await j(e,t);if("simulator"===e.kind){await Z(e);let t=await h("xcrun",["simctl","terminate",e.id,n],{allowFailure:!0});if(0!==t.exitCode){if(t.stderr.toLowerCase().includes("found nothing to terminate"))return;throw new p("COMMAND_FAILED",`xcrun exited with code ${t.exitCode}`,{cmd:"xcrun",args:["simctl","terminate",e.id,n],stdout:t.stdout,stderr:t.stderr,exitCode:t.exitCode})}return}await h("xcrun",["devicectl","device","process","terminate","--device",e.id,n])}async function $(e,t,n){throw H(e,"press"),await Z(e),new p("UNSUPPORTED_OPERATION","simctl io tap is not available; use the XCTest runner for input")}async function G(e,t,n,i=800){throw H(e,"long-press"),await Z(e),new p("UNSUPPORTED_OPERATION","long-press is not supported on iOS simulators without XCTest runner support")}async function B(e,t,n){await $(e,t,n)}async function J(e,t){throw H(e,"type"),await Z(e),new p("UNSUPPORTED_OPERATION","simctl io keyboard is not available; use the XCTest runner for input")}async function q(e,t,n,i){await B(e,t,n),await J(e,i)}async function W(e,t,n=.6){throw H(e,"scroll"),await Z(e),new p("UNSUPPORTED_OPERATION","simctl io swipe is not available; use the XCTest runner for input")}async function X(e){throw new p("UNSUPPORTED_OPERATION",`scrollintoview is not supported on iOS without UI automation (${e})`)}async function z(e,t){if("simulator"===e.kind){await Z(e),await h("xcrun",["simctl","io",e.id,"screenshot",t]);return}await h("xcrun",["devicectl","device","screenshot","--device",e.id,t])}function H(e,t){if("simulator"!==e.kind)throw new p("UNSUPPORTED_OPERATION",`${t} is only supported on iOS simulators in v1`)}async function Y(e){let t=(await h("xcrun",["simctl","listapps",e.id],{allowFailure:!0})).stdout;if(!t.trim().startsWith("{"))return[];try{let e=JSON.parse(t);return Object.entries(e).map(([e,t])=>({bundleId:e,name:t.CFBundleDisplayName??t.CFBundleName??e}))}catch{return[]}}async function Z(e){"simulator"!==e.kind||"Booted"!==await K(e.id)&&(await h("xcrun",["simctl","boot",e.id],{allowFailure:!0}),await h("xcrun",["simctl","bootstatus",e.id,"-b"],{allowFailure:!0}))}async function K(e){let t=await h("xcrun",["simctl","list","devices","-j"],{allowFailure:!0});if(0!==t.exitCode)return null;try{let n=JSON.parse(t.stdout);for(let t of Object.values(n.devices??{})){let n=t.find(t=>t.udid===e);if(n)return n.state}}catch{}return null}let Q=new Map;async function ee(e,t,n={}){if("simulator"!==e.kind)throw new p("UNSUPPORTED_OPERATION","iOS runner only supports simulators in v1");try{let i=await ei(e,n),r=await es(e,i.port,t,n.logPath),a=await r.text(),o={};try{o=JSON.parse(a)}catch{throw new p("COMMAND_FAILED","Invalid runner response",{text:a})}if(!o.ok)throw new p("COMMAND_FAILED",o.error?.message??"Runner error",{runner:o,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:n.logPath});return o.data??{}}catch(r){let i=r instanceof p?r:new p("COMMAND_FAILED",String(r));if("COMMAND_FAILED"===i.code&&"string"==typeof i.message&&i.message.includes("Runner did not accept connection")){await et(e.id);let i=await ei(e,n),r=await es(e,i.port,t,n.logPath),a=await r.text(),o={};try{o=JSON.parse(a)}catch{throw new p("COMMAND_FAILED","Invalid runner response",{text:a})}if(!o.ok)throw new p("COMMAND_FAILED",o.error?.message??"Runner error",{runner:o,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:n.logPath});return o.data??{}}throw r}}async function et(e){let t=Q.get(e);if(t){try{await es(t.device,t.port,{command:"shutdown"})}catch{}try{await t.testPromise}catch{}ef(t.xctestrunPath),ef(t.jsonPath),Q.delete(e)}}async function en(e){await h("xcrun",["simctl","bootstatus",e,"-b"],{allowFailure:!0})}async function ei(e,t){let n=Q.get(e.id);if(n)return n;await en(e.id);let i=await er(e.id,t),r=await ec(),a=process.env.AGENT_DEVICE_RUNNER_TIMEOUT??"300",{xctestrunPath:s,jsonPath:l}=await ed(i,{AGENT_DEVICE_RUNNER_PORT:String(r),AGENT_DEVICE_RUNNER_TIMEOUT:a},`session-${e.id}-${r}`),c=o("xcodebuild",["test-without-building","-only-testing","AgentDeviceRunnerUITests/RunnerTests/testCommand","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-xctestrun",s,"-destination",`platform=iOS Simulator,id=${e.id}`],{onStdoutChunk:e=>{eo(e,t.logPath,t.verbose)},onStderrChunk:e=>{eo(e,t.logPath,t.verbose)},allowFailure:!0,env:{...process.env,AGENT_DEVICE_RUNNER_PORT:String(r),AGENT_DEVICE_RUNNER_TIMEOUT:a}}),u={device:e,deviceId:e.id,port:r,xctestrunPath:s,jsonPath:l,testPromise:c};return Q.set(e.id,u),u}async function er(e,t){let n,i=a.join(d.homedir(),".agent-device","ios-runner"),r=a.join(i,"derived");if((n=process.env.AGENT_DEVICE_IOS_CLEAN_DERIVED)&&["1","true","yes","on"].includes(n.toLowerCase()))try{u.rmSync(r,{recursive:!0,force:!0})}catch{}let s=ea(r);if(s)return s;let l=function(){let e=a.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=a.join(t,"package.json");if(u.existsSync(e))return t;t=a.dirname(t)}return e}(),f=a.join(l,"ios-runner","AgentDeviceRunner","AgentDeviceRunner.xcodeproj");if(!u.existsSync(f))throw new p("COMMAND_FAILED","iOS runner project not found",{projectPath:f});try{await o("xcodebuild",["build-for-testing","-project",f,"-scheme","AgentDeviceRunner","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-destination",`platform=iOS Simulator,id=${e}`,"-derivedDataPath",r],{onStdoutChunk:e=>{eo(e,t.logPath,t.verbose)},onStderrChunk:e=>{eo(e,t.logPath,t.verbose)}})}catch(n){let e=n instanceof p?n:new p("COMMAND_FAILED",String(n));throw new p("COMMAND_FAILED","xcodebuild build-for-testing failed",{error:e.message,details:e.details,logPath:t.logPath})}let h=ea(r);if(!h)throw new p("COMMAND_FAILED","Failed to locate .xctestrun after build");return h}function ea(e){if(!u.existsSync(e))return null;let t=[],n=[e];for(;n.length>0;){let e=n.pop();for(let i of u.readdirSync(e,{withFileTypes:!0})){let r=a.join(e,i.name);if(i.isDirectory()){n.push(r);continue}if(i.isFile()&&i.name.endsWith(".xctestrun"))try{let e=u.statSync(r);t.push({path:r,mtimeMs:e.mtimeMs})}catch{}}}return 0===t.length?null:(t.sort((e,t)=>t.mtimeMs-e.mtimeMs),t[0]?.path??null)}function eo(e,t,n){t&&u.appendFileSync(t,e),n&&process.stderr.write(e)}async function es(e,t,n,i){i&&await eu(i,4e3);let r=Date.now(),a=null;for(;Date.now()-r<8e3;)try{return await fetch(`http://127.0.0.1:${t}/command`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})}catch(e){a=e,await new Promise(e=>setTimeout(e,100))}if("simulator"===e.kind){let i=await el(e.id,t,n);return new Response(i.body,{status:i.status})}let o=i?function(e){try{if(!u.existsSync(e))return null;let t=u.readFileSync(e,"utf8").match(/AGENT_DEVICE_RUNNER_PORT=(\d+)/);if(t)return Number(t[1])}catch{}return null}(i):null;if(o&&o!==t)try{return await fetch(`http://127.0.0.1:${o}/command`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})}catch(e){a=e}throw new p("COMMAND_FAILED","Runner did not accept connection",{port:t,fallbackPort:o,logPath:i,lastError:a?String(a):void 0})}async function el(e,t,n){let i=JSON.stringify(n),r=await h("xcrun",["simctl","spawn",e,"/usr/bin/curl","-s","-X","POST","-H","Content-Type: application/json","--data",i,`http://127.0.0.1:${t}/command`],{allowFailure:!0}),a=r.stdout;if(0!==r.exitCode)throw new p("COMMAND_FAILED","Runner did not accept connection (simctl spawn)",{port:t,stdout:r.stdout,stderr:r.stderr,exitCode:r.exitCode});return{status:200,body:a}}async function ec(){return await new Promise((e,t)=>{let n=f.createServer();n.listen(0,"127.0.0.1",()=>{let i=n.address();n.close(),"object"==typeof i&&i?.port?e(i.port):t(new p("COMMAND_FAILED","Failed to allocate port"))}),n.on("error",t)})}async function eu(e,t){if(!u.existsSync(e))return;let n=Date.now(),i=0;for(;Date.now()-n<t;){if(!u.existsSync(e))return;let t=u.statSync(e);if(t.size>i){let n=u.openSync(e,"r"),r=Buffer.alloc(t.size-i);u.readSync(n,r,0,r.length,i),u.closeSync(n),i=t.size;let a=r.toString("utf8");if(a.includes("AGENT_DEVICE_RUNNER_LISTENER_READY")||a.includes("AGENT_DEVICE_RUNNER_PORT="))return}await new Promise(e=>setTimeout(e,100))}}async function ed(e,t,n){let i,r=a.dirname(e),o=n.replace(/[^a-zA-Z0-9._-]/g,"_"),s=a.join(r,`AgentDeviceRunner.env.${o}.json`),l=a.join(r,`AgentDeviceRunner.env.${o}.xctestrun`),c=await h("plutil",["-convert","json","-o","-",e],{allowFailure:!0});if(0!==c.exitCode||!c.stdout.trim())throw new p("COMMAND_FAILED","Failed to read xctestrun plist",{xctestrunPath:e,stderr:c.stderr});try{i=JSON.parse(c.stdout)}catch(t){throw new p("COMMAND_FAILED","Failed to parse xctestrun JSON",{xctestrunPath:e,error:String(t)})}let d=e=>{e.EnvironmentVariables={...e.EnvironmentVariables??{},...t},e.UITestEnvironmentVariables={...e.UITestEnvironmentVariables??{},...t},e.UITargetAppEnvironmentVariables={...e.UITargetAppEnvironmentVariables??{},...t},e.TestingEnvironmentVariables={...e.TestingEnvironmentVariables??{},...t}},f=i.TestConfigurations;if(Array.isArray(f))for(let e of f){if(!e||"object"!=typeof e)continue;let t=e.TestTargets;if(Array.isArray(t))for(let e of t)e&&"object"==typeof e&&d(e)}for(let[e,t]of Object.entries(i))t&&"object"==typeof t&&t.TestBundlePath&&(d(t),i[e]=t);u.writeFileSync(s,JSON.stringify(i,null,2));let m=await h("plutil",["-convert","xml1","-o",l,s],{allowFailure:!0});if(0!==m.exitCode)throw new p("COMMAND_FAILED","Failed to write xctestrun plist",{tmpXctestrunPath:l,stderr:m.stderr});return{xctestrunPath:l,jsonPath:s}}function ef(e){try{u.existsSync(e)&&u.unlinkSync(e)}catch{}}async function ep(e){let t,n;if("ios"!==e.platform||"simulator"!==e.kind)throw new p("UNSUPPORTED_OPERATION","AX snapshot is only supported on iOS simulators");let i=await eh(),r=await h(i,[],{allowFailure:!0});if(0!==r.exitCode)throw new p("COMMAND_FAILED","AX snapshot failed",{stderr:r.stderr,stdout:r.stdout});try{let e=JSON.parse(r.stdout);if(e&&"object"==typeof e&&"root"in e){if(!e.root)throw Error("AX snapshot missing root");t=e.root,n=e.windowFrame??void 0}else t=e}catch(e){throw new p("COMMAND_FAILED","Invalid AX snapshot JSON",{error:String(e)})}let a=t.frame??n,o=[],s=[],l=(e,t)=>{e.frame&&o.push(e.frame);let n=e.frame&&a?{x:e.frame.x-a.x,y:e.frame.y-a.y,width:e.frame.width,height:e.frame.height}:e.frame;for(let i of(s.push({...e,frame:n,children:void 0,depth:t}),e.children??[]))l(i,t+1)};return l(t,0),{nodes:(function(e,t,n){if(!t||0===n.length)return e;let i=1/0,r=1/0;for(let e of n)e.x<i&&(i=e.x),e.y<r&&(r=e.y);return i<=5&&r<=5?e.map(e=>({...e,frame:e.frame?{x:e.frame.x+t.x,y:e.frame.y+t.y,width:e.frame.width,height:e.frame.height}:void 0})):e})(s,a,o).map((e,t)=>({index:t,type:e.subrole??e.role,label:e.label,value:e.value,identifier:e.identifier,rect:e.frame?{x:e.frame.x,y:e.frame.y,width:e.frame.width,height:e.frame.height}:void 0,depth:e.depth})),rootRect:a}}async function eh(){let e=function(){let e=process.cwd();for(let t=0;t<6;t+=1){let t=a.join(e,"package.json");if(u.existsSync(t))return e;e=a.dirname(e)}return process.cwd()}(),t=a.join(e,"ios-runner","AXSnapshot"),n=process.env.AGENT_DEVICE_AX_BINARY;if(n&&u.existsSync(n))return n;for(let t of[a.join(e,"bin","axsnapshot"),a.join(e,"dist","bin","axsnapshot"),a.join(e,"dist","axsnapshot")])if(u.existsSync(t))return t;let i=a.join(t,".build","release","axsnapshot");if(u.existsSync(i))return i;let r=await h("swift",["build","-c","release"],{cwd:t,allowFailure:!0});if(0!==r.exitCode||!u.existsSync(i))throw new p("COMMAND_FAILED","Failed to build AX snapshot tool",{stderr:r.stderr,stdout:r.stdout});return i}async function em(e){let t={platform:e.platform,deviceName:e.device,udid:e.udid,serial:e.serial};if("android"===t.platform){await P();let e=await g();return await w(e,t)}if("ios"===t.platform){let e=await M();return await w(e,t)}let n=[];try{n.push(...await g())}catch{}try{n.push(...await M())}catch{}return await w(n,t)}async function ew(e,t,n,i,r){let a=function(e){switch(e.platform){case"android":return{open:t=>I(e,t),close:t=>A(e,t),tap:(t,n)=>S(e,t,n),longPress:(t,n,i)=>O(e,t,n,i),focus:(t,n)=>x(e,t,n),type:t=>D(e,t),fill:(t,n,i)=>_(e,t,n,i),scroll:(t,n)=>E(e,t,n),scrollIntoView:t=>k(e,t),screenshot:t=>C(e,t)};case"ios":return{open:t=>U(e,t),close:t=>V(e,t),tap:(t,n)=>$(e,t,n),longPress:(t,n,i)=>G(e,t,n,i),focus:(t,n)=>B(e,t,n),type:t=>J(e,t),fill:(t,n,i)=>q(e,t,n,i),scroll:(t,n)=>W(e,t,n),scrollIntoView:e=>X(e),screenshot:t=>z(e,t)};default:throw new p("UNSUPPORTED_PLATFORM",`Unsupported platform: ${e.platform}`)}}(e);switch(t){case"open":{let e=n[0];if(!e)throw new p("INVALID_ARGS","open requires an app name or bundle/package id");return await a.open(e),{app:e}}case"close":{let e=n[0];if(!e)return{closed:"session"};return await a.close(e),{app:e}}case"press":{let[t,i]=n.map(Number);if(Number.isNaN(t)||Number.isNaN(i))throw new p("INVALID_ARGS","press requires x y");return"ios"===e.platform&&"simulator"===e.kind?await ee(e,{command:"tap",x:t,y:i,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath}):await a.tap(t,i),{x:t,y:i}}case"long-press":{let e=Number(n[0]),t=Number(n[1]),i=n[2]?Number(n[2]):void 0;if(Number.isNaN(e)||Number.isNaN(t))throw new p("INVALID_ARGS","long-press requires x y [durationMs]");return await a.longPress(e,t,i),{x:e,y:t,durationMs:i}}case"focus":{let[t,i]=n.map(Number);if(Number.isNaN(t)||Number.isNaN(i))throw new p("INVALID_ARGS","focus requires x y");return"ios"===e.platform&&"simulator"===e.kind?await ee(e,{command:"tap",x:t,y:i,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath}):await a.focus(t,i),{x:t,y:i}}case"type":{let t=n.join(" ");if(!t)throw new p("INVALID_ARGS","type requires text");return"ios"===e.platform&&"simulator"===e.kind?await ee(e,{command:"type",text:t,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath}):await a.type(t),{text:t}}case"fill":{let t=Number(n[0]),i=Number(n[1]),o=n.slice(2).join(" ");if(Number.isNaN(t)||Number.isNaN(i)||!o)throw new p("INVALID_ARGS","fill requires x y text");return"ios"===e.platform&&"simulator"===e.kind?(await ee(e,{command:"tap",x:t,y:i,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath}),await ee(e,{command:"type",text:o,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath})):await a.fill(t,i,o),{x:t,y:i,text:o}}case"scroll":{let t=n[0],i=n[1]?Number(n[1]):void 0;if(!t)throw new p("INVALID_ARGS","scroll requires direction");if("ios"===e.platform&&"simulator"===e.kind){if(!["up","down","left","right"].includes(t))throw new p("INVALID_ARGS",`Unknown direction: ${t}`);let n=function(e){switch(e){case"up":return"down";case"down":return"up";case"left":return"right";case"right":return"left"}}(t);await ee(e,{command:"swipe",direction:n,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath})}else await a.scroll(t,i);return{direction:t,amount:i}}case"scrollintoview":{let e=n.join(" ");if(!e)throw new p("INVALID_ARGS","scrollintoview requires text");return await a.scrollIntoView(e),{text:e}}case"screenshot":{let e=i??`./screenshot-${Date.now()}.png`;return await a.screenshot(e),{path:e}}case"snapshot":{let t=r?.snapshotBackend??"ax";if("ios"===e.platform){if("simulator"!==e.kind)throw new p("UNSUPPORTED_OPERATION","snapshot is only supported on iOS simulators in v1");if("ax"===t)try{let t=await ep(e);return{nodes:t.nodes??[],truncated:!1,backend:"ax",rootRect:t.rootRect}}catch(e){if(r?.snapshotBackend==="ax")throw e}let n=await ee(e,{command:"snapshot",appBundleId:r?.appBundleId,interactiveOnly:r?.snapshotInteractiveOnly,compact:r?.snapshotCompact,depth:r?.snapshotDepth,scope:r?.snapshotScope,raw:r?.snapshotRaw},{verbose:r?.verbose,logPath:r?.logPath});return{nodes:n.nodes??[],truncated:n.truncated??!1,backend:"xctest"}}let n=await R(e,{interactiveOnly:r?.snapshotInteractiveOnly,compact:r?.snapshotCompact,depth:r?.snapshotDepth,scope:r?.snapshotScope,raw:r?.snapshotRaw});return{nodes:n.nodes??[],truncated:n.truncated??!1,backend:"android"}}default:throw new p("INVALID_ARGS",`Unknown command: ${t}`)}}function eg(e){let t=e.trim();return t.startsWith("@")?t.slice(1)||null:t.startsWith("e")?t:null}function ey(e,t){return e.find(e=>e.ref===t)??null}function eN(e){return{x:Math.round(e.x+e.width/2),y:Math.round(e.y+e.height/2)}}let ev=new Map,eb=a.join(d.homedir(),".agent-device"),eI=a.join(eb,"daemon.json"),eA=a.join(eb,"daemon.log"),eS=a.join(eb,"sessions"),eO=function(){try{let e=function(){let e=a.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=a.join(t,"package.json");if(u.existsSync(e))return t;t=a.dirname(t)}return e}();return JSON.parse(u.readFileSync(a.join(e,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}(),eD=n.randomBytes(24).toString("hex");function ex(e,t){return{appBundleId:t,verbose:e?.verbose,logPath:eA,snapshotInteractiveOnly:e?.snapshotInteractiveOnly,snapshotCompact:e?.snapshotCompact,snapshotDepth:e?.snapshotDepth,snapshotScope:e?.snapshotScope,snapshotRaw:e?.snapshotRaw,snapshotBackend:e?.snapshotBackend}}async function e_(e){if(e.token!==eD)return{ok:!1,error:{code:"UNAUTHORIZED",message:"Invalid token"}};let t=e.command,n=e.session||"default";if("session_list"===t)return{ok:!0,data:{sessions:Array.from(ev.values()).map(e=>({name:e.name,platform:e.device.platform,device:e.device.name,id:e.device.id,createdAt:e.createdAt}))}};if("open"===t){let i,r=await em(e.flags??{}),a=e.positionals?.[0];if("ios"===r.platform)try{let{resolveIosApp:t}=await Promise.resolve().then(()=>({resolveIosApp:j}));i=await t(r,e.positionals?.[0]??"")}catch{i=void 0}await ew(r,"open",e.positionals??[],e.flags?.out,{...ex(e.flags,i)});let o={name:n,device:r,createdAt:Date.now(),appBundleId:i,appName:a,actions:[]};return eE(o,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:n}}),ev.set(n,o),{ok:!0,data:{session:n}}}if("replay"===t){let t=e.positionals?.[0];if(!t)return{ok:!1,error:{code:"INVALID_ARGS",message:"replay requires a path"}};try{var i;let e=(i=t).startsWith("~/")?a.join(d.homedir(),i.slice(2)):a.resolve(i),r=JSON.parse(u.readFileSync(e,"utf8")),o=r.optimizedActions??r.actions??[];for(let e of o)e&&"replay"!==e.command&&await e_({token:eD,session:n,command:e.command,positionals:e.positionals??[],flags:e.flags??{}});return{ok:!0,data:{replayed:o.length,session:n}}}catch(t){let e=l(t);return{ok:!1,error:{code:e.code,message:e.message}}}}if("close"===t){let i=ev.get(n);return i?(e.positionals&&e.positionals.length>0&&await ew(i.device,"close",e.positionals??[],e.flags?.out,{...ex(e.flags,i.appBundleId)}),"ios"===i.device.platform&&"simulator"===i.device.kind&&await et(i.device.id),eE(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:n}}),ek(i),ev.delete(n),{ok:!0,data:{session:n}}):{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session"}}}if("snapshot"===t){let i=ev.get(n),r=i?.device??await em(e.flags??{}),a=i?.appBundleId,o=await ew(r,"snapshot",[],e.flags?.out,{...ex(e.flags,a)}),s=(function(e){let t=[],n=[];for(let i of e){let e=i.depth??0;for(;t.length>0&&e<=t[t.length-1];)t.pop();let r=function(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase();return t.startsWith("ax")&&(t=t.replace(/^ax/,"")),t}(i.type??"");if("group"===r||"ioscontentgroup"===r){t.push(e);continue}let a=Math.max(0,e-t.length);n.push({...i,depth:a})}return n})(o?.nodes??[]).map((e,t)=>({...e,ref:`e${t+1}`})),l={nodes:s,truncated:o?.truncated,createdAt:Date.now(),backend:o?.backend},c={name:n,device:r,createdAt:i?.createdAt??Date.now(),appBundleId:a,snapshot:l,actions:i?.actions??[]};return eE(c,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{nodes:s.length,truncated:o?.truncated??!1}}),ev.set(n,c),{ok:!0,data:{nodes:s,truncated:o?.truncated??!1,appName:i?.appName??a??r.name,appBundleId:a}}}if("click"===t){let i=ev.get(n);if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let r=e.positionals?.[0]??"",a=eg(r);if(!a)return{ok:!1,error:{code:"INVALID_ARGS",message:"click requires a ref like @e2"}};let o=ey(i.snapshot.nodes,a);if(!o?.rect&&e.positionals.length>1){let t=e.positionals.slice(1).join(" ").trim();t.length>0&&(o=eR(i.snapshot.nodes,t))}if(!o?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${r} not found or has no bounds`}};let s=eP(o,i.snapshot.nodes),l=o.label?.trim();if("ios"===i.device.platform&&"simulator"===i.device.kind&&l&&function(e,t){let n=t.trim().toLowerCase();if(!n)return!1;let i=0;for(let t of e)if((t.label??"").trim().toLowerCase()===n&&(i+=1)>1)return!1;return 1===i}(i.snapshot.nodes,l))return await ee(i.device,{command:"tap",text:l,appBundleId:i.appBundleId},{verbose:e.flags?.verbose,logPath:eA}),eE(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:a,refLabel:l,mode:"text"}}),{ok:!0,data:{ref:a,mode:"text"}};let{x:c,y:u}=eN(o.rect);return await ew(i.device,"press",[String(c),String(u)],e.flags?.out,{...ex(e.flags,i.appBundleId)}),eE(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:a,x:c,y:u,refLabel:s}}),{ok:!0,data:{ref:a,x:c,y:u}}}if("fill"===t){let i=ev.get(n);if(e.positionals?.[0]?.startsWith("@")){if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let n=eg(e.positionals[0]);if(!n)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires a ref like @e2"}};let r=e.positionals.length>=3?e.positionals[1]:"",a=e.positionals.length>=3?e.positionals.slice(2).join(" "):e.positionals.slice(1).join(" ");if(!a)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires text after ref"}};let o=ey(i.snapshot.nodes,n);if(!o?.rect&&r&&(o=eR(i.snapshot.nodes,r)),!o?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${e.positionals[0]} not found or has no bounds`}};let s=eP(o,i.snapshot.nodes),{x:l,y:c}=eN(o.rect),u=await ew(i.device,"fill",[String(l),String(c),a],e.flags?.out,{...ex(e.flags,i.appBundleId)});return eE(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:u??{ref:n,x:l,y:c,refLabel:s}}),{ok:!0,data:u??{ref:n,x:l,y:c}}}}if("get"===t){let i=e.positionals?.[0],r=e.positionals?.[1];if("text"!==i&&"attrs"!==i)return{ok:!1,error:{code:"INVALID_ARGS",message:"get only supports text or attrs"}};let a=ev.get(n);if(!a?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let o=eg(r??"");if(!o)return{ok:!1,error:{code:"INVALID_ARGS",message:"get text requires a ref like @e2"}};let s=ey(a.snapshot.nodes,o);if(!s&&e.positionals.length>2){let t=e.positionals.slice(2).join(" ").trim();t.length>0&&(s=eR(a.snapshot.nodes,t))}if(!s)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${r} not found`}};if("attrs"===i)return eE(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:o}}),{ok:!0,data:{ref:o,node:s}};let l=[s.label,s.value,s.identifier].map(e=>"string"==typeof e?e.trim():"").filter(e=>e.length>0)[0]??"";return eE(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:o,text:l,refLabel:l||void 0}}),{ok:!0,data:{ref:o,text:l,node:s}}}if("rect"===t){let i=ev.get(n);if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let r=eg(e.positionals?.[0]??""),a="";if(r){let e=ey(i.snapshot.nodes,r);a=e?.label?.trim()??""}else a=e.positionals.join(" ").trim();if(!a)return{ok:!1,error:{code:"INVALID_ARGS",message:"rect requires a label or ref with label"}};if("ios"!==i.device.platform||"simulator"!==i.device.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"rect is only supported on iOS simulators"}};let o=await ee(i.device,{command:"rect",text:a,appBundleId:i.appBundleId},{verbose:e.flags?.verbose,logPath:eA});return eE(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{label:a,rect:o?.rect}}),{ok:!0,data:{label:a,rect:o?.rect}}}let r=ev.get(n);if(!r)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session. Run open first."}};let o=await ew(r.device,t,e.positionals??[],e.flags?.out,{...ex(e.flags,r.appBundleId)});return eE(r,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:o??{}}),{ok:!0,data:o??{}}}function eE(e,t){t.flags?.noRecord||e.actions.push({ts:Date.now(),command:t.command,positionals:t.positionals,flags:function(e){if(!e)return{};let{platform:t,device:n,udid:i,serial:r,out:a,verbose:o,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:u,snapshotRaw:d,snapshotBackend:f,noRecord:p,recordJson:h}=e;return{platform:t,device:n,udid:i,serial:r,out:a,verbose:o,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:u,snapshotRaw:d,snapshotBackend:f,noRecord:p,recordJson:h}}(t.flags),result:t.result})}function ek(e){try{u.existsSync(eS)||u.mkdirSync(eS,{recursive:!0});let t=e.name.replace(/[^a-zA-Z0-9._-]/g,"_"),n=new Date(e.createdAt).toISOString().replace(/[:.]/g,"-"),i=a.join(eS,`${t}-${n}.ad`),r=a.join(eS,`${t}-${n}.json`),o={name:e.name,device:e.device,createdAt:e.createdAt,appBundleId:e.appBundleId,actions:e.actions,optimizedActions:function(e){let t=[];for(let n of e.actions)if("snapshot"!==n.command){if("click"===n.command||"fill"===n.command||"get"===n.command){let i=n.result?.refLabel;"string"==typeof i&&i.trim().length>0&&t.push({ts:n.ts,command:"snapshot",positionals:[],flags:{platform:e.device.platform,snapshotInteractiveOnly:!0,snapshotCompact:!0,snapshotScope:i.trim()},result:{scope:i.trim()}})}t.push(n)}return t}(e)},s=function(e,t){let n=[],i=e.device.name.replace(/"/g,'\\"'),r=e.device.kind?` kind=${e.device.kind}`:"";for(let a of(n.push(`context platform=${e.device.platform} device="${i}"${r} theme=unknown`),t))a.flags?.noRecord||n.push(function(e){let t=[e.command];if("click"===e.command){let n=e.positionals?.[0];if(n){t.push(eC(n));let i=e.result?.refLabel;return"string"==typeof i&&i.trim().length>0&&t.push(eC(i)),t.join(" ")}}if("fill"===e.command){let n=e.positionals?.[0];if(n&&n.startsWith("@")){t.push(eC(n));let i=e.result?.refLabel,r=e.positionals.slice(1).join(" ");return"string"==typeof i&&i.trim().length>0&&t.push(eC(i)),r&&t.push(eC(r)),t.join(" ")}}if("get"===e.command){let n=e.positionals?.[0],i=e.positionals?.[1];if(n&&i){t.push(eC(n)),t.push(eC(i));let r=e.result?.refLabel;return"string"==typeof r&&r.trim().length>0&&t.push(eC(r)),t.join(" ")}}if("snapshot"===e.command)return e.flags?.snapshotInteractiveOnly&&t.push("-i"),e.flags?.snapshotCompact&&t.push("-c"),"number"==typeof e.flags?.snapshotDepth&&t.push("-d",String(e.flags.snapshotDepth)),e.flags?.snapshotScope&&t.push("-s",eC(e.flags.snapshotScope)),e.flags?.snapshotRaw&&t.push("--raw"),e.flags?.snapshotBackend&&t.push("--backend",e.flags.snapshotBackend),t.join(" ");for(let n of e.positionals??[])t.push(eC(n));return t.join(" ")}(a));return`${n.join("\n")}
1
+ let e,t;import n from"node:crypto";import{isCancel as i,select as r}from"@clack/prompts";import{node_path as a,runCmdStreaming as o,promises as s,asAppError as l,fileURLToPath as c,node_fs as u,node_os as d,node_net as f,errors_AppError as p,runCmd as h,whichCmd as m}from"./861.js";async function w(e,t){let n=e,a=e=>e.toLowerCase().replace(/_/g," ").replace(/\s+/g," ").trim();if(t.platform&&(n=n.filter(e=>e.platform===t.platform)),t.udid){let e=n.find(e=>e.id===t.udid&&"ios"===e.platform);if(!e)throw new p("DEVICE_NOT_FOUND",`No iOS device with UDID ${t.udid}`);return e}if(t.serial){let e=n.find(e=>e.id===t.serial&&"android"===e.platform);if(!e)throw new p("DEVICE_NOT_FOUND",`No Android device with serial ${t.serial}`);return e}if(t.deviceName){let e=a(t.deviceName),i=n.find(t=>a(t.name)===e);if(!i)throw new p("DEVICE_NOT_FOUND",`No device named ${t.deviceName}`);return i}if(1===n.length)return n[0];if(0===n.length)throw new p("DEVICE_NOT_FOUND","No devices found",{selector:t});let o=n.filter(e=>e.booted);if(1===o.length)return o[0];if(!process.env.CI&&process.stdin.isTTY&&process.stdout.isTTY){let e=await r({message:"Multiple devices available. Choose a device to continue:",options:(o.length>0?o:n).map(e=>({label:`${e.name} (${e.platform}${e.kind?`, ${e.kind}`:""}${e.booted?", booted":""})`,value:e.id}))});if(i(e))throw new p("INVALID_ARGS","Device selection cancelled");if(e){let t=n.find(t=>t.id===e);if(t)return t}}return o[0]??n[0]}async function g(){if(!await m("adb"))throw new p("TOOL_MISSING","adb not found in PATH");let e=(await h("adb",["devices","-l"])).stdout.split("\n").map(e=>e.trim()),t=[];for(let n of e){if(!n||n.startsWith("List of devices"))continue;let e=n.split(/\s+/),i=e[0];if("device"!==e[1])continue;let r=(e.find(e=>e.startsWith("model:"))??"").replace("model:","").replace(/_/g," ").trim()||i;if(i.startsWith("emulator-")){let e=await h("adb",["-s",i,"emu","avd","name"],{allowFailure:!0}),t=e.stdout.trim();0===e.exitCode&&t&&(r=t.replace(/_/g," "))}let a=await y(i);t.push({platform:"android",id:i,name:r,kind:i.startsWith("emulator-")?"emulator":"device",booted:a})}return t}async function y(e){try{let t=await h("adb",["-s",e,"shell","getprop","sys.boot_completed"],{allowFailure:!0});return"1"===t.stdout.trim()}catch{return!1}}let N={settings:{type:"intent",value:"android.settings.SETTINGS"}};function b(e,t){return["-s",e.id,...t]}async function v(e,t){let n=t.trim();if(n.includes("."))return{type:"package",value:n};let i=N[n.toLowerCase()];if(i)return i;let r=(await h("adb",b(e,["shell","pm","list","packages"]))).stdout.split("\n").map(e=>e.replace("package:","").trim()).filter(Boolean).filter(e=>e.toLowerCase().includes(n.toLowerCase()));if(1===r.length)return{type:"package",value:r[0]};if(r.length>1)throw new p("INVALID_ARGS",`Multiple packages matched "${t}"`,{matches:r});throw new p("APP_NOT_INSTALLED",`No package found matching "${t}"`)}async function I(e,t){let n=await v(e,t);"intent"===n.type?await h("adb",b(e,["shell","am","start","-a",n.value])):await h("adb",b(e,["shell","monkey","-p",n.value,"-c","android.intent.category.LAUNCHER","1"]))}async function S(e,t){if("settings"===t.trim().toLowerCase())return void await h("adb",b(e,["shell","am","force-stop","com.android.settings"]));let n=await v(e,t);if("intent"===n.type)throw new p("INVALID_ARGS","Close requires a package name, not an intent");await h("adb",b(e,["shell","am","force-stop",n.value]))}async function A(e,t,n){await h("adb",b(e,["shell","input","tap",String(t),String(n)]))}async function O(e,t,n,i=800){await h("adb",b(e,["shell","input","swipe",String(t),String(n),String(t),String(n),String(i)]))}async function D(e,t){let n=t.replace(/ /g,"%s");await h("adb",b(e,["shell","input","text",n]))}async function x(e,t,n){await A(e,t,n)}async function _(e,t,n,i){await x(e,t,n),await D(e,i)}async function E(e,t,n=.6){let{width:i,height:r}=await T(e),a=Math.floor(i*n),o=Math.floor(r*n),s=Math.floor(i/2),l=Math.floor(r/2),c=s,u=l,d=s,f=l;switch(t){case"up":u=l-Math.floor(o/2),f=l+Math.floor(o/2);break;case"down":u=l+Math.floor(o/2),f=l-Math.floor(o/2);break;case"left":c=s-Math.floor(a/2),d=s+Math.floor(a/2);break;case"right":c=s+Math.floor(a/2),d=s-Math.floor(a/2);break;default:throw new p("INVALID_ARGS",`Unknown direction: ${t}`)}await h("adb",b(e,["shell","input","swipe",String(c),String(u),String(d),String(f),"300"]))}async function k(e,t){for(let n=0;n<8;n+=1){let n="";try{n=await L(e)}catch(t){let e=t instanceof Error?t.message:String(t);throw new p("UNSUPPORTED_OPERATION",`uiautomator dump failed: ${e}`)}if(function(e,t){let n=t.toLowerCase(),i=/<node[^>]+>/g,r=i.exec(e);for(;r;){let t=r[0],a=/text="([^"]*)"/.exec(t),o=/content-desc="([^"]*)"/.exec(t),s=(a?.[1]??"").toLowerCase(),l=(o?.[1]??"").toLowerCase();if(s.includes(n)||l.includes(n)){let e=/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/.exec(t);if(e){let t=Number(e[1]),n=Number(e[2]);return{x:Math.floor((t+Number(e[3]))/2),y:Math.floor((n+Number(e[4]))/2)}}return{x:0,y:0}}r=i.exec(e)}return null}(n,t))return;await E(e,"down",.5)}throw new p("COMMAND_FAILED",`Could not find element containing "${t}" after scrolling`)}async function C(e,t){let n=await h("adb",b(e,["exec-out","screencap","-p"]),{binaryStdout:!0});if(!n.stdoutBuffer)throw new p("COMMAND_FAILED","Failed to capture screenshot");await s.writeFile(t,n.stdoutBuffer)}async function R(e,t={}){return function(e,t,n){let i=function(e){let t={type:null,label:null,value:null,identifier:null,depth:-1,children:[]},n=[t],i=/<node\b[^>]*>|<\/node>/g,r=i.exec(e);for(;r;){let t=r[0];if(t.startsWith("</node")){n.length>1&&n.pop(),r=i.exec(e);continue}let a=function(e){let t=t=>{let n=RegExp(`${t}="([^"]*)"`).exec(e);return n?n[1]:null},n=e=>{let n=t(e);if(null!==n)return"true"===n};return{text:t("text"),desc:t("content-desc"),resourceId:t("resource-id"),className:t("class"),bounds:t("bounds"),clickable:n("clickable"),enabled:n("enabled"),focusable:n("focusable")}}(t),o=function(e){if(!e)return;let t=/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/.exec(e);if(!t)return;let n=Number(t[1]),i=Number(t[2]);return{x:n,y:i,width:Math.max(0,Number(t[3])-n),height:Math.max(0,Number(t[4])-i)}}(a.bounds),s=n[n.length-1],l={type:a.className,label:a.text||a.desc,value:a.text,identifier:a.resourceId,rect:o,enabled:a.enabled,hittable:a.clickable??a.focusable,depth:s.depth+1,parentIndex:void 0,children:[]};s.children.push(l),t.endsWith("/>")||n.push(l),r=i.exec(e)}return t}(e),r=[],a=!1,o=n.depth??1/0,s=n.scope?function(e,t){let n=t.toLowerCase(),i=[...e.children];for(;i.length>0;){let e=i.shift(),t=e.label?.toLowerCase()??"",r=e.value?.toLowerCase()??"",a=e.identifier?.toLowerCase()??"";if(t.includes(n)||r.includes(n)||a.includes(n))return e;i.push(...e.children)}return null}(i,n.scope):null,l=s?[s]:i.children,c=(e,t)=>{if(r.length>=800){a=!0;return}if(!(t>o)){for(let i of((n.raw||function(e,t){if(t.interactiveOnly)return!!e.hittable;if(t.compact){let t=!!(e.label&&e.label.trim().length>0),n=!!(e.identifier&&e.identifier.trim().length>0);return t||n||!!e.hittable}return!0}(e,n))&&r.push({index:r.length,type:e.type??void 0,label:e.label??void 0,value:e.value??void 0,identifier:e.identifier??void 0,rect:e.rect,enabled:e.enabled,hittable:e.hittable,depth:t,parentIndex:e.parentIndex}),e.children))if(c(i,t+1),a)return}};for(let e of l)if(c(e,0),a)break;return a?{nodes:r,truncated:a}:{nodes:r}}(await L(e),800,t)}async function P(){if(!await m("adb"))throw new p("TOOL_MISSING","adb not found in PATH")}async function T(e){let t=(await h("adb",b(e,["shell","wm","size"]))).stdout.match(/Physical size:\s*(\d+)x(\d+)/);if(!t)throw new p("COMMAND_FAILED","Unable to read screen size");return{width:Number(t[1]),height:Number(t[2])}}async function L(e){return await h("adb",b(e,["shell","uiautomator","dump","/sdcard/window_dump.xml"])),(await h("adb",b(e,["shell","cat","/sdcard/window_dump.xml"]))).stdout}async function M(){if("darwin"!==process.platform)throw new p("UNSUPPORTED_PLATFORM","iOS tools are only available on macOS");if(!await m("xcrun"))throw new p("TOOL_MISSING","xcrun not found in PATH");let e=[],t=await h("xcrun",["simctl","list","devices","-j"]);try{let n=JSON.parse(t.stdout);for(let t of Object.values(n.devices))for(let n of t)n.isAvailable&&e.push({platform:"ios",id:n.udid,name:n.name,kind:"simulator",booted:"Booted"===n.state})}catch(e){throw new p("COMMAND_FAILED","Failed to parse simctl devices JSON",void 0,e)}if(await m("xcrun"))try{let t=await h("xcrun",["devicectl","list","devices","--json"]);for(let n of JSON.parse(t.stdout).devices??[])n.platform?.toLowerCase().includes("ios")&&e.push({platform:"ios",id:n.identifier,name:n.name,kind:"device",booted:!0})}catch{}return e}let F={settings:"com.apple.Preferences"};async function j(e,t){let n=t.trim();if(n.includes("."))return n;let i=F[n.toLowerCase()];if(i)return i;if("simulator"===e.kind){let i=(await Y(e)).filter(e=>e.name.toLowerCase()===n.toLowerCase());if(1===i.length)return i[0].bundleId;if(i.length>1)throw new p("INVALID_ARGS",`Multiple apps matched "${t}"`,{matches:i})}throw new p("APP_NOT_INSTALLED",`No app found matching "${t}"`)}async function U(e,t){let n=await j(e,t);if("simulator"===e.kind){await Z(e),await h("xcrun",["simctl","launch",e.id,n]);return}await h("xcrun",["devicectl","device","process","launch","--device",e.id,n])}async function V(e,t){let n=await j(e,t);if("simulator"===e.kind){await Z(e);let t=await h("xcrun",["simctl","terminate",e.id,n],{allowFailure:!0});if(0!==t.exitCode){if(t.stderr.toLowerCase().includes("found nothing to terminate"))return;throw new p("COMMAND_FAILED",`xcrun exited with code ${t.exitCode}`,{cmd:"xcrun",args:["simctl","terminate",e.id,n],stdout:t.stdout,stderr:t.stderr,exitCode:t.exitCode})}return}await h("xcrun",["devicectl","device","process","terminate","--device",e.id,n])}async function $(e,t,n){throw H(e,"press"),await Z(e),new p("UNSUPPORTED_OPERATION","simctl io tap is not available; use the XCTest runner for input")}async function G(e,t,n,i=800){throw H(e,"long-press"),await Z(e),new p("UNSUPPORTED_OPERATION","long-press is not supported on iOS simulators without XCTest runner support")}async function B(e,t,n){await $(e,t,n)}async function J(e,t){throw H(e,"type"),await Z(e),new p("UNSUPPORTED_OPERATION","simctl io keyboard is not available; use the XCTest runner for input")}async function q(e,t,n,i){await B(e,t,n),await J(e,i)}async function W(e,t,n=.6){throw H(e,"scroll"),await Z(e),new p("UNSUPPORTED_OPERATION","simctl io swipe is not available; use the XCTest runner for input")}async function X(e){throw new p("UNSUPPORTED_OPERATION",`scrollintoview is not supported on iOS without UI automation (${e})`)}async function z(e,t){if("simulator"===e.kind){await Z(e),await h("xcrun",["simctl","io",e.id,"screenshot",t]);return}await h("xcrun",["devicectl","device","screenshot","--device",e.id,t])}function H(e,t){if("simulator"!==e.kind)throw new p("UNSUPPORTED_OPERATION",`${t} is only supported on iOS simulators in v1`)}async function Y(e){let t=(await h("xcrun",["simctl","listapps",e.id],{allowFailure:!0})).stdout;if(!t.trim().startsWith("{"))return[];try{let e=JSON.parse(t);return Object.entries(e).map(([e,t])=>({bundleId:e,name:t.CFBundleDisplayName??t.CFBundleName??e}))}catch{return[]}}async function Z(e){"simulator"!==e.kind||"Booted"!==await K(e.id)&&(await h("xcrun",["simctl","boot",e.id],{allowFailure:!0}),await h("xcrun",["simctl","bootstatus",e.id,"-b"],{allowFailure:!0}))}async function K(e){let t=await h("xcrun",["simctl","list","devices","-j"],{allowFailure:!0});if(0!==t.exitCode)return null;try{let n=JSON.parse(t.stdout);for(let t of Object.values(n.devices??{})){let n=t.find(t=>t.udid===e);if(n)return n.state}}catch{}return null}let Q=new Map;async function ee(e,t,n={}){if("simulator"!==e.kind)throw new p("UNSUPPORTED_OPERATION","iOS runner only supports simulators in v1");try{let i=await ei(e,n),r=await es(e,i.port,t,n.logPath),a=await r.text(),o={};try{o=JSON.parse(a)}catch{throw new p("COMMAND_FAILED","Invalid runner response",{text:a})}if(!o.ok)throw new p("COMMAND_FAILED",o.error?.message??"Runner error",{runner:o,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:n.logPath});return o.data??{}}catch(r){let i=r instanceof p?r:new p("COMMAND_FAILED",String(r));if("COMMAND_FAILED"===i.code&&"string"==typeof i.message&&i.message.includes("Runner did not accept connection")){await et(e.id);let i=await ei(e,n),r=await es(e,i.port,t,n.logPath),a=await r.text(),o={};try{o=JSON.parse(a)}catch{throw new p("COMMAND_FAILED","Invalid runner response",{text:a})}if(!o.ok)throw new p("COMMAND_FAILED",o.error?.message??"Runner error",{runner:o,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:n.logPath});return o.data??{}}throw r}}async function et(e){let t=Q.get(e);if(t){try{await es(t.device,t.port,{command:"shutdown"})}catch{}try{await t.testPromise}catch{}ef(t.xctestrunPath),ef(t.jsonPath),Q.delete(e)}}async function en(e){await h("xcrun",["simctl","bootstatus",e,"-b"],{allowFailure:!0})}async function ei(e,t){let n=Q.get(e.id);if(n)return n;await en(e.id);let i=await er(e.id,t),r=await ec(),a=process.env.AGENT_DEVICE_RUNNER_TIMEOUT??"300",{xctestrunPath:s,jsonPath:l}=await ed(i,{AGENT_DEVICE_RUNNER_PORT:String(r),AGENT_DEVICE_RUNNER_TIMEOUT:a},`session-${e.id}-${r}`),c=o("xcodebuild",["test-without-building","-only-testing","AgentDeviceRunnerUITests/RunnerTests/testCommand","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-xctestrun",s,"-destination",`platform=iOS Simulator,id=${e.id}`],{onStdoutChunk:e=>{eo(e,t.logPath,t.verbose)},onStderrChunk:e=>{eo(e,t.logPath,t.verbose)},allowFailure:!0,env:{...process.env,AGENT_DEVICE_RUNNER_PORT:String(r),AGENT_DEVICE_RUNNER_TIMEOUT:a}}),u={device:e,deviceId:e.id,port:r,xctestrunPath:s,jsonPath:l,testPromise:c};return Q.set(e.id,u),u}async function er(e,t){let n,i=a.join(d.homedir(),".agent-device","ios-runner"),r=a.join(i,"derived");if((n=process.env.AGENT_DEVICE_IOS_CLEAN_DERIVED)&&["1","true","yes","on"].includes(n.toLowerCase()))try{u.rmSync(r,{recursive:!0,force:!0})}catch{}let s=ea(r);if(s)return s;let l=function(){let e=a.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=a.join(t,"package.json");if(u.existsSync(e))return t;t=a.dirname(t)}return e}(),f=a.join(l,"ios-runner","AgentDeviceRunner","AgentDeviceRunner.xcodeproj");if(!u.existsSync(f))throw new p("COMMAND_FAILED","iOS runner project not found",{projectPath:f});try{await o("xcodebuild",["build-for-testing","-project",f,"-scheme","AgentDeviceRunner","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-destination",`platform=iOS Simulator,id=${e}`,"-derivedDataPath",r],{onStdoutChunk:e=>{eo(e,t.logPath,t.verbose)},onStderrChunk:e=>{eo(e,t.logPath,t.verbose)}})}catch(n){let e=n instanceof p?n:new p("COMMAND_FAILED",String(n));throw new p("COMMAND_FAILED","xcodebuild build-for-testing failed",{error:e.message,details:e.details,logPath:t.logPath})}let h=ea(r);if(!h)throw new p("COMMAND_FAILED","Failed to locate .xctestrun after build");return h}function ea(e){if(!u.existsSync(e))return null;let t=[],n=[e];for(;n.length>0;){let e=n.pop();for(let i of u.readdirSync(e,{withFileTypes:!0})){let r=a.join(e,i.name);if(i.isDirectory()){n.push(r);continue}if(i.isFile()&&i.name.endsWith(".xctestrun"))try{let e=u.statSync(r);t.push({path:r,mtimeMs:e.mtimeMs})}catch{}}}return 0===t.length?null:(t.sort((e,t)=>t.mtimeMs-e.mtimeMs),t[0]?.path??null)}function eo(e,t,n){t&&u.appendFileSync(t,e),n&&process.stderr.write(e)}async function es(e,t,n,i){i&&await eu(i,4e3);let r=Date.now(),a=null;for(;Date.now()-r<8e3;)try{return await fetch(`http://127.0.0.1:${t}/command`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})}catch(e){a=e,await new Promise(e=>setTimeout(e,100))}if("simulator"===e.kind){let i=await el(e.id,t,n);return new Response(i.body,{status:i.status})}let o=i?function(e){try{if(!u.existsSync(e))return null;let t=u.readFileSync(e,"utf8").match(/AGENT_DEVICE_RUNNER_PORT=(\d+)/);if(t)return Number(t[1])}catch{}return null}(i):null;if(o&&o!==t)try{return await fetch(`http://127.0.0.1:${o}/command`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})}catch(e){a=e}throw new p("COMMAND_FAILED","Runner did not accept connection",{port:t,fallbackPort:o,logPath:i,lastError:a?String(a):void 0})}async function el(e,t,n){let i=JSON.stringify(n),r=await h("xcrun",["simctl","spawn",e,"/usr/bin/curl","-s","-X","POST","-H","Content-Type: application/json","--data",i,`http://127.0.0.1:${t}/command`],{allowFailure:!0}),a=r.stdout;if(0!==r.exitCode)throw new p("COMMAND_FAILED","Runner did not accept connection (simctl spawn)",{port:t,stdout:r.stdout,stderr:r.stderr,exitCode:r.exitCode});return{status:200,body:a}}async function ec(){return await new Promise((e,t)=>{let n=f.createServer();n.listen(0,"127.0.0.1",()=>{let i=n.address();n.close(),"object"==typeof i&&i?.port?e(i.port):t(new p("COMMAND_FAILED","Failed to allocate port"))}),n.on("error",t)})}async function eu(e,t){if(!u.existsSync(e))return;let n=Date.now(),i=0;for(;Date.now()-n<t;){if(!u.existsSync(e))return;let t=u.statSync(e);if(t.size>i){let n=u.openSync(e,"r"),r=Buffer.alloc(t.size-i);u.readSync(n,r,0,r.length,i),u.closeSync(n),i=t.size;let a=r.toString("utf8");if(a.includes("AGENT_DEVICE_RUNNER_LISTENER_READY")||a.includes("AGENT_DEVICE_RUNNER_PORT="))return}await new Promise(e=>setTimeout(e,100))}}async function ed(e,t,n){let i,r=a.dirname(e),o=n.replace(/[^a-zA-Z0-9._-]/g,"_"),s=a.join(r,`AgentDeviceRunner.env.${o}.json`),l=a.join(r,`AgentDeviceRunner.env.${o}.xctestrun`),c=await h("plutil",["-convert","json","-o","-",e],{allowFailure:!0});if(0!==c.exitCode||!c.stdout.trim())throw new p("COMMAND_FAILED","Failed to read xctestrun plist",{xctestrunPath:e,stderr:c.stderr});try{i=JSON.parse(c.stdout)}catch(t){throw new p("COMMAND_FAILED","Failed to parse xctestrun JSON",{xctestrunPath:e,error:String(t)})}let d=e=>{e.EnvironmentVariables={...e.EnvironmentVariables??{},...t},e.UITestEnvironmentVariables={...e.UITestEnvironmentVariables??{},...t},e.UITargetAppEnvironmentVariables={...e.UITargetAppEnvironmentVariables??{},...t},e.TestingEnvironmentVariables={...e.TestingEnvironmentVariables??{},...t}},f=i.TestConfigurations;if(Array.isArray(f))for(let e of f){if(!e||"object"!=typeof e)continue;let t=e.TestTargets;if(Array.isArray(t))for(let e of t)e&&"object"==typeof e&&d(e)}for(let[e,t]of Object.entries(i))t&&"object"==typeof t&&t.TestBundlePath&&(d(t),i[e]=t);u.writeFileSync(s,JSON.stringify(i,null,2));let m=await h("plutil",["-convert","xml1","-o",l,s],{allowFailure:!0});if(0!==m.exitCode)throw new p("COMMAND_FAILED","Failed to write xctestrun plist",{tmpXctestrunPath:l,stderr:m.stderr});return{xctestrunPath:l,jsonPath:s}}function ef(e){try{u.existsSync(e)&&u.unlinkSync(e)}catch{}}async function ep(e){let t,n;if("ios"!==e.platform||"simulator"!==e.kind)throw new p("UNSUPPORTED_OPERATION","AX snapshot is only supported on iOS simulators");let i=await eh(),r=await h(i,[],{allowFailure:!0});if(0!==r.exitCode){let e=(r.stderr??"").toString(),t="";throw e.toLowerCase().includes("accessibility permission")&&(t=" Enable Accessibility for your terminal in System Settings > Privacy & Security > Accessibility, or use --backend xctest (slower snapshots via XCTest)."),new p("COMMAND_FAILED","AX snapshot failed",{stderr:`${e}${t}`,stdout:r.stdout})}try{let e=JSON.parse(r.stdout);if(e&&"object"==typeof e&&"root"in e){if(!e.root)throw Error("AX snapshot missing root");t=e.root,n=e.windowFrame??void 0}else t=e}catch(e){throw new p("COMMAND_FAILED","Invalid AX snapshot JSON",{error:String(e)})}let a=t.frame??n,o=[],s=[],l=(e,t)=>{e.frame&&o.push(e.frame);let n=e.frame&&a?{x:e.frame.x-a.x,y:e.frame.y-a.y,width:e.frame.width,height:e.frame.height}:e.frame;for(let i of(s.push({...e,frame:n,children:void 0,depth:t}),e.children??[]))l(i,t+1)};return l(t,0),{nodes:(function(e,t,n){if(!t||0===n.length)return e;let i=1/0,r=1/0;for(let e of n)e.x<i&&(i=e.x),e.y<r&&(r=e.y);return i<=5&&r<=5?e.map(e=>({...e,frame:e.frame?{x:e.frame.x+t.x,y:e.frame.y+t.y,width:e.frame.width,height:e.frame.height}:void 0})):e})(s,a,o).map((e,t)=>({index:t,type:e.subrole??e.role,label:e.label,value:e.value,identifier:e.identifier,rect:e.frame?{x:e.frame.x,y:e.frame.y,width:e.frame.width,height:e.frame.height}:void 0,depth:e.depth})),rootRect:a}}async function eh(){let e=function(){let e=process.cwd();for(let t=0;t<6;t+=1){let t=a.join(e,"package.json");if(u.existsSync(t))return e;e=a.dirname(e)}return process.cwd()}(),t=a.join(e,"ios-runner","AXSnapshot"),n=process.env.AGENT_DEVICE_AX_BINARY;if(n&&u.existsSync(n))return n;for(let t of[a.join(e,"bin","axsnapshot"),a.join(e,"dist","bin","axsnapshot"),a.join(e,"dist","axsnapshot")])if(u.existsSync(t))return t;let i=a.join(t,".build","release","axsnapshot");if(u.existsSync(i))return i;let r=await h("swift",["build","-c","release"],{cwd:t,allowFailure:!0});if(0!==r.exitCode||!u.existsSync(i))throw new p("COMMAND_FAILED","Failed to build AX snapshot tool",{stderr:r.stderr,stdout:r.stdout});return i}async function em(e){let t={platform:e.platform,deviceName:e.device,udid:e.udid,serial:e.serial};if("android"===t.platform){await P();let e=await g();return await w(e,t)}if("ios"===t.platform){let e=await M();return await w(e,t)}let n=[];try{n.push(...await g())}catch{}try{n.push(...await M())}catch{}return await w(n,t)}async function ew(e,t,n,i,r){let a=function(e){switch(e.platform){case"android":return{open:t=>I(e,t),close:t=>S(e,t),tap:(t,n)=>A(e,t,n),longPress:(t,n,i)=>O(e,t,n,i),focus:(t,n)=>x(e,t,n),type:t=>D(e,t),fill:(t,n,i)=>_(e,t,n,i),scroll:(t,n)=>E(e,t,n),scrollIntoView:t=>k(e,t),screenshot:t=>C(e,t)};case"ios":return{open:t=>U(e,t),close:t=>V(e,t),tap:(t,n)=>$(e,t,n),longPress:(t,n,i)=>G(e,t,n,i),focus:(t,n)=>B(e,t,n),type:t=>J(e,t),fill:(t,n,i)=>q(e,t,n,i),scroll:(t,n)=>W(e,t,n),scrollIntoView:e=>X(e),screenshot:t=>z(e,t)};default:throw new p("UNSUPPORTED_PLATFORM",`Unsupported platform: ${e.platform}`)}}(e);switch(t){case"open":{let e=n[0];if(!e)throw new p("INVALID_ARGS","open requires an app name or bundle/package id");return await a.open(e),{app:e}}case"close":{let e=n[0];if(!e)return{closed:"session"};return await a.close(e),{app:e}}case"press":{let[t,i]=n.map(Number);if(Number.isNaN(t)||Number.isNaN(i))throw new p("INVALID_ARGS","press requires x y");return"ios"===e.platform&&"simulator"===e.kind?await ee(e,{command:"tap",x:t,y:i,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath}):await a.tap(t,i),{x:t,y:i}}case"long-press":{let e=Number(n[0]),t=Number(n[1]),i=n[2]?Number(n[2]):void 0;if(Number.isNaN(e)||Number.isNaN(t))throw new p("INVALID_ARGS","long-press requires x y [durationMs]");return await a.longPress(e,t,i),{x:e,y:t,durationMs:i}}case"focus":{let[t,i]=n.map(Number);if(Number.isNaN(t)||Number.isNaN(i))throw new p("INVALID_ARGS","focus requires x y");return"ios"===e.platform&&"simulator"===e.kind?await ee(e,{command:"tap",x:t,y:i,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath}):await a.focus(t,i),{x:t,y:i}}case"type":{let t=n.join(" ");if(!t)throw new p("INVALID_ARGS","type requires text");return"ios"===e.platform&&"simulator"===e.kind?await ee(e,{command:"type",text:t,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath}):await a.type(t),{text:t}}case"fill":{let t=Number(n[0]),i=Number(n[1]),o=n.slice(2).join(" ");if(Number.isNaN(t)||Number.isNaN(i)||!o)throw new p("INVALID_ARGS","fill requires x y text");return"ios"===e.platform&&"simulator"===e.kind?(await ee(e,{command:"tap",x:t,y:i,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath}),await ee(e,{command:"type",text:o,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath})):await a.fill(t,i,o),{x:t,y:i,text:o}}case"scroll":{let t=n[0],i=n[1]?Number(n[1]):void 0;if(!t)throw new p("INVALID_ARGS","scroll requires direction");if("ios"===e.platform&&"simulator"===e.kind){if(!["up","down","left","right"].includes(t))throw new p("INVALID_ARGS",`Unknown direction: ${t}`);let n=function(e){switch(e){case"up":return"down";case"down":return"up";case"left":return"right";case"right":return"left"}}(t);await ee(e,{command:"swipe",direction:n,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath})}else await a.scroll(t,i);return{direction:t,amount:i}}case"scrollintoview":{let e=n.join(" ");if(!e)throw new p("INVALID_ARGS","scrollintoview requires text");return await a.scrollIntoView(e),{text:e}}case"screenshot":{let e=i??`./screenshot-${Date.now()}.png`;return await a.screenshot(e),{path:e}}case"snapshot":{let t=r?.snapshotBackend??"ax";if("ios"===e.platform){if("simulator"!==e.kind)throw new p("UNSUPPORTED_OPERATION","snapshot is only supported on iOS simulators in v1");if("ax"===t){let t=await ep(e);return{nodes:t.nodes??[],truncated:!1,backend:"ax",rootRect:t.rootRect}}let n=await ee(e,{command:"snapshot",appBundleId:r?.appBundleId,interactiveOnly:r?.snapshotInteractiveOnly,compact:r?.snapshotCompact,depth:r?.snapshotDepth,scope:r?.snapshotScope,raw:r?.snapshotRaw},{verbose:r?.verbose,logPath:r?.logPath});return{nodes:n.nodes??[],truncated:n.truncated??!1,backend:"xctest"}}let n=await R(e,{interactiveOnly:r?.snapshotInteractiveOnly,compact:r?.snapshotCompact,depth:r?.snapshotDepth,scope:r?.snapshotScope,raw:r?.snapshotRaw});return{nodes:n.nodes??[],truncated:n.truncated??!1,backend:"android"}}default:throw new p("INVALID_ARGS",`Unknown command: ${t}`)}}function eg(e){let t=e.trim();return t.startsWith("@")?t.slice(1)||null:t.startsWith("e")?t:null}function ey(e,t){return e.find(e=>e.ref===t)??null}function eN(e){return{x:Math.round(e.x+e.width/2),y:Math.round(e.y+e.height/2)}}let eb=new Map,ev=a.join(d.homedir(),".agent-device"),eI=a.join(ev,"daemon.json"),eS=a.join(ev,"daemon.log"),eA=a.join(ev,"sessions"),eO=function(){try{let e=function(){let e=a.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=a.join(t,"package.json");if(u.existsSync(e))return t;t=a.dirname(t)}return e}();return JSON.parse(u.readFileSync(a.join(e,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}(),eD=n.randomBytes(24).toString("hex");function ex(e,t){return{appBundleId:t,verbose:e?.verbose,logPath:eS,snapshotInteractiveOnly:e?.snapshotInteractiveOnly,snapshotCompact:e?.snapshotCompact,snapshotDepth:e?.snapshotDepth,snapshotScope:e?.snapshotScope,snapshotRaw:e?.snapshotRaw,snapshotBackend:e?.snapshotBackend}}async function e_(e){if(e.token!==eD)return{ok:!1,error:{code:"UNAUTHORIZED",message:"Invalid token"}};let t=e.command,n=e.session||"default";if("session_list"===t)return{ok:!0,data:{sessions:Array.from(eb.values()).map(e=>({name:e.name,platform:e.device.platform,device:e.device.name,id:e.device.id,createdAt:e.createdAt}))}};if("open"===t){let i,r=await em(e.flags??{}),a=e.positionals?.[0];if("ios"===r.platform)try{let{resolveIosApp:t}=await Promise.resolve().then(()=>({resolveIosApp:j}));i=await t(r,e.positionals?.[0]??"")}catch{i=void 0}await ew(r,"open",e.positionals??[],e.flags?.out,{...ex(e.flags,i)});let o={name:n,device:r,createdAt:Date.now(),appBundleId:i,appName:a,actions:[]};return eE(o,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:n}}),eb.set(n,o),{ok:!0,data:{session:n}}}if("replay"===t){let t=e.positionals?.[0];if(!t)return{ok:!1,error:{code:"INVALID_ARGS",message:"replay requires a path"}};try{var i;let e=(i=t).startsWith("~/")?a.join(d.homedir(),i.slice(2)):a.resolve(i),r=JSON.parse(u.readFileSync(e,"utf8")),o=r.optimizedActions??r.actions??[];for(let e of o)e&&"replay"!==e.command&&await e_({token:eD,session:n,command:e.command,positionals:e.positionals??[],flags:e.flags??{}});return{ok:!0,data:{replayed:o.length,session:n}}}catch(t){let e=l(t);return{ok:!1,error:{code:e.code,message:e.message}}}}if("close"===t){let i=eb.get(n);return i?(e.positionals&&e.positionals.length>0&&await ew(i.device,"close",e.positionals??[],e.flags?.out,{...ex(e.flags,i.appBundleId)}),"ios"===i.device.platform&&"simulator"===i.device.kind&&await et(i.device.id),eE(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:n}}),ek(i),eb.delete(n),{ok:!0,data:{session:n}}):{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session"}}}if("snapshot"===t){let i=eb.get(n),r=i?.device??await em(e.flags??{}),a=i?.appBundleId,o=await ew(r,"snapshot",[],e.flags?.out,{...ex(e.flags,a)}),s=(function(e){let t=[],n=[];for(let i of e){let e=i.depth??0;for(;t.length>0&&e<=t[t.length-1];)t.pop();let r=function(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase();return t.startsWith("ax")&&(t=t.replace(/^ax/,"")),t}(i.type??"");if("group"===r||"ioscontentgroup"===r){t.push(e);continue}let a=Math.max(0,e-t.length);n.push({...i,depth:a})}return n})(o?.nodes??[]).map((e,t)=>({...e,ref:`e${t+1}`})),l={nodes:s,truncated:o?.truncated,createdAt:Date.now(),backend:o?.backend},c={name:n,device:r,createdAt:i?.createdAt??Date.now(),appBundleId:a,snapshot:l,actions:i?.actions??[]};return eE(c,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{nodes:s.length,truncated:o?.truncated??!1}}),eb.set(n,c),{ok:!0,data:{nodes:s,truncated:o?.truncated??!1,appName:i?.appName??a??r.name,appBundleId:a}}}if("click"===t){let i=eb.get(n);if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let r=e.positionals?.[0]??"",a=eg(r);if(!a)return{ok:!1,error:{code:"INVALID_ARGS",message:"click requires a ref like @e2"}};let o=ey(i.snapshot.nodes,a);if(!o?.rect&&e.positionals.length>1){let t=e.positionals.slice(1).join(" ").trim();t.length>0&&(o=eR(i.snapshot.nodes,t))}if(!o?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${r} not found or has no bounds`}};let s=eP(o,i.snapshot.nodes),l=o.label?.trim();if("ios"===i.device.platform&&"simulator"===i.device.kind&&l&&function(e,t){let n=t.trim().toLowerCase();if(!n)return!1;let i=0;for(let t of e)if((t.label??"").trim().toLowerCase()===n&&(i+=1)>1)return!1;return 1===i}(i.snapshot.nodes,l))return await ee(i.device,{command:"tap",text:l,appBundleId:i.appBundleId},{verbose:e.flags?.verbose,logPath:eS}),eE(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:a,refLabel:l,mode:"text"}}),{ok:!0,data:{ref:a,mode:"text"}};let{x:c,y:u}=eN(o.rect);return await ew(i.device,"press",[String(c),String(u)],e.flags?.out,{...ex(e.flags,i.appBundleId)}),eE(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:a,x:c,y:u,refLabel:s}}),{ok:!0,data:{ref:a,x:c,y:u}}}if("fill"===t){let i=eb.get(n);if(e.positionals?.[0]?.startsWith("@")){if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let n=eg(e.positionals[0]);if(!n)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires a ref like @e2"}};let r=e.positionals.length>=3?e.positionals[1]:"",a=e.positionals.length>=3?e.positionals.slice(2).join(" "):e.positionals.slice(1).join(" ");if(!a)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires text after ref"}};let o=ey(i.snapshot.nodes,n);if(!o?.rect&&r&&(o=eR(i.snapshot.nodes,r)),!o?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${e.positionals[0]} not found or has no bounds`}};let s=eP(o,i.snapshot.nodes),{x:l,y:c}=eN(o.rect),u=await ew(i.device,"fill",[String(l),String(c),a],e.flags?.out,{...ex(e.flags,i.appBundleId)});return eE(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:u??{ref:n,x:l,y:c,refLabel:s}}),{ok:!0,data:u??{ref:n,x:l,y:c}}}}if("get"===t){let i=e.positionals?.[0],r=e.positionals?.[1];if("text"!==i&&"attrs"!==i)return{ok:!1,error:{code:"INVALID_ARGS",message:"get only supports text or attrs"}};let a=eb.get(n);if(!a?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let o=eg(r??"");if(!o)return{ok:!1,error:{code:"INVALID_ARGS",message:"get text requires a ref like @e2"}};let s=ey(a.snapshot.nodes,o);if(!s&&e.positionals.length>2){let t=e.positionals.slice(2).join(" ").trim();t.length>0&&(s=eR(a.snapshot.nodes,t))}if(!s)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${r} not found`}};if("attrs"===i)return eE(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:o}}),{ok:!0,data:{ref:o,node:s}};let l=[s.label,s.value,s.identifier].map(e=>"string"==typeof e?e.trim():"").filter(e=>e.length>0)[0]??"";return eE(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:o,text:l,refLabel:l||void 0}}),{ok:!0,data:{ref:o,text:l,node:s}}}if("rect"===t){let i=eb.get(n);if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let r=eg(e.positionals?.[0]??""),a="";if(r){let e=ey(i.snapshot.nodes,r);a=e?.label?.trim()??""}else a=e.positionals.join(" ").trim();if(!a)return{ok:!1,error:{code:"INVALID_ARGS",message:"rect requires a label or ref with label"}};if("ios"!==i.device.platform||"simulator"!==i.device.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"rect is only supported on iOS simulators"}};let o=await ee(i.device,{command:"rect",text:a,appBundleId:i.appBundleId},{verbose:e.flags?.verbose,logPath:eS});return eE(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{label:a,rect:o?.rect}}),{ok:!0,data:{label:a,rect:o?.rect}}}let r=eb.get(n);if(!r)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session. Run open first."}};let o=await ew(r.device,t,e.positionals??[],e.flags?.out,{...ex(e.flags,r.appBundleId)});return eE(r,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:o??{}}),{ok:!0,data:o??{}}}function eE(e,t){t.flags?.noRecord||e.actions.push({ts:Date.now(),command:t.command,positionals:t.positionals,flags:function(e){if(!e)return{};let{platform:t,device:n,udid:i,serial:r,out:a,verbose:o,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:u,snapshotRaw:d,snapshotBackend:f,noRecord:p,recordJson:h}=e;return{platform:t,device:n,udid:i,serial:r,out:a,verbose:o,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:u,snapshotRaw:d,snapshotBackend:f,noRecord:p,recordJson:h}}(t.flags),result:t.result})}function ek(e){try{u.existsSync(eA)||u.mkdirSync(eA,{recursive:!0});let t=e.name.replace(/[^a-zA-Z0-9._-]/g,"_"),n=new Date(e.createdAt).toISOString().replace(/[:.]/g,"-"),i=a.join(eA,`${t}-${n}.ad`),r=a.join(eA,`${t}-${n}.json`),o={name:e.name,device:e.device,createdAt:e.createdAt,appBundleId:e.appBundleId,actions:e.actions,optimizedActions:function(e){let t=[];for(let n of e.actions)if("snapshot"!==n.command){if("click"===n.command||"fill"===n.command||"get"===n.command){let i=n.result?.refLabel;"string"==typeof i&&i.trim().length>0&&t.push({ts:n.ts,command:"snapshot",positionals:[],flags:{platform:e.device.platform,snapshotInteractiveOnly:!0,snapshotCompact:!0,snapshotScope:i.trim()},result:{scope:i.trim()}})}t.push(n)}return t}(e)},s=function(e,t){let n=[],i=e.device.name.replace(/"/g,'\\"'),r=e.device.kind?` kind=${e.device.kind}`:"";for(let a of(n.push(`context platform=${e.device.platform} device="${i}"${r} theme=unknown`),t))a.flags?.noRecord||n.push(function(e){let t=[e.command];if("click"===e.command){let n=e.positionals?.[0];if(n){t.push(eC(n));let i=e.result?.refLabel;return"string"==typeof i&&i.trim().length>0&&t.push(eC(i)),t.join(" ")}}if("fill"===e.command){let n=e.positionals?.[0];if(n&&n.startsWith("@")){t.push(eC(n));let i=e.result?.refLabel,r=e.positionals.slice(1).join(" ");return"string"==typeof i&&i.trim().length>0&&t.push(eC(i)),r&&t.push(eC(r)),t.join(" ")}}if("get"===e.command){let n=e.positionals?.[0],i=e.positionals?.[1];if(n&&i){t.push(eC(n)),t.push(eC(i));let r=e.result?.refLabel;return"string"==typeof r&&r.trim().length>0&&t.push(eC(r)),t.join(" ")}}if("snapshot"===e.command)return e.flags?.snapshotInteractiveOnly&&t.push("-i"),e.flags?.snapshotCompact&&t.push("-c"),"number"==typeof e.flags?.snapshotDepth&&t.push("-d",String(e.flags.snapshotDepth)),e.flags?.snapshotScope&&t.push("-s",eC(e.flags.snapshotScope)),e.flags?.snapshotRaw&&t.push("--raw"),e.flags?.snapshotBackend&&t.push("--backend",e.flags.snapshotBackend),t.join(" ");for(let n of e.positionals??[])t.push(eC(n));return t.join(" ")}(a));return`${n.join("\n")}
2
2
  `}(e,o.optimizedActions);u.writeFileSync(i,s),e.actions.some(e=>e.flags?.recordJson)&&u.writeFileSync(r,JSON.stringify(o,null,2))}catch{}}function eC(e){let t=e.trim();return t.startsWith("@")||/^-?\d+(\.\d+)?$/.test(t)?t:JSON.stringify(t)}function eR(e,t){let n=t.toLowerCase();return e.find(e=>{let t=(e.label??"").toLowerCase(),i=(e.value??"").toLowerCase(),r=(e.identifier??"").toLowerCase();return t.includes(n)||i.includes(n)||r.includes(n)})??null}function eP(e,t){let n=[e.label,e.value,e.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0);return n&&eT(n)?n:function(e,t){if(!e.rect)return;let n=e.rect.y+e.rect.height/2,i=null;for(let e of t){if(!e.rect)continue;let t=[e.label,e.value,e.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0);if(!t||!eT(t))continue;let r=Math.abs(e.rect.y+e.rect.height/2-n);(!i||r<i.distance)&&(i={label:t,distance:r})}return i?.label}(e,t)??(n&&eT(n)?n:void 0)}function eT(e){let t=e.trim();return!(!t||/^(true|false)$/i.test(t)||/^\d+$/.test(t))}(e=f.createServer(e=>{let t="";e.setEncoding("utf8"),e.on("data",async n=>{let i=(t+=n).indexOf("\n");for(;-1!==i;){let n,r=t.slice(0,i).trim();if(t=t.slice(i+1),0===r.length){i=t.indexOf("\n");continue}try{let e=JSON.parse(r);n=await e_(e)}catch(t){let e=l(t);n={ok:!1,error:{code:e.code,message:e.message,details:e.details}}}e.write(`${JSON.stringify(n)}
3
- `),i=t.indexOf("\n")}})})).listen(0,"127.0.0.1",()=>{let t=e.address();if("object"==typeof t&&t?.port){var n;n=t.port,u.existsSync(eb)||u.mkdirSync(eb,{recursive:!0}),u.writeFileSync(eA,""),u.writeFileSync(eI,JSON.stringify({port:n,token:eD,pid:process.pid,version:eO},null,2),{mode:384}),process.stdout.write(`AGENT_DEVICE_DAEMON_PORT=${t.port}
4
- `)}}),t=async()=>{for(let e of Array.from(ev.values()))"ios"===e.device.platform&&"simulator"===e.device.kind&&await et(e.device.id),ek(e);e.close(()=>{u.existsSync(eI)&&u.unlinkSync(eI),process.exit(0)})},process.on("SIGINT",()=>{t()}),process.on("SIGTERM",()=>{t()}),process.on("SIGHUP",()=>{t()}),process.on("uncaughtException",e=>{let n=e instanceof p?e:l(e);process.stderr.write(`Daemon error: ${n.message}
3
+ `),i=t.indexOf("\n")}})})).listen(0,"127.0.0.1",()=>{let t=e.address();if("object"==typeof t&&t?.port){var n;n=t.port,u.existsSync(ev)||u.mkdirSync(ev,{recursive:!0}),u.writeFileSync(eS,""),u.writeFileSync(eI,JSON.stringify({port:n,token:eD,pid:process.pid,version:eO},null,2),{mode:384}),process.stdout.write(`AGENT_DEVICE_DAEMON_PORT=${t.port}
4
+ `)}}),t=async()=>{for(let e of Array.from(eb.values()))"ios"===e.device.platform&&"simulator"===e.device.kind&&await et(e.device.id),ek(e);e.close(()=>{u.existsSync(eI)&&u.unlinkSync(eI),process.exit(0)})},process.on("SIGINT",()=>{t()}),process.on("SIGTERM",()=>{t()}),process.on("SIGHUP",()=>{t()}),process.on("uncaughtException",e=>{let n=e instanceof p?e:l(e);process.stderr.write(`Daemon error: ${n.message}
5
5
  `),t()});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-device",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Unified control plane for physical and virtual devices via an agent-driven CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Callstack",
@@ -20,6 +20,7 @@
20
20
  "build:clis": "pnpm build:node && pnpm build:axsnapshot",
21
21
  "format": "prettier --write .",
22
22
  "prepublishOnly": "pnpm build:node && pnpm build:axsnapshot",
23
+ "prepack": "pnpm build:node && pnpm build:axsnapshot",
23
24
  "typecheck": "tsc -p tsconfig.json",
24
25
  "test": "node --test",
25
26
  "test:smoke": "node --test test/smoke/*.test.ts",
@@ -205,14 +205,8 @@ export async function dispatchCommand(
205
205
  );
206
206
  }
207
207
  if (backend === 'ax') {
208
- try {
209
- const ax = await snapshotAx(device);
210
- return { nodes: ax.nodes ?? [], truncated: false, backend: 'ax', rootRect: ax.rootRect };
211
- } catch (err) {
212
- if (context?.snapshotBackend === 'ax') {
213
- throw err;
214
- }
215
- }
208
+ const ax = await snapshotAx(device);
209
+ return { nodes: ax.nodes ?? [], truncated: false, backend: 'ax', rootRect: ax.rootRect };
216
210
  }
217
211
  const result = (await runIosRunnerCommand(
218
212
  device,
@@ -25,8 +25,15 @@ export async function snapshotAx(
25
25
  const binary = await ensureAxSnapshotBinary();
26
26
  const result = await runCmd(binary, [], { allowFailure: true });
27
27
  if (result.exitCode !== 0) {
28
+ const stderrText = (result.stderr ?? '').toString();
29
+ let hint = '';
30
+ if (stderrText.toLowerCase().includes('accessibility permission')) {
31
+ hint =
32
+ ' Enable Accessibility for your terminal in System Settings > Privacy & Security > Accessibility, ' +
33
+ 'or use --backend xctest (slower snapshots via XCTest).';
34
+ }
28
35
  throw new AppError('COMMAND_FAILED', 'AX snapshot failed', {
29
- stderr: result.stderr,
36
+ stderr: `${stderrText}${hint}`,
30
37
  stdout: result.stdout,
31
38
  });
32
39
  }